Skip to content

Say which git-fi runs, and how to replace a token - #12

Draft
chris-peterson wants to merge 2 commits into
mainfrom
leftover-gem-binstubs
Draft

Say which git-fi runs, and how to replace a token#12
chris-peterson wants to merge 2 commits into
mainfrom
leftover-gem-binstubs

Conversation

@chris-peterson

@chris-peterson chris-peterson commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Context

git-fi shipped as a Ruby gem until 0.9.x and now ships on npm. A user migrating across removed the gem's directory and gemspec by hand, which left the launcher RubyGems had written into Ruby's bin directory — a directory earlier on his PATH than npm's prefix. git fi kept reaching that dead launcher while the npm install sat behind it, complete and unreachable. Nothing could tell him: the copy that would notice is the copy not being run. Then, once that was fixed, his two-year-old GITLAB_ACCESS_TOKEN turned out to have expired, and the 401 offered exactly one remedy — turn CI status off — so he went looking for a doc on where GitLab tokens come from.

This adds a requirement for each (INSTALL-01, AUTH-13), plus the Windows CI coverage that made the first one designable.

Review guide

Tier 1 — the new behavior

  • src/which.tsshadowingLauncher is the whole idea: resolve git-fi on PATH, compare it to the copy running, return the winner only when they differ. isSameInstall is the part worth a second look, and launcherTarget is what makes it work on Windows — see Approach below for why identity isn't the package name.
  • src/index.ts — the only call site. --version and nothing else.
  • src/gitlab.tsrejectedTokenMessage. The prefilled-form URL already existed for --auth=login (AUTH-10); this puts it on the 401 too, sharing REQUIRED_SCOPE with auth.ts so the sentence and the URL's scopes= can't disagree.
  • src/style.tshintsOptedOut splits the explicit GIT_FI_NO_HINTS switch out of hintsEnabled. This notice wants the switch without the ambient conditions (CI, a pipe), and that's one gate composed two ways rather than the env var read in two places.

Tier 2 — the CI that proves it

  • .github/workflows/ci.yml — a windows-install job. The existing matrix runs windows-latest, but the suite drives the compiled binary out of the checkout, so nothing ever installed globally or reached git-fi by the name a shell resolves. This installs the packed tarball, checks git fi reaches it, then plants a stale launcher ahead of it and checks the report names it. The no-shadow assertion is the one guarding the other direction — without it, an identity check that regressed to flagging every install as shadowed by its own launcher would pass. Windows PowerShell rather than pwsh, because that's the shell the report came from.

Tier 3 — spec, ledger, docs

  • SPEC.md INSTALL-01 and AUTH-13.
  • docs/quickstart.md — the migration callout now says why gem uninstall beats deleting the files, and Windows guidance moved into its own callout.
  • STATUS.md / SPEC.md headings — the ID prefix is now the heading with the category name beneath, so a section is shareable as #perf rather than #performance. Ledger is 121 → 123, still all Covered.

Approach & trade-offs

Install identity is "does the launcher's target resolve to the copy answering", not "does the launcher mention this package". One rule reached two ways: on POSIX npm links its launcher as a symlink to the entry point, so resolving both settles it; on Windows the launcher is a generated wrapper, so the target is read out of the script it is. Matching the package name instead would suppress the notice in the case it exists for — two npm prefixes (an nvm/fnm switch) each hold a launcher naming this same package, and the second would read as the copy running. It also keeps a package-name constant from drifting against package.json.

The check is not in the installer, where it was first proposed. npm has run lifecycle scripts in the background since v7, so postinstall output is hidden unless --foreground-scripts is passed. A warning printed there would go where the existing COMPLETE-07 completion message already goes — nowhere. That's a separate pre-existing defect this turned up, and it needs its own fix.

Only --version reports. A second git-fi on PATH isn't a fault on its own, and a line on every run would be noise to anyone who has a reason for it. --version is where someone is already asking which copy they have. It writes to stderr, so stdout stays a bare version string.

Four SPEC sections kept their names. SPEC.md is organised by feature, STATUS.md by prefix, so BRANCH is declared under both Commands and Default Branch Detection, MERGE under both Merge Process and CI Integration, and COMMAND across four per-command sections. A slug heading in each would collide on exactly the anchor it exists to provide. Making them unique means reorganising SPEC.md, which is a bigger change than a heading rename.

Testing

28 new tests — 20 for INSTALL-01, 8 for AUTH-13; suite is 166 green on both platforms. Two are worth naming: a second npm prefix must read as a different install, and the 401 message must carry no colour of its ownabort wraps what it's given in red, and an inner SGR reset ends that colour for every line after it.

Windows naming order (a .bat sibling beating an extensionless launcher) is asserted from any host by passing the platform and PATHEXT in. The reverse — simulating POSIX from Windows — is not available: the fixture directories are real, so they carry a drive letter.

Not tests-first: implementation and tests were written together. They then caught two defects in it — the platform argument wasn't driving the PATH separator, and an explicit undefined entry point fell through to a default — CI caught a third that only appears on Windows, and a cleanup pass caught the identity one described above.

Validation

What Evidence
The diagnosis The reporter ran Get-Command git-fi, got C:\tools\ruby33\bin\git-fi, deleted it, and git fi resolved to npm's git-fi.ps1. Fixed.
The token path Same reporter created a new PAT from the GitLab form and CI status started working. That's the link AUTH-13 now puts in the error.
Windows behavior A throwaway probe on a windows-latest runner (run 31668275278) established that a .bat sibling wins over an extensionless launcher, that Get-Command does list an extensionless one, and that npm links its shims before postinstall runs.
windows-install itself Green on run 31735499699, all 8 jobs. Its first run (31734327651) failed and was worth having: the tests punctuated a PATH of real directories with : while asking for POSIX resolution, so C:\Users\... split at the drive letter and the fixture silently found nothing; and the job's own step treated the report reaching stderr as a failure, since this shell defaults ErrorActionPreference to stop. Both fixed here.
The notice end to end Run against a planted launcher on PATH: prints the version on stdout and the report on stderr, and GIT_FI_NO_HINTS=1 suppresses it.

@chris-peterson chris-peterson self-assigned this Aug 13, 2026
@chris-peterson
chris-peterson force-pushed the leftover-gem-binstubs branch 2 times, most recently from 662078b to 4136d91 Compare August 13, 2026 20:07
One user migrating off the Ruby gem hit both of these in a row, and neither
left him anywhere to go.

He removed the gem's directory and gemspec by hand, which left the launcher
RubyGems had written into Ruby's bin directory — earlier on his PATH than
npm's prefix. `git fi` kept reaching that dead launcher while the npm install
sat behind it, complete and unreachable. Nothing could say so: the copy that
would notice is the copy not being run. What did work was `npx git-fi`, so
INSTALL-01 makes `--version` compare the `git-fi` that PATH resolves against
the copy answering, and name both when they differ. It reports on stderr, so
stdout stays a bare version string, and only for `--version`: a second
installation is not a fault, and a line on every run would be noise to
whoever has a reason for it.

Past that, his two-year-old GITLAB_ACCESS_TOKEN had expired, and the 401
printed GitLab's JSON, named the branch the request happened to carry, and
offered one remedy — turn CI status off. He went looking for a doc on where
tokens come from. git-fi already knew: AUTH-10 built the prefilled form for
`--auth=login`. AUTH-13 puts that link on the 401 too, with the source that
supplied the rejected token, and notes that a stored token outranks the
export it replaces.

None of this reproduces on macOS, and CI could not see it either: the matrix
already ran windows-latest, but the suite drives the compiled binary out of
the checkout, so nothing installed globally or reached git-fi by the name a
shell resolves. A throwaway probe on a real runner supplied the design, and
three of its findings are load-bearing.

A `.bat` sibling wins over an extensionless launcher in PowerShell, so a
report has to name the file actually in the way rather than the one assumed
to be. `Get-Command` does list a launcher with no extension, which is what
identified the culprit in the field. And npm has hidden postinstall output
since v7, which is why this check is not in the installer where it was first
proposed — the COMPLETE-07 completion message has been going nowhere for the
same reason, and needs its own fix.

The probe is replaced here by the guard it earned: a Windows job that installs
the packed tarball, checks `git fi` reaches it, then plants a stale launcher
ahead of it and checks the report names it. It runs under Windows PowerShell
rather than pwsh, the shell the report came from.

Its first run failed twice over, both times in the scaffolding around the
feature rather than in the feature. The unit tests punctuated a PATH of real
directories with `:` while asking for POSIX resolution, so on Windows
`C:\Users\...` split at the drive letter and the fixture found nothing — which
reads as a passing "found nothing" assertion rather than as a broken fixture.
They build a PATH with the running platform's own separator now. The job's own
step then failed on the report reaching stderr, which is where the report
belongs: this shell defaults `ErrorActionPreference` to stop, so a native
command writing anything to stderr ends the step.

Adding a category to STATUS.md also surfaced that its section headings named
the category but not the ID prefix under it, so finding the rows for `PERF` or
`STORAGE` meant scanning the tables. The prefix is now the heading itself with
the category name beneath it, which puts the prefix in the anchor: a row set is
shareable as `#perf` rather than `#performance-perf`. The one STATUS.md section
holding two prefixes names both.

SPEC.md takes the same shape wherever a prefix has one home there, which is 15
of its sections. The other four keep their names: SPEC.md is organised by
feature while STATUS.md is organised by prefix, so `BRANCH` is declared under
both Commands and Default Branch Detection, `MERGE` under both Merge Process
and CI Integration, and `COMMAND` across four per-command sections. A slug
heading in each would collide on exactly the anchor it exists to provide.
Renaming moved three anchors, and the links pointing at them moved too.

A cleanup pass over the above found one thing that would have shipped wrong on
the platform it matters on. Identity was decided by matching the package name
inside the launcher, so two npm prefixes — an nvm or fnm switch — each hold a
launcher naming this same package, and the second reads as the copy running.
That is the shadow the notice exists for, suppressed. Identity is now the same
question the POSIX side already asked: does the launcher's target resolve to
the copy answering. The Windows launcher names that target in the script it is,
so it is read out rather than matched for, which also retires a constant that
duplicated the package name.

Two smaller ones from the same pass. The 401 message styled a few words itself,
and `abort` wraps what it is given in red, so the first inner reset ended the
colour for every line after it. And the PATH walk collected every launcher when
only the first is ever read, which on a Windows PATH is a dozen probes per
directory for a value nobody looks at; it yields lazily now.
INSTALL-01 ends by asking for silence where the entry point cannot be
resolved, and `shadowingLauncher`'s own doc comment repeats it, but neither
held. A failing realpath on the entry made `isSameInstall` return false,
which reads as "the launcher is a different install" — so the notice
printed, naming a copy at a path that resolves to nothing.

The test named for that case passes an undefined entry, which stops at the
guard a line above and never reaches the resolve. So the state INSTALL-01
names had no test, while the suite read as covering it. The new one hands
in a path that does not exist and asserts silence.

The `GIT_FI_NO_HINTS` entries in SPEC.md and docs/ci-integration.md still
described the variable as a convenience for an interactive terminal, which
followed from its two consumers already being suppressed under `$CI` and
off a TTY. INSTALL-01 is neither, by design — it answers a question the
user just asked — so the switch is the only thing that quiets it, and both
tables now say which of the three that applies to.

STATUS.md's summary counted 123 against the 125 rows the ledger carries.
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