Skip to content

Commit 670597e

Browse files
author
David Szervanszky
committed
Optimistic batching for batch merging
Fix for issue #164. Previously we added trailers to each MR and merged the constituent MRs of a batch MR to the target branch. Now we will now optimistically add the trailers to each MR and rebase on the batch branch. And when the batch branch passes CI we merge it which will automatically merge each constituent branch.
1 parent 5de1f8c commit 670597e

File tree

6 files changed

+44
-67
lines changed

6 files changed

+44
-67
lines changed

marge/batch_job.py

Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ def accept_mr(
112112
self,
113113
merge_request,
114114
expected_remote_target_branch_sha,
115-
source_repo_url=None,
116115
):
117116
log.info('Fusing MR !%s', merge_request.iid)
118117
approvals = merge_request.fetch_approvals()
@@ -122,21 +121,6 @@ def accept_mr(
122121
if new_target_sha != expected_remote_target_branch_sha:
123122
raise CannotBatch('Someone was naughty and by-passed marge')
124123

125-
# FIXME: we should only add tested-by for the last MR in the batch
126-
_, _, actual_sha = self.update_from_target_branch_and_push(
127-
merge_request,
128-
source_repo_url=source_repo_url,
129-
)
130-
131-
sha_now = Commit.last_on_branch(
132-
merge_request.source_project_id, merge_request.source_branch, self._api,
133-
).id
134-
# Make sure no-one managed to race and push to the branch in the
135-
# meantime, because we're about to impersonate the approvers, and
136-
# we don't want to approve unreviewed commits
137-
if sha_now != actual_sha:
138-
raise CannotMerge('Someone pushed to branch while we were trying to merge')
139-
140124
# As we're not using the API to merge the MR, we don't strictly need to reapprove it. However,
141125
# it's a little weird to look at the merged MR to find it has no approvals, so let's do it anyway.
142126
self.maybe_reapprove(merge_request, approvals)
@@ -198,6 +182,9 @@ def execute(self):
198182
merge_request.source_branch,
199183
'%s/%s' % (merge_request_remote, merge_request.source_branch),
200184
)
185+
# Apply the trailers before running the batch MR
186+
self.add_trailers(merge_request)
187+
self.push_force_to_mr(merge_request, True, source_repo_url, skip_ci=True)
201188

202189
# Update <source_branch> on latest <batch> branch so it contains previous MRs
203190
self.fuse(
@@ -207,6 +194,9 @@ def execute(self):
207194
local=True,
208195
)
209196

197+
# Ensure that individual branch commit SHA matches matches that of its equivalent in batch MR
198+
self.push_force_to_mr(merge_request, True, source_repo_url, skip_ci=True)
199+
210200
# Update <batch> branch with MR changes
211201
self._repo.fast_forward(
212202
BatchMergeJob.BATCH_BRANCH_NAME,
@@ -225,36 +215,34 @@ def execute(self):
225215
working_merge_requests.append(merge_request)
226216
if len(working_merge_requests) <= 1:
227217
raise CannotBatch('not enough ready merge requests')
228-
if self._project.only_allow_merge_if_pipeline_succeeds:
229-
# This switches git to <batch> branch
230-
self.push_batch()
231-
batch_mr = self.create_batch_mr(
232-
target_branch=target_branch,
233-
)
218+
# This switches git to <batch> branch
219+
self.push_batch()
220+
batch_mr = self.create_batch_mr(
221+
target_branch=target_branch,
222+
)
223+
for merge_request in working_merge_requests:
224+
merge_request.comment('I will attempt to batch this MR (!{})...'.format(batch_mr.iid))
225+
try:
226+
self.wait_for_ci_to_pass(batch_mr)
227+
except CannotMerge as err:
234228
for merge_request in working_merge_requests:
235-
merge_request.comment('I will attempt to batch this MR (!{})...'.format(batch_mr.iid))
236-
try:
237-
self.wait_for_ci_to_pass(batch_mr)
238-
except CannotMerge as err:
239-
for merge_request in working_merge_requests:
240-
merge_request.comment(
241-
'Batch MR !{batch_mr_iid} failed: {error} I will retry later...'.format(
242-
batch_mr_iid=batch_mr.iid,
243-
error=err.reason,
244-
),
245-
)
246-
raise CannotBatch(err.reason) from err
229+
merge_request.comment(
230+
'Batch MR !{batch_mr_iid} failed: {error} I will retry later...'.format(
231+
batch_mr_iid=batch_mr.iid,
232+
error=err.reason,
233+
),
234+
)
235+
raise CannotBatch(err.reason) from err
247236
for merge_request in working_merge_requests:
248237
try:
249238
# FIXME: this should probably be part of the merge request
250239
_, source_repo_url, merge_request_remote = self.fetch_source_project(merge_request)
251-
self.ensure_mr_not_changed(merge_request)
252240
self.ensure_mergeable_mr(merge_request)
253-
remote_target_branch_sha = self.accept_mr(
254-
merge_request,
255-
remote_target_branch_sha,
256-
source_repo_url=source_repo_url,
257-
)
241+
if merge_request == working_merge_requests[-1]:
242+
self.accept_mr(
243+
batch_mr,
244+
remote_target_branch_sha,
245+
)
258246
except CannotBatch as err:
259247
merge_request.comment(
260248
"I couldn't merge this branch: {error} I will retry later...".format(

marge/git.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def remove_branch(self, branch, *, new_current_branch='master'):
131131
def checkout_branch(self, branch, start_point=''):
132132
self.git('checkout', '-B', branch, start_point, '--')
133133

134-
def push(self, branch, *, source_repo_url=None, force=False):
134+
def push(self, branch, *, source_repo_url=None, force=False, skip_ci=False):
135135
self.git('checkout', branch, '--')
136136

137137
self.git('diff-index', '--quiet', 'HEAD') # check it is not dirty
@@ -146,7 +146,14 @@ def push(self, branch, *, source_repo_url=None, force=False):
146146
else:
147147
source = 'origin'
148148
force_flag = '--force' if force else ''
149-
self.git('push', force_flag, source, '%s:%s' % (branch, branch))
149+
# The following needs to be split into 2 variables as any whitespace in the string
150+
# causes it to be quoted which makes the string ignored by git
151+
if skip_ci:
152+
skip_1 = '-o'
153+
skip_2 = 'ci.skip'
154+
else:
155+
skip_1, skip_2 = '', ''
156+
self.git('push', force_flag, skip_1, skip_2, source, '%s:%s' % (branch, branch))
150157

151158
def get_commit_hash(self, rev='HEAD'):
152159
"""Return commit hash for `rev` (default "HEAD")."""

marge/job.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,12 +336,14 @@ def push_force_to_mr(
336336
merge_request,
337337
branch_was_modified,
338338
source_repo_url=None,
339+
skip_ci=False,
339340
):
340341
try:
341342
self._repo.push(
342343
merge_request.source_branch,
343344
source_repo_url=source_repo_url,
344345
force=True,
346+
skip_ci=skip_ci
345347
)
346348
except git.GitError:
347349
def fetch_remote_branch():

pylintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ max-line-length=110
3030
[DESIGN]
3131
max-args=10
3232
max-attributes=15
33+
max-locals=16
3334
max-public-methods=35
3435

3536
[REPORTS]

tests/git_repo_mock.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,11 @@ def merge(self, arg):
178178
self._local_repo.set_ref(self._branch, new_sha)
179179

180180
def push(self, *args):
181-
force_flag, remote_name, refspec = args
181+
force_flag, skip_1, skip_2, remote_name, refspec = args
182182

183183
assert force_flag in ('', '--force')
184+
assert skip_1 in ('', '-o')
185+
assert skip_2 in ('', 'ci.skip')
184186

185187
branch, remote_branch = refspec.split(':')
186188
remote_url = self._remotes[remote_name]

tests/test_batch_job.py

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@
77
import marge.project
88
import marge.user
99
from marge.batch_job import BatchMergeJob, CannotBatch
10-
from marge.gitlab import GET
1110
from marge.job import CannotMerge, MergeJobOptions
1211
from marge.merge_request import MergeRequest
13-
from tests.gitlab_api_mock import MockLab, Ok, commit
12+
from tests.gitlab_api_mock import MockLab
1413

1514

1615
class TestBatchJob:
@@ -160,25 +159,3 @@ def test_fuse_mr_when_target_branch_was_moved(self, api, mocklab):
160159
with pytest.raises(CannotBatch) as exc_info:
161160
batch_merge_job.accept_mr(merge_request, 'abc')
162161
assert str(exc_info.value) == 'Someone was naughty and by-passed marge'
163-
164-
def test_fuse_mr_when_source_branch_was_moved(self, api, mocklab):
165-
batch_merge_job = self.get_batch_merge_job(api, mocklab)
166-
merge_request = self._mock_merge_request(
167-
source_project_id=mocklab.merge_request_info['source_project_id'],
168-
target_branch='master',
169-
source_branch=mocklab.merge_request_info['source_branch'],
170-
)
171-
172-
api.add_transition(
173-
GET(
174-
'/projects/{project_iid}/repository/branches/useless_new_feature'.format(
175-
project_iid=mocklab.merge_request_info['source_project_id'],
176-
),
177-
),
178-
Ok({'commit': commit(commit_id='abc', status='running')}),
179-
)
180-
181-
with pytest.raises(CannotMerge) as exc_info:
182-
batch_merge_job.accept_mr(merge_request, mocklab.initial_master_sha)
183-
184-
assert str(exc_info.value) == 'Someone pushed to branch while we were trying to merge'

0 commit comments

Comments
 (0)