77from typing import Dict , List
88from pathlib import Path
99import argparse
10+ import functools
1011import json
1112import os
1213import re
4748
4849ARGS = None # global arguments, see parse_args()
4950
50- def stdout (* msg ):
51- # Print a diagnostic message to standard error.
52-
53- print (f'{ PROG } :' , * msg )
51+ def stdout (msg ):
52+ print (f'{ PROG } : { msg } ' , file = sys .stdout )
5453 sys .stdout .flush ()
5554
55+ die_switch = None
56+
57+ def die (s ):
58+ print (f'ERROR: { s } ' , file = sys .stderr )
59+ if die_switch :
60+ # Switch back
61+ stdout ('die: switch' )
62+ try_switch_back (die_switch )
63+ sys .exit (1 )
64+
5665def gh_pr_split (s ):
5766 sl = s .split ('/' )
5867 if len (sl ) != 3 :
@@ -72,16 +81,16 @@ def parse_args():
7281 stdout (f'pr: { ARGS .pr } upstream: { ARGS .upstream } ' )
7382
7483 if ARGS .target == 'none' :
75- sys . exit ('--target is required' )
84+ die ('--target is required' )
7685
7786 if ARGS .upstream == 'none' :
78- sys . exit ('--upstream is required' )
87+ die ('--upstream is required' )
7988
8089 if ARGS .baserev == 'none' and ARGS .pr == 'none' :
81- sys . exit ('--baserev or --pr is required' )
90+ die ('--baserev or --pr is required' )
8291
8392 if ARGS .baserev != 'none' and ARGS .pr != 'none' :
84- sys . exit ('--baserev and --pr are mutually exclusive' )
93+ die ('--baserev and --pr are mutually exclusive' )
8594
8695def ssplit (cmd ):
8796 if isinstance (cmd , str ):
@@ -105,20 +114,20 @@ def runc(cmd, exit_on_cpe=True, **kwargs):
105114 ret = subprocess .run (ssplit (cmd ), ** kwargs )
106115 except subprocess .CalledProcessError as e :
107116 if exit_on_cpe :
108- sys . exit (f'Execution of { cmd } failed with { e .returncode } ' )
117+ die (f'Execution of { cmd } failed with { e .returncode } ' )
109118 else :
110119 raise
111120
112121 return ret
113122
114- def runc_out (cmd , exit_on_cpe = True , ** kwargs ):
123+ def runc_out (cmd , exit_on_cpe = True , suppress_stderr = False , ** kwargs ):
115124 # A shorthand for running a simple shell command and getting its output.
116125
117126 cwd = kwargs .get ('cwd' , os .getcwd ())
118127
119- if ARGS .quiet_subprocesses :
128+ if ARGS .quiet_subprocesses or suppress_stderr :
120129 kwargs ['stderr' ] = subprocess .DEVNULL
121- else :
130+ if not ARGS . quiet_subprocesses :
122131 stdout (f'running "{ cmd } " in "{ cwd } "' )
123132
124133 kwargs ['check' ] = True
@@ -129,82 +138,183 @@ def runc_out(cmd, exit_on_cpe=True, **kwargs):
129138 cp = subprocess .run (ssplit (cmd ), ** kwargs )
130139 except subprocess .CalledProcessError as e :
131140 if exit_on_cpe :
132- sys . exit (f'Execution of { cmd } failed with { e .returncode } ' )
141+ die (f'Execution of { cmd } failed with { e .returncode } ' )
133142 else :
134143 raise
135144
136145 return cp .stdout .rstrip ()
137146
138- def fetch_upstream (gh , upstream ):
139- pass
147+ @functools .cache
148+ def fetch_branch (repo , branch , target ):
149+ ref = f'nrf/ref/{ branch } '
150+ runc (f'git -C { target } fetch { repo .clone_url } { branch } :{ ref } ' )
151+ return ref
152+
153+ @functools .cache
154+ def fetch_pr (repo , prn , target ):
155+ pr = repo .get_pull (prn )
156+ if pr .is_merged ():
157+ die (f'PR #{ prn } is merged, please use [nrf fromtree] instead' )
158+ if pr .state == 'closed' :
159+ die (f'PR #{ prn } is closed and not merged' )
140160
141- def fetch_pr (gh , pr ):
142- pass
161+ shas = [c .sha for c in pr .get_commits ()]
162+ ref = f'nrf/pull/{ prn } '
163+ runc (f'git -C { target } fetch { repo .clone_url } pull/{ prn } /head:{ ref } ' )
164+
165+ stdout (f'PR #{ prn } ref: { ref } ' )
166+ stdout (f'PR #{ prn } shas: { shas } ' )
167+ return ref , shas
143168
144169def merge_base (target , base , head ):
145170 mb = runc_out (f'git -C { target } merge-base { base } { head } ' )
146171 stdout (f'merge base { mb } ' )
147172 return mb
148173
149- def check_commit ( urepo , target , sha ):
150- stdout ( f'Checking commit { sha } ' )
174+ @ functools . cache
175+ def get_commit_msg ( target , sha ):
151176 cm = runc_out (f'git -C { target } show -s --format=%B { sha } ' ).split ('\n ' )
152- title = cm [0 ]
177+ title = cm [0 ]. lstrip (). rstrip ()
153178 body = '\n ' .join (cm [1 :])
154- # stdout(f'{title}')
155- # stdout(f'{body}')
179+
180+ return title , body
181+
182+ def range_diff (target , s1 , s2 , stat = False ):
183+ # Compare commit ranges (sha^! is a range for sha itself)
184+ stats = ' --stat' if stat else ''
185+ out = runc_out (f'git -C { target } range-diff --no-color{ stats } { s1 } ^! { s2 } ^!' )
186+ return out
187+
188+ def get_commit_diff (target , sha ):
189+ # Get the diff of a commit (sha^! is a range for sha itself)
190+ diff = runc_out (f'git -C { target } diff { sha } ^!' ).split ('\n ' )
191+
192+ return diff
193+
194+ def try_switch_back (target ):
195+ try :
196+ _ = runc_out (f'git -C { target } switch -' , exit_on_cpe = False ,
197+ suppress_stderr = True )
198+ except subprocess .CalledProcessError as e :
199+ pass
200+
201+ def check_commit (urepo , ubranch , target , sha , merge ):
202+ stdout (f'--- Checking commit { sha } ' )
203+ title , body = get_commit_msg (target , sha )
156204 m = re .match (r'^(Revert\s+\")?\[nrf (mergeup|fromtree|fromlist|noup)\]\s+' ,
157205 title )
158206 if not m :
159- sys_exit (f'{ sha } : Title does not contain a sauce tag' )
207+ die (f'{ sha } : Title does not contain a sauce tag' )
160208 revert = m .group (1 )
161209 tag = m .group (2 )
162210 if not tag :
163- sys_exit (f'{ sha } : Title does not contain a sauce tag' )
211+ die (f'{ sha } : Title does not contain a sauce tag' )
164212
165- #stdout(tag)
166-
167- # Skip the rest of checks if the commit is a revert
168213 if revert :
169214 if tag == 'mergeup' :
170- sys .exit ('Mergeup cannot be reverted' )
171- stdout (f'Revert commit, skipping additional checks' )
172- return
173-
174- if tag == 'mergeup' :
215+ die ('Mergeup cannot be reverted' )
216+ regex = r'^This reverts commit \b([a-f0-9]{40})\b\.'
217+ match = re .search (regex , body , re .MULTILINE )
218+ if not match :
219+ die (f'{ sha } : revert commit missing reverted SHA' )
220+ stdout (f'revert: { match .group (1 )} ' )
221+ # The SHA to replay is the revert commmit's
222+ usha = sha
223+ elif tag == 'mergeup' :
175224 # Count the merges in this commit range (sha^! is a range for sha
176225 # itself)
177- count = runc_out ('git rev-list --merges --count {sha}^!' )
226+ count = runc_out (f 'git -C { target } rev-list --merges --count { sha } ^!' )
178227 if count != '1' :
179- sys . exit ('mergeup used in a non-merge commit' )
228+ die ('mergeup used in a non-merge commit' )
180229 if not re .match (r'^\[nrf mergeup\] Merge upstream up to commit \b([a-f0-9]{40})\b' ,
181230 title ):
182- sys .exit ('{sha}: Invalid mergeup commit title' )
231+ die (f'{ sha } : Invalid mergeup commit title' )
232+
233+ # We cannot replay the mergeup commit
234+ return True
183235 elif tag == 'fromlist' :
184- regex = r'^Upstream PR: (' \
185- r'https://github\.com/.*/pull/(\d+)|' \
186- r'http://lists.infradead.org/pipermail/hostap/.*\.html' \
187- r')'
236+ regex = r'^Upstream PR: https://github\.com/.*/pull/(\d+)'
188237
189238 match = re .search (regex , body , re .MULTILINE )
190239 if not match :
191- sys_exit (f'{ sha } : fromlist commit missing an Upstream PR reference' )
240+ die (f'{ sha } : fromlist commit missing an Upstream PR reference' )
192241
193- #stdout(f'fromlist: {match.group(1)}')
194- if urepo :
195- upr = match .group (2 )
196- stdout (f'fromlist: { upr } ' )
242+ upr = match .group (1 )
243+ stdout (f'fromlist: { upr } ' )
244+
245+ # Check and Fetch the upstream Pull Request
246+ ref , shas = fetch_pr (urepo , int (upr ), target )
247+
248+ # Match a commit
249+ usha = None
250+ for s in shas :
251+ t , b = get_commit_msg (target , s )
252+ # Match the upstream commit title with the downstream one
253+ if t in title :
254+ stdout (f'fromlist: Matched upstream PR commit { s } ' )
255+ usha = s
256+ break
257+
258+ if not usha :
259+ die (f'{ sha } : unable to match any commit from PR { upr } ' )
260+
197261 elif tag == 'fromtree' :
198262 regex = r'^\(cherry picked from commit \b([a-f0-9]{40})\b\)'
199263 match = re .search (regex , body , re .MULTILINE )
200264 if not match :
201- sys_exit (f'{ sha } : fromtree commit missing cherry-pick reference' )
265+ die (f'{ sha } : fromtree commit missing cherry-pick reference' )
202266 #stdout(f'fromtree: {match.group(0)}')
203- if urepo :
204- usha = match .group (1 )
205- stdout (f'fromtree: { usha } ' )
267+
268+ usha = match .group (1 )
269+ stdout (f'fromtree: { usha } ' )
270+
271+ # Fetch the upstream main branch
272+ ref = fetch_branch (urepo , ubranch , target )
273+
274+ # Verify that the commit exists at all in the working tree
275+ _ = runc_out (f'git -C { target } rev-parse --verify { usha } ^{{commit}}' )
276+
277+ # Verify that the commit is in the required branch
278+ contains = runc_out (f'git -C { target } branch { ref } --contains { usha } ' )
279+
280+ if not re .match (rf'^.*{ ref } .*$' , contains ):
281+ die (f'Branch { ref } does not contain commit { usha } ' )
282+
283+ elif tag == 'noup' :
284+ stdout ('noup' )
285+ # The SHA to replay is the noup commmit's
286+ usha = sha
287+
288+ # Skip cherry-picking if a merge has been found
289+ if merge :
290+ stdout (f'merge: skipping cherry-pick of { sha } ' )
291+ return True
292+
293+ # Cherry-pick the commit into the replay branch
294+ try :
295+ out = runc_out (f'git -C { target } cherry-pick { usha } ' , exit_on_cpe = False )
296+ except subprocess .CalledProcessError as e :
297+ # Make sure we abort the cherry-pick
298+ try :
299+ _ = runc_out (f'git -C { target } cherry-pick --abort' ,
300+ exit_on_cpe = False )
301+ except subprocess .CalledProcessError as e :
302+ pass
303+ # Ignore it and exit forcefully
304+ die (f'ERROR: unable to cherry-pick { usha } ' )
305+
306+ # Execute a diff between the replay branch and the sha to make sure the
307+ # commit has not been modified
308+ diff = runc_out (f'git -C { target } diff { sha } ' )
309+
310+ if diff :
311+ die ('SHA {sha} non-empty diff:\n {diff}' )
312+
313+ return False
206314
207315def main ():
316+ global die_switch
317+
208318 parse_args ()
209319
210320 token = os .environ .get ('GITHUB_TOKEN' , None )
@@ -217,38 +327,55 @@ def main():
217327
218328 target = Path (ARGS .target ).absolute ()
219329 if not target .is_dir ():
220- sys . exit (f'target repo { target } does not exist; check path' )
330+ die (f'target repo { target } does not exist; check path' )
221331
222332 org_str , repo_str , br_str = gh_pr_split (ARGS .upstream )
223333 urepo = gh .get_repo (f'{ org_str } /{ repo_str } ' )
224334
225335 if ARGS .pr != 'none' :
226336 org_str , repo_str , pr_str = gh_pr_split (ARGS .pr )
227337 drepo = gh .get_repo (f'{ org_str } /{ repo_str } ' )
228- dpr = drepo .get_pull (int (pr_str ))
338+ prn = int (pr_str )
339+ dpr = drepo .get_pull (prn )
229340 baserev = merge_base (target , dpr .base .sha , dpr .head .sha )
230341 headrev = dpr .head .sha
231342 dcommits = [c for c in dpr .get_commits ()]
232343 dshas = [c .sha for c in dcommits ]
233- stdout (f'SHAs found in PR: { dshas } ' )
344+ stdout (f'{ len ( dshas ) } commits found in PR' )
234345 else :
235346 baserev = ARGS .baserev
236347 headrev = 'HEAD'
348+ prn = 0
237349
238350 stdout (f'baserev: { baserev } ' )
239351 stdout (f'headrev: { headrev } ' )
240352 revs = runc_out (f'git -C { target } rev-list --first-parent { baserev } ..{ headrev } ' )
241353 revs = revs .split ('\n ' )
242354 revs .reverse ()
243- stdout (f'revs: { revs } ' )
244-
245- rev_str = f"{ ',' .join (revs )} ,"
246-
355+ stdout (f'{ len (revs )} commits found with rev-list' )
356+
357+ # Prepare a replay branch
358+ replay = f'nrf/replay/{ prn } '
359+ # Create the replay branch
360+ runc (f'git -C { target } branch -f { replay } { baserev } ' )
361+ # Switch to it
362+ runc (f'git -C { target } switch { replay } ' )
363+ die_switch = target
364+
365+ merge = False
366+ count = 0
247367 for r in revs :
248- check_commit (urepo , target , r )
368+ merge = check_commit (urepo , br_str , target , r , merge )
369+ count += 1
370+ stdout (f'- Processed commit { count } ' )
371+
372+ # Switch back to the previous branch
373+ stdout ('main: switch' )
374+ die_switch = None
375+ try_switch_back (target )
249376
250- if dshas and dshas != revs :
251- sys . exit (f'{ dshas } is different from { revs } ' )
377+ if not merge and ( dshas and dshas != revs ) :
378+ die (f'{ dshas } is different from { revs } ' )
252379
253380if __name__ == '__main__' :
254381 main ()
0 commit comments