Commit 87d74366 authored by Shinya Maeda's avatar Shinya Maeda

Slash command support for merge train

This commit adds slash command support for merge train
parent 0a0077c3
......@@ -49,6 +49,14 @@ module AutoMerge
end
end
def available_for?(merge_request)
strong_memoize("available_for_#{merge_request.id}") do
merge_request.can_be_merged_by?(current_user) &&
merge_request.mergeable_state?(skip_ci_check: true) &&
yield
end
end
private
def strategy
......
......@@ -30,7 +30,9 @@ module AutoMerge
end
def available_for?(merge_request)
super do
merge_request.actual_head_pipeline&.active?
end
end
end
end
# frozen_string_literal: true
class AutoMergeService < BaseService
include Gitlab::Utils::StrongMemoize
STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS = 'merge_when_pipeline_succeeds'
STRATEGIES = [STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS].freeze
class << self
def all_strategies
def all_strategies_ordered_by_preference
STRATEGIES
end
def get_service_class(strategy)
return unless all_strategies.include?(strategy)
return unless all_strategies_ordered_by_preference.include?(strategy)
"::AutoMerge::#{strategy.camelize}Service".constantize
end
end
def execute(merge_request, strategy)
service = get_service_instance(strategy)
def execute(merge_request, strategy = nil)
strategy ||= preferred_strategy(merge_request)
service = get_service_instance(merge_request, strategy)
return :failed unless service&.available_for?(merge_request)
......@@ -27,38 +30,48 @@ class AutoMergeService < BaseService
def update(merge_request)
return :failed unless merge_request.auto_merge_enabled?
get_service_instance(merge_request.auto_merge_strategy).update(merge_request)
strategy = merge_request.auto_merge_strategy
get_service_instance(merge_request, strategy).update(merge_request)
end
def process(merge_request)
return unless merge_request.auto_merge_enabled?
get_service_instance(merge_request.auto_merge_strategy).process(merge_request)
strategy = merge_request.auto_merge_strategy
get_service_instance(merge_request, strategy).process(merge_request)
end
def cancel(merge_request)
return error("Can't cancel the automatic merge", 406) unless merge_request.auto_merge_enabled?
get_service_instance(merge_request.auto_merge_strategy).cancel(merge_request)
strategy = merge_request.auto_merge_strategy
get_service_instance(merge_request, strategy).cancel(merge_request)
end
def abort(merge_request, reason)
return error("Can't abort the automatic merge", 406) unless merge_request.auto_merge_enabled?
get_service_instance(merge_request.auto_merge_strategy).abort(merge_request, reason)
strategy = merge_request.auto_merge_strategy
get_service_instance(merge_request, strategy).abort(merge_request, reason)
end
def available_strategies(merge_request)
self.class.all_strategies.select do |strategy|
get_service_instance(strategy).available_for?(merge_request)
self.class.all_strategies_ordered_by_preference.select do |strategy|
get_service_instance(merge_request, strategy).available_for?(merge_request)
end
end
def preferred_strategy(merge_request)
available_strategies(merge_request).first
end
private
def get_service_instance(strategy)
def get_service_instance(merge_request, strategy)
strong_memoize("service_instance_#{merge_request.id}_#{strategy}") do
self.class.get_service_class(strategy)&.new(project, current_user, params)
end
end
end
AutoMergeService.prepend_if_ee('EE::AutoMergeService')
# frozen_string_literal: true
module MergeRequests
class MergeOrchestrationService < ::BaseService
def execute(merge_request)
return unless can_merge?(merge_request)
merge_request.update(merge_error: nil)
if can_merge_automatically?(merge_request)
auto_merge_service.execute(merge_request)
else
merge_request.merge_async(current_user.id, params)
end
end
def can_merge?(merge_request)
can_merge_automatically?(merge_request) || can_merge_immediately?(merge_request)
end
def preferred_auto_merge_strategy(merge_request)
auto_merge_service.preferred_strategy(merge_request)
end
private
def can_merge_immediately?(merge_request)
merge_request.can_be_merged_by?(current_user) &&
merge_request.mergeable_state?
end
def can_merge_automatically?(merge_request)
auto_merge_service.available_strategies(merge_request).any?
end
def auto_merge_service
@auto_merge_service ||= AutoMergeService.new(project, current_user, params)
end
end
end
......@@ -79,6 +79,12 @@ module MergeRequests
def merge_from_quick_action(merge_request)
last_diff_sha = params.delete(:merge)
if Feature.enabled?(:merge_orchestration_service, merge_request.project, default_enabled: true)
MergeRequests::MergeOrchestrationService
.new(project, current_user, { sha: last_diff_sha })
.execute(merge_request)
else
return unless merge_request.mergeable_with_quick_action?(current_user, last_diff_sha: last_diff_sha)
merge_request.update(merge_error: nil)
......@@ -89,6 +95,7 @@ module MergeRequests
merge_request.merge_async(current_user.id, { sha: last_diff_sha })
end
end
end
def reopen_service
MergeRequests::ReopenService
......
---
title: Add slash command support for merge train
merge_request: 28532
author:
type: added
......@@ -70,7 +70,7 @@ The following quick actions are applicable to descriptions, discussions and thre
| `/wip` | | ✓ | | Toggle the Work In Progress status |
| `/approve` | | ✓ | | Approve the merge request **(STARTER)** |
| `/submit_review` | | ✓ | | Submit a pending review. ([Introduced in GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/issues/8041)) **(PREMIUM)** |
| `/merge` | | ✓ | | Merge (when pipeline succeeds) |
| `/merge` | | ✓ | | Merge changes. Depending on the project setting, this may be [when the pipeline succeeds](merge_requests/merge_when_pipeline_succeeds.md), adding to a [Merge Train](../../ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md), etc). |
| `/child_epic <epic>` | | | ✓ | Add child epic to `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic. ([Introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab/issues/7330)) **(ULTIMATE)** |
| `/remove_child_epic <epic>` | | | ✓ | Remove child epic from `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic. ([Introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab/issues/7330)) **(ULTIMATE)** |
| `/parent_epic <epic>` | | | ✓ | Set parent epic to `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic. ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab/issues/10556)) **(ULTIMATE)** |
......
......@@ -31,10 +31,11 @@ module AutoMerge
end
def available_for?(merge_request)
super do
merge_request.project.merge_trains_enabled? &&
!merge_request.for_fork? &&
merge_request.actual_head_pipeline&.active? &&
merge_request.mergeable_state?(skip_ci_check: true)
merge_request.actual_head_pipeline&.active?
end
end
end
end
......@@ -45,12 +45,11 @@ module AutoMerge
end
def available_for?(merge_request)
return false unless merge_request.project.merge_trains_enabled?
return false if merge_request.for_fork?
return false unless merge_request.actual_head_pipeline&.complete?
return false unless merge_request.mergeable_state?(skip_ci_check: true)
true
super do
merge_request.project.merge_trains_enabled? &&
!merge_request.for_fork? &&
merge_request.actual_head_pipeline&.complete?
end
end
end
end
......@@ -12,10 +12,10 @@ module EE
extend ::Gitlab::Utils::Override
include ::Gitlab::Utils::StrongMemoize
override :all_strategies
def all_strategies
strong_memoize(:all_strategies) do
super + EE_STRATEGIES
override :all_strategies_ordered_by_preference
def all_strategies_ordered_by_preference
strong_memoize(:all_strategies_ordered_by_preference) do
EE_STRATEGIES + super
end
end
end
......
......@@ -41,6 +41,10 @@ describe AutoMerge::AddToMergeTrainWhenPipelineSucceedsService do
describe '#process' do
subject { service.process(merge_request) }
before do
service.execute(merge_request)
end
context 'when the latest pipeline in the merge request has succeeded' do
before do
pipeline.succeed!
......@@ -119,6 +123,12 @@ describe AutoMerge::AddToMergeTrainWhenPipelineSucceedsService do
it { is_expected.to eq(true) }
it 'memoizes the result' do
expect(merge_request).to receive(:can_be_merged_by?).once.and_call_original
2.times { is_expected.to be_truthy }
end
context 'when merge trains option is disabled' do
before do
expect(merge_request.project).to receive(:merge_trains_enabled?) { false }
......@@ -150,5 +160,13 @@ describe AutoMerge::AddToMergeTrainWhenPipelineSucceedsService do
it { is_expected.to eq(false) }
end
context 'when the user does not have permission to merge' do
before do
allow(merge_request).to receive(:can_be_merged_by?) { false }
end
it { is_expected.to be_falsy }
end
end
end
......@@ -262,6 +262,12 @@ describe AutoMerge::MergeTrainService do
it { is_expected.to be_truthy }
it 'memoizes the result' do
expect(merge_request).to receive(:can_be_merged_by?).once.and_call_original
2.times { is_expected.to be_truthy }
end
context 'when merge trains project option is disabled' do
before do
stub_feature_flags(disable_merge_trains: true)
......@@ -278,6 +284,14 @@ describe AutoMerge::MergeTrainService do
it { is_expected.to be_falsy }
end
context 'when the user does not have permission to merge' do
before do
allow(merge_request).to receive(:can_be_merged_by?) { false }
end
it { is_expected.to be_falsy }
end
context 'when merge request is submitted from a forked project' do
before do
allow(merge_request).to receive(:for_fork?) { true }
......
......@@ -3,12 +3,13 @@
require 'spec_helper'
describe AutoMergeService do
describe '.all_strategies' do
subject { described_class.all_strategies }
describe '.all_strategies_ordered_by_preference' do
subject { described_class.all_strategies_ordered_by_preference }
it 'includes all strategies' do
is_expected.to include(AutoMergeService::STRATEGY_MERGE_TRAIN,
AutoMergeService::STRATEGY_ADD_TO_MERGE_TRAIN_WHEN_PIPELINE_SUCCEEDS)
it 'returns all strategies in preference order' do
is_expected.to eq([AutoMergeService::STRATEGY_MERGE_TRAIN,
AutoMergeService::STRATEGY_ADD_TO_MERGE_TRAIN_WHEN_PIPELINE_SUCCEEDS,
AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS])
end
end
end
......@@ -8,15 +8,50 @@ module Gitlab
included do
# MergeRequest only quick actions definitions
desc _('Merge (when the pipeline succeeds)')
explanation _('Merges this merge request when the pipeline succeeds.')
execution_message _('Scheduled to merge this merge request when the pipeline succeeds.')
desc do
if Feature.enabled?(:merge_orchestration_service, quick_action_target.project, default_enabled: true)
if preferred_strategy = preferred_auto_merge_strategy(quick_action_target)
_("Merge automatically (%{strategy})") % { strategy: preferred_strategy.humanize }
else
_("Merge immediately")
end
else
_('Merge (when the pipeline succeeds)')
end
end
explanation do
if Feature.enabled?(:merge_orchestration_service, quick_action_target.project, default_enabled: true)
if preferred_strategy = preferred_auto_merge_strategy(quick_action_target)
_("Schedules to merge this merge request (%{strategy}).") % { strategy: preferred_strategy.humanize }
else
_('Merges this merge request immediately.')
end
else
_('Merges this merge request when the pipeline succeeds.')
end
end
execution_message do
if Feature.enabled?(:merge_orchestration_service, quick_action_target.project, default_enabled: true)
if preferred_strategy = preferred_auto_merge_strategy(quick_action_target)
_("Scheduled to merge this merge request (%{strategy}).") % { strategy: preferred_strategy.humanize }
else
_('Merged this merge request.')
end
else
_('Scheduled to merge this merge request when the pipeline succeeds.')
end
end
types MergeRequest
condition do
if Feature.enabled?(:merge_orchestration_service, quick_action_target.project, default_enabled: true)
quick_action_target.persisted? &&
merge_orchestration_service.can_merge?(quick_action_target)
else
last_diff_sha = params && params[:merge_request_diff_head_sha]
quick_action_target.persisted? &&
quick_action_target.mergeable_with_quick_action?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha)
end
end
command :merge do
@updates[:merge] = params[:merge_request_diff_head_sha]
end
......@@ -70,6 +105,14 @@ module Gitlab
@updates[:target_branch] = branch_name if project.repository.branch_exists?(branch_name)
end
end
def merge_orchestration_service
@merge_orchestration_service ||= MergeRequests::MergeOrchestrationService.new(project, current_user)
end
def preferred_auto_merge_strategy(merge_request)
merge_orchestration_service.preferred_auto_merge_strategy(merge_request)
end
end
end
end
......@@ -12704,6 +12704,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
msgid "Merge automatically (%{strategy})"
msgstr ""
msgid "Merge commit message"
msgstr ""
......@@ -12857,6 +12860,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
msgid "Merged this merge request."
msgstr ""
msgid "Merges this merge request immediately."
msgstr ""
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
......@@ -17681,12 +17690,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
msgid "Scheduled to merge this merge request (%{strategy})."
msgstr ""
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
msgid "Schedules to merge this merge request (%{strategy})."
msgstr ""
msgid "Scheduling"
msgstr ""
......
......@@ -94,6 +94,10 @@ describe MergeRequestPollWidgetEntity do
end
describe 'auto merge' do
before do
project.add_maintainer(user)
end
context 'when auto merge is enabled' do
let(:resource) { create(:merge_request, :merge_when_pipeline_succeeds) }
......
......@@ -3,8 +3,8 @@
require 'spec_helper'
describe AutoMerge::MergeWhenPipelineSucceedsService do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let(:mr_merge_if_green_enabled) do
create(:merge_request, merge_when_pipeline_succeeds: true, merge_user: user,
......@@ -20,6 +20,10 @@ describe AutoMerge::MergeWhenPipelineSucceedsService do
described_class.new(project, user, commit_message: 'Awesome message')
end
before_all do
project.add_maintainer(user)
end
describe "#available_for?" do
subject { service.available_for?(mr_merge_if_green_enabled) }
......@@ -34,11 +38,25 @@ describe AutoMerge::MergeWhenPipelineSucceedsService do
it { is_expected.to be_truthy }
it 'memoizes the result' do
expect(mr_merge_if_green_enabled).to receive(:can_be_merged_by?).once.and_call_original
2.times { is_expected.to be_truthy }
end
context 'when the head pipeline succeeded' do
let(:pipeline_status) { :success }
it { is_expected.to be_falsy }
end
context 'when the user does not have permission to merge' do
before do
allow(mr_merge_if_green_enabled).to receive(:can_be_merged_by?) { false }
end
it { is_expected.to be_falsy }
end
end
describe "#execute" do
......
......@@ -3,22 +3,36 @@
require 'spec_helper'
describe AutoMergeService do
let_it_be(:project) { create(:project) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let(:service) { described_class.new(project, user) }
describe '.all_strategies' do
subject { described_class.all_strategies }
before_all do
project.add_maintainer(user)
end
describe '.all_strategies_ordered_by_preference' do
subject { described_class.all_strategies_ordered_by_preference }
it 'includes merge when pipeline succeeds' do
is_expected.to include(AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
it 'returns all strategies in preference order' do
if Gitlab.ee?
is_expected.to eq(
[AutoMergeService::STRATEGY_MERGE_TRAIN,
AutoMergeService::STRATEGY_ADD_TO_MERGE_TRAIN_WHEN_PIPELINE_SUCCEEDS,
AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS])
else
is_expected.to eq([AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS])
end
end
end
describe '#available_strategies' do
subject { service.available_strategies(merge_request) }
let(:merge_request) { create(:merge_request) }
let(:merge_request) do
create(:merge_request, source_project: project)
end
let(:pipeline_status) { :running }
before do
......@@ -42,6 +56,36 @@ describe AutoMergeService do
end
end
describe '#preferred_strategy' do
subject { service.preferred_strategy(merge_request) }
let(:merge_request) do
create(:merge_request, source_project: project)
end
let(:pipeline_status) { :running }
before do
create(:ci_pipeline, pipeline_status, ref: merge_request.source_branch,
sha: merge_request.diff_head_sha,
project: merge_request.source_project)
merge_request.update_head_pipeline
end
it 'returns preferred strategy' do
is_expected.to eq('merge_when_pipeline_succeeds')
end
context 'when the head piipeline succeeded' do
let(:pipeline_status) { :success }
it 'returns available strategies' do
is_expected.to be_nil
end
end
end
describe '.get_service_class' do
subject { described_class.get_service_class(strategy) }
......@@ -63,7 +107,10 @@ describe AutoMergeService do
describe '#execute' do
subject { service.execute(merge_request, strategy) }
let(:merge_request) { create(:merge_request) }
let(:merge_request) do
create(:merge_request, source_project: project)
end
let(:pipeline_status) { :running }
let(:strategy) { AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS }
......@@ -90,6 +137,14 @@ describe AutoMergeService do
is_expected.to eq(:failed)
end
end
context 'when strategy is not specified' do
let(:strategy) { }
it 'chooses the most preferred strategy' do
is_expected.to eq(:merge_when_pipeline_succeeds)
end
end
end
describe '#update' do
......
# frozen_string_literal: true
require 'spec_helper'
describe MergeRequests::MergeOrchestrationService do
let_it_be(:maintainer) { create(:user) }
let(:merge_params) { { sha: merge_request.diff_head_sha } }
let(:user) { maintainer }
let(:service) { described_class.new(project, user, merge_params) }
let!(:merge_request) do
create(:merge_request, source_project: project, source_branch: 'feature',
target_project: project, target_branch: 'master')
end
shared_context 'fresh repository' do
let_it_be(:project) { create(:project, :repository) }
before_all do
project.add_maintainer(maintainer)
end
end
describe '#execute' do
subject { service.execute(merge_request) }
include_context 'fresh repository'
context 'when merge request is mergeable' do
context 'when merge request can be merged automatically' do
before do
create(:ci_pipeline, :detached_merge_request_pipeline, project: project, merge_request: merge_request)
merge_request.update_head_pipeline
end
it 'schedules auto merge' do
expect_next_instance_of(AutoMergeService, project, user, merge_params) do |service|
expect(service).to receive(:execute).with(merge_request).and_call_original
end
subject
expect(merge_request).to be_auto_merge_enabled
expect(merge_request.auto_merge_strategy).to eq(AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
expect(merge_request).not_to be_merged
end
end
context 'when merge request cannot be merged automatically' do
it 'merges immediately', :sidekiq_inline do
expect(merge_request)
.to receive(:merge_async).with(user.id, merge_params)
.and_call_original
subject
merge_request.reset
expect(merge_request).to be_merged
expect(merge_request).not_to be_auto_merge_enabled
end
end
end
context 'when merge request is not mergeable' do
before do
allow(merge_request).to receive(:mergeable_state?) { false }
end
it 'does nothing' do
subject
expect(merge_request).not_to be_auto_merge_enabled
expect(merge_request).not_to be_merged
end
end
end
describe '#can_merge?' do
subject { service.can_merge?(merge_request) }
include_context 'fresh repository'
context 'when merge request is mergeable' do
it { is_expected.to eq(true) }
end
context 'when merge request is not mergeable' do
before do
allow(merge_request).to receive(:mergeable_state?) { false }
end
it { is_expected.to eq(false) }
end
end
describe '#preferred_auto_merge_strategy' do
subject { service.preferred_auto_merge_strategy(merge_request) }
include_context 'fresh repository'
context 'when merge request can be merged automatically' do
before do
create(:ci_pipeline, :detached_merge_request_pipeline, project: project, merge_request: merge_request)
merge_request.update_head_pipeline
end
it 'fetches perferred auto merge strategy' do
is_expected.to eq(AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
end
end
context 'when merge request cannot be merged automatically' do
it { is_expected.to be_nil }
end
end
end
......@@ -208,7 +208,7 @@ describe MergeRequests::UpdateService, :mailer do
end
end
context 'merge' do
shared_examples_for 'correct merge behavior' do
let(:opts) do
{
merge: merge_request.diff_head_sha
......@@ -311,6 +311,18 @@ describe MergeRequests::UpdateService, :mailer do
end
end
describe 'merge' do
it_behaves_like 'correct merge behavior'
context 'when merge_orchestration_service feature flag is disabled' do
before do
stub_feature_flags(merge_orchestration_service: false)
end
it_behaves_like 'correct merge behavior'
end
end
context 'todos' do
let!(:pending_todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) }
......
......@@ -492,7 +492,7 @@ describe QuickActions::InterpretService do
end
end
shared_examples 'merge command' do
shared_examples 'merge immediately command' do
let(:project) { create(:project, :repository) }
it 'runs merge command if content contains /merge' do
......@@ -504,7 +504,18 @@ describe QuickActions::InterpretService do
it 'returns them merge message' do
_, _, message = service.execute(content, issuable)
expect(message).to eq('Scheduled to merge this merge request when the pipeline succeeds.')
expect(message).to eq('Merged this merge request.')
end
end
shared_examples 'merge automatically command' do
let(:project) { create(:project, :repository) }
it 'runs merge command if content contains /merge and returns merge message' do
_, updates, message = service.execute(content, issuable)
expect(updates).to eq(merge: merge_request.diff_head_sha)
expect(message).to eq('Scheduled to merge this merge request (Merge when pipeline succeeds).')
end
end
......@@ -675,11 +686,23 @@ describe QuickActions::InterpretService do
context 'merge command' do
let(:service) { described_class.new(project, developer, { merge_request_diff_head_sha: merge_request.diff_head_sha }) }
it_behaves_like 'merge command' do
it_behaves_like 'merge immediately command' do
let(:content) { '/merge' }
let(:issuable) { merge_request }
end
context 'when the head pipeline of merge request is running' do
before do
create(:ci_pipeline, :detached_merge_request_pipeline, merge_request: merge_request)
merge_request.update_head_pipeline
end
it_behaves_like 'merge automatically command' do
let(:content) { '/merge' }
let(:issuable) { merge_request }
end
end
context 'can not be merged when logged user does not have permissions' do
let(:service) { described_class.new(project, create(:user)) }
......
......@@ -10,10 +10,27 @@ RSpec.shared_examples 'merge quick action' do
it 'merges the MR', :sidekiq_might_not_need_inline do
add_note("/merge")
expect(page).to have_content 'Scheduled to merge this merge request when the pipeline succeeds.'
expect(page).to have_content 'Merged this merge request.'
expect(merge_request.reload).to be_merged
end
context 'when auto merge is avialable' do
before do
create(:ci_pipeline, :detached_merge_request_pipeline,
project: project, merge_request: merge_request)
merge_request.update_head_pipeline
end
it 'schedules to merge the MR' do
add_note("/merge")
expect(page).to have_content "Scheduled to merge this merge request (Merge when pipeline succeeds)."
expect(merge_request.reload).to be_auto_merge_enabled
expect(merge_request.reload).not_to be_merged
end
end
end
context 'when the head diff changes in the meanwhile' do
......
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