Commit 6b3e31bf authored by Sean McGivern's avatar Sean McGivern

Merge branch 'master' into fabsrc/gitlab-ce-2105-add-setting-for-first-day-of-the-week-ee

parents 8cea0690 41c4c991
<script>
export default {
props: {
value: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
inputId: {
type: String,
required: true,
},
},
};
</script>
<template>
<li>
<div class="commit-message-editor">
<div class="d-flex flex-wrap align-items-center justify-content-between">
<label class="col-form-label" :for="inputId">
<strong>{{ label }}</strong>
</label>
<slot name="header"></slot>
</div>
<textarea
:id="inputId"
:value="value"
class="form-control js-gfm-input append-bottom-default commit-message-edit"
required="required"
rows="7"
@input="$emit('input', $event.target.value)"
></textarea>
<slot name="checkbox"></slot>
</div>
</li>
</template>
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
export default {
components: {
GlDropdown,
GlDropdownItem,
},
props: {
commits: {
type: Array,
required: true,
default: () => [],
},
},
};
</script>
<template>
<div>
<gl-dropdown
right
no-caret
text="Use an existing commit message"
variant="link"
class="mr-commit-dropdown"
>
<gl-dropdown-item
v-for="commit in commits"
:key="commit.short_id"
class="text-nowrap text-truncate"
@click="$emit('input', commit.message)"
>
<span class="monospace mr-2">{{ commit.short_id }}</span> {{ commit.title }}
</gl-dropdown-item>
</gl-dropdown>
</div>
</template>
<script>
import { GlButton } from '@gitlab/ui';
import _ from 'underscore';
import { __, n__, sprintf, s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
GlButton,
},
props: {
isSquashEnabled: {
type: Boolean,
required: true,
},
commitsCount: {
type: Number,
required: false,
default: 0,
},
targetBranch: {
type: String,
required: true,
},
},
data() {
return {
expanded: false,
};
},
computed: {
collapseIcon() {
return this.expanded ? 'chevron-down' : 'chevron-right';
},
commitsCountMessage() {
return n__(__('%d commit'), __('%d commits'), this.isSquashEnabled ? 1 : this.commitsCount);
},
modifyLinkMessage() {
return this.isSquashEnabled ? __('Modify commit messages') : __('Modify merge commit');
},
ariaLabel() {
return this.expanded ? __('Collapse') : __('Expand');
},
message() {
return sprintf(
s__(
'mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}.',
),
{
commitCount: `<strong class="commits-count-message">${this.commitsCountMessage}</strong>`,
mergeCommitCount: `<strong>${s__('mrWidgetCommitsAdded|1 merge commit')}</strong>`,
targetBranch: `<span class="label-branch">${_.escape(this.targetBranch)}</span>`,
},
false,
);
},
},
methods: {
toggle() {
this.expanded = !this.expanded;
},
},
};
</script>
<template>
<div>
<div
class="js-mr-widget-commits-count mr-widget-extension clickable d-flex align-items-center px-3 py-2"
@click="toggle()"
>
<gl-button
:aria-label="ariaLabel"
variant="blank"
class="commit-edit-toggle mr-2"
@click.stop="toggle()"
>
<icon :name="collapseIcon" :size="16" />
</gl-button>
<span v-if="expanded">{{ __('Collapse') }}</span>
<span v-else>
<span v-html="message"></span>
<gl-button variant="link" class="modify-message-button">
{{ modifyLinkMessage }}
</gl-button>
</span>
</div>
<div v-show="expanded"><slot></slot></div>
</div>
</template>
...@@ -2,17 +2,24 @@ ...@@ -2,17 +2,24 @@
import successSvg from 'icons/_icon_status_success.svg'; import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg'; import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll'; import simplePoll from '~/lib/utils/simple_poll';
import { __ } from '~/locale';
import MergeRequest from '../../../merge_request'; import MergeRequest from '../../../merge_request';
import Flash from '../../../flash'; import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue'; import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import SquashBeforeMerge from './squash_before_merge.vue'; import SquashBeforeMerge from './squash_before_merge.vue';
import CommitsHeader from './commits_header.vue';
import CommitEdit from './commit_edit.vue';
import CommitMessageDropdown from './commit_message_dropdown.vue';
export default { export default {
name: 'ReadyToMerge', name: 'ReadyToMerge',
components: { components: {
statusIcon, statusIcon,
SquashBeforeMerge, SquashBeforeMerge,
CommitsHeader,
CommitEdit,
CommitMessageDropdown,
}, },
props: { props: {
mr: { type: Object, required: true }, mr: { type: Object, required: true },
...@@ -22,27 +29,20 @@ export default { ...@@ -22,27 +29,20 @@ export default {
return { return {
removeSourceBranch: this.mr.shouldRemoveSourceBranch, removeSourceBranch: this.mr.shouldRemoveSourceBranch,
mergeWhenBuildSucceeds: false, mergeWhenBuildSucceeds: false,
useCommitMessageWithDescription: false,
setToMergeWhenPipelineSucceeds: false, setToMergeWhenPipelineSucceeds: false,
showCommitMessageEditor: false,
isMakingRequest: false, isMakingRequest: false,
isMergingImmediately: false, isMergingImmediately: false,
commitMessage: this.mr.commitMessage, commitMessage: this.mr.commitMessage,
squashBeforeMerge: this.mr.squash, squashBeforeMerge: this.mr.squash,
successSvg, successSvg,
warningSvg, warningSvg,
squashCommitMessage: this.mr.squashCommitMessage,
}; };
}, },
computed: { computed: {
shouldShowMergeWhenPipelineSucceedsText() { shouldShowMergeWhenPipelineSucceedsText() {
return this.mr.isPipelineActive; return this.mr.isPipelineActive;
}, },
commitMessageLinkTitle() {
const withDesc = 'Include description in commit message';
const withoutDesc = "Don't include description in commit message";
return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
},
status() { status() {
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr; const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
...@@ -84,9 +84,9 @@ export default { ...@@ -84,9 +84,9 @@ export default {
}, },
mergeButtonText() { mergeButtonText() {
if (this.isMergingImmediately) { if (this.isMergingImmediately) {
return 'Merge in progress'; return __('Merge in progress');
} else if (this.shouldShowMergeWhenPipelineSucceedsText) { } else if (this.shouldShowMergeWhenPipelineSucceedsText) {
return 'Merge when pipeline succeeds'; return __('Merge when pipeline succeeds');
} }
return 'Merge'; return 'Merge';
...@@ -98,7 +98,7 @@ export default { ...@@ -98,7 +98,7 @@ export default {
const { commitMessage } = this; const { commitMessage } = this;
return Boolean( return Boolean(
!commitMessage.length || !commitMessage.length ||
!this.shouldShowMergeControls() || !this.shouldShowMergeControls ||
this.isMakingRequest || this.isMakingRequest ||
this.isApprovalNeeded || this.isApprovalNeeded ||
this.mr.preventMerge, this.mr.preventMerge,
...@@ -114,18 +114,14 @@ export default { ...@@ -114,18 +114,14 @@ export default {
isApprovalNeeded() { isApprovalNeeded() {
return this.mr.approvalsRequired ? !this.mr.isApproved : false; return this.mr.approvalsRequired ? !this.mr.isApproved : false;
}, },
},
methods: {
shouldShowMergeControls() { shouldShowMergeControls() {
return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText; return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText;
}, },
updateCommitMessage() { },
const cmwd = this.mr.commitMessageWithDescription; methods: {
this.useCommitMessageWithDescription = !this.useCommitMessageWithDescription; updateMergeCommitMessage(includeDescription) {
this.commitMessage = this.useCommitMessageWithDescription ? cmwd : this.mr.commitMessage; const { commitMessageWithDescription, commitMessage } = this.mr;
}, this.commitMessage = includeDescription ? commitMessageWithDescription : commitMessage;
toggleCommitMessageEditor() {
this.showCommitMessageEditor = !this.showCommitMessageEditor;
}, },
handleMergeButtonClick(mergeWhenBuildSucceeds, mergeImmediately) { handleMergeButtonClick(mergeWhenBuildSucceeds, mergeImmediately) {
// TODO: Remove no-param-reassign // TODO: Remove no-param-reassign
...@@ -143,6 +139,7 @@ export default { ...@@ -143,6 +139,7 @@ export default {
merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds, merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds,
should_remove_source_branch: this.removeSourceBranch === true, should_remove_source_branch: this.removeSourceBranch === true,
squash: this.squashBeforeMerge, squash: this.squashBeforeMerge,
squash_commit_message: this.squashCommitMessage,
}; };
this.isMakingRequest = true; this.isMakingRequest = true;
...@@ -162,7 +159,7 @@ export default { ...@@ -162,7 +159,7 @@ export default {
}) })
.catch(() => { .catch(() => {
this.isMakingRequest = false; this.isMakingRequest = false;
new Flash('Something went wrong. Please try again.'); // eslint-disable-line new Flash(__('Something went wrong. Please try again.')); // eslint-disable-line
}); });
}, },
initiateMergePolling() { initiateMergePolling() {
...@@ -198,7 +195,7 @@ export default { ...@@ -198,7 +195,7 @@ export default {
} }
}) })
.catch(() => { .catch(() => {
new Flash('Something went wrong while merging this merge request. Please try again.'); // eslint-disable-line new Flash(__('Something went wrong while merging this merge request. Please try again.')); // eslint-disable-line
}); });
}, },
initiateRemoveSourceBranchPolling() { initiateRemoveSourceBranchPolling() {
...@@ -227,7 +224,7 @@ export default { ...@@ -227,7 +224,7 @@ export default {
} }
}) })
.catch(() => { .catch(() => {
new Flash('Something went wrong while deleting the source branch. Please try again.'); // eslint-disable-line new Flash(__('Something went wrong while deleting the source branch. Please try again.')); // eslint-disable-line
}); });
}, },
}, },
...@@ -235,127 +232,136 @@ export default { ...@@ -235,127 +232,136 @@ export default {
</script> </script>
<template> <template>
<div class="mr-widget-body media"> <div>
<status-icon :status="iconClass" /> <div class="mr-widget-body media">
<div class="media-body"> <status-icon :status="iconClass" />
<div class="mr-widget-body-controls media space-children"> <div class="media-body">
<span class="btn-group"> <div class="mr-widget-body-controls media space-children">
<button <span class="btn-group">
:disabled="isMergeButtonDisabled"
:class="mergeButtonClass"
type="button"
class="qa-merge-button"
@click="handleMergeButtonClick()"
>
<i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
{{ mergeButtonText }}
</button>
<button
v-if="shouldShowMergeOptionsDropdown"
:disabled="isMergeButtonDisabled"
type="button"
class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
data-toggle="dropdown"
aria-label="Select merge moment"
>
<i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i>
</button>
<ul
v-if="shouldShowMergeOptionsDropdown"
class="dropdown-menu dropdown-menu-right"
role="menu"
>
<li>
<a
class="merge_when_pipeline_succeeds qa-merge-when-pipeline-succeeds-option"
href="#"
@click.prevent="handleMergeButtonClick(true)"
>
<span class="media">
<span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span>
<span class="media-body merge-opt-title">Merge when pipeline succeeds</span>
</span>
</a>
</li>
<li>
<a
class="accept-merge-request qa-merge-immediately-option"
href="#"
@click.prevent="handleMergeButtonClick(false, true)"
>
<span class="media">
<span class="merge-opt-icon" aria-hidden="true" v-html="warningSvg"></span>
<span class="media-body merge-opt-title">Merge immediately</span>
</span>
</a>
</li>
</ul>
</span>
<div class="media-body-wrap space-children">
<template v-if="shouldShowMergeControls()">
<label v-if="mr.canRemoveSourceBranch">
<input
id="remove-source-branch-input"
v-model="removeSourceBranch"
:disabled="isRemoveSourceBranchButtonDisabled"
class="js-remove-source-branch-checkbox"
type="checkbox"
/>
Delete source branch
</label>
<!-- Placeholder for EE extension of this component -->
<squash-before-merge
v-if="shouldShowSquashBeforeMerge"
v-model="squashBeforeMerge"
:help-path="mr.squashBeforeMergeHelpPath"
:is-disabled="isMergeButtonDisabled"
/>
<span v-if="mr.ffOnlyEnabled" class="js-fast-forward-message">
Fast-forward merge without a merge commit
</span>
<button <button
v-else
:disabled="isMergeButtonDisabled" :disabled="isMergeButtonDisabled"
class="js-modify-commit-message-button btn btn-default btn-sm" :class="mergeButtonClass"
type="button" type="button"
@click="toggleCommitMessageEditor" class="qa-merge-button"
@click="handleMergeButtonClick()"
> >
Modify commit message <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
{{ mergeButtonText }}
</button> </button>
</template> <button
<template v-else> v-if="shouldShowMergeOptionsDropdown"
<span class="bold js-resolve-mr-widget-items-message"> :disabled="isMergeButtonDisabled"
You can only merge once the items above are resolved type="button"
</span> class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
</template> data-toggle="dropdown"
</div> aria-label="Select merge moment"
</div> >
<div v-if="showCommitMessageEditor" class="prepend-top-default commit-message-editor"> <i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i>
<div class="form-group clearfix"> </button>
<label class="col-form-label" for="commit-message"> Commit message </label> <ul
<div class="col-sm-10"> v-if="shouldShowMergeOptionsDropdown"
<div class="commit-message-container"> class="dropdown-menu dropdown-menu-right"
<div class="max-width-marker"></div> role="menu"
<textarea >
id="commit-message" <li>
v-model="commitMessage" <a
class="form-control js-commit-message" class="merge_when_pipeline_succeeds qa-merge-when-pipeline-succeeds-option"
required="required" href="#"
rows="14" @click.prevent="handleMergeButtonClick(true)"
name="Commit message" >
></textarea> <span class="media">
</div> <span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span>
<p class="hint"> <span class="media-body merge-opt-title">{{
Try to keep the first line under 52 characters and the others under 72 __('Merge when pipeline succeeds')
</p> }}</span>
<div class="hint"> </span>
<a href="#" @click.prevent="updateCommitMessage"> {{ commitMessageLinkTitle }} </a> </a>
</div> </li>
<li>
<a
class="accept-merge-request qa-merge-immediately-option"
href="#"
@click.prevent="handleMergeButtonClick(false, true)"
>
<span class="media">
<span class="merge-opt-icon" aria-hidden="true" v-html="warningSvg"></span>
<span class="media-body merge-opt-title">{{ __('Merge immediately') }}</span>
</span>
</a>
</li>
</ul>
</span>
<div class="media-body-wrap space-children">
<template v-if="shouldShowMergeControls">
<label v-if="mr.canRemoveSourceBranch">
<input
id="remove-source-branch-input"
v-model="removeSourceBranch"
:disabled="isRemoveSourceBranchButtonDisabled"
class="js-remove-source-branch-checkbox"
type="checkbox"
/>
{{ __('Delete source branch') }}
</label>
<!-- Placeholder for EE extension of this component -->
<squash-before-merge
v-if="shouldShowSquashBeforeMerge"
v-model="squashBeforeMerge"
:help-path="mr.squashBeforeMergeHelpPath"
:is-disabled="isMergeButtonDisabled"
/>
</template>
<template v-else>
<span class="bold js-resolve-mr-widget-items-message">
{{ __('You can only merge once the items above are resolved') }}
</span>
</template>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<template v-if="shouldShowMergeControls">
<div v-if="mr.ffOnlyEnabled" class="mr-fast-forward-message">
{{ __('Fast-forward merge without a merge commit') }}
</div>
<template v-else>
<commits-header
:is-squash-enabled="squashBeforeMerge"
:commits-count="mr.commitsCount"
:target-branch="mr.targetBranch"
>
<ul class="border-top content-list commits-list flex-list">
<commit-edit
v-if="squashBeforeMerge"
v-model="squashCommitMessage"
:label="__('Squash commit message')"
input-id="squash-message-edit"
squash
>
<commit-message-dropdown
slot="header"
v-model="squashCommitMessage"
:commits="mr.commits"
/>
</commit-edit>
<commit-edit
v-model="commitMessage"
:label="__('Merge commit message')"
input-id="merge-message-edit"
>
<label slot="checkbox">
<input
id="include-description"
type="checkbox"
@change="updateMergeCommitMessage($event.target.checked)"
/>
{{ __('Include merge request description') }}
</label>
</commit-edit>
</ul>
</commits-header>
</template>
</template>
</div> </div>
</template> </template>
...@@ -315,7 +315,7 @@ export default { ...@@ -315,7 +315,7 @@ export default {
:endpoint="mr.testResultsPath" :endpoint="mr.testResultsPath"
/> />
<div class="mr-widget-section"> <div class="mr-widget-section p-0">
<component :is="componentName" :mr="mr" :service="service" /> <component :is="componentName" :mr="mr" :service="service" />
<section v-if="shouldRenderCollaborationStatus" class="mr-info-list mr-links"> <section v-if="shouldRenderCollaborationStatus" class="mr-info-list mr-links">
......
...@@ -42,6 +42,8 @@ export default class MergeRequestStore { ...@@ -42,6 +42,8 @@ export default class MergeRequestStore {
this.mergePipeline = data.merge_pipeline || {}; this.mergePipeline = data.merge_pipeline || {};
this.deployments = this.deployments || data.deployments || []; this.deployments = this.deployments || data.deployments || [];
this.postMergeDeployments = this.postMergeDeployments || []; this.postMergeDeployments = this.postMergeDeployments || [];
this.commits = data.commits_without_merge_commits || [];
this.squashCommitMessage = data.default_squash_commit_message;
this.initRebase(data); this.initRebase(data);
if (data.issues_links) { if (data.issues_links) {
......
...@@ -395,6 +395,11 @@ img.emoji { ...@@ -395,6 +395,11 @@ img.emoji {
.flex-no-shrink { flex-shrink: 0; } .flex-no-shrink { flex-shrink: 0; }
.ws-initial { white-space: initial; } .ws-initial { white-space: initial; }
.overflow-auto { overflow: auto; } .overflow-auto { overflow: auto; }
.d-flex-center {
display: flex;
align-items: center;
justify-content: center;
}
/** COMMON SIZING CLASSES **/ /** COMMON SIZING CLASSES **/
.w-0 { width: 0; } .w-0 { width: 0; }
...@@ -406,6 +411,10 @@ img.emoji { ...@@ -406,6 +411,10 @@ img.emoji {
.min-height-0 { min-height: 0; } .min-height-0 { min-height: 0; }
.w-3 { width: #{3 * $grid-size}; }
.h-3 { width: #{3 * $grid-size}; }
/** COMMON SPACING CLASSES **/ /** COMMON SPACING CLASSES **/
.gl-pl-0 { padding-left: 0; } .gl-pl-0 { padding-left: 0; }
.gl-pl-1 { padding-left: #{0.5 * $grid-size}; } .gl-pl-1 { padding-left: #{0.5 * $grid-size}; }
......
...@@ -244,6 +244,7 @@ $gl-padding-8: 8px; ...@@ -244,6 +244,7 @@ $gl-padding-8: 8px;
$gl-padding: 16px; $gl-padding: 16px;
$gl-padding-24: 24px; $gl-padding-24: 24px;
$gl-padding-32: 32px; $gl-padding-32: 32px;
$gl-padding-50: 50px;
$gl-col-padding: 15px; $gl-col-padding: 15px;
$gl-input-padding: 10px; $gl-input-padding: 10px;
$gl-vert-padding: 6px; $gl-vert-padding: 6px;
......
...@@ -38,9 +38,7 @@ ...@@ -38,9 +38,7 @@
} }
.mr-widget-section { .mr-widget-section {
.media { border-radius: $border-radius-default $border-radius-default 0 0;
align-items: center;
}
.code-text { .code-text {
flex: 1; flex: 1;
...@@ -56,6 +54,11 @@ ...@@ -56,6 +54,11 @@
.mr-widget-extension { .mr-widget-extension {
border-top: 1px solid $border-color; border-top: 1px solid $border-color;
background-color: $gray-light; background-color: $gray-light;
&.clickable:hover {
background-color: $gl-gray-200;
cursor: pointer;
}
} }
.mr-widget-workflow { .mr-widget-workflow {
...@@ -78,6 +81,7 @@ ...@@ -78,6 +81,7 @@
border-top: 0; border-top: 0;
} }
.mr-widget-body,
.mr-widget-section, .mr-widget-section,
.mr-widget-content, .mr-widget-content,
.mr-widget-footer { .mr-widget-footer {
...@@ -87,11 +91,38 @@ ...@@ -87,11 +91,38 @@
.mr-state-widget { .mr-state-widget {
color: $gl-text-color; color: $gl-text-color;
.commit-message-edit {
border-radius: $border-radius-default;
}
.mr-widget-section, .mr-widget-section,
.mr-widget-footer { .mr-widget-footer {
border-top: solid 1px $border-color; border-top: solid 1px $border-color;
} }
.mr-fast-forward-message {
padding-left: $gl-padding-50;
padding-bottom: $gl-padding;
}
.commits-list {
> li {
padding: $gl-padding;
@include media-breakpoint-up(md) {
padding-left: $gl-padding-50;
}
}
}
.mr-commit-dropdown {
.dropdown-menu {
@include media-breakpoint-up(md) {
width: 150%;
}
}
}
.mr-widget-footer { .mr-widget-footer {
padding: 0; padding: 0;
} }
...@@ -405,7 +436,7 @@ ...@@ -405,7 +436,7 @@
} }
.mr-widget-help { .mr-widget-help {
padding: 10px 16px 10px 48px; padding: 10px 16px 10px $gl-padding-50;
font-style: italic; font-style: italic;
} }
...@@ -423,10 +454,6 @@ ...@@ -423,10 +454,6 @@
} }
} }
.mr-widget-body-controls {
flex-wrap: wrap;
}
.mr_source_commit, .mr_source_commit,
.mr_target_commit { .mr_target_commit {
margin-bottom: 0; margin-bottom: 0;
......
---
title: Default squash commit message is now selected from the longest commit when
squashing merge requests
merge_request: 24518
author:
type: changed
...@@ -23,11 +23,14 @@ The squashed commit's commit message will be either: ...@@ -23,11 +23,14 @@ The squashed commit's commit message will be either:
- Taken from the first multi-line commit message in the merge. - Taken from the first multi-line commit message in the merge.
- The merge request's title if no multi-line commit message is found. - The merge request's title if no multi-line commit message is found.
Note that the squashed commit is still followed by a merge commit, It can be customized before merging a merge request.
as the merge method for this example repository uses a merge commit.
Squashing also works with the fast-forward merge strategy, see ![A squash commit message editor](img/squash_mr_message.png)
[squashing and fast-forward merge](#squash-and-fast-forward-merge) for more
details. NOTE: **Note:**
The squashed commit in this example is followed by a merge commit, as the merge method for this example repository uses a merge commit.
Squashing also works with the fast-forward merge strategy, see [squashing and fast-forward merge](#squash-and-fast-forward-merge) for more details.
## Use cases ## Use cases
...@@ -60,7 +63,7 @@ This can then be overridden at the time of accepting the merge request: ...@@ -60,7 +63,7 @@ This can then be overridden at the time of accepting the merge request:
The squashed commit has the following metadata: The squashed commit has the following metadata:
- Message: the message of the squash commit. - Message: the message of the squash commit, or a customized message.
- Author: the author of the merge request. - Author: the author of the merge request.
- Committer: the user who initiated the squash. - Committer: the user who initiated the squash.
......
...@@ -223,11 +223,19 @@ class Geo::ProjectRegistry < Geo::BaseRegistry ...@@ -223,11 +223,19 @@ class Geo::ProjectRegistry < Geo::BaseRegistry
end end
def repository_sync_due?(scheduled_time) def repository_sync_due?(scheduled_time)
never_synced_repository? || repository_sync_needed?(scheduled_time) return true if last_repository_synced_at.nil?
return false unless resync_repository?
return false if repository_retry_at && scheduled_time < repository_retry_at
scheduled_time > last_repository_synced_at
end end
def wiki_sync_due?(scheduled_time) def wiki_sync_due?(scheduled_time)
never_synced_wiki? || wiki_sync_needed?(scheduled_time) return true if last_wiki_synced_at.nil?
return false unless resync_wiki?
return false if wiki_retry_at && scheduled_time < wiki_retry_at
scheduled_time > last_wiki_synced_at
end end
# Returns whether repository is pending verification check # Returns whether repository is pending verification check
...@@ -365,28 +373,6 @@ class Geo::ProjectRegistry < Geo::BaseRegistry ...@@ -365,28 +373,6 @@ class Geo::ProjectRegistry < Geo::BaseRegistry
"projects/#{project_id}/fetches_since_gc" "projects/#{project_id}/fetches_since_gc"
end end
def never_synced_repository?
last_repository_synced_at.nil?
end
def never_synced_wiki?
last_wiki_synced_at.nil?
end
def repository_sync_needed?(timestamp)
return false unless resync_repository?
return false if repository_retry_at && timestamp < repository_retry_at
last_repository_synced_at && timestamp > last_repository_synced_at
end
def wiki_sync_needed?(timestamp)
return false unless resync_wiki?
return false if wiki_retry_at && timestamp < wiki_retry_at
last_wiki_synced_at && timestamp > last_wiki_synced_at
end
# How many times have we retried syncing it? # How many times have we retried syncing it?
# #
# @param [String] type must be one of the values in TYPES # @param [String] type must be one of the values in TYPES
......
...@@ -158,7 +158,11 @@ module Geo ...@@ -158,7 +158,11 @@ module Geo
def reschedule_sync def reschedule_sync
log_info("Reschedule #{type} sync because a RepositoryUpdateEvent was processed during the sync") log_info("Reschedule #{type} sync because a RepositoryUpdateEvent was processed during the sync")
::Geo::ProjectSyncWorker.perform_async(project.id, Time.now) ::Geo::ProjectSyncWorker.perform_async(
project.id,
sync_repository: type.repository?,
sync_wiki: type.wiki?
)
end end
def fail_registry!(message, error, attrs = {}) def fail_registry!(message, error, attrs = {})
...@@ -170,7 +174,7 @@ module Geo ...@@ -170,7 +174,7 @@ module Geo
end end
def type def type
self.class.type @type ||= self.class.type.to_s.inquiry
end end
def update_delay_in_seconds def update_delay_in_seconds
......
...@@ -15,7 +15,7 @@ module Geo ...@@ -15,7 +15,7 @@ module Geo
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def perform(project_id, scheduled_time) def perform(project_id, options = {})
registry = Geo::ProjectRegistry.find_or_initialize_by(project_id: project_id) registry = Geo::ProjectRegistry.find_or_initialize_by(project_id: project_id)
project = registry.project project = registry.project
...@@ -30,9 +30,37 @@ module Geo ...@@ -30,9 +30,37 @@ module Geo
return return
end end
Geo::RepositorySyncService.new(project).execute if registry.repository_sync_due?(scheduled_time) options = extract_options(registry, options)
Geo::WikiSyncService.new(project).execute if registry.wiki_sync_due?(scheduled_time)
sync_repository(registry, options)
sync_wiki(registry, options)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def sync_repository(registry, options)
return unless options[:sync_repository] && registry.resync_repository?
Geo::RepositorySyncService.new(registry.project).execute
end
def sync_wiki(registry, options)
return unless options[:sync_wiki] && registry.resync_wiki?
Geo::WikiSyncService.new(registry.project).execute
end
def extract_options(registry, options)
options.is_a?(Hash) ? options.symbolize_keys : backward_options(registry, options)
end
# Before GitLab 11.8 we used to pass the scheduled time instead of an options hash,
# this method makes the job arguments backward compatible and
# can be removed in any version after GitLab 12.0.
def backward_options(registry, schedule_time)
{
sync_repository: registry.repository_sync_due?(schedule_time),
sync_wiki: registry.wiki_sync_due?(schedule_time)
}
end
end end
end end
...@@ -42,11 +42,19 @@ module Geo ...@@ -42,11 +42,19 @@ module Geo
[1, capacity_per_shard.to_i].max [1, capacity_per_shard.to_i].max
end end
# rubocop: disable CodeReuse/ActiveRecord
def schedule_job(project_id) def schedule_job(project_id)
job_id = Geo::ProjectSyncWorker.perform_async(project_id, Time.now) registry = Geo::ProjectRegistry.find_or_initialize_by(project_id: project_id)
job_id = Geo::ProjectSyncWorker.perform_async(
project_id,
sync_repository: registry.repository_sync_due?(Time.now),
sync_wiki: registry.wiki_sync_due?(Time.now)
)
{ project_id: project_id, job_id: job_id } if job_id { project_id: project_id, job_id: job_id } if job_id
end end
# rubocop: enable CodeReuse/ActiveRecord
def scheduled_project_ids def scheduled_project_ids
scheduled_jobs.map { |data| data[:project_id] } scheduled_jobs.map { |data| data[:project_id] }
......
---
title: 'Geo: Handle repository and wiki sync separately in Geo::ProjectSyncWorker'
merge_request: 9360
author:
type: changed
...@@ -12,7 +12,7 @@ module Gitlab ...@@ -12,7 +12,7 @@ module Gitlab
registry.repository_created!(event) registry.repository_created!(event)
enqueue_job_if_shard_healthy(event) do enqueue_job_if_shard_healthy(event) do
::Geo::ProjectSyncWorker.perform_async(event.project_id, Time.now) ::Geo::ProjectSyncWorker.perform_async(event.project_id, sync_repository: true, sync_wiki: true)
end end
end end
......
...@@ -11,7 +11,11 @@ module Gitlab ...@@ -11,7 +11,11 @@ module Gitlab
registry.repository_updated!(event.source, scheduled_at) registry.repository_updated!(event.source, scheduled_at)
job_id = enqueue_job_if_shard_healthy(event) do job_id = enqueue_job_if_shard_healthy(event) do
::Geo::ProjectSyncWorker.perform_async(event.project_id, scheduled_at) ::Geo::ProjectSyncWorker.perform_async(
event.project_id,
sync_repository: event.repository?,
sync_wiki: event.wiki?
)
end end
log_event(job_id) log_event(job_id)
......
...@@ -21,7 +21,7 @@ describe Gitlab::Geo::LogCursor::Events::RepositoryUpdatedEvent, :postgresql, :c ...@@ -21,7 +21,7 @@ describe Gitlab::Geo::LogCursor::Events::RepositoryUpdatedEvent, :postgresql, :c
allow(Gitlab::ShardHealthCache).to receive(:healthy_shard?).with('broken').and_return(false) allow(Gitlab::ShardHealthCache).to receive(:healthy_shard?).with('broken').and_return(false)
end end
RSpec.shared_examples 'RepositoryUpdatedEvent' do shared_examples 'RepositoryUpdatedEvent' do
it 'creates a new project registry if it does not exist' do it 'creates a new project registry if it does not exist' do
expect { subject.process }.to change(Geo::ProjectRegistry, :count).by(1) expect { subject.process }.to change(Geo::ProjectRegistry, :count).by(1)
end end
...@@ -108,9 +108,37 @@ describe Gitlab::Geo::LogCursor::Events::RepositoryUpdatedEvent, :postgresql, :c ...@@ -108,9 +108,37 @@ describe Gitlab::Geo::LogCursor::Events::RepositoryUpdatedEvent, :postgresql, :c
it_behaves_like 'RepositoryUpdatedEvent' it_behaves_like 'RepositoryUpdatedEvent'
it 'schedules a Geo::ProjectSyncWorker' do it 'schedules a Geo::ProjectSyncWorker' do
expect(Geo::ProjectSyncWorker).to receive(:perform_async).with(project.id, now).once expect(Geo::ProjectSyncWorker).to receive(:perform_async).with(project.id, sync_repository: true, sync_wiki: false).once
Timecop.freeze(now) { subject.process } subject.process
end
context 'enqueues the job with the proper args' do
let!(:registry) { create(:geo_project_registry, :synced, project: repository_updated_event.project) }
before do
repository_updated_event.update!(source: event_source)
end
context 'enqueues wiki sync' do
let(:event_source) { Geo::RepositoryUpdatedEvent::WIKI }
it 'passes correct options' do
expect(Geo::ProjectSyncWorker).to receive(:perform_async).with(project.id, { sync_repository: false, sync_wiki: true })
subject.process
end
end
context 'enqueues repository sync' do
let(:event_source) { Geo::RepositoryUpdatedEvent::REPOSITORY }
it 'passes correct options' do
expect(Geo::ProjectSyncWorker).to receive(:perform_async).with(project.id, { sync_repository: true, sync_wiki: false })
subject.process
end
end
end end
end end
...@@ -120,9 +148,9 @@ describe Gitlab::Geo::LogCursor::Events::RepositoryUpdatedEvent, :postgresql, :c ...@@ -120,9 +148,9 @@ describe Gitlab::Geo::LogCursor::Events::RepositoryUpdatedEvent, :postgresql, :c
it_behaves_like 'RepositoryUpdatedEvent' it_behaves_like 'RepositoryUpdatedEvent'
it 'does not schedule a Geo::ProjectSyncWorker job' do it 'does not schedule a Geo::ProjectSyncWorker job' do
expect(Geo::ProjectSyncWorker).not_to receive(:perform_async).with(project.id, now) expect(Geo::ProjectSyncWorker).not_to receive(:perform_async).with(project.id, anything)
Timecop.freeze(now) { subject.process } subject.process
end end
end end
end end
......
...@@ -21,11 +21,20 @@ RSpec.describe Geo::ProjectSyncWorker do ...@@ -21,11 +21,20 @@ RSpec.describe Geo::ProjectSyncWorker do
.with(instance_of(Project)).once.and_return(wiki_sync_service) .with(instance_of(Project)).once.and_return(wiki_sync_service)
end end
context 'backward compatibility' do
it 'performs sync for the given project when time is passed' do
subject.perform(project.id, Time.now)
expect(repository_sync_service).to have_received(:execute)
expect(wiki_sync_service).to have_received(:execute)
end
end
context 'when project could not be found' do context 'when project could not be found' do
it 'logs an error and returns' do it 'logs an error and returns' do
expect(subject).to receive(:log_error).with("Couldn't find project, skipping syncing", project_id: 999) expect(subject).to receive(:log_error).with("Couldn't find project, skipping syncing", project_id: 999)
expect { subject.perform(999, Time.now) }.not_to raise_error expect { subject.perform(999) }.not_to raise_error
end end
end end
...@@ -35,21 +44,23 @@ RSpec.describe Geo::ProjectSyncWorker do ...@@ -35,21 +44,23 @@ RSpec.describe Geo::ProjectSyncWorker do
expect(repository_sync_service).not_to receive(:execute) expect(repository_sync_service).not_to receive(:execute)
expect(wiki_sync_service).not_to receive(:execute) expect(wiki_sync_service).not_to receive(:execute)
subject.perform(project_with_broken_storage.id, Time.now) subject.perform(project_with_broken_storage.id)
end end
end end
context 'when project repositories has never been synced' do context 'when project repositories has never been synced' do
it 'performs Geo::RepositorySyncService for the given project' do it 'performs Geo::RepositorySyncService for the given project' do
subject.perform(project.id, Time.now) subject.perform(project.id, sync_repository: true)
expect(repository_sync_service).to have_received(:execute).once expect(repository_sync_service).to have_received(:execute).once
expect(wiki_sync_service).not_to have_received(:execute)
end end
it 'performs Geo::WikiSyncService for the given project' do it 'performs Geo::WikiSyncService for the given project' do
subject.perform(project.id, Time.now) subject.perform(project.id, sync_wiki: true)
expect(wiki_sync_service).to have_received(:execute).once expect(wiki_sync_service).to have_received(:execute).once
expect(repository_sync_service).not_to have_received(:execute)
end end
end end
...@@ -57,13 +68,13 @@ RSpec.describe Geo::ProjectSyncWorker do ...@@ -57,13 +68,13 @@ RSpec.describe Geo::ProjectSyncWorker do
let!(:registry) { create(:geo_project_registry, :synced, project: project) } let!(:registry) { create(:geo_project_registry, :synced, project: project) }
it 'does not perform Geo::RepositorySyncService for the given project' do it 'does not perform Geo::RepositorySyncService for the given project' do
subject.perform(project.id, Time.now) subject.perform(project.id, sync_repository: true)
expect(repository_sync_service).not_to have_received(:execute) expect(repository_sync_service).not_to have_received(:execute)
end end
it 'does not perform Geo::WikiSyncService for the given project' do it 'does not perform Geo::WikiSyncService for the given project' do
subject.perform(project.id, Time.now) subject.perform(project.id, sync_wiki: true)
expect(wiki_sync_service).not_to have_received(:execute) expect(wiki_sync_service).not_to have_received(:execute)
end end
...@@ -73,72 +84,16 @@ RSpec.describe Geo::ProjectSyncWorker do ...@@ -73,72 +84,16 @@ RSpec.describe Geo::ProjectSyncWorker do
let!(:registry) { create(:geo_project_registry, :sync_failed, project: project) } let!(:registry) { create(:geo_project_registry, :sync_failed, project: project) }
it 'performs Geo::RepositorySyncService for the given project' do it 'performs Geo::RepositorySyncService for the given project' do
subject.perform(project.id, Time.now) subject.perform(project.id, sync_repository: true)
expect(repository_sync_service).to have_received(:execute).once expect(repository_sync_service).to have_received(:execute).once
end end
it 'performs Geo::WikiSyncService for the given project' do it 'performs Geo::WikiSyncService for the given project' do
subject.perform(project.id, Time.now) subject.perform(project.id, sync_wiki: true)
expect(wiki_sync_service).to have_received(:execute).once expect(wiki_sync_service).to have_received(:execute).once
end end
end end
context 'when project repository is dirty' do
let!(:registry) do
create(:geo_project_registry, :synced, :repository_dirty, project: project)
end
it 'performs Geo::RepositorySyncService for the given project' do
subject.perform(project.id, Time.now)
expect(repository_sync_service).to have_received(:execute).once
end
it 'does not perform Geo::WikiSyncService for the given project' do
subject.perform(project.id, Time.now)
expect(wiki_sync_service).not_to have_received(:execute)
end
end
context 'when wiki is dirty' do
let!(:registry) do
create(:geo_project_registry, :synced, :wiki_dirty, project: project)
end
it 'does not perform Geo::RepositorySyncService for the given project' do
subject.perform(project.id, Time.now)
expect(repository_sync_service).not_to have_received(:execute)
end
it 'performs Geo::WikiSyncService for the given project' do
subject.perform(project.id, Time.now)
expect(wiki_sync_service).to have_received(:execute)
end
end
context 'when project repository was synced after the time the job was scheduled in' do
it 'does not perform Geo::RepositorySyncService for the given project' do
create(:geo_project_registry, :synced, :repository_dirty, project: project, last_repository_synced_at: Time.now)
subject.perform(project.id, Time.now - 5.minutes)
expect(repository_sync_service).not_to have_received(:execute)
end
end
context 'when wiki repository was synced after the time the job was scheduled in' do
it 'does not perform Geo::RepositorySyncService for the given project' do
create(:geo_project_registry, :synced, :wiki_dirty, project: project, last_wiki_synced_at: Time.now)
subject.perform(project.id, Time.now - 5.minutes)
expect(wiki_sync_service).not_to have_received(:execute)
end
end
end end
end end
...@@ -122,7 +122,7 @@ describe Geo::RepositoryShardSyncWorker, :geo, :delete, :clean_gitlab_redis_cach ...@@ -122,7 +122,7 @@ describe Geo::RepositoryShardSyncWorker, :geo, :delete, :clean_gitlab_redis_cach
it 'does not perform Geo::ProjectSyncWorker for projects that do not belong to selected namespaces to replicate' do it 'does not perform Geo::ProjectSyncWorker for projects that do not belong to selected namespaces to replicate' do
expect(Geo::ProjectSyncWorker).to receive(:perform_async) expect(Geo::ProjectSyncWorker).to receive(:perform_async)
.with(unsynced_project_in_restricted_group.id, within(1.minute).of(Time.now)) .with(unsynced_project_in_restricted_group.id, sync_repository: true, sync_wiki: true)
.once .once
.and_return(spy) .and_return(spy)
...@@ -134,7 +134,7 @@ describe Geo::RepositoryShardSyncWorker, :geo, :delete, :clean_gitlab_redis_cach ...@@ -134,7 +134,7 @@ describe Geo::RepositoryShardSyncWorker, :geo, :delete, :clean_gitlab_redis_cach
create(:geo_project_registry, :synced, :repository_dirty, project: unsynced_project) create(:geo_project_registry, :synced, :repository_dirty, project: unsynced_project)
expect(Geo::ProjectSyncWorker).to receive(:perform_async) expect(Geo::ProjectSyncWorker).to receive(:perform_async)
.with(unsynced_project_in_restricted_group.id, within(1.minute).of(Time.now)) .with(unsynced_project_in_restricted_group.id, sync_repository: true, sync_wiki: false)
.once .once
.and_return(spy) .and_return(spy)
...@@ -182,6 +182,32 @@ describe Geo::RepositoryShardSyncWorker, :geo, :delete, :clean_gitlab_redis_cach ...@@ -182,6 +182,32 @@ describe Geo::RepositoryShardSyncWorker, :geo, :delete, :clean_gitlab_redis_cach
end end
end end
context 'projects that require resync' do
context 'when project repository is dirty' do
it 'syncs repository only' do
create(:geo_project_registry, :synced, :repository_dirty, project: unsynced_project)
create(:geo_project_registry, :synced, :repository_dirty, project: unsynced_project_in_restricted_group)
expect(Geo::ProjectSyncWorker).to receive(:perform_async).with(unsynced_project.id, sync_repository: true, sync_wiki: false)
expect(Geo::ProjectSyncWorker).to receive(:perform_async).with(unsynced_project_in_restricted_group.id, sync_repository: true, sync_wiki: false)
subject.perform(shard_name)
end
end
context 'when project wiki is dirty' do
it 'syncs wiki only' do
create(:geo_project_registry, :synced, :wiki_dirty, project: unsynced_project)
create(:geo_project_registry, :synced, :wiki_dirty, project: unsynced_project_in_restricted_group)
expect(Geo::ProjectSyncWorker).to receive(:perform_async).with(unsynced_project.id, sync_repository: false, sync_wiki: true)
expect(Geo::ProjectSyncWorker).to receive(:perform_async).with(unsynced_project_in_restricted_group.id, sync_repository: false, sync_wiki: true)
subject.perform(shard_name)
end
end
end
context 'all repositories fail' do context 'all repositories fail' do
let!(:project_list) { create_list(:project, 4, :random_last_repository_updated_at) } let!(:project_list) { create_list(:project, 4, :random_last_repository_updated_at) }
......
...@@ -54,6 +54,9 @@ msgid_plural "%d commits behind" ...@@ -54,6 +54,9 @@ msgid_plural "%d commits behind"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%d commits"
msgstr ""
msgid "%d exporter" msgid "%d exporter"
msgid_plural "%d exporters" msgid_plural "%d exporters"
msgstr[0] "" msgstr[0] ""
...@@ -3021,6 +3024,9 @@ msgstr "" ...@@ -3021,6 +3024,9 @@ msgstr ""
msgid "Delete list" msgid "Delete list"
msgstr "" msgstr ""
msgid "Delete source branch"
msgstr ""
msgid "Delete this attachment" msgid "Delete this attachment"
msgstr "" msgstr ""
...@@ -3881,6 +3887,9 @@ msgstr "" ...@@ -3881,6 +3887,9 @@ msgstr ""
msgid "Failure" msgid "Failure"
msgstr "" msgstr ""
msgid "Fast-forward merge without a merge commit"
msgstr ""
msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)" msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
msgstr "" msgstr ""
...@@ -5106,6 +5115,9 @@ msgstr "" ...@@ -5106,6 +5115,9 @@ msgstr ""
msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept." msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
msgstr "" msgstr ""
msgid "Include merge request description"
msgstr ""
msgid "Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>." msgid "Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>."
msgstr "" msgstr ""
...@@ -5826,9 +5838,18 @@ msgstr "" ...@@ -5826,9 +5838,18 @@ msgstr ""
msgid "Merge Requests created" msgid "Merge Requests created"
msgstr "" msgstr ""
msgid "Merge commit message"
msgstr ""
msgid "Merge events" msgid "Merge events"
msgstr "" msgstr ""
msgid "Merge immediately"
msgstr ""
msgid "Merge in progress"
msgstr ""
msgid "Merge request" msgid "Merge request"
msgstr "" msgstr ""
...@@ -5841,6 +5862,9 @@ msgstr "" ...@@ -5841,6 +5862,9 @@ msgstr ""
msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others" msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr "" msgstr ""
msgid "Merge when pipeline succeeds"
msgstr ""
msgid "MergeRequests|Add a reply" msgid "MergeRequests|Add a reply"
msgstr "" msgstr ""
...@@ -6108,6 +6132,12 @@ msgstr "" ...@@ -6108,6 +6132,12 @@ msgstr ""
msgid "Modal|Close" msgid "Modal|Close"
msgstr "" msgstr ""
msgid "Modify commit messages"
msgstr ""
msgid "Modify merge commit"
msgstr ""
msgid "Monday" msgid "Monday"
msgstr "" msgstr ""
...@@ -8671,6 +8701,9 @@ msgstr "" ...@@ -8671,6 +8701,9 @@ msgstr ""
msgid "Something went wrong while closing the %{issuable}. Please try again later" msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr "" msgstr ""
msgid "Something went wrong while deleting the source branch. Please try again."
msgstr ""
msgid "Something went wrong while fetching %{listType} list" msgid "Something went wrong while fetching %{listType} list"
msgstr "" msgstr ""
...@@ -8689,6 +8722,9 @@ msgstr "" ...@@ -8689,6 +8722,9 @@ msgstr ""
msgid "Something went wrong while fetching the registry list." msgid "Something went wrong while fetching the registry list."
msgstr "" msgstr ""
msgid "Something went wrong while merging this merge request. Please try again."
msgstr ""
msgid "Something went wrong while reopening the %{issuable}. Please try again later" msgid "Something went wrong while reopening the %{issuable}. Please try again later"
msgstr "" msgstr ""
...@@ -8863,6 +8899,9 @@ msgstr "" ...@@ -8863,6 +8899,9 @@ msgstr ""
msgid "Specify the following URL during the Runner setup:" msgid "Specify the following URL during the Runner setup:"
msgstr "" msgstr ""
msgid "Squash commit message"
msgstr ""
msgid "Squash commits" msgid "Squash commits"
msgstr "" msgstr ""
...@@ -10745,6 +10784,9 @@ msgstr "" ...@@ -10745,6 +10784,9 @@ msgstr ""
msgid "You can only edit files when you are on a branch" msgid "You can only edit files when you are on a branch"
msgstr "" msgstr ""
msgid "You can only merge once the items above are resolved"
msgstr ""
msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}" msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
msgstr "" msgstr ""
...@@ -11327,6 +11369,12 @@ msgstr[1] "" ...@@ -11327,6 +11369,12 @@ msgstr[1] ""
msgid "missing" msgid "missing"
msgstr "" msgstr ""
msgid "mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}."
msgstr ""
msgid "mrWidgetCommitsAdded|1 merge commit"
msgstr ""
msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch" msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
msgstr "" msgstr ""
......
...@@ -80,8 +80,8 @@ describe 'User accepts a merge request', :js do ...@@ -80,8 +80,8 @@ describe 'User accepts a merge request', :js do
end end
it 'accepts a merge request' do it 'accepts a merge request' do
click_button('Modify commit message') find('.js-mr-widget-commits-count').click
fill_in('Commit message', with: 'wow such merge') fill_in('merge-message-edit', with: 'wow such merge')
click_button('Merge') click_button('Merge')
......
...@@ -13,7 +13,7 @@ describe 'Merge request < User customizes merge commit message', :js do ...@@ -13,7 +13,7 @@ describe 'Merge request < User customizes merge commit message', :js do
description: "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" description: "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}"
) )
end end
let(:textbox) { page.find(:css, '.js-commit-message', visible: false) } let(:textbox) { page.find(:css, '#merge-message-edit', visible: false) }
let(:default_message) do let(:default_message) do
[ [
"Merge branch 'feature' into 'master'", "Merge branch 'feature' into 'master'",
...@@ -38,16 +38,16 @@ describe 'Merge request < User customizes merge commit message', :js do ...@@ -38,16 +38,16 @@ describe 'Merge request < User customizes merge commit message', :js do
end end
it 'toggles commit message between message with description and without description' do it 'toggles commit message between message with description and without description' do
expect(page).not_to have_selector('.js-commit-message') expect(page).not_to have_selector('#merge-message-edit')
click_button "Modify commit message" first('.js-mr-widget-commits-count').click
expect(textbox).to be_visible expect(textbox).to be_visible
expect(textbox.value).to eq(default_message) expect(textbox.value).to eq(default_message)
click_link "Include description in commit message" check('Include merge request description')
expect(textbox.value).to eq(message_with_description) expect(textbox.value).to eq(message_with_description)
click_link "Don't include description in commit message" uncheck('Include merge request description')
expect(textbox.value).to eq(default_message) expect(textbox.value).to eq(default_message)
end end
......
import { createLocalVue, shallowMount } from '@vue/test-utils';
import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit.vue';
const localVue = createLocalVue();
const testCommitMessage = 'Test commit message';
const testLabel = 'Test label';
const testInputId = 'test-input-id';
describe('Commits edit component', () => {
let wrapper;
const createComponent = (slots = {}) => {
wrapper = shallowMount(localVue.extend(CommitEdit), {
localVue,
sync: false,
propsData: {
value: testCommitMessage,
label: testLabel,
inputId: testInputId,
},
slots: {
...slots,
},
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
const findTextarea = () => wrapper.find('.form-control');
it('has a correct label', () => {
const labelElement = wrapper.find('.col-form-label');
expect(labelElement.text()).toBe(testLabel);
});
describe('textarea', () => {
it('has a correct ID', () => {
expect(findTextarea().attributes('id')).toBe(testInputId);
});
it('has a correct value', () => {
expect(findTextarea().element.value).toBe(testCommitMessage);
});
it('emits an input event and receives changed value', () => {
const changedCommitMessage = 'Changed commit message';
findTextarea().element.value = changedCommitMessage;
findTextarea().trigger('input');
expect(wrapper.emitted().input[0]).toEqual([changedCommitMessage]);
expect(findTextarea().element.value).toBe(changedCommitMessage);
});
});
describe('when slots are present', () => {
beforeEach(() => {
createComponent({
header: `<div class="test-header">${testCommitMessage}</div>`,
checkbox: `<label slot="checkbox" class="test-checkbox">${testLabel}</label >`,
});
});
it('renders header slot correctly', () => {
const headerSlotElement = wrapper.find('.test-header');
expect(headerSlotElement.exists()).toBe(true);
expect(headerSlotElement.text()).toBe(testCommitMessage);
});
it('renders checkbox slot correctly', () => {
const checkboxSlotElement = wrapper.find('.test-checkbox');
expect(checkboxSlotElement.exists()).toBe(true);
expect(checkboxSlotElement.text()).toBe(testLabel);
});
});
});
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlDropdownItem } from '@gitlab/ui';
import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue';
const localVue = createLocalVue();
const commits = [
{
title: 'Commit 1',
short_id: '78d5b7',
message: 'Update test.txt',
},
{
title: 'Commit 2',
short_id: '34cbe28b',
message: 'Fixed test',
},
{
title: 'Commit 3',
short_id: 'fa42932a',
message: 'Added changelog',
},
];
describe('Commits message dropdown component', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(localVue.extend(CommitMessageDropdown), {
localVue,
sync: false,
propsData: {
commits,
},
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
const findDropdownElements = () => wrapper.findAll(GlDropdownItem);
const findFirstDropdownElement = () => findDropdownElements().at(0);
it('should have 3 elements in dropdown list', () => {
expect(findDropdownElements().length).toBe(3);
});
it('should have correct message for the first dropdown list element', () => {
expect(findFirstDropdownElement().text()).toBe('78d5b7 Commit 1');
});
it('should emit a commit title on selecting commit', () => {
findFirstDropdownElement().vm.$emit('click');
expect(wrapper.emitted().input[0]).toEqual(['Update test.txt']);
});
});
import { createLocalVue, shallowMount } from '@vue/test-utils';
import CommitsHeader from '~/vue_merge_request_widget/components/states/commits_header.vue';
import Icon from '~/vue_shared/components/icon.vue';
const localVue = createLocalVue();
describe('Commits header component', () => {
let wrapper;
const createComponent = props => {
wrapper = shallowMount(localVue.extend(CommitsHeader), {
localVue,
sync: false,
propsData: {
isSquashEnabled: false,
targetBranch: 'master',
commitsCount: 5,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
});
const findHeaderWrapper = () => wrapper.find('.js-mr-widget-commits-count');
const findCommitToggle = () => wrapper.find('.commit-edit-toggle');
const findIcon = () => wrapper.find(Icon);
const findCommitsCountMessage = () => wrapper.find('.commits-count-message');
const findTargetBranchMessage = () => wrapper.find('.label-branch');
const findModifyButton = () => wrapper.find('.modify-message-button');
describe('when collapsed', () => {
it('toggle has aria-label equal to Expand', () => {
createComponent();
expect(findCommitToggle().attributes('aria-label')).toBe('Expand');
});
it('has a chevron-right icon', () => {
createComponent();
wrapper.setData({ expanded: false });
expect(findIcon().props('name')).toBe('chevron-right');
});
describe('when squash is disabled', () => {
beforeEach(() => {
createComponent();
});
it('has commits count message showing correct amount of commits', () => {
expect(findCommitsCountMessage().text()).toBe('5 commits');
});
it('has button with modify merge commit message', () => {
expect(findModifyButton().text()).toBe('Modify merge commit');
});
});
describe('when squash is enabled', () => {
beforeEach(() => {
createComponent({ isSquashEnabled: true });
});
it('has commits count message showing one commit when squash is enabled', () => {
expect(findCommitsCountMessage().text()).toBe('1 commit');
});
it('has button with modify commit messages text', () => {
expect(findModifyButton().text()).toBe('Modify commit messages');
});
});
it('has correct target branch displayed', () => {
createComponent();
expect(findTargetBranchMessage().text()).toBe('master');
});
});
describe('when expanded', () => {
beforeEach(() => {
createComponent();
wrapper.setData({ expanded: true });
});
it('toggle has aria-label equal to collapse', done => {
wrapper.vm.$nextTick(() => {
expect(findCommitToggle().attributes('aria-label')).toBe('Collapse');
done();
});
});
it('has a chevron-down icon', done => {
wrapper.vm.$nextTick(() => {
expect(findIcon().props('name')).toBe('chevron-down');
done();
});
});
it('has a collapse text', done => {
wrapper.vm.$nextTick(() => {
expect(findHeaderWrapper().text()).toBe('Collapse');
done();
});
});
});
});
import Vue from 'vue'; import Vue from 'vue';
import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue'; import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue';
import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue'; import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue';
import CommitsHeader from '~/vue_merge_request_widget/components/states/commits_header.vue';
import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit.vue';
import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue';
import eventHub from '~/vue_merge_request_widget/event_hub'; import eventHub from '~/vue_merge_request_widget/event_hub';
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
const commitMessage = 'This is the commit message'; const commitMessage = 'This is the commit message';
const squashCommitMessage = 'This is the squash commit message';
const commitMessageWithDescription = 'This is the commit message description'; const commitMessageWithDescription = 'This is the commit message description';
const createTestMr = customConfig => { const createTestMr = customConfig => {
const mr = { const mr = {
...@@ -19,9 +23,11 @@ const createTestMr = customConfig => { ...@@ -19,9 +23,11 @@ const createTestMr = customConfig => {
sha: '12345678', sha: '12345678',
squash: false, squash: false,
commitMessage, commitMessage,
squashCommitMessage,
commitMessageWithDescription, commitMessageWithDescription,
shouldRemoveSourceBranch: true, shouldRemoveSourceBranch: true,
canRemoveSourceBranch: false, canRemoveSourceBranch: false,
targetBranch: 'master',
}; };
Object.assign(mr, customConfig.mr); Object.assign(mr, customConfig.mr);
...@@ -98,21 +104,6 @@ describe('ReadyToMerge', () => { ...@@ -98,21 +104,6 @@ describe('ReadyToMerge', () => {
}); });
}); });
describe('commitMessageLinkTitle', () => {
const withDesc = 'Include description in commit message';
const withoutDesc = "Don't include description in commit message";
it('should return message with description', () => {
expect(vm.commitMessageLinkTitle).toEqual(withDesc);
});
it('should return message without description', () => {
vm.useCommitMessageWithDescription = true;
expect(vm.commitMessageLinkTitle).toEqual(withoutDesc);
});
});
describe('status', () => { describe('status', () => {
it('defaults to success', () => { it('defaults to success', () => {
vm.mr.pipeline = true; vm.mr.pipeline = true;
...@@ -279,55 +270,43 @@ describe('ReadyToMerge', () => { ...@@ -279,55 +270,43 @@ describe('ReadyToMerge', () => {
vm.mr.isMergeAllowed = false; vm.mr.isMergeAllowed = false;
vm.mr.isPipelineActive = false; vm.mr.isPipelineActive = false;
expect(vm.shouldShowMergeControls()).toBeFalsy(); expect(vm.shouldShowMergeControls).toBeFalsy();
}); });
it('should return true when the build succeeded or build not required to succeed', () => { it('should return true when the build succeeded or build not required to succeed', () => {
vm.mr.isMergeAllowed = true; vm.mr.isMergeAllowed = true;
vm.mr.isPipelineActive = false; vm.mr.isPipelineActive = false;
expect(vm.shouldShowMergeControls()).toBeTruthy(); expect(vm.shouldShowMergeControls).toBeTruthy();
}); });
it('should return true when showing the MWPS button and a pipeline is running that needs to be successful', () => { it('should return true when showing the MWPS button and a pipeline is running that needs to be successful', () => {
vm.mr.isMergeAllowed = false; vm.mr.isMergeAllowed = false;
vm.mr.isPipelineActive = true; vm.mr.isPipelineActive = true;
expect(vm.shouldShowMergeControls()).toBeTruthy(); expect(vm.shouldShowMergeControls).toBeTruthy();
}); });
it('should return true when showing the MWPS button but not required for the pipeline to succeed', () => { it('should return true when showing the MWPS button but not required for the pipeline to succeed', () => {
vm.mr.isMergeAllowed = true; vm.mr.isMergeAllowed = true;
vm.mr.isPipelineActive = true; vm.mr.isPipelineActive = true;
expect(vm.shouldShowMergeControls()).toBeTruthy(); expect(vm.shouldShowMergeControls).toBeTruthy();
}); });
}); });
describe('updateCommitMessage', () => { describe('updateMergeCommitMessage', () => {
it('should revert flag and change commitMessage', () => { it('should revert flag and change commitMessage', () => {
expect(vm.useCommitMessageWithDescription).toBeFalsy();
expect(vm.commitMessage).toEqual(commitMessage); expect(vm.commitMessage).toEqual(commitMessage);
vm.updateCommitMessage(); vm.updateMergeCommitMessage(true);
expect(vm.useCommitMessageWithDescription).toBeTruthy();
expect(vm.commitMessage).toEqual(commitMessageWithDescription); expect(vm.commitMessage).toEqual(commitMessageWithDescription);
vm.updateCommitMessage(); vm.updateMergeCommitMessage(false);
expect(vm.useCommitMessageWithDescription).toBeFalsy();
expect(vm.commitMessage).toEqual(commitMessage); expect(vm.commitMessage).toEqual(commitMessage);
}); });
}); });
describe('toggleCommitMessageEditor', () => {
it('should toggle showCommitMessageEditor flag', () => {
expect(vm.showCommitMessageEditor).toBeFalsy();
vm.toggleCommitMessageEditor();
expect(vm.showCommitMessageEditor).toBeTruthy();
});
});
describe('handleMergeButtonClick', () => { describe('handleMergeButtonClick', () => {
const returnPromise = status => const returnPromise = status =>
new Promise(resolve => { new Promise(resolve => {
...@@ -623,7 +602,7 @@ describe('ReadyToMerge', () => { ...@@ -623,7 +602,7 @@ describe('ReadyToMerge', () => {
}); });
}); });
describe('Squash checkbox component', () => { describe('render children components', () => {
let wrapper; let wrapper;
const localVue = createLocalVue(); const localVue = createLocalVue();
...@@ -642,25 +621,101 @@ describe('ReadyToMerge', () => { ...@@ -642,25 +621,101 @@ describe('ReadyToMerge', () => {
}); });
const findCheckboxElement = () => wrapper.find(SquashBeforeMerge); const findCheckboxElement = () => wrapper.find(SquashBeforeMerge);
const findCommitsHeaderElement = () => wrapper.find(CommitsHeader);
const findCommitEditElements = () => wrapper.findAll(CommitEdit);
const findCommitDropdownElement = () => wrapper.find(CommitMessageDropdown);
describe('squash checkbox', () => {
it('should be rendered when squash before merge is enabled and there is more than 1 commit', () => {
createLocalComponent({
mr: { commitsCount: 2, enableSquashBeforeMerge: true },
});
it('should be rendered when squash before merge is enabled and there is more than 1 commit', () => { expect(findCheckboxElement().exists()).toBeTruthy();
createLocalComponent({
mr: { commitsCount: 2, enableSquashBeforeMerge: true },
}); });
expect(findCheckboxElement().exists()).toBeTruthy(); it('should not be rendered when squash before merge is disabled', () => {
createLocalComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: false } });
expect(findCheckboxElement().exists()).toBeFalsy();
});
it('should not be rendered when there is only 1 commit', () => {
createLocalComponent({ mr: { commitsCount: 1, enableSquashBeforeMerge: true } });
expect(findCheckboxElement().exists()).toBeFalsy();
});
}); });
it('should not be rendered when squash before merge is disabled', () => { describe('commits count collapsible header', () => {
createLocalComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: false } }); it('should be rendered if fast-forward is disabled', () => {
createLocalComponent();
expect(findCheckboxElement().exists()).toBeFalsy(); expect(findCommitsHeaderElement().exists()).toBeTruthy();
});
it('should not be rendered if fast-forward is enabled', () => {
createLocalComponent({ mr: { ffOnlyEnabled: true } });
expect(findCommitsHeaderElement().exists()).toBeFalsy();
});
}); });
it('should not be rendered when there is only 1 commit', () => { describe('commits edit components', () => {
createLocalComponent({ mr: { commitsCount: 1, enableSquashBeforeMerge: true } }); it('should have one edit component when squash is disabled', () => {
createLocalComponent();
expect(findCommitEditElements().length).toBe(1);
});
expect(findCheckboxElement().exists()).toBeFalsy(); const findFirstCommitEditLabel = () =>
findCommitEditElements()
.at(0)
.props('label');
it('should have two edit components when squash is enabled', () => {
createLocalComponent({
mr: {
commitsCount: 2,
squash: true,
enableSquashBeforeMerge: true,
},
});
expect(findCommitEditElements().length).toBe(2);
});
it('should have correct edit merge commit label', () => {
createLocalComponent();
expect(findFirstCommitEditLabel()).toBe('Merge commit message');
});
it('should have correct edit squash commit label', () => {
createLocalComponent({
mr: {
commitsCount: 2,
squash: true,
enableSquashBeforeMerge: true,
},
});
expect(findFirstCommitEditLabel()).toBe('Squash commit message');
});
});
describe('commits dropdown', () => {
it('should not be rendered if squash is disabled', () => {
createLocalComponent();
expect(findCommitDropdownElement().exists()).toBeFalsy();
});
it('should be rendered if squash is enabled', () => {
createLocalComponent({ mr: { squash: true } });
expect(findCommitDropdownElement().exists()).toBeTruthy();
});
}); });
}); });
...@@ -696,10 +751,6 @@ describe('ReadyToMerge', () => { ...@@ -696,10 +751,6 @@ describe('ReadyToMerge', () => {
expect(vm.$el.querySelector('.js-remove-source-branch-checkbox')).toBeNull(); expect(vm.$el.querySelector('.js-remove-source-branch-checkbox')).toBeNull();
}); });
it('does not show modify commit message button', () => {
expect(vm.$el.querySelector('.js-modify-commit-message-button')).toBeNull();
});
it('shows message to resolve all items before being allowed to merge', () => { it('shows message to resolve all items before being allowed to merge', () => {
expect(vm.$el.querySelector('.js-resolve-mr-widget-items-message')).toBeDefined(); expect(vm.$el.querySelector('.js-resolve-mr-widget-items-message')).toBeDefined();
}); });
...@@ -712,7 +763,7 @@ describe('ReadyToMerge', () => { ...@@ -712,7 +763,7 @@ describe('ReadyToMerge', () => {
mr: { ffOnlyEnabled: false }, mr: { ffOnlyEnabled: false },
}); });
expect(customVm.$el.querySelector('.js-fast-forward-message')).toBeNull(); expect(customVm.$el.querySelector('.mr-fast-forward-message')).toBeNull();
expect(customVm.$el.querySelector('.js-modify-commit-message-button')).toBeDefined(); expect(customVm.$el.querySelector('.js-modify-commit-message-button')).toBeDefined();
}); });
...@@ -721,7 +772,7 @@ describe('ReadyToMerge', () => { ...@@ -721,7 +772,7 @@ describe('ReadyToMerge', () => {
mr: { ffOnlyEnabled: true }, mr: { ffOnlyEnabled: true },
}); });
expect(customVm.$el.querySelector('.js-fast-forward-message')).toBeDefined(); expect(customVm.$el.querySelector('.mr-fast-forward-message')).toBeDefined();
expect(customVm.$el.querySelector('.js-modify-commit-message-button')).toBeNull(); expect(customVm.$el.querySelector('.js-modify-commit-message-button')).toBeNull();
}); });
}); });
......
...@@ -215,6 +215,7 @@ export default { ...@@ -215,6 +215,7 @@ export default {
project_archived: false, project_archived: false,
default_merge_commit_message_with_description: default_merge_commit_message_with_description:
"Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22", "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
default_squash_commit_message: 'Test squash commit message',
diverged_commits_count: 0, diverged_commits_count: 0,
only_allow_merge_if_pipeline_succeeds: false, only_allow_merge_if_pipeline_succeeds: false,
commit_change_content_path: '/root/acets-app/merge_requests/22/commit_change_content', commit_change_content_path: '/root/acets-app/merge_requests/22/commit_change_content',
...@@ -231,6 +232,7 @@ export default { ...@@ -231,6 +232,7 @@ export default {
merge_commit_path: merge_commit_path:
'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775', 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775',
troubleshooting_docs_path: 'help', troubleshooting_docs_path: 'help',
squash: true,
}; };
// Codeclimate // Codeclimate
export const headIssues = [ export const headIssues = [
......
...@@ -141,11 +141,18 @@ describe PostReceive do ...@@ -141,11 +141,18 @@ describe PostReceive do
let(:gl_repository) { "wiki-#{project.id}" } let(:gl_repository) { "wiki-#{project.id}" }
it 'updates project activity' do it 'updates project activity' do
described_class.new.perform(gl_repository, key_id, base64_changes) # Force Project#set_timestamps_for_create to initialize timestamps
project
expect { project.reload } # MySQL drops milliseconds in the timestamps, so advance at least
.to change(project, :last_activity_at) # a second to ensure we see changes.
.and change(project, :last_repository_updated_at) Timecop.freeze(1.second.from_now) do
expect do
described_class.new.perform(gl_repository, key_id, base64_changes)
project.reload
end.to change(project, :last_activity_at)
.and change(project, :last_repository_updated_at)
end
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