Last active
May 6, 2025 08:31
-
-
Save chuckleplant/bc4dbecdc7d63a0119f796a7f9862e73 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
""" | |
apply_ifdef_patch.py | |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
Automates the workflow: | |
1. Extract a commit‑diff from a *source* Git repository. | |
2. Apply that patch to a *target* (non‑Git) tree using GNU **patch(1)** | |
with –D KEEP_OLD so both versions of the code are kept behind | |
#ifdef / #else / #endif. | |
3. Insert user‑defined comment banners **before** and **after** every | |
generated KEEP_OLD block in each affected file. | |
USAGE | |
----- | |
python apply_ifdef_patch.py \ | |
--source C:/dev/engine-src \ | |
--commit abc1234 \ | |
--target C:/legacy/engine \ | |
--pre "/* === BEGIN cherry abc1234 === */" \ | |
--post "/* === END cherry abc1234 === */" | |
REQUIREMENTS | |
------------ | |
* Git for Windows – git.exe must be on PATH. | |
* GNU patch – patch.exe on PATH (comes with Git for Windows). | |
The script uses only the standard Python library. | |
""" | |
from __future__ import annotations | |
import argparse | |
import os | |
import re | |
import subprocess as sp | |
import sys | |
import tempfile | |
from pathlib import Path | |
from typing import List, Sequence | |
MACRO = "KEEP_OLD" # change here if you need another default guard | |
def run(cmd: Sequence[str], *, cwd: Path | None = None, capture: bool = False) -> sp.CompletedProcess: | |
"""Run *cmd*. Raise if non‑zero exit code.""" | |
print("$", " ".join(cmd)) | |
return sp.run(cmd, cwd=cwd, text=True, capture_output=capture, check=True) | |
def p4_checkout(target: Path, rel_paths: list[str]) -> None: | |
""" | |
Make every file in *rel_paths* writable under Perforce: | |
• p4 edit for files that already exist | |
• p4 add for brand-new files | |
""" | |
if not rel_paths: | |
return | |
# Convert Git-style forward slashes to the local OS and group by status | |
to_edit, to_add = [], [] | |
for rel in rel_paths: | |
f = target / rel.replace("/", os.sep) | |
if f.exists(): | |
to_edit.append(str(f)) | |
else: | |
# ensure directories exist so p4 add succeeds | |
f.parent.mkdir(parents=True, exist_ok=True) | |
to_add.append(str(f)) | |
if to_edit: | |
run(["p4", "edit", *to_edit], cwd=target) | |
if to_add: | |
run(["p4", "add", *to_add], cwd=target) | |
def changed_paths(repo: Path, commit: str) -> List[str]: | |
"""Return the list of files changed by *commit*.""" | |
cp = run([ | |
"git", "-C", str(repo), | |
"diff", "--name-only", f"{commit}~1", commit | |
], capture=True) | |
return [p for p in cp.stdout.splitlines() if p] | |
def create_patch(repo: Path, commit: str, outfile: Path) -> None: | |
run([ | |
"git", "-C", str(repo), | |
"diff", "-u", f"{commit}~1", commit | |
], capture=True) | |
with outfile.open("wb") as f: | |
sp.run([ | |
"git", "-C", str(repo), | |
"diff", "-u", f"{commit}~1", commit | |
], stdout=f, check=True) | |
def apply_patch(target: Path, patch_file: Path) -> None: | |
run([ | |
"patch", "-p0", "-D", MACRO, "-i", str(patch_file) | |
], cwd=target) | |
def bannerize_file(path: Path, pre: str, post: str) -> None: | |
""" | |
Replace all #ifndef KEEP_OLD blocks with: | |
// pre | |
#if 0 | |
... | |
#else | |
... | |
#endif | |
// post | |
""" | |
with path.open("r", encoding="utf-8", errors="ignore") as f: | |
lines = f.readlines() | |
output = [] | |
inside_block = False | |
pat_ifdef = re.compile(rf"^\s*#\s*ifndef\s+{re.escape(MACRO)}\b") | |
pat_else = re.compile(r"^\s*#\s*else\b") | |
pat_endif = re.compile(rf"^\s*#\s*endif\b.*{re.escape(MACRO)}") | |
for line in lines: | |
if not inside_block and pat_ifdef.match(line): | |
output.append(pre + "\n") | |
output.append("#if 0\n") | |
inside_block = True | |
continue | |
if inside_block and pat_else.match(line): | |
output.append("#else\n") | |
continue | |
if inside_block and pat_endif.match(line): | |
output.append("#endif\n") | |
output.append(post + "\n") | |
inside_block = False | |
continue | |
output.append(line) | |
with path.open("w", encoding="utf-8") as f: | |
f.writelines(output) | |
def bannerize_files(target: Path, files: List[str], pre: str, post: str) -> None: | |
for rel in files: | |
fpath = target / rel.replace("/", os.sep) | |
if fpath.exists(): | |
bannerize_file(fpath, pre, post) | |
else: | |
print(f"[warn] {fpath} missing — skipped") | |
def main(argv: Sequence[str] | None = None) -> None: | |
p = argparse.ArgumentParser(description="Cherry‑pick a Git commit into a non‑Git tree with #ifdef KEEP_OLD blocks and comment banners.") | |
p.add_argument("--source", required=True, help="Path to Git repository that contains the commit") | |
p.add_argument("--commit", required=True, help="Commit SHA/branch/tag to export") | |
p.add_argument("--target", required=True, help="Path to destination project (does not need Git)") | |
p.add_argument("--pre", required=True, help="Comment banner to insert BEFORE each KEEP_OLD block") | |
p.add_argument("--post", required=True, help="Comment banner to insert AFTER each KEEP_OLD block") | |
args = p.parse_args(argv) | |
src = Path(args.source).resolve() | |
tgt = Path(args.target).resolve() | |
if not src.is_dir(): | |
sys.exit(f"source repo not found: {src}") | |
if not tgt.is_dir(): | |
sys.exit(f"target path not found: {tgt}") | |
# 1. List changed files for later bannerisation | |
files = changed_paths(src, args.commit) | |
if not files: | |
sys.exit("Commit appears to have no textual changes.") | |
# 2. Create patch in a temp file | |
with tempfile.TemporaryDirectory() as td: | |
patch_path = Path(td) / f"{args.commit}.patch" | |
create_patch(src, args.commit, patch_path) | |
print(f"Patch written to {patch_path}") | |
# 3. Apply patch with -D KEEP_OLD | |
apply_patch(tgt, patch_path) | |
# 4. Add banners to each affected file in target | |
bannerize_files(tgt, files, args.pre, args.post) | |
print("All done.") | |
if __name__ == "__main__": | |
try: | |
main() | |
except sp.CalledProcessError as e: | |
sys.exit(f"Command failed: {e.cmd}\nReturn code: {e.returncode}\n{e.stderr}") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment