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
34 changes: 33 additions & 1 deletion tests/copyright_gate_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ git -C "$tmp" commit -qm base

# captured into a variable, never piped: `gate | grep -q` would make grep close the pipe early and, with pipefail,
# report the gate's SIGPIPE instead of its verdict
NOTICE_BLOCK=$(python3 - "$tmp/copyright.py" <<'PYN'
import sys
src = open(sys.argv[1]).read()
notice = src.split('NOTICE = """')[1].split('""".split')[0]
print("".join("# " + l + "\n" if l.strip() else "#\n" for l in notice.splitlines()), end="")
PYN
)
gate() { ( cd "$tmp" && python3 copyright.py --check 2>&1 ); }
gate_has() { case "$(gate)" in *"$1"*) return 0;; *) return 1;; esac; }

Expand Down Expand Up @@ -137,5 +144,30 @@ check "near-miss beside an existing notice leaves exactly two copyright lines" \
check "and the near-miss spelling is the one that went" "[ $(grep -c 'bong-water-bong$' "$both") -eq 0 ]" "$(grep 'Copyright' "$both")"
rm -f "$both"; git -C "$tmp" update-index --force-remove both.py 2>/dev/null

# 2. the two-liner without the licence block: --fix must leave ONE notice, not a copy of the pair beside the block
# (engine#2129 -- --fix used to prepend the block, keep the two-liner, and then fail its own --check with "duplicated
# inside the header window", so the file it had just written was invalid and neither mode said so).
printf '# Copyright 2026 bong-water-bong\n# SPDX-License-Identifier: Apache-2.0\n\nx = 1\n' > "$tmp/twoliner.py"
git -C "$tmp" add twoliner.py
( cd "$tmp" && python3 copyright.py --fix >/dev/null 2>&1 )
check "--fix on a two-liner leaves a file that its own --check accepts" \
"! ( cd \"$tmp\" && python3 copyright.py --check 2>&1 | grep -q duplicated )" \
"$( cd "$tmp" && python3 copyright.py --check 2>&1 )"
check "...and exactly one SPDX line is left in the window" \
"[ \$(head -20 \"$tmp/twoliner.py\" | grep -c 'SPDX-License-Identifier') -eq 1 ]" \
"window: $(head -20 "$tmp/twoliner.py" | grep -c 'SPDX-License-Identifier') SPDX line(s)"

# 3. the block pasted twice: --fix must leave ONE notice. This is #2192's second shape -- `--fix` printed `repaired:` and
# left the copy in place, so the file it had just written still failed the very check that sent it there.
printf '%s%s\nx = 1\n' "$NOTICE_BLOCK" "$NOTICE_BLOCK" > "$tmp/doubled.py"
git -C "$tmp" add doubled.py
( cd "$tmp" && python3 copyright.py --fix >/dev/null 2>&1 )
check "--fix on a doubled block leaves a file that its own --check accepts" \
"! ( cd \"$tmp\" && python3 copyright.py --check 2>&1 | grep -q duplicated )" \
"$( cd "$tmp" && python3 copyright.py --check 2>&1 )"
check "...and exactly one SPDX line survives the repair" \
"[ \$(grep -c 'SPDX-License-Identifier' \"$tmp/doubled.py\") -eq 1 ]" \
"$(grep -c 'SPDX-License-Identifier' "$tmp/doubled.py") SPDX line(s)"

echo "$([ $fail -eq 0 ] && echo PASS || echo FAIL)"
exit $((fail))
exit $((fail))
31 changes: 30 additions & 1 deletion tools/copyright.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -229,15 +229,35 @@ def near_miss_holder(line):
return n >= HOLDER_NEAR_MISS_PREFIX


_CANON_LINES = {re.sub(r"^\W+", "", n).strip() for n in NOTICE}


def canonical_notice_line(line):
"""True when the line IS one of the canonical notice lines, spelled exactly.

engine#2129, met while fixing a new file's header: an SPDX two-liner without the licence block is "missing" to
has_notice(), so --fix prepends the block -- and the two-liner it just merged into that block stayed behind, so the
file --fix had just written failed --check with "duplicated inside the header window". near_miss_holder() cannot
cover it by construction: it returns False for an exact holder, because an exact holder next to the canonical block
is the *repaired* state. The block already contains both lines, so a copy inside the window is not a second claim.
"""
return re.sub(r"^\W+", "", line).strip() in _CANON_LINES


def repair_notice(text, block):
"""The file already HAS the canonical notice, plus a near-miss of our holder: delete the misspelling.

This is the state engine#1606 shipped - canonical block present, hand-written `bong-water-bong` beside it -
and it needed its own path because it is neither "notice missing" (has_notice() is true, so --fix skipped
the file) nor "duplicate canonical pair" (duplicated_notice() only reported it, even under --fix). Dropping
the copyright line and the SPDX line directly beneath it keeps a near-miss pair from leaving an orphan.

It also keeps the FIRST canonical notice and drops later copies of the same line, which is the doubled-block shape:
--fix used to answer `repaired:` for that state while `--check` went on reporting the duplicate, i.e. the one mode
that could fix it claimed to and did not.
"""
out, drop_next_spdx = [], False
seen_canonical = set()
for line in text.splitlines(keepends=True):
if near_miss_holder(line):
drop_next_spdx = True
Expand All @@ -246,6 +266,15 @@ def repair_notice(text, block):
drop_next_spdx = False
continue
drop_next_spdx = False
# A SECOND canonical notice is the same defect as a near-miss beside the first: the block pasted twice. `--check`
# has always reported it (`duplicated_notice`); `--fix` used to print "repaired" and leave the copy in place, so the
# file it had just written still failed the very check that sent it there. The first occurrence is kept and later
# ones dropped, which is what "delete the second copy" means.
if canonical_notice_line(line):
key = re.sub(r"^\W+", "", line).strip()
if key in seen_canonical:
continue
seen_canonical.add(key)
out.append(line)
repaired = "".join(out)
return repaired if has_notice(repaired) else add_notice(repaired, block)
Expand All @@ -258,7 +287,7 @@ def add_notice(text, block):
# block and leave the hand-written one in place, which is how a file ended up with two notices and a green
# `--check` (engine#1606). Dual attribution with a different holder is untouched - only the window is read.
head = lines[:NOTICE_WINDOW]
kept = [l for l in head if not near_miss_holder(l)]
kept = [l for l in head if not near_miss_holder(l) and not canonical_notice_line(l)]
lines = kept + lines[NOTICE_WINDOW:]
return "".join(lines[:insert_at]) + block + "".join(lines[insert_at:])

Expand Down
Loading