@@ -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 \n Reviewed-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
860954if __name__ == "__main__" :
861955 try :
0 commit comments