Skip to content

Commit a1d4f5e

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 a1d4f5e

1 file changed

Lines changed: 95 additions & 10 deletions

File tree

bors.py

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

640+
try:
641+
self.dst().git().refs().heads(self.batch_ref).delete()
642+
except github.ApiError:
643+
self.log.info('deleting batch branch {} failed'.format(self.batch_ref))
644+
559645
self.maybe_delete_source_branch()
560646

561647
except github.ApiError:
@@ -583,7 +669,7 @@ def fresh(self):
583669
target_sha in test_parents and
584670
self.sha in test_parents)
585671

586-
def try_advance(self):
672+
def try_advance(self, pulls, cfg):
587673
s = self.current_state()
588674

589675
self.log.info("considering %s", self.desc())
@@ -601,8 +687,7 @@ def try_advance(self):
601687
self.src_repo,
602688
self.sha))))
603689

604-
self.reset_test_ref_to_target()
605-
self.merge_pull_head_to_test_ref()
690+
self.merge_or_batch(pulls, cfg)
606691

607692
elif s == STATE_PENDING:
608693
# Make sure the optional merge sha is loaded
@@ -612,13 +697,12 @@ def try_advance(self):
612697
test_sha = test_head["object"]["sha"].encode("utf8")
613698
self.merge_sha = test_sha
614699

615-
if not self.fresh():
700+
if not self.fresh() and not self.batched():
616701
c = ("Merge sha %.8s is stale."
617702
% (self.merge_sha,))
618703
self.log.info(c)
619704
self.add_comment(self.sha, c)
620-
self.reset_test_ref_to_target()
621-
self.merge_pull_head_to_test_ref()
705+
self.merge_or_batch(pulls, cfg)
622706
return
623707
self.log.info("%s - found pending state, checking tests", self.short())
624708
assert self.merge_sha is not None
@@ -676,16 +760,15 @@ def try_advance(self):
676760
self.log.info("%s - tests successful, waiting for merge approval",
677761
self.short())
678762
return
679-
if self.fresh():
763+
if self.fresh() or self.batched():
680764
self.log.info("%s - tests successful, attempting landing", self.short())
681765
self.advance_target_ref_to_test()
682766
else:
683767
c = ("Merge sha %.8s is stale."
684768
% (self.merge_sha,))
685769
self.log.info(c)
686770
self.add_comment(self.sha, c)
687-
self.reset_test_ref_to_target()
688-
self.merge_pull_head_to_test_ref()
771+
self.merge_or_batch(pulls, cfg)
689772

690773

691774

@@ -850,12 +933,14 @@ def main():
850933
pull.priority(),
851934
pull.desc())
852935

936+
all_pulls = pulls
937+
853938
max_pulls_per_run = cfg.get('max_pulls_per_run')
854939
if max_pulls_per_run:
855940
logging.info("Only considering %d pull-requests this run", max_pulls_per_run)
856941
pulls = pulls[-max_pulls_per_run:]
857942

858-
[p.try_advance() for p in reversed(pulls)]
943+
[p.try_advance(reversed(all_pulls), cfg) for p in reversed(pulls)]
859944

860945
if __name__ == "__main__":
861946
try:

0 commit comments

Comments
 (0)