tools/infra_scripts/import-hirschgarten-pr.py (85 lines of code) (raw):
#!/usr/bin/env python3
"""
import-hirschgarten-pr — fetch pr from jetbrains/hirschgarten, squash its *own* changes,
rewrite paths, emit patch, apply to current branch, and narrate everything.
usage:
./import-hirschgarten-pr.py 291
"""
import argparse, json, os, re, subprocess, sys, urllib.request
from pathlib import Path
from typing import List, Optional, Tuple
HIRSCH = "https://github.com/JetBrains/hirschgarten.git"
# ---------- helpers ---------------------------------------------------------
def sh(cmd: List[str], *, capture: bool = True, check: bool = True, env=None) -> str:
print(f"$ {' '.join(cmd)}", flush=True)
res = subprocess.run(cmd, text=True, capture_output=capture, check=check, env=env)
if capture and res.stdout:
print(res.stdout.rstrip(), flush=True)
return res.stdout.strip() if capture else ""
def pr_info(num: int) -> Tuple[Optional[str], str]:
url = f"https://api.github.com/repos/JetBrains/hirschgarten/pulls/{num}"
print(f"# hitting gh api: {url}")
try:
with urllib.request.urlopen(url) as r:
d = json.load(r)
return d["head"]["sha"], d["base"]["ref"]
except Exception as e:
print(f"# gh api failed ({e}); defaulting base to main")
return None, "main"
def add_prefix(p: str, pre: str) -> str:
return p if p.startswith(pre) or p == "/dev/null" else pre + p
def rewrite(patch: str, pre: str) -> str:
out = []
for ln in patch.splitlines(keepends=True):
if ln.startswith("diff --git"):
m = re.match(r"diff --git a/(.+?) b/(.+)", ln)
if m:
a, b = m.groups()
ln = f"diff --git a/{add_prefix(a, pre)} b/{add_prefix(b, pre)}\n"
elif ln.startswith("--- a/"):
ln = f"--- a/{add_prefix(ln[6:].strip(), pre)}\n"
elif ln.startswith("+++ b/"):
ln = f"+++ b/{add_prefix(ln[6:].strip(), pre)}\n"
elif ln.startswith("rename from "):
ln = f"rename from {add_prefix(ln[12:].strip(), pre)}\n"
elif ln.startswith("rename to "):
ln = f"rename to {add_prefix(ln[10:].strip(), pre)}\n"
out.append(ln)
return "".join(out)
# ---------- main ------------------------------------------------------------
def main(argv=None):
ap = argparse.ArgumentParser(description="squash pr, rewrite paths, apply patch")
ap.add_argument("pr", type=int)
ap.add_argument("--prefix", default="plugins/bazel/")
ap.add_argument("--keep-pr", action="store_true")
a = ap.parse_args(argv)
pr_branch = f"pr-{a.pr}"
base_branch = f"{pr_branch}-base"
tmp_branch = f"{pr_branch}-squash-tmp"
print(f"# fetch pr {a.pr}")
sh(["git","fetch",HIRSCH,f"pull/{a.pr}/head:{pr_branch}"], capture=False)
head_sha, base_ref = pr_info(a.pr)
print(f"# fetch base ref {base_ref}")
sh(["git","fetch",HIRSCH,f"refs/heads/{base_ref}:{base_branch}"], capture=False)
# true base is the merge‑base, so merges from main inside the pr don’t bloat the diff
base_sha = sh(["git","merge-base",pr_branch,base_branch])
print(f"# using merge‑base {base_sha[:7]}")
# squash
sh(["git","checkout","-q","-B",tmp_branch,pr_branch], capture=False)
sh(["git","reset","--soft",base_sha], capture=False)
commit_shas = sh(["git","rev-list","--reverse",f"{base_sha}..{pr_branch}"]).split()
if not commit_shas:
print("no commits to squash", file=sys.stderr); sys.exit(1)
author = sh(["git","show","-s","--format=%an <%ae>",commit_shas[0]])
message = "\n\n".join(sh(["git","show","-s","--format=%B",c]) for c in commit_shas).strip()
date = sh(["git","show","-s","--format=%cI",commit_shas[-1]])
env = {**os.environ,"GIT_COMMITTER_DATE":date}
sh(["git","commit","--author",author,"-m",message,"--date",date], capture=False, env=env)
# patchify
raw = sh(["git","format-patch","-1","--stdout"])
pat = rewrite(raw,a.prefix)
ppth = Path(f"pr-{a.pr}-bazel.patch").resolve()
ppth.write_text(pat)
print(f"# patch written to {ppth}")
# cleanup tmp
sh(["git","checkout","-q","-"], capture=False)
sh(["git","branch","-D",tmp_branch], capture=False)
if not a.keep_pr:
sh(["git","branch","-D",pr_branch], capture=False)
sh(["git","branch","-D",base_branch], capture=False)
# apply
print("# git am -3")
try:
sh(["git","am","-3",str(ppth)], capture=False)
print("# applied ✔")
except subprocess.CalledProcessError:
print("# git am failed; fix then `git am --continue` or `--abort`", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
sys.exit(main())