Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion skills/update-copyright/scripts/update_copyright.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,11 +379,26 @@ def strip_existing_header(text: str, style: str) -> tuple[str, bool]:
return strip_leading_blank_lines(text[close + 3 :]), True

if style == "hash":
# A freshly rendered header (see build_header) never has a truly
# empty line inside it — blank notice-paragraph separators render as
# a bare "#" — so a blank line found after the accumulated candidate
# already reads as a copyright header marks the end of that header,
# and anything past it (e.g. a doc comment separated by one blank
# line) must not be swallowed. But some legacy/hand-written headers
# use a genuine blank line as their own internal paragraph
# separator, before "Licensed under"/"All rights reserved" has
# appeared yet; stopping unconditionally at the first blank line
# would leave those headers unrecognized (and so never re-stamped).
# Only treat a blank line as the header's end once the text seen so
# far already satisfies is_copyright_header; otherwise keep going.
lines = text.splitlines(keepends=True)
end = 0
for line in lines:
stripped = line.strip()
if stripped == "" or stripped.startswith("#"):
if stripped.startswith("#"):
end += len(line)
continue
if stripped == "" and not is_copyright_header(text[:end]):
end += len(line)
continue
break
Comment thread
alexander-yevsyukov marked this conversation as resolved.
Expand Down
79 changes: 79 additions & 0 deletions skills/update-copyright/tests/test_update_copyright.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,85 @@ def test_existing_header_is_updated(self) -> None:
"class Foo {}\n",
)

def test_hash_header_update_preserves_doc_comment_after_blank_line(self) -> None:
# Regression test: a hash-style header followed by a blank line and
# then an unrelated doc comment (e.g. script usage notes) must keep
# that doc comment. The header-detection loop must stop at the first
# blank line, since blank notice-paragraph separators are rendered
# as a bare "#", never as a truly empty line.
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
self.write_profile(root)
source = root / "run.sh"
source.write_text(
"#!/usr/bin/env bash\n"
"\n"
"# Copyright 2024 ACME\n"
"# All rights reserved\n"
"\n"
"# Does something useful.\n"
"#\n"
"# Params:\n"
"# 1. A thing.\n"
"\n"
"echo hello\n",
encoding="utf-8",
)

result = self.run_script(root, "--year", "2026", "run.sh")

self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("Updated 1 file(s).", result.stdout)
self.assertEqual(
source.read_text(encoding="utf-8"),
"#!/usr/bin/env bash\n"
"\n"
"# Copyright 2026 ACME\n"
"# All rights reserved\n"
"\n"
"# Does something useful.\n"
"#\n"
"# Params:\n"
"# 1. A thing.\n"
"\n"
"echo hello\n",
)

def test_hash_header_with_real_blank_line_separator_is_still_updated(self) -> None:
# Regression test: some legacy/hand-written headers use a genuine
# blank line as their own internal paragraph separator (rather than
# the bare "#" a freshly rendered header uses). The header-detection
# loop must not stop at that blank line before "All rights reserved"
# has been seen, or the stale header is silently left unchanged.
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
self.write_profile(root)
source = root / "run.sh"
source.write_text(
"#!/usr/bin/env bash\n"
"\n"
"# Copyright 2024 ACME\n"
"\n"
"# All rights reserved\n"
"\n"
"echo hello\n",
encoding="utf-8",
)

result = self.run_script(root, "--year", "2026", "run.sh")

self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("Updated 1 file(s).", result.stdout)
self.assertEqual(
source.read_text(encoding="utf-8"),
"#!/usr/bin/env bash\n"
"\n"
"# Copyright 2026 ACME\n"
"# All rights reserved\n"
"\n"
"echo hello\n",
)

def test_default_run_skips_tracked_files_deleted_from_working_tree(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
Expand Down