1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 02:59:00 +03:00

header update

This commit is contained in:
Anton Afanasyeu
2026-05-20 17:24:50 +02:00
parent 5d8e82d2e6
commit ee4947a348
241 changed files with 1214 additions and 783 deletions

View File

@@ -12,6 +12,7 @@ from header_lib import (
apply_header_to_content,
has_valid_header,
header_needs_identity_refresh,
hook_skip_requested,
is_excluded_path,
resolve_header_contributor,
update_header_timestamps,
@@ -48,6 +49,9 @@ def is_new_file(path: Path) -> bool:
def main() -> int:
if hook_skip_requested():
return 0
errors = []
contributor = resolve_header_contributor()
for path in staged_files():

View File

@@ -16,6 +16,7 @@ BANNER_START = "/***************************************************************
BANNER_END = "**********************************************************************/"
MARKER_CREATED = "Created at:"
MARKER_UPDATED = "Updated at:"
MARKER_COMMIT = "Commit:"
MARKER_DIGEST = "Digest:"
MARKER_CONTRIBUTORS = "Contributors:"
MARKER_CLASS = re.compile(r"^\s*\*?\s*(class|interface|enum)\s+(\w+)", re.I)
@@ -62,6 +63,7 @@ class FileMeta:
last_contributor: str
contributors_block: str
language: str
commit_sha: str = ""
def iter_source_files(root: Path) -> Iterable[Path]:
@@ -151,6 +153,45 @@ def format_identity(name: str, email: str = "") -> str:
return name
def hook_skip_requested() -> bool:
import os
return os.environ.get("HEADER_HOOK_SKIP", "").strip() in {"1", "true", "yes"}
def git_head_sha() -> str:
return run_git(["rev-parse", "HEAD"], REPO_ROOT)
def git_last_commit_sha_for_file(path: Path) -> str:
rel = repo_relative(path)
return run_git(["log", "-1", "--format=%H", "--", rel], REPO_ROOT)
def parse_commit_from_header(text: str) -> str:
m = re.search(r"\* Commit:\s*(\S+)", text)
return m.group(1).strip() if m else ""
def files_in_git_commit(commit: str = "HEAD") -> list[Path]:
out = run_git(
["diff-tree", "--no-commit-id", "--name-only", "-r", commit],
REPO_ROOT,
)
paths: list[Path] = []
for line in out.splitlines():
rel = line.strip()
if not rel:
continue
p = REPO_ROOT / rel
if not p.is_file() or p.suffix.lower() not in SOURCE_SUFFIXES:
continue
if is_excluded_path(p):
continue
paths.append(p)
return paths
def format_git_date(raw: str) -> str:
if not raw:
return datetime.now().astimezone().strftime("%a %d %b %Y %H:%M:%S %z")
@@ -332,18 +373,45 @@ def package_line_for_file(source_text: str, rel_path: str, suffix: str) -> str:
return f"/* package {rel_path} */"
def strip_existing_header_php(text: str) -> str:
"""Remove <?php, block-comment header(s), and closing ?> before real body."""
work = text.lstrip("\ufeff")
while True:
changed = False
w = work.lstrip()
if w.startswith("<?php"):
work = w[5:].lstrip("\n")
changed = True
w = work.lstrip()
if w.startswith("?>"):
work = w[2:].lstrip("\n")
changed = True
w = work.lstrip()
if w.startswith("/*") or w.startswith("/*****"):
close = w.find("*/")
if close >= 0:
work = w[close + 2 :].lstrip("\n")
changed = True
else:
break
if not changed:
break
return work.lstrip("\n")
def php_body_is_markup(body: str) -> bool:
"""True when body is HTML/templates after the commented header (needs ?>)."""
s = body.lstrip()
return bool(s) and s[0] == "<"
def strip_existing_header(text: str, suffix: str) -> tuple[str, str]:
"""Returns (body without header/package, preserved leading e.g. <?php)."""
if suffix == ".php":
return strip_existing_header_php(text), ""
leading = ""
work = text
if suffix == ".php" and work.startswith("<?php"):
first_nl = work.find("\n")
if first_nl >= 0:
leading = work[: first_nl + 1]
work = work[first_nl + 1 :]
else:
leading = work + "\n"
work = ""
lines = work.splitlines(keepends=True)
i = 0
@@ -392,6 +460,7 @@ def build_meta(path: Path, body: str, source_text: str = "") -> FileMeta:
lang = "java" if suffix in {".java", ".aidl"} else ("php" if suffix == ".php" else "c")
class_line = detect_class_line(body, suffix)
commit_sha = parse_commit_from_header(source_text) or git_last_commit_sha_for_file(path)
return FileMeta(
rel_path=rel,
filename=path.name,
@@ -402,34 +471,83 @@ def build_meta(path: Path, body: str, source_text: str = "") -> FileMeta:
last_contributor=author,
contributors_block=contributors_block(rows),
language=lang,
commit_sha=commit_sha,
)
def render_header(meta: FileMeta, digest_hex: str | None) -> str:
def _inner_header_lines(meta: FileMeta, *, for_digest: bool) -> list[str]:
"""Banner lines shared by PHP and C-style headers."""
lines = [
meta.package_line,
"",
BANNER_START,
f" * {meta.filename}",
f" * {MARKER_CREATED} {meta.created_at}",
f" * {MARKER_UPDATED} {meta.updated_at} by {meta.last_contributor}",
]
if not for_digest and meta.commit_sha:
lines.append(f" * {MARKER_COMMIT} {meta.commit_sha}")
if meta.class_line:
lines.append(meta.class_line)
lines.append(f" * {MARKER_CONTRIBUTORS}")
lines.append(meta.contributors_block)
if digest_hex:
lines.extend(meta.contributors_block.splitlines())
return lines
def render_php_header(meta: FileMeta, digest_hex: str | None, *, for_digest: bool = False) -> str:
"""
PHP: header is one block comment inside <?php ... ?> so the engine ignores it.
No nested '/***** ... */' banners (inner */ would break the comment).
"""
inner = [f" * package {meta.rel_path}"]
inner.extend(_inner_header_lines(meta, for_digest=for_digest))
if digest_hex and not for_digest:
inner.append(f" * {MARKER_DIGEST} SHA256\t{digest_hex}")
return "<?php\n/*\n" + "\n".join(inner) + "\n */\n"
def render_header(
meta: FileMeta, digest_hex: str | None, suffix: str = "", *, for_digest: bool = False) -> str:
if suffix == ".php" or meta.language == "php":
return render_php_header(meta, digest_hex, for_digest=for_digest)
lines = [
meta.package_line,
"",
BANNER_START,
*_inner_header_lines(meta, for_digest=for_digest),
]
if digest_hex and not for_digest:
lines.append(f" * {MARKER_DIGEST} SHA256\t{digest_hex}")
lines.append(BANNER_END)
lines.append("")
return "\n".join(lines)
def compute_digest(prefix: str, body: str) -> str:
payload = prefix + body
def wrap_php_header_and_body(header: str, body: str) -> str:
"""
Pure PHP: <?php /* header */ code... (declare stays first statement after <?php).
Templates: <?php /* header */ ?> then HTML.
"""
body = body.lstrip("\n")
if body.startswith("<?php"):
body = body[5:].lstrip("\n")
if php_body_is_markup(body):
return header + "?>\n" + body
return header + body
def compute_digest(header_prefix: str, body: str) -> str:
"""Hash file body + header without Digest/Commit lines."""
payload = header_prefix + body
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
def finalize_header(meta: FileMeta, body: str, suffix: str, leading: str = "") -> str:
prefix_for_digest = render_header(meta, None, suffix, for_digest=True)
digest = compute_digest(prefix_for_digest, body)
prefix = render_header(meta, digest, suffix, for_digest=False)
if suffix == ".php":
return wrap_php_header_and_body(prefix, body)
return leading + prefix + body
def apply_header_to_content(path: Path, text: str, update_only: bool = False) -> str:
suffix = path.suffix.lower()
body, leading = strip_existing_header(text, suffix)
@@ -440,14 +558,15 @@ def apply_header_to_content(path: Path, text: str, update_only: bool = False) ->
if m:
meta.created_at = m.group(1).strip()
prefix_without_digest = render_header(meta, None)
digest = compute_digest(prefix_without_digest, body)
prefix = render_header(meta, digest)
return leading + prefix + body
return finalize_header(meta, body, suffix, leading)
def has_valid_header(text: str) -> bool:
return MARKER_CREATED in text and BANNER_START in text and BANNER_END in text
if MARKER_CREATED not in text:
return False
if text.lstrip().startswith("<?php") and "/*" in text[:800]:
return "*/" in text[:1200]
return BANNER_START in text and BANNER_END in text
def header_needs_identity_refresh(text: str, path: Path) -> bool:
@@ -475,7 +594,8 @@ def header_needs_identity_refresh(text: str, path: Path) -> bool:
return False
def update_header_timestamps(text: str, path: Path, contributor: str | None = None) -> str:
def update_header_timestamps(
text: str, path: Path, contributor: str | None = None, *, touch_commit: bool = False) -> str:
suffix = path.suffix.lower()
body, leading = strip_existing_header(text, suffix)
if contributor:
@@ -492,8 +612,27 @@ def update_header_timestamps(text: str, path: Path, contributor: str | None = No
meta.created_at = created
meta.updated_at = updated_line
meta.last_contributor = who
if touch_commit:
meta.commit_sha = git_head_sha()
else:
meta.commit_sha = parse_commit_from_header(text)
prefix_without_digest = render_header(meta, None)
digest = compute_digest(prefix_without_digest, body)
prefix = render_header(meta, digest)
return leading + prefix + body
return finalize_header(meta, body, suffix, leading)
def update_header_commit(text: str, path: Path, commit_sha: str) -> str:
"""Set Commit: line after post-commit; recomputes Digest (Commit excluded from hash)."""
if parse_commit_from_header(text) == commit_sha:
return text
suffix = path.suffix.lower()
body, leading = strip_existing_header(text, suffix)
meta = build_meta(path, body, text)
m_created = re.search(r"\* Created at: (.+)", text)
m_updated = re.search(r"\* Updated at: (.+?) by (.+)$", text, re.MULTILINE)
if m_created:
meta.created_at = m_created.group(1).strip()
if m_updated:
meta.updated_at = m_updated.group(1).strip()
meta.last_contributor = m_updated.group(2).strip()
meta.commit_sha = commit_sha
return finalize_header(meta, body, suffix, leading)

View File

@@ -1,9 +1,11 @@
#!/usr/bin/env bash
# Install repo git hooks (header pre-commit).
# Install repo git hooks (header pre-commit + post-commit).
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
git -C "$ROOT" config core.hooksPath .githooks
chmod +x "$ROOT/.githooks/pre-commit"
chmod +x "$ROOT/.githooks/post-commit"
chmod +x "$ROOT/scripts/header-pre-commit.py"
chmod +x "$ROOT/scripts/header-post-commit.py"
chmod +x "$ROOT/scripts/apply-project-headers.py"
echo "Installed hooks via core.hooksPath=.githooks"
echo "Installed hooks via core.hooksPath=.githooks (pre-commit + post-commit)"

View File

@@ -8,12 +8,15 @@ from pathlib import Path
from header_lib import (
apply_header_to_content,
compute_digest,
format_identity,
has_valid_header,
is_excluded_path,
parse_identity,
render_header,
resolve_header_contributor,
strip_existing_header,
build_meta,
)
from pathlib import Path
import header_lib
@@ -39,15 +42,41 @@ public class Foo {
self.assertIn("public class Foo", body)
self.assertNotIn("package com.example", body.split("public class")[0])
def test_strip_php_opener(self) -> None:
def test_php_header_wrapped_in_comment(self) -> None:
src = "<?php\ndeclare(strict_types=1);\n"
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "bootstrap.php"
path.write_text(src, encoding="utf-8")
out = apply_header_to_content(path, src)
self.assertTrue(out.startswith("<?php\n"))
self.assertIn("/* package bootstrap.php */", out)
self.assertTrue(out.startswith("<?php\n/*\n"))
self.assertIn("*/\ndeclare(strict_types=1)", out)
self.assertNotIn("?>\n<?php", out)
self.assertNotIn("/*********************************************************************", out)
body, _ = strip_existing_header(out, ".php")
self.assertTrue(body.startswith("declare(strict_types=1)"))
def test_php_html_template(self) -> None:
src = "<!DOCTYPE html>\n<html></html>\n"
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "page.php"
path.write_text(src, encoding="utf-8")
out = apply_header_to_content(path, src)
self.assertIn("?>\n<!DOCTYPE", out)
self.assertNotIn("<?php\n<!DOCTYPE", out)
def test_commit_excluded_from_digest(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "X.java"
path.write_text("package p;\npublic class X {}\n", encoding="utf-8")
body, _ = strip_existing_header(
apply_header_to_content(path, path.read_text()), ".java")
meta = build_meta(path, body, path.read_text())
meta.commit_sha = "aaa"
d1 = compute_digest(render_header(meta, None, ".java", for_digest=True), body)
meta.commit_sha = "bbb"
d2 = compute_digest(render_header(meta, None, ".java", for_digest=True), body)
self.assertEqual(d1, d2)
def test_format_identity(self) -> None:
self.assertEqual("Ada <a@b.c>", format_identity("Ada", "a@b.c"))