Skip to content

Instantly share code, notes, and snippets.

@chuckleplant
Last active May 6, 2025 08:31
Show Gist options
  • Save chuckleplant/bc4dbecdc7d63a0119f796a7f9862e73 to your computer and use it in GitHub Desktop.
Save chuckleplant/bc4dbecdc7d63a0119f796a7f9862e73 to your computer and use it in GitHub Desktop.
#!/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