from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
def modified_files() -> list[str]:
out = subprocess.run(
["git", "diff", "HEAD", "--name-only"],
check=True,
capture_output=True,
text=True,
)
return [line for line in out.stdout.splitlines() if line]
def head_blob(path: str) -> bytes | None:
res = subprocess.run(
["git", "show", f"HEAD:{path}"],
capture_output=True,
)
if res.returncode != 0:
return None
return res.stdout
def strip_ws(data: bytes) -> bytes:
return bytes(b for b in data if b not in (0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x20))
def rustfmt(data: bytes) -> bytes | None:
res = subprocess.run(
["rustfmt", "--edition", "2024", "--emit", "stdout"],
input=data,
capture_output=True,
)
if res.returncode != 0:
return None
return res.stdout
def equivalent_after_rustfmt(head: bytes, wt: bytes) -> bool | None:
h = rustfmt(head)
if h is None:
return None
w = rustfmt(wt)
if w is None:
return None
return h == w
def classify(path: str) -> str:
head = head_blob(path)
if head is None:
return "untracked"
wt = Path(path)
if not wt.exists():
return "missing"
wt_bytes = wt.read_bytes()
if path.endswith(".rs"):
eq = equivalent_after_rustfmt(head, wt_bytes)
if eq is not None:
return "formatting" if eq else "semantic"
return "formatting" if strip_ws(head) == strip_ws(wt_bytes) else "semantic"
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"--semantic",
action="store_true",
help="only list files whose stripped contents differ",
)
group.add_argument(
"--formatting",
action="store_true",
help="only list files whose stripped contents match",
)
args = parser.parse_args()
files = modified_files()
if not files:
print("no modified files vs HEAD")
return 0
buckets: dict[str, list[str]] = {
"formatting": [],
"semantic": [],
"untracked": [],
"missing": [],
}
for path in files:
buckets[classify(path)].append(path)
if args.semantic:
for path in buckets["semantic"]:
print(path)
return 0
if args.formatting:
for path in buckets["formatting"]:
print(path)
return 0
for label in ("semantic", "formatting", "untracked", "missing"):
entries = buckets[label]
if not entries:
continue
print(f"{label} ({len(entries)}):")
for path in entries:
print(f" {path}")
print()
return 0
if __name__ == "__main__":
sys.exit(main())