Skip to content

Commit f57c6cc

Browse files
committed
Add batched merge (rollup) feature
Currently it works as follows: 1. Mark the commits to be merged together as rollup. (e.g. r+ rollup) These commits will have an implicit priority of -1 to postpone the individual merge. 2. If one of the marked commits reaches the top of the queue, all the marked commits will be merged together and tested. 3. While merging commits, those commits that fail to merge are ignored. 4. You can prioritize the rollup by setting p=. Typical usage: 1. We have three commits to be tested together. 2. Set r+ rollup to the first commit. 3. Set r+ rollup to the second commit. 4. Set r+ rollup p=10 to the last commit, which will trigger the rollup process soon. Fixes #34.
1 parent 5d20f5b commit f57c6cc

1 file changed

Lines changed: 105 additions & 11 deletions

File tree

bors.py

Lines changed: 105 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,8 @@ def __init__(self, cfg, gh, j):
243243
else:
244244
self.test_ref = '%s-integration-%s-%s' % (self.user, self.num, self.ref)
245245

246+
self.batch_ref = 'batch'
247+
246248
self.title=ustr(j["title"])
247249
self.body=ustr(j["body"])
248250
self.merge_sha = None
@@ -342,12 +344,21 @@ def approval_list(self):
342344

343345
def priority(self):
344346
p = 0
347+
348+
if self.batched(): p = -1
349+
345350
for (d, u, c) in self.head_comments:
346351
m = re.search(r"\bp=(-?\d+)\b", c)
347352
if m is not None:
348353
p = max(p, int(m.group(1)))
349354
return p
350355

356+
def batched(self):
357+
for date, user, comment in self.head_comments:
358+
if re.search(r'\brollup\b', comment):
359+
return True
360+
return False
361+
351362
def prioritized_state(self):
352363
return (self.current_state(),
353364
self.priority(),
@@ -456,7 +467,7 @@ def get_merge_sha(self):
456467
if s["creator"]["login"].encode("utf8") == self.user and s["state"].encode("utf8") == "pending"]
457468
if len(statusdescs) > 0:
458469
# parse it
459-
m = re.match(r"running tests for candidate ([a-z0-9]+)", statusdescs[0])
470+
m = re.match(r"running tests for.*?candidate ([a-z0-9]+)", statusdescs[0])
460471
if m:
461472
self.merge_sha = m.group(1)
462473

@@ -540,6 +551,84 @@ def merge_pull_head_to_test_ref(self):
540551
self.add_comment(self.sha, s)
541552
self.set_error(s)
542553

554+
def merge_batched_pull_reqs_to_test_ref(self, pulls, cfg):
555+
if cfg.get('max_pulls_per_run') != 1:
556+
msg = 'max_pulls_per_run must be 1 to merge batched pull requests'
557+
self.log.info(msg)
558+
self.add_comment(self.sha, msg)
559+
self.set_error(msg)
560+
return
561+
562+
batch_msg = 'batch merging {} pull requests into {}'.format(
563+
len([x for x in pulls if x.current_state() == STATE_APPROVED]),
564+
self.batch_ref,
565+
)
566+
self.log.info(batch_msg)
567+
self.add_comment(self.sha, batch_msg)
568+
569+
info = self.dst().git().refs().heads(self.target_ref).get()
570+
target_sha = info['object']['sha'].encode('utf-8')
571+
try:
572+
self.dst().git().refs().heads(self.batch_ref).get()
573+
self.dst().git().refs().heads(self.batch_ref).patch(sha=target_sha, force=True)
574+
except github.ApiError:
575+
self.dst().git().refs().post(sha=target_sha, ref='refs/heads/' + self.batch_ref)
576+
577+
successes = []
578+
failures = []
579+
580+
batch_sha = ''
581+
582+
for pull in pulls:
583+
if pull.current_state() == STATE_APPROVED:
584+
self.log.info('merging {} into {}'.format(pull.short(), self.batch_ref))
585+
586+
msg = 'Merge pull request #{} from {}/{}\n\n{}\n\nReviewed-by: {}'.format(
587+
pull.num,
588+
pull.src_owner, pull.ref,
589+
pull.title,
590+
', '.join(pull.approval_list())
591+
)
592+
pull_repr = '- {}/{} = {}: {}'.format(pull.src_owner, pull.ref, pull.sha, pull.title)
593+
594+
try:
595+
info = self.dst().merges().post(base=self.batch_ref, head=pull.sha, commit_message=msg)
596+
batch_sha = info['sha'].encode('utf-8')
597+
except github.ApiError:
598+
failures.append(pull_repr)
599+
else:
600+
successes.append(pull_repr)
601+
602+
if batch_sha:
603+
try:
604+
self.dst().git().refs().heads(self.test_ref).get()
605+
self.dst().git().refs().heads(self.test_ref).patch(sha=batch_sha)
606+
except github.ApiError as e:
607+
self.dst().git().refs().post(sha=batch_sha, ref='refs/heads/' + self.test_ref)
608+
609+
url = 'https://{}/{}/{}/commit/{}'.format(self.gh_host, self.dst_owner, self.dst_repo, batch_sha)
610+
short_msg = 'running tests for rollup candidate {} ({} successes, {} failures)'.format(batch_sha, len(successes), len(failures))
611+
msg = 'Testing rollup candidate = {:.8}'.format(batch_sha)
612+
if successes: msg += '\n\n**Successful merges:**\n\n{}'.format('\n'.join(successes))
613+
if failures: msg += '\n\n**Failed merges:**\n\n{}'.format('\n'.join(failures))
614+
615+
self.log.info(short_msg)
616+
self.add_comment(self.sha, msg)
617+
self.set_pending(short_msg, url)
618+
else:
619+
batch_msg += ' failed'
620+
621+
self.log.info(batch_msg)
622+
self.add_comment(self.sha, batch_msg)
623+
self.set_error(batch_msg)
624+
625+
def merge_or_batch(self, pulls, cfg):
626+
self.reset_test_ref_to_target()
627+
if self.batched():
628+
self.merge_batched_pull_reqs_to_test_ref(pulls, cfg)
629+
else:
630+
self.merge_pull_head_to_test_ref()
631+
543632
def advance_target_ref_to_test(self):
544633
assert self.merge_sha is not None
545634
s = ("fast-forwarding %s to %s = %.8s" %
@@ -556,6 +645,11 @@ def advance_target_ref_to_test(self):
556645
except github.ApiError:
557646
self.log.info("deleting integration branch %s failed" % self.test_ref)
558647

648+
try:
649+
self.dst().git().refs().heads(self.batch_ref).delete()
650+
except github.ApiError:
651+
self.log.info('deleting batch branch {} failed'.format(self.batch_ref))
652+
559653
self.maybe_delete_source_branch()
560654

561655
except github.ApiError:
@@ -573,6 +667,7 @@ def fresh(self):
573667
# parents of the merge sha are
574668
# the tip of the merge-target and the
575669
# feature branch
670+
assert self.merge_sha
576671
owner = self.cfg["owner"].encode("utf8")
577672
repo = self.cfg["repo"].encode("utf8")
578673
target_head = self.gh.repos(owner)(repo).git().refs().heads(self.target_ref).get()
@@ -583,7 +678,7 @@ def fresh(self):
583678
target_sha in test_parents and
584679
self.sha in test_parents)
585680

586-
def try_advance(self):
681+
def try_advance(self, pulls, cfg):
587682
s = self.current_state()
588683

589684
self.log.info("considering %s", self.desc())
@@ -601,8 +696,7 @@ def try_advance(self):
601696
self.src_repo,
602697
self.sha))))
603698

604-
self.reset_test_ref_to_target()
605-
self.merge_pull_head_to_test_ref()
699+
self.merge_or_batch(pulls, cfg)
606700

607701
elif s == STATE_PENDING:
608702
# Make sure the optional merge sha is loaded
@@ -612,13 +706,12 @@ def try_advance(self):
612706
test_sha = test_head["object"]["sha"].encode("utf8")
613707
self.merge_sha = test_sha
614708

615-
if not self.fresh():
709+
if not self.fresh() and not self.batched():
616710
c = ("Merge sha %.8s is stale."
617711
% (self.merge_sha,))
618712
self.log.info(c)
619713
self.add_comment(self.sha, c)
620-
self.reset_test_ref_to_target()
621-
self.merge_pull_head_to_test_ref()
714+
self.merge_or_batch(pulls, cfg)
622715
return
623716
self.log.info("%s - found pending state, checking tests", self.short())
624717
assert self.merge_sha is not None
@@ -676,16 +769,15 @@ def try_advance(self):
676769
self.log.info("%s - tests successful, waiting for merge approval",
677770
self.short())
678771
return
679-
if self.fresh():
772+
if self.fresh() or self.batched():
680773
self.log.info("%s - tests successful, attempting landing", self.short())
681774
self.advance_target_ref_to_test()
682775
else:
683776
c = ("Merge sha %.8s is stale."
684777
% (self.merge_sha,))
685778
self.log.info(c)
686779
self.add_comment(self.sha, c)
687-
self.reset_test_ref_to_target()
688-
self.merge_pull_head_to_test_ref()
780+
self.merge_or_batch(pulls, cfg)
689781

690782

691783

@@ -850,12 +942,14 @@ def main():
850942
pull.priority(),
851943
pull.desc())
852944

945+
all_pulls = pulls
946+
853947
max_pulls_per_run = cfg.get('max_pulls_per_run')
854948
if max_pulls_per_run:
855949
logging.info("Only considering %d pull-requests this run", max_pulls_per_run)
856950
pulls = pulls[-max_pulls_per_run:]
857951

858-
[p.try_advance() for p in reversed(pulls)]
952+
[p.try_advance(list(reversed(all_pulls)), cfg) for p in reversed(pulls)]
859953

860954
if __name__ == "__main__":
861955
try:

0 commit comments

Comments
 (0)