Skip to content

fix: six correctness issues in author matching, excludes and lead time - #7

Merged
juangracia merged 2 commits into
mainfrom
fix/audit-findings
Aug 17, 2026
Merged

juangracia merged 2 commits into
mainfrom
fix/audit-findings

Conversation

@juangracia

@juangracia juangracia commented Aug 17, 2026 •

Copy link
Copy Markdown
Owner

Six findings from an adversarial review pass. Each was reproduced before fixing and has a regression test.

HIGH

1. Author matching was case-sensitive, producing a silent zero

--fixed-strings does not fold case. A repo whose ident reads Alice@Corp.com:

$ gitrespect -a Alice@Corp.com    ->  14 added, 1 commit
$ gitrespect -a alice@corp.com    ->   0 added, 0 commits, exit 0, no stderr

Email domains are case-insensitive by spec and local parts are in practice, so people type their own address lowercase. The result was indistinguishable from "you did no work this period". --regexp-ignore-case now composes with --fixed-strings.

2. An uncompilable --exclude glob excluded nothing

filepath.Match reports a bad pattern as ErrBadPattern rather than a non-match, and all three call sites discarded the error:

$ gitrespect -e '[invalid'    ->  14 added, identical to no exclude, exit 0

Patterns are now validated once at flag-parse time and rejected loudly.

3. authoredLeadTime selected exactly the wrong commits

A committer date later than the author date identifies a rewritten commit, not a reviewed one. Cherry-picks, amends, rebases and whole-history rewrites all qualify, while commits that went straight to main have ct == at and were excluded. The sample was biased entirely toward rewritten history.

A repo with two healthy same-day commits plus one cherry-pick of two-year-old work:

Lead time (branch → main): Median 732.0 days (1 commit, authored → landed)

A repo that has run filter-repo rewrites the committer date on every commit, so every sample would be garbage.

Two guards: gaps longer than the analysed period are discarded, since work that took longer than the window did not flow through it, and a median now requires at least three samples. Verified both directions — a genuine rebase repo with four commits landing two days after authoring still reports Median 2.0 days (4 commits), while the cherry-pick case now reports no signal.

MEDIUM

4. dir/** excluded nothing while dir/* excluded everything

-e 'vendor/*'   -> 10 added  (correctly drops the whole subtree)
-e 'vendor/**'  -> 14 added  (drops nothing)

filepath.Match has no globstar, and the directory fast path that makes the single star recursive did not fire for **. Backwards from gitignore intuition: the more explicit pattern was the one that silently did nothing. ** now folds to *.

5. A leading ./ made a pattern a no-op

-e './vendor/*' excluded nothing. numstat paths are repo-relative with no ./, and the directory branch split on the first / giving . as the prefix, which never matched. Tab-completing a path is the obvious way to produce this. Patterns are now cleaned.

6. Non-ASCII filenames defeated every exclude

core.quotePath defaults on, so git wraps such paths in double quotes with octal escapes:

2	0	"caf\303\251.txt"
8	0	wk.txt

The surrounding quotes make every glob miss, so -e '*.txt' left 2 lines counted. All three numstat readers now go through a shared git.LogArgs that sets core.quotePath=false.

LOW

A daily average below 0.5 printed as a flat 0 lines/day because of %.0f. That is the headline number and "0" reads as "shipped nothing"; small rates now show a decimal.

Confirmed clean during the same pass

Worth recording since these were probed hard and held up:

  • Week bucketing. (int(t.Weekday())+6)%7 is a correct Monday anchor; keys sort lexically and chronologically across a year boundary; no DST exposure because only calendar dates are used. Verified across Mon 2025-12-29 → Mon 2026-01-05.
  • <> anchoring cannot be defeated by a crafted name. Committing as GIT_AUTHOR_NAME='Mallory <alice@corp.com>' makes git store Mallory alice@corp.com <mallory@evil.com>; the angle brackets are stripped from the name field, so the impersonation never reaches the searched string.
  • Mailmap. --author matches the mailmap-resolved ident, so a .mailmap unifying two addresses is honoured. Desirable, but it means totals depend on each repo's .mailmap when -r aggregates across repos. Worth documenting later.

Verification

  • go build, go vet, go test, gofmt -l clean
  • 20 JSON paths parse with no NaN/Infinity; 7 HTML paths render with no unrendered directives
  • Core fixture numbers unchanged across the whole scenario matrix
  • Merge-commit lead time still reports via the merge path, unchanged

Follow-up in this PR: a Windows regression the CI caught

The first pass normalised patterns with filepath.Clean, whose separator is a backslash on Windows. That rewrote vendor/* to vendor\*, matching nothing, so --exclude stopped working entirely on that platform:

--- FAIL: TestAnalyzeExcludePatterns
    Added with exclude = 110, want 10

git reports forward-slash paths on every platform, so pattern handling now uses path rather than filepath throughout; filepath stays where it belongs, for real filesystem walks. Removing the shadow also surfaced that IsGitRepo and FindRepos took a parameter named path, now dir.

This is the cross-platform CI added in #4 paying for itself on its first real test.

Found by an adversarial review pass and each reproduced before fixing.

Author matching was case-sensitive. --fixed-strings does not fold case, so
a repo whose ident reads "Alice@Corp.com" returned 0 added, 0 commits and
exit status 0 for anyone typing their address in lower case. Email domains
are case-insensitive by spec and local parts are in practice, so this was a
confident silent zero, indistinguishable from having shipped nothing.
--regexp-ignore-case now composes with --fixed-strings.

An uncompilable --exclude glob excluded nothing, silently. filepath.Match
reports a bad pattern as an error rather than a non-match and every call
site discarded it, so `-e '[invalid'` produced totals identical to no
exclude at all. Patterns are now validated once at flag-parse time.

"vendor/**" excluded nothing while "vendor/*" excluded the whole subtree.
filepath.Match has no globstar, and the directory fast path that makes the
single star recursive did not fire for the double star. The more explicit
pattern was the one that quietly did nothing. "**" now folds to "*", and a
leading "./" is stripped, which previously made './vendor/*' a no-op.

Non-ASCII filenames defeated every exclude. core.quotePath defaults on, so
git wraps such paths in double quotes with octal escapes
("caf\303\251.txt") and the surrounding quotes make globs miss. All three
numstat readers now go through git.LogArgs, which disables it.

authoredLeadTime selected the wrong commits. A committer date later than
the author date identifies a rewritten commit, not a reviewed one, so
cherry-picks, amends and history rewrites qualified while commits that went
straight to main were excluded. A repo with two healthy same-day commits
and one cherry-pick of two-year-old work reported "Median 732.0 days". Gaps
longer than the analysed period are now discarded and a median needs at
least three samples, which keeps genuine rebase workflows working while
suppressing rewritten history.

Also: a daily average below 0.5 printed as a flat "0 lines/day" because of
%.0f, which reads as "shipped nothing" on the headline number.
@juangracia juangracia self-assigned this Aug 17, 2026
The previous commit normalised exclude patterns with filepath.Clean, whose
separator is a backslash on Windows. That rewrote "vendor/*" to "vendor\*",
which matched nothing, so --exclude stopped working entirely there. CI
caught it: TestAnalyzeExcludePatterns reported 110 added instead of 10.

git reports forward-slash paths on every platform, so pattern handling now
uses path rather than filepath throughout. filepath remains where it
belongs, for real filesystem walks.

Also renames the IsGitRepo and FindRepos parameters from "path" to "dir".
They shadowed the newly imported package, which the compiler surfaced as
soon as the shadow was removed.
@juangracia
juangracia merged commit 2d3b796 into main Aug 17, 2026
5 checks passed
@juangracia
juangracia deleted the fix/audit-findings branch August 17, 2026 20:28
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.

1 participant