Skip to content

Commit 698437e

Browse files
committed
Whitespace fixes
1 parent 159eb6d commit 698437e

File tree

3 files changed

+129
-17
lines changed

3 files changed

+129
-17
lines changed

.github/workflows/sync-upstream.yml

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ on:
1111
description: 'Remove files deleted upstream'
1212
required: false
1313
default: 'false'
14+
paths:
15+
description: 'Space-separated globs to limit scope (e.g., "pyballistic/** tests/** docs/**")'
16+
required: false
17+
default: ''
18+
ignore_ws:
19+
description: 'Ignore trailing whitespace in text comparisons'
20+
required: false
21+
default: 'true'
22+
unicode_norm:
23+
description: 'Unicode normalization (NFC|NFD|NFKC|NFKD|NONE)'
24+
required: false
25+
default: 'NFKC'
1426
# schedule:
1527
# - cron: '17 5 * * *'
1628

@@ -32,12 +44,33 @@ jobs:
3244
with:
3345
python-version: '3.11'
3446

35-
- name: Run upstream sync
36-
env:
37-
SYNC_REF: ${{ github.event.inputs.ref }}
47+
- name: Compute args
48+
id: args
49+
run: |
50+
CLEAN_ARG=""; [ "${{ github.event.inputs.clean }}" = "true" ] && CLEAN_ARG="--clean"
51+
WS_ARG=""; [ "${{ github.event.inputs.ignore_ws }}" = "true" ] && WS_ARG="--ignore-ws"
52+
PATHS_ARG=""
53+
if [ -n "${{ github.event.inputs.paths }}" ]; then
54+
# shellwords: convert to array safely
55+
read -ra P <<< "${{ github.event.inputs.paths }}"
56+
PATHS_ARG="--paths ${P[*]}"
57+
fi
58+
echo "clean=$CLEAN_ARG" >> $GITHUB_OUTPUT
59+
echo "ws=$WS_ARG" >> $GITHUB_OUTPUT
60+
echo "paths=$PATHS_ARG" >> $GITHUB_OUTPUT
61+
62+
- name: Show diff (no changes applied)
63+
run: |
64+
python scripts/sync_upstream.py --show-diff --ref "${{ github.event.inputs.ref }}" \
65+
${{ steps.args.outputs.clean }} ${{ steps.args.outputs.ws }} \
66+
--unicode-norm "${{ github.event.inputs.unicode_norm }}" ${{ steps.args.outputs.paths }}
67+
68+
- name: Apply and stage if needed
3869
run: |
39-
if [ "${{ github.event.inputs.clean }}" = "true" ]; then CLEAN_ARG="--clean"; else CLEAN_ARG=""; fi
40-
python scripts/sync_upstream.py $CLEAN_ARG
70+
# If no diff detected, the following apply will be a no-op due to normalized equality checks
71+
python scripts/sync_upstream.py --stage --ref "${{ github.event.inputs.ref }}" \
72+
${{ steps.args.outputs.clean }} ${{ steps.args.outputs.ws }} \
73+
--unicode-norm "${{ github.event.inputs.unicode_norm }}" ${{ steps.args.outputs.paths }}
4174
4275
- name: Create Pull Request
4376
uses: peter-evans/create-pull-request@v6

scripts/sync_upstream.py

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -112,20 +112,81 @@ def transform_text_files(tmp_dir: Path, replacements: List[Dict], binary_pattern
112112
path.write_text(content, encoding="utf-8")
113113

114114

115-
def rsync_into_repo(tmp_dir: Path, exclude_paths: List[str]):
116-
# Bring transformed files into repo, excluding configured paths
115+
def rsync_into_repo(tmp_dir: Path, exclude_paths: List[str], include_patterns: List[str] | None = None,
116+
normalize_opts: Dict | None = None):
117+
# Bring transformed files into repo, excluding configured paths; skip copying if content is effectively equal
117118
from fnmatch import fnmatch
119+
text_exts = {
120+
".py", ".md", ".rst", ".toml", ".yml", ".yaml", ".ini", ".cfg", ".txt", ".json",
121+
".sh", ".ps1", ".bat", ".ipynb", ".in", ".pyi", ".pyx", ".pxd", ".pxi"
122+
}
123+
special_text_names = {".gitignore", ".gitattributes", "Makefile", "LICENSE", "COPYING", "pre-commit"}
124+
125+
def is_probably_text(p: Path) -> bool:
126+
try:
127+
with p.open('rb') as f:
128+
head = f.read(4096)
129+
if b"\x00" in head:
130+
return False
131+
return True
132+
except Exception:
133+
return False
134+
135+
def norm_bytes_for_compare(p: Path):
136+
if normalize_opts is None:
137+
return p.read_bytes()
138+
if (p.suffix.lower() not in text_exts) and (p.name not in special_text_names) and not is_probably_text(p):
139+
return p.read_bytes()
140+
import unicodedata
141+
b = p.read_bytes().replace(b"\r\n", b"\n").replace(b"\r", b"\n")
142+
if b.startswith(b"\xef\xbb\xbf"):
143+
b = b[3:]
144+
try:
145+
s = b.decode("utf-8", errors="replace")
146+
except Exception:
147+
s = b.decode("latin-1", errors="replace")
148+
norm_form = (normalize_opts.get("unicode_norm") or "NFKC").upper()
149+
if norm_form != "NONE":
150+
if norm_form == "NFC":
151+
s = unicodedata.normalize("NFC", s)
152+
elif norm_form == "NFD":
153+
s = unicodedata.normalize("NFD", s)
154+
elif norm_form == "NFKC":
155+
s = unicodedata.normalize("NFKC", s)
156+
elif norm_form == "NFKD":
157+
s = unicodedata.normalize("NFKD", s)
158+
if normalize_opts.get("ignore_ws"):
159+
s = "\n".join(ln.rstrip() for ln in s.splitlines())
160+
return s.encode("utf-8")
161+
162+
def equal_file(a: Path, b: Path) -> bool:
163+
try:
164+
if a.exists() and b.exists():
165+
try:
166+
if a.read_bytes() == b.read_bytes():
167+
return True
168+
except Exception:
169+
pass
170+
return norm_bytes_for_compare(a) == norm_bytes_for_compare(b)
171+
return False
172+
except Exception:
173+
return False
174+
118175
for src in tmp_dir.rglob("*"):
119176
rel = src.relative_to(tmp_dir)
120177
if rel.parts and rel.parts[0] in {".git", ".github", ".sync_upstream", ".sync_diff"}:
121178
continue
179+
if include_patterns and not _matches(rel, include_patterns):
180+
continue
122181
if any(fnmatch(str(rel).replace("\\", "/"), pat) for pat in exclude_paths):
123182
continue
124183
dest = ROOT / rel
125184
if src.is_dir():
126185
dest.mkdir(parents=True, exist_ok=True)
127186
else:
128187
dest.parent.mkdir(parents=True, exist_ok=True)
188+
if dest.exists() and equal_file(src, dest):
189+
continue
129190
shutil.copy2(src, dest)
130191

131192

@@ -221,7 +282,9 @@ def main():
221282
parser.add_argument("--clean", action="store_true", help="Delete files removed upstream (excluding excluded paths)")
222283
parser.add_argument("--dry-run", action="store_true", help="Run transforms without copying into repo")
223284
parser.add_argument("--paths", nargs="+", help="Glob patterns relative to repo root to limit the diff/sync scope (e.g., pyballistic/** docs/**)")
224-
parser.add_argument("--show-diff", action="store_true", help="Print diff of changes without modifying working tree")
285+
parser.add_argument("--show-diff", action="store_true", help="Print diff of changes; does not modify working tree unless --apply/--stage is set")
286+
parser.add_argument("--apply", action="store_true", help="Apply transformed upstream into working tree (respects --paths, --clean)")
287+
parser.add_argument("--stage", action="store_true", help="Stage changes after applying (implies --apply)")
225288
parser.add_argument("--keep-temp", action="store_true", help="Keep .sync_diff folder for manual inspection")
226289
parser.add_argument("--check", nargs="*", help="Specific files to verify (relative to repo root); prints if changed and why")
227290
parser.add_argument("--sample-diff", type=int, default=0, help="When using --check, print up to N lines of unified diff for each checked file")
@@ -432,14 +495,25 @@ def sha256(p: Path) -> str:
432495
print("[dry-run] Transforms applied in", WORKTREE)
433496
return
434497

435-
# Sync files into current repo, respecting exclusions
436-
rsync_into_repo(WORKTREE, exclude_paths)
437-
if args.clean:
438-
clean_deleted_files(WORKTREE, exclude_paths)
498+
# If only showing diff, avoid modifying working tree unless apply/stage is set
499+
if args.show_diff and not (args.apply or args.stage):
500+
print("[info] show-diff only; no changes applied.")
501+
return
439502

440-
# Stage and leave to workflow to open PR
441-
run(["git", "add", "-A"], cwd=ROOT)
442-
# Don't commit here; create-pull-request action will commit
503+
# Backward compatibility default: if neither show-diff nor apply/stage provided,
504+
# proceed to apply+stage (keeps CI behavior unchanged)
505+
do_apply = args.apply or args.stage or (not args.show_diff and not args.dry_run)
506+
if do_apply:
507+
rsync_into_repo(
508+
WORKTREE,
509+
exclude_paths,
510+
include_patterns=args.paths,
511+
normalize_opts={"ignore_ws": args.ignore_ws, "unicode_norm": args.unicode_norm},
512+
)
513+
if args.clean:
514+
clean_deleted_files(WORKTREE, exclude_paths, dest_root=ROOT, include_patterns=args.paths)
515+
if args.stage or (not args.show_diff and not args.dry_run):
516+
run(["git", "add", "-A"], cwd=ROOT)
443517

444518

445519
if __name__ == "__main__":

sync_config.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,17 @@
1212
}
1313
],
1414
"text_replacements": [
15+
{ "from": "https://github.com/o-murphy/py-ballisticcalc", "to": "https://github.com/dbookstaber/pyballistic" },
16+
{ "from": "https://o-murphy.github.io/py-ballisticcalc", "to": "https://dbookstaber.github.io/pyballistic" },
17+
{ "from": "o-murphy/py_ballisticcalc", "to": "dbookstaber/pyballistic" },
18+
{ "from": "o-murphy/py-ballisticcalc", "to": "dbookstaber/pyballistic" },
1519
{ "from": "py_ballisticcalc", "to": "pyballistic" },
16-
{ "from": "py-ballisticcalc", "to": "pyballistic" },
17-
{ "from": "https://github.com/o-murphy/py-ballisticcalc", "to": "https://github.com/dbookstaber/pyballistic" }
20+
{ "from": "py-ballisticcalc", "to": "pyballistic" }
1821
],
1922
"exclude_paths": [
2023
"README.md",
24+
"pyballistic.exts/README.md",
25+
"contributors.md",
2126
".gitignore"
2227
],
2328
"binary_patterns": [

0 commit comments

Comments
 (0)