Skip to content

Commit 371d15d

Browse files
committed
Merge 718ac5b into merged_master (Elements PR ElementsProject#1118)
2 parents 3aef5e4 + 718ac5b commit 371d15d

File tree

1 file changed

+162
-67
lines changed

1 file changed

+162
-67
lines changed

contrib/review-prs.py

Lines changed: 162 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,35 @@
1313

1414
from typing import List, Dict, Any
1515

16-
# XXX: Requires bitcoin, upstream, apolestra remotes to exist!!
1716
BITCOIN_MASTER = "bitcoin/master"
1817
ELEMENTS_MASTER = "upstream/master"
19-
MERGED_MASTER = "apoelstra/merged-master"
18+
MERGED_MASTER = "upstream/merged-master"
2019
MERGED_MASTER_REVIEWED = "merged-master-reviewed"
2120

21+
# XXX: This is not actually a good idea. On many systems (incl. macOS) random
22+
# files in the worktree will start to go missing a few days after its creation,
23+
# because of temp-directory-cleaning scripts that work on a file-by-file basis.
24+
WORKTREE_LOCATION = "/tmp/elements-merge-review-worktree"
25+
26+
def print_startup_warning() -> None:
27+
print()
28+
print("To prepare to use this script, make sure the following things are set up:")
29+
print(" - The remotes 'bitcoin' and 'upstream' should point to Bitcoin Core and")
30+
print(" Elements upstream repos, respectively.")
31+
print(f" - The latest {MERGED_MASTER_REVIEWED} branch should be checked out locally.")
32+
print(f" - We are currently using '{MERGED_MASTER}' as our branch to review.")
33+
print(" To change this, edit the constant MERGED_MASTER in the script.")
34+
print(" - We will reuse or create a git worktree at:")
35+
print(f" {WORKTREE_LOCATION}")
36+
print(" If this fails, delete that directory, run 'git worktree prune', and retry.")
37+
38+
print()
39+
result = prompt_chars("Continue?", "yn")
40+
if (result != "y"):
41+
sys.exit(1)
42+
2243
ECHO_COMMANDS = True
44+
GIT_DIFF_OPTS = "--color=always --color-moved=dimmed-zebra --color-moved-ws=allow-indentation-change --ignore-space-change --histogram"
2345

2446
class CommandFailed(Exception):
2547
"""Exception for failure of a shell command."""
@@ -57,22 +79,24 @@ def prompt_chars(prompt: str, chars: str) -> str:
5779
def get_bitcoin_commits():
5880
"""Get the list of upstream bitcoin commits which should be included in merges not yet reviewed."""
5981

60-
BTC_COMMITS_CMD=f"git log {BITCOIN_MASTER} --not {MERGED_MASTER_REVIEWED} --merges --first-parent --reverse --pretty='format:%ct %cI %H Bitcoin %s'"
82+
BTC_COMMITS_CMD=f"git log '{BITCOIN_MASTER}' --not '{MERGED_MASTER_REVIEWED}' --merges --first-parent --reverse --pretty='format:%ct %cI %H Bitcoin %s'"
6183

6284
bitcoin_commits_raw = shell(BTC_COMMITS_CMD)
6385
bitcoin_commits = []
6486
for line in bitcoin_commits_raw:
65-
m = re.match(r"\d+ ([^ ]+) ([0-9a-f]+) Bitcoin Merge ((?:bitcoin-core/gui)?)#(\d+): (.*)", line)
87+
m = re.match(r"\d+ ([^ ]+) ([0-9a-f]+) Bitcoin Merge ((?:[-_a-zA-Z0-9]*/[-_a-zA-Z0-9]*)?)#(\d+): (.*)", line)
6688
if m is None:
67-
raise Exception("Merge commit message in Bitcoin Core history had unexpected format")
68-
[date, cid, guirepo, prno, msg] = m.groups()
89+
raise Exception(f"Merge commit message in Bitcoin Core history had unexpected format:\n > {line}")
90+
[date, cid, repo, prno, msg] = m.groups()
6991
fromgui = None
70-
if guirepo == "":
92+
if repo == "":
7193
fromgui = False
72-
elif guirepo == "bitcoin-core/gui":
94+
elif repo == "bitcoin/bitcoin":
95+
fromgui = False
96+
elif repo == "bitcoin-core/gui":
7397
fromgui = True
7498
else:
75-
raise Exception("Merge into Bitcoin Core from unexpected repo, or commit message had unexpected format")
99+
raise Exception(f"Merge into Bitcoin Core from unexpected repo, or commit message had unexpected format: \n > {line}")
76100

77101
bitcoin_commits.append({
78102
"date": date,
@@ -86,15 +110,17 @@ def get_bitcoin_commits():
86110
def get_elements_commits():
87111
"""Get the list of elements commits which should be included in merges not yet reviewed."""
88112

89-
ELT_COMMITS_CMD=f"git log {ELEMENTS_MASTER} --not {MERGED_MASTER_REVIEWED} --merges --first-parent --reverse --pretty='format:%ct %cI %H Elements %s'"
113+
ELT_COMMITS_CMD=f"git log '{ELEMENTS_MASTER}' --not '{MERGED_MASTER_REVIEWED}' --merges --first-parent --reverse --pretty='format:%ct %cI %H Elements %s'"
90114

91115
elements_commits_raw = shell(ELT_COMMITS_CMD)
92116
elements_commits = []
93117
for line in elements_commits_raw:
94-
m = re.fullmatch(r"\d+ ([^ ]+) ([0-9a-f]+) Elements Merge (?:pull request )?#(\d+):? (.*)", line)
118+
m = re.fullmatch(r"\d+ ([^ ]+) ([0-9a-f]+) Elements Merge (?:pull request )?((?:[-_a-zA-Z0-9]*/[-_a-zA-Z0-9]*)?)#(\d+):? (.*)", line)
95119
if m is None:
96-
raise Exception("Merge commit message in Elements history had unexpected format")
97-
[date, cid, prno, msg] = m.groups()
120+
raise Exception(f"Merge commit message in Elements history had unexpected format: \n > {line}")
121+
[date, cid, repo, prno, msg] = m.groups()
122+
if repo != "" and repo != "ElementsProject/elements":
123+
raise Exception(f"Merge into Elements from unexpected repo, or commit message had unexpected format: \n > {line}")
98124
elements_commits.append({
99125
"date": date,
100126
"cid": cid,
@@ -106,31 +132,48 @@ def get_elements_commits():
106132
def get_merged_commits():
107133
"""Get the list of merge commits still needing to be reviewed."""
108134

109-
MERGED_COMMITS_CMD=f"git log {MERGED_MASTER} --not {MERGED_MASTER_REVIEWED} --first-parent --reverse --pretty='format:%ct %cI %H Merged %s'"
135+
MERGED_COMMITS_CMD=f"git log '{MERGED_MASTER}' --not '{MERGED_MASTER_REVIEWED}' --first-parent --reverse --pretty='format:%ct %cI %H Merged %s'"
110136

111137
merged_commits_raw = shell(MERGED_COMMITS_CMD)
112138
merged_commits = []
113139
for line in merged_commits_raw:
114140
#print(line)
115-
m = re.fullmatch(r"\d+ ([^ ]+) ([0-9a-f]+) Merged Merge ([0-9a-f]+) into merged_master \((.+) PR ((?:bitcoin-core/gui)?)#(\d+)\)(.*)", line)
141+
m = re.fullmatch(r"\d+ ([^ ]+) ([0-9a-f]+) Merged Merge ([0-9a-f]+) into merged_master \((.+) PR (?:pull )?((?:[-_a-zA-Z0-9]*/[-_a-zA-Z0-9]*)?)#(\d+)\)(.*)", line)
116142
m_secp = re.fullmatch(r"\d+ ([^ ]+) ([0-9a-f]+) Merged update libsecp256k1-zkp to ([0-9a-f]+)", line)
143+
m_misc = re.fullmatch(r"\d+ ([^ ]+) ([0-9a-f]+) Merged (.*)$", line)
117144

118145
fromgui = False
119146
fromsecp = False
147+
120148
if m is not None:
121-
[date, cid, merged_cid, chain, guirepo, prno, trailing] = m.groups()
122-
if guirepo == "bitcoin-core/gui":
123-
fromgui = True
124-
elif guirepo != "":
125-
raise Exception("Merge into Bitcoin Core from unexpected repo, or commit message had unexpected format")
149+
[date, cid, merged_cid, chain, repo, prno, trailing] = m.groups()
150+
if chain == "Bitcoin":
151+
if repo == "bitcoin-core/gui":
152+
fromgui = True
153+
elif repo != "" and repo != "bitcoin/bitcoin":
154+
raise Exception(f"Merge into Bitcoin Core from unexpected repo, or commit message had unexpected format:\n > {line}")
155+
elif chain == "Elements":
156+
if repo != "" and repo != "ElementsProject/elements":
157+
raise Exception(f"Merge into Elements from unexpected repo, or commit message had unexpected format:\n > {line}")
158+
else:
159+
raise Exception(f"Commit message had unexpected format:\n > {line}")
126160
elif m_secp is not None:
127161
[date, cid, merged_cid] = m_secp.groups()
128162
fromsecp = True
129163
chain = ""
130164
prno = ""
131165
trailing = ""
166+
elif m_misc is not None:
167+
[date, cid, trailing] = m_misc.groups()
168+
merged_commits.append({
169+
"date": date,
170+
"cid": cid,
171+
"trailing": trailing,
172+
"handreview": True,
173+
})
174+
continue
132175
else:
133-
raise Exception(f"Commit {cid} did not match any of the expected templates. Please hand-review.")
176+
raise Exception(f"Commit {cid} did not match any of the expected templates. Not sure what to do.\n > {line}")
134177

135178
merged_commits.append({
136179
"date": date,
@@ -140,7 +183,7 @@ def get_merged_commits():
140183
"prno": prno,
141184
"trailing": trailing,
142185
"fromgui": fromgui,
143-
"fromsecp": fromsecp
186+
"fromsecp": fromsecp,
144187
})
145188
return merged_commits
146189

@@ -150,7 +193,8 @@ def check_commit(commit, last_reviewed, incoming_commit) -> None:
150193
cid = commit["cid"]
151194
parents = shell(f"git cat-file -p {cid} | grep '^parent'")
152195
if len(parents) != 2:
153-
raise Exception(f"Expected merge, but commit {cid} has {parents} parents, which is not two! Please hand-review.")
196+
print(f"WARNING: Expected merge, but commit {cid} has {parents} parents, which is not two! Please hand-review.")
197+
return
154198

155199
parent_cids = []
156200
for p in parents:
@@ -189,15 +233,16 @@ def check_or_create_worktree(path) -> None:
189233
def main() -> None:
190234
"""Interactively review commits."""
191235

192-
WORKTREE_LOCATION = "/tmp/elements-merge-review-worktree"
236+
print_startup_warning()
237+
193238
check_or_create_worktree(WORKTREE_LOCATION)
194239
WQ = shlex.quote(WORKTREE_LOCATION)
195240

196241
print("Fetching all remotes...")
197242
os.system(f"git -C {WQ} fetch --all")
198243

199244
print()
200-
print(f"For our already-reviewed base branch, using `{MERGED_MASTER_REVIEWED}`. Starting from there:")
245+
print(f"For our already-reviewed base branch, using `{MERGED_MASTER_REVIEWED}`. If this fails, create / check out that branch locally from upstream's copy. Starting from there:")
201246

202247
bitcoin_commits = get_bitcoin_commits()
203248
print(f"Found a total of {len(bitcoin_commits)} Bitcoin Core commits to review (on `{BITCOIN_MASTER}`).")
@@ -227,24 +272,27 @@ def main() -> None:
227272
sys.exit(1)
228273
print()
229274

230-
SKIP_CLEAN = False
275+
SKIP_CLEAN = True
231276

232277
for commit in merged_commits:
233278
REQUIRE_MANUAL_REVIEW = False
234279
# Figure out what incoming/upstream commit this is merging, from which side.
235280
incoming_commit: Dict[str, Any] = {}
236-
if commit["chain"] == "Bitcoin":
281+
if commit.get("chain", None) == "Bitcoin":
237282
incoming_commit = bitcoin_commits.pop(0)
238-
elif commit["chain"] == "Elements":
283+
elif commit.get("chain", None) == "Elements":
239284
incoming_commit = elements_commits.pop(0)
240-
elif commit["fromsecp"]:
285+
elif commit.get("fromsecp", False):
241286
incoming_commit = {"cid": commit["merged_cid"]}
242287
print()
243288
print("WARNING: The next commit claims to merge a libsecp256k1 commit. We have no way to verify where it came from.")
244289
print()
245290
REQUIRE_MANUAL_REVIEW = True
246291
else:
247-
raise Exception(f"This is neither a Bitcoin nor an Elements merge; I don't know what to do with it. Details: {commit}")
292+
# TODO: A bunch of stuff doesn't really work right in this mode. Fix the UI so it makes sense.
293+
incoming_commit = None
294+
print(f"WARNING: This is neither a Bitcoin nor an Elements merge; I don't know what to do with it. Details: {commit}")
295+
REQUIRE_MANUAL_REVIEW = True
248296

249297
check_commit(commit, last_reviewed, incoming_commit)
250298

@@ -255,57 +303,104 @@ def main() -> None:
255303
# stderr is going to yell at us about throwing away our temp commit, so ignore it
256304
shell(f"git -C {WQ} checkout {last_reviewed}", suppress_stderr=True)
257305

258-
# We expect this to complain if the merge is not clean, but that's okay, so we ignore it.
259-
shell(f"git -C {WQ} merge --no-rerere-autoupdate --no-ff --no-commit {incoming_commit['cid']}", suppress_stderr=True, check_returncode=False)
306+
if incoming_commit is not None:
307+
# We expect this to complain if the merge is not clean, but that's okay, so we ignore it.
308+
shell(f"git -C {WQ} merge --no-rerere-autoupdate --no-ff --no-commit {incoming_commit['cid']}", suppress_stderr=True, check_returncode=False)
309+
310+
shell(f"git -C {WQ} add -u")
311+
shell(f"git -C {WQ} commit -m 'TEMP REVIEW COMMIT'")
260312

261-
shell(f"git -C {WQ} add -u")
262-
shell(f"git -C {WQ} commit -m 'TEMP REVIEW COMMIT'")
313+
diff = shell(f"git -C {WQ} diff --histogram HEAD {cid}")
314+
diffstr = "\n".join(diff)
315+
else:
316+
diffstr = "<Could not find incoming commit; no diff available>"
263317

264-
diff = shell(f"git -C {WQ} diff --histogram HEAD {cid}")
265-
diffstr = "\n".join(diff)
266318
if len(diffstr) == 0 and SKIP_CLEAN and (not REQUIRE_MANUAL_REVIEW):
267319
last_reviewed = cid
268320
continue
269321

270322
# Force 'less' to always wait before exiting, so that short output doesn't get lost when followed by long output.
271323
os.environ["LESS"] = "RSX"
272-
said = ""
273-
while said != "y":
274-
print()
275-
print("** COMMIT WE ARE REVIEWING:")
276-
os.system(f"git -C {WQ} log -1 --pretty=full {cid}")
277-
print()
324+
325+
print()
326+
print("** COMMIT WE ARE REVIEWING:")
327+
os.system(f"git -C {WQ} log -1 --pretty=full {cid}")
328+
print()
329+
330+
def diff4():
331+
os.system(f"git -C {WQ} diff {GIT_DIFF_OPTS} HEAD {cid}")
332+
333+
if incoming_commit is not None:
278334
print("** UPSTREAM COMMIT MERGED THEREIN:")
279335
os.system(f"git -C {WQ} log -1 --pretty=full {incoming_commit['cid']}")
280336
print()
281337

282-
if len(diffstr) > 0:
283-
print("** AND HERE'S THE DIFF:")
284-
print()
285-
os.system(f"git -C {WQ} diff --color=always --color-moved=dimmed_zebra --ignore-space-change --histogram HEAD {cid}")
286-
else:
287-
print("** No diff, merge was clean!")
338+
diff4()
288339

289-
print()
290-
said = prompt_chars("Accept this commit? (If unsure, say 'N', which will exit the review script.) Or [r]edisplay commit, or [a]utoskip clean diffs?", "ynra")
291-
292-
if said == "a":
293-
SKIP_CLEAN = not SKIP_CLEAN
294-
print("Autoskip clean diffs: " + ("on" if SKIP_CLEAN else "off"))
295-
said = "y"
296-
continue
297-
elif said == "r":
298-
continue
299-
elif said == "n":
300-
print(f"The current commit (in `{WORKTREE_LOCATION}`) is the conflicted diff as git produced it. The command we used to diff it against the actual merge was:")
301-
print(f"git -C {WQ} diff --color=always --color=moved=dimmed_zebra --ignore-space-change --histogram --exit-code HEAD {cid}")
302-
print("Look over the commits however you like, or contact someone for assistance. If you are satisfied that the diff is okay, run the review script again.")
303-
sys.exit(1)
304-
305-
if prompt_chars("Locally mark all commits up to and including this commit as reviewed, so we skip them in the future?", "yn") == "y":
306-
shell(f"git -C {WQ} branch -f {MERGED_MASTER_REVIEWED} {cid}")
307-
print(f"Done. (Updated local branch `{MERGED_MASTER_REVIEWED}` in `{WORKTREE_LOCATION}`. ** You will need to push this branch to gitlab if you want to persist this. **)")
340+
said = ""
341+
if incoming_commit is not None:
342+
while said != "y":
343+
said = prompt_chars("Accept this commit? (If unsure, say 'N', which will exit the review script.) Or show a [d]iff, [r]edisplay commit, or [a]utoskip clean diffs [toggle]?", "yndra")
344+
345+
if said == "a":
346+
SKIP_CLEAN = not SKIP_CLEAN
347+
print("Autoskip clean diffs now: " + ("on" if SKIP_CLEAN else "off"))
348+
said = "y"
349+
continue
350+
elif said == "r":
351+
print()
352+
print("** COMMIT WE ARE REVIEWING:")
353+
os.system(f"git -C {WQ} log -1 --pretty=full {cid}")
354+
print()
355+
356+
if incoming_commit is not None:
357+
print("** UPSTREAM COMMIT MERGED THEREIN:")
358+
os.system(f"git -C {WQ} log -1 --pretty=full {incoming_commit['cid']}")
359+
print()
360+
361+
elif said == "d":
362+
diff = prompt_chars("Type of diff to show: [4]way, [o]rig-commits, [m]erged-commits, merged-[f]lattened, or get [h]elp?", "4omfh")
363+
if diff == "4":
364+
diff4()
365+
elif diff == "o":
366+
os.system(f"git -C {WQ} show {GIT_DIFF_OPTS} {commit['merged_cid']}^1..{commit['merged_cid']}")
367+
elif diff == "m":
368+
os.system(f"git -C {WQ} show {GIT_DIFF_OPTS} {cid}^1..{cid}")
369+
elif diff == "f":
370+
os.system(f"git -C {WQ} diff {GIT_DIFF_OPTS} {cid}^1..{cid}")
371+
elif diff == "h":
372+
print("Diff modes supported:")
373+
print(" - 4way: shows a very confusing diff between (1) the conflicted merge git would have made, left to its own devices, and (2) the resolved merge in the actual merged history.")
374+
print(" - orig-commits: shows the commits from the original (upstream) PR, using `git show`.")
375+
print(" - merged-commits: shows the commits of the PR as merged into the actual merged history, using `git show`. This will normally be the same commits as upstream, plus a merge commit resolving the merge conflicts, which is shown in a confusing format as described under `COMBINED DIFF FORMAT` in `man git-diff`.")
376+
print(" - merged-flattened: shows a single flattened diff of the entire PR as applied in the actual merged history.")
377+
# XXX: It would be nice to have a mode showing merged-flattened and orig-flattened (which I maybe could add) side-by-side.
378+
print()
379+
380+
elif said == "n":
381+
break
382+
else: # incoming_commit is None
383+
os.system(f"git -C {WQ} show {GIT_DIFF_OPTS} {cid}")
384+
while said != "y":
385+
said = prompt_chars("Accept this commit? (If unsure, say 'N', which will exit the review script.) Or [r]edisplay commit and diff?", "ynr")
386+
387+
if said == "r":
388+
print()
389+
print("** COMMIT WE ARE REVIEWING:")
390+
os.system(f"git -C {WQ} log -1 --pretty=full {cid}")
391+
print()
392+
os.system(f"git -C {WQ} show {GIT_DIFF_OPTS} {cid}")
393+
elif said == "n":
394+
break
395+
396+
if said == "n":
397+
print(f"The current commit (in `{WORKTREE_LOCATION}`) is the conflicted diff as git produced it. The command we used to diff it against the actual merge was:")
398+
print(f"git -C {WQ} diff {GIT_DIFF_OPTS} HEAD {cid}")
399+
print("Look over the commits however you like, or contact someone for assistance. If you are satisfied that the diff is okay, run the review script again.")
400+
sys.exit(1)
308401

402+
shell(f"git -C {WQ} branch -f {MERGED_MASTER_REVIEWED} {cid}")
403+
print(f"Done. (Updated local branch `{MERGED_MASTER_REVIEWED}` in `{WORKTREE_LOCATION}`. ** You will need to push this branch to gitlab if you want to persist this. **)")
309404
print()
310405

311406
last_reviewed = cid

0 commit comments

Comments
 (0)