Skip to content

Match .gitattributes semantics when asking which files a pin covers - #70

Merged
ptr727 merged 2 commits into
developfrom
fix-dead-pattern-matcher
Aug 9, 2026
Merged

Match .gitattributes semantics when asking which files a pin covers#70
ptr727 merged 2 commits into
developfrom
fix-dead-pattern-matcher

Conversation

@ptr727

@ptr727 ptr727 commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Found by Copilot review on #68. Its remedy was right and its evidence was not, and both halves are worth stating.

The claim, measured

In this repo, .gitattributes includes *.sh, but there are no root-level .sh files, so git ls-files -- '*.sh' will return nothing and this script will incorrectly flag *.sh as a dead pin (and fail CI).

It does not:

$ git ls-files -- '*.sh'
capture/run-wp2hugo.sh
checks/check-live-urls.sh
deploy/make-release.sh
ops/install.sh
repo-config/configure.sh

Bare pathspec lets * cross a /, so all five nested files match. That is also why CI was green rather than failing as predicted.

The real defect, which is the opposite direction

Pathspec and gitattributes genuinely do not share glob semantics, and the divergence that bites is the one the comment did not name: a slash-free pattern with no glob character. In gitattributes it matches its basename at any depth. In bare pathspec it is a root-relative path.

Ground truth from git check-attr, which is what git actually applies on checkout, against a tree holding Docker/Dockerfile, pkg/mod.py, pkg/sub/nested.py:

Pattern check-attr says eol=lf bare ls-files :(glob) converted
Dockerfile Docker/Dockerfile (nothing) Docker/Dockerfile
pkg/*.py pkg/mod.py pkg/mod.py, pkg/sub/nested.py pkg/mod.py

The bare form gets both wrong. The first row is the one with teeth: a live pin reported dead, failing CI on correct content. The second can only ever hide a dead pin, never invent one.

The fix

:(glob) gives * and ** their gitattributes meaning, and a **/ prefix supplies the any-depth match for a slash-free pattern. The conversion selects exactly the set check-attr resolves, on both rows.

Differential proof on that tree — same script, same repo, only the matcher swapped:

=== new matcher ===
FAIL - 1 line-ending pin finding(s)          <- the deliberately unpinned test file

=== old bare-pathspec matcher ===
error: dead: pattern 'Dockerfile' matches no tracked file
FAIL - 2 line-ending pin finding(s)          <- plus the false one

Scope

No pattern in this repository changes result, bare or converted — all twelve match identically, because nothing here is slash-free-and-literal today. This fixes a latent defect rather than a live one, and it matters most if the check is ever lifted into the hub, where .gitattributes carries exactly that shape (Dockerfile text eol=lf).

Reported upstream on ptr727/ProjectTemplate#633, where my original implementation note claimed the pathspec divergence "errs toward not reporting, which is the safe bias." That was half right, and the wrong half is this. Correcting it there.

The dead-pattern direction asked `git ls-files -- <pattern>`, and pathspec
does not share gitattributes glob semantics. It is wrong in both
directions:

  under  a slash-free pattern matches its basename at any depth in
         gitattributes, where bare pathspec reads it as a root-relative
         path. `Dockerfile` covers `Docker/Dockerfile`; bare pathspec
         returns nothing, so a live pin reports dead and reds CI.
  over   `*` does not cross a `/` in gitattributes and does in bare
         pathspec, so `pkg/*.py` wrongly picks up `pkg/sub/nested.py`.
         Harmless here, since it can only hide a dead pin.

The first is the one with teeth: a false failure on a correct pin.

`:(glob)` gives `*` and `**` their gitattributes meaning, and a `**/`
prefix supplies the any-depth match for a slash-free pattern.

Verified against `git check-attr`, which is what git actually applies,
rather than against the documentation. On a tree holding
`Docker/Dockerfile`, `pkg/mod.py` and `pkg/sub/nested.py`, check-attr
resolves eol=lf for exactly the first two, and the conversion selects
exactly those two where the bare form selects the wrong set both times.
Differentially: on that tree the old matcher reports `Dockerfile` dead
and the new one does not.

No pattern in this repository changes result, bare or converted, so this
fixes a latent defect rather than a live one. Nothing here is
slash-free-and-literal today.

Found by Copilot review on #68. Its remedy was right and its evidence was
not: it predicted `*.sh` would be flagged dead here for want of a
root-level `.sh` file, and `git ls-files -- '*.sh'` returns all five
nested ones, because bare pathspec lets `*` cross a `/`. That is the
over-match above, and the reason CI was green rather than failing as the
comment predicted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the .gitattributes “dead pin” check in checks/check-eol-pins.py so that it queries tracked files using pathspec glob semantics that better match how .gitattributes patterns are interpreted, avoiding false “dead pattern” failures in CI for certain pattern shapes.

Changes:

  • Introduces pathspec_for() to translate a .gitattributes pattern into an equivalent git ls-files pathspec using :(glob) and (when appropriate) a **/ prefix.
  • Switches the dead-pattern check to call git ls-files with the converted pathspec instead of the raw pattern.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread checks/check-eol-pins.py
`/Dockerfile` is root-anchored and covers only the root file, where a bare
`Dockerfile` matches at any depth. `pathspec_for` stripped the leading
slash and then tested the remainder for one, which destroys the evidence
the decision needs, so it built `:(glob)**/Dockerfile` and matched
`Docker/Dockerfile` too. Verified against check-attr, which resolves
eol=lf for the root file alone. Anchoring is now decided before the strip.

That is the second conversion defect in this function, one under-matching
and one over-matching, and both were found by review rather than by the
gate. Fixing each shape as it surfaces leaves the next one to be found the
same way, so the matcher is now checked against `git check-attr`, which is
the thing it exists to predict:

  every file resolving to eol=lf is matched by some converted pattern, and
  every file a converted pattern matches resolves to eol=lf.

A missing file means the conversion under-matched, which is what produces
a false dead report. An extra file means it over-matched, which hides a
real one. Both are reported as matcher findings rather than as pin
findings, because they are defects in this script and not in the pins.

Verified by reintroducing each defect against a tree holding
`Docker/Dockerfile`, a root `Dockerfile`, `pkg/mod.py` and
`pkg/sub/nested.py`. The bare-pathspec version is caught in both
directions at once, the leading-slash version as an over-match, and with
the matcher correct the third direction reports nothing on the same tree.

This holds because no pattern here clears `eol` once another has set it,
which the comment records, since a future pattern that unsets it would
need the comparison to account for precedence.

Found by Copilot review on #70.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 9, 2026 01:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

@ptr727
ptr727 merged commit d962e7b into develop Aug 9, 2026
5 checks passed
@ptr727
ptr727 deleted the fix-dead-pattern-matcher branch August 9, 2026 02:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants