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>
......@@ -315,7 +315,7 @@ export default {
:endpoint="mr.testResultsPath"
/>
<div class="mr-widget-section">
<div class="mr-widget-section p-0">
<component :is="componentName" :mr="mr" :service="service" />
<section v-if="shouldRenderCollaborationStatus" class="mr-info-list mr-links">
......
......@@ -42,6 +42,8 @@ export default class MergeRequestStore {
this.mergePipeline = data.merge_pipeline || {};
this.deployments = this.deployments || data.deployments || [];
this.postMergeDeployments = this.postMergeDeployments || [];
this.commits = data.commits_without_merge_commits || [];
this.squashCommitMessage = data.default_squash_commit_message;
this.initRebase(data);
if (data.issues_links) {
......
......@@ -395,6 +395,11 @@ img.emoji {
.flex-no-shrink { flex-shrink: 0; }
.ws-initial { white-space: initial; }
.overflow-auto { overflow: auto; }
.d-flex-center {
display: flex;
align-items: center;
justify-content: center;
}
/** COMMON SIZING CLASSES **/
.w-0 { width: 0; }
......@@ -406,6 +411,10 @@ img.emoji {
.min-height-0 { min-height: 0; }
.w-3 { width: #{3 * $grid-size}; }
.h-3 { width: #{3 * $grid-size}; }
/** COMMON SPACING CLASSES **/
.gl-pl-0 { padding-left: 0; }
.gl-pl-1 { padding-left: #{0.5 * $grid-size}; }
......
......@@ -244,6 +244,7 @@ $gl-padding-8: 8px;
$gl-padding: 16px;
$gl-padding-24: 24px;
$gl-padding-32: 32px;
$gl-padding-50: 50px;
$gl-col-padding: 15px;
$gl-input-padding: 10px;
$gl-vert-padding: 6px;
......
......@@ -38,9 +38,7 @@
}
.mr-widget-section {
.media {
align-items: center;
}
border-radius: $border-radius-default $border-radius-default 0 0;
.code-text {
flex: 1;
......@@ -56,6 +54,11 @@
.mr-widget-extension {
border-top: 1px solid $border-color;
background-color: $gray-light;
&.clickable:hover {
background-color: $gl-gray-200;
cursor: pointer;
}
}
.mr-widget-workflow {
......@@ -78,6 +81,7 @@
border-top: 0;
}
.mr-widget-body,
.mr-widget-section,
.mr-widget-content,
.mr-widget-footer {
......@@ -87,11 +91,38 @@
.mr-state-widget {
color: $gl-text-color;
.commit-message-edit {
border-radius: $border-radius-default;
}
.mr-widget-section,
.mr-widget-footer {
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 {
padding: 0;
}
......@@ -405,7 +436,7 @@
}
.mr-widget-help {
padding: 10px 16px 10px 48px;
padding: 10px 16px 10px $gl-padding-50;
font-style: italic;
}
......@@ -423,10 +454,6 @@
}
}
.mr-widget-body-controls {
flex-wrap: wrap;
}
.mr_source_commit,
.mr_target_commit {
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:
- Taken from the first multi-line commit message in the merge.
- 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,
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.
It can be customized before merging a merge request.
![A squash commit message editor](img/squash_mr_message.png)
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
......@@ -60,7 +63,7 @@ This can then be overridden at the time of accepting the merge request:
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.
- Committer: the user who initiated the squash.
......
......@@ -223,11 +223,19 @@ class Geo::ProjectRegistry < Geo::BaseRegistry
end
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
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
# Returns whether repository is pending verification check
......@@ -365,28 +373,6 @@ class Geo::ProjectRegistry < Geo::BaseRegistry
"projects/#{project_id}/fetches_since_gc"
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?
#
# @param [String] type must be one of the values in TYPES
......
......@@ -158,7 +158,11 @@ module Geo
def reschedule_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
def fail_registry!(message, error, attrs = {})
......@@ -170,7 +174,7 @@ module Geo
end
def type
self.class.type
@type ||= self.class.type.to_s.inquiry
end
def update_delay_in_seconds
......
......@@ -15,7 +15,7 @@ module Geo
end
# 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)
project = registry.project
......@@ -30,9 +30,37 @@ module Geo
return
end
Geo::RepositorySyncService.new(project).execute if registry.repository_sync_due?(scheduled_time)
Geo::WikiSyncService.new(project).execute if registry.wiki_sync_due?(scheduled_time)
options = extract_options(registry, options)
sync_repository(registry, options)
sync_wiki(registry, options)
end
# 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
......@@ -42,11 +42,19 @@ module Geo
[1, capacity_per_shard.to_i].max
end
# rubocop: disable CodeReuse/ActiveRecord
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
end
# rubocop: enable CodeReuse/ActiveRecord
def scheduled_project_ids
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
registry.repository_created!(event)
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
......
......@@ -11,7 +11,11 @@ module Gitlab
registry.repository_updated!(event.source, scheduled_at)
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
log_event(job_id)
......
......@@ -21,7 +21,7 @@ describe Gitlab::Geo::LogCursor::Events::RepositoryUpdatedEvent, :postgresql, :c
allow(Gitlab::ShardHealthCache).to receive(:healthy_shard?).with('broken').and_return(false)
end
RSpec.shared_examples 'RepositoryUpdatedEvent' do
shared_examples 'RepositoryUpdatedEvent' do
it 'creates a new project registry if it does not exist' do
expect { subject.process }.to change(Geo::ProjectRegistry, :count).by(1)
end
......@@ -108,9 +108,37 @@ describe Gitlab::Geo::LogCursor::Events::RepositoryUpdatedEvent, :postgresql, :c
it_behaves_like 'RepositoryUpdatedEvent'
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
......@@ -120,9 +148,9 @@ describe Gitlab::Geo::LogCursor::Events::RepositoryUpdatedEvent, :postgresql, :c
it_behaves_like 'RepositoryUpdatedEvent'
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
......
......@@ -21,11 +21,20 @@ RSpec.describe Geo::ProjectSyncWorker do
.with(instance_of(Project)).once.and_return(wiki_sync_service)
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
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.perform(999, Time.now) }.not_to raise_error
expect { subject.perform(999) }.not_to raise_error
end
end
......@@ -35,21 +44,23 @@ RSpec.describe Geo::ProjectSyncWorker do
expect(repository_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
context 'when project repositories has never been synced' 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(wiki_sync_service).not_to have_received(:execute)
end
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(repository_sync_service).not_to have_received(:execute)
end
end
......@@ -57,13 +68,13 @@ RSpec.describe Geo::ProjectSyncWorker do
let!(:registry) { create(:geo_project_registry, :synced, project: project) }
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)
end
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)
end
......@@ -73,72 +84,16 @@ RSpec.describe Geo::ProjectSyncWorker do
let!(:registry) { create(:geo_project_registry, :sync_failed, project: project) }
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
end
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
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
......@@ -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
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
.and_return(spy)
......@@ -134,7 +134,7 @@ describe Geo::RepositoryShardSyncWorker, :geo, :delete, :clean_gitlab_redis_cach
create(:geo_project_registry, :synced, :repository_dirty, project: unsynced_project)
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
.and_return(spy)
......@@ -182,6 +182,32 @@ describe Geo::RepositoryShardSyncWorker, :geo, :delete, :clean_gitlab_redis_cach
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
let!(:project_list) { create_list(:project, 4, :random_last_repository_updated_at) }
......
......@@ -54,6 +54,9 @@ msgid_plural "%d commits behind"
msgstr[0] ""
msgstr[1] ""
msgid "%d commits"
msgstr ""
msgid "%d exporter"
msgid_plural "%d exporters"
msgstr[0] ""
......@@ -3021,6 +3024,9 @@ msgstr ""
msgid "Delete list"
msgstr ""
msgid "Delete source branch"
msgstr ""
msgid "Delete this attachment"
msgstr ""
......@@ -3881,6 +3887,9 @@ msgstr ""
msgid "Failure"
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)"
msgstr ""
......@@ -5106,6 +5115,9 @@ msgstr ""
msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
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>."
msgstr ""
......@@ -5826,9 +5838,18 @@ msgstr ""
msgid "Merge Requests created"
msgstr ""
msgid "Merge commit message"
msgstr ""
msgid "Merge events"
msgstr ""
msgid "Merge immediately"
msgstr ""
msgid "Merge in progress"
msgstr ""
msgid "Merge request"
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"
msgstr ""
msgid "Merge when pipeline succeeds"
msgstr ""
msgid "MergeRequests|Add a reply"
msgstr ""
......@@ -6108,6 +6132,12 @@ msgstr ""
msgid "Modal|Close"
msgstr ""
msgid "Modify commit messages"
msgstr ""
msgid "Modify merge commit"
msgstr ""
msgid "Monday"
msgstr ""
......@@ -8671,6 +8701,9 @@ msgstr ""
msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while deleting the source branch. Please try again."
msgstr ""
msgid "Something went wrong while fetching %{listType} list"
msgstr ""
......@@ -8689,6 +8722,9 @@ msgstr ""
msgid "Something went wrong while fetching the registry list."
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"
msgstr ""
......@@ -8863,6 +8899,9 @@ msgstr ""
msgid "Specify the following URL during the Runner setup:"
msgstr ""
msgid "Squash commit message"
msgstr ""
msgid "Squash commits"
msgstr ""
......@@ -10745,6 +10784,9 @@ msgstr ""
msgid "You can only edit files when you are on a branch"
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}"
msgstr ""
......@@ -11327,6 +11369,12 @@ msgstr[1] ""
msgid "missing"
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"
msgstr ""
......
......@@ -80,8 +80,8 @@ describe 'User accepts a merge request', :js do
end
it 'accepts a merge request' do
click_button('Modify commit message')
fill_in('Commit message', with: 'wow such merge')
find('.js-mr-widget-commits-count').click
fill_in('merge-message-edit', with: 'wow such merge')
click_button('Merge')
......
......@@ -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}"
)
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
[
"Merge branch 'feature' into 'master'",
......@@ -38,16 +38,16 @@ describe 'Merge request < User customizes merge commit message', :js do
end
it 'toggles commit message between message with description and without description' do
expect(page).not_to have_selector('.js-commit-message')
click_button "Modify commit message"
expect(page).not_to have_selector('#merge-message-edit')
first('.js-mr-widget-commits-count').click
expect(textbox).to be_visible
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)
click_link "Don't include description in commit message"
uncheck('Include merge request description')
expect(textbox.value).to eq(default_message)
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 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 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 { createLocalVue, shallowMount } from '@vue/test-utils';
const commitMessage = 'This is the commit message';
const squashCommitMessage = 'This is the squash commit message';
const commitMessageWithDescription = 'This is the commit message description';
const createTestMr = customConfig => {
const mr = {
......@@ -19,9 +23,11 @@ const createTestMr = customConfig => {
sha: '12345678',
squash: false,
commitMessage,
squashCommitMessage,
commitMessageWithDescription,
shouldRemoveSourceBranch: true,
canRemoveSourceBranch: false,
targetBranch: 'master',
};
Object.assign(mr, customConfig.mr);
......@@ -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', () => {
it('defaults to success', () => {
vm.mr.pipeline = true;
......@@ -279,55 +270,43 @@ describe('ReadyToMerge', () => {
vm.mr.isMergeAllowed = 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', () => {
vm.mr.isMergeAllowed = true;
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', () => {
vm.mr.isMergeAllowed = false;
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', () => {
vm.mr.isMergeAllowed = true;
vm.mr.isPipelineActive = true;
expect(vm.shouldShowMergeControls()).toBeTruthy();
expect(vm.shouldShowMergeControls).toBeTruthy();
});
});
describe('updateCommitMessage', () => {
describe('updateMergeCommitMessage', () => {
it('should revert flag and change commitMessage', () => {
expect(vm.useCommitMessageWithDescription).toBeFalsy();
expect(vm.commitMessage).toEqual(commitMessage);
vm.updateCommitMessage();
vm.updateMergeCommitMessage(true);
expect(vm.useCommitMessageWithDescription).toBeTruthy();
expect(vm.commitMessage).toEqual(commitMessageWithDescription);
vm.updateCommitMessage();
vm.updateMergeCommitMessage(false);
expect(vm.useCommitMessageWithDescription).toBeFalsy();
expect(vm.commitMessage).toEqual(commitMessage);
});
});
describe('toggleCommitMessageEditor', () => {
it('should toggle showCommitMessageEditor flag', () => {
expect(vm.showCommitMessageEditor).toBeFalsy();
vm.toggleCommitMessageEditor();
expect(vm.showCommitMessageEditor).toBeTruthy();
});
});
describe('handleMergeButtonClick', () => {
const returnPromise = status =>
new Promise(resolve => {
......@@ -623,7 +602,7 @@ describe('ReadyToMerge', () => {
});
});
describe('Squash checkbox component', () => {
describe('render children components', () => {
let wrapper;
const localVue = createLocalVue();
......@@ -642,25 +621,101 @@ describe('ReadyToMerge', () => {
});
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', () => {
createLocalComponent({
mr: { commitsCount: 2, enableSquashBeforeMerge: true },
expect(findCheckboxElement().exists()).toBeTruthy();
});
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', () => {
createLocalComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: false } });
describe('commits count collapsible header', () => {
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', () => {
createLocalComponent({ mr: { commitsCount: 1, enableSquashBeforeMerge: true } });
describe('commits edit components', () => {
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', () => {
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', () => {
expect(vm.$el.querySelector('.js-resolve-mr-widget-items-message')).toBeDefined();
});
......@@ -712,7 +763,7 @@ describe('ReadyToMerge', () => {
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();
});
......@@ -721,7 +772,7 @@ describe('ReadyToMerge', () => {
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();
});
});
......
......@@ -215,6 +215,7 @@ export default {
project_archived: false,
default_merge_commit_message_with_description:
"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,
only_allow_merge_if_pipeline_succeeds: false,
commit_change_content_path: '/root/acets-app/merge_requests/22/commit_change_content',
......@@ -231,6 +232,7 @@ export default {
merge_commit_path:
'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775',
troubleshooting_docs_path: 'help',
squash: true,
};
// Codeclimate
export const headIssues = [
......
......@@ -141,11 +141,18 @@ describe PostReceive do
let(:gl_repository) { "wiki-#{project.id}" }
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 }
.to change(project, :last_activity_at)
.and change(project, :last_repository_updated_at)
# MySQL drops milliseconds in the timestamps, so advance at least
# a second to ensure we see changes.
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
......
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