Say which git-fi runs, and how to replace a token - #12
Draft
chris-peterson wants to merge 2 commits into
Draft
Conversation
chris-peterson
force-pushed
the
leftover-gem-binstubs
branch
2 times, most recently
from
August 13, 2026 20:07
662078b to
4136d91
Compare
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.
chris-peterson
force-pushed
the
leftover-gem-binstubs
branch
from
September 6, 2026 15:43
4136d91 to
49ec692
Compare
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
bindirectory — a directory earlier on hisPATHthan npm's prefix.git fikept 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-oldGITLAB_ACCESS_TOKENturned 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.ts—shadowingLauncheris the whole idea: resolvegit-fionPATH, compare it to the copy running, return the winner only when they differ.isSameInstallis the part worth a second look, andlauncherTargetis what makes it work on Windows — see Approach below for why identity isn't the package name.src/index.ts— the only call site.--versionand nothing else.src/gitlab.ts—rejectedTokenMessage. The prefilled-form URL already existed for--auth=login(AUTH-10); this puts it on the 401 too, sharingREQUIRED_SCOPEwithauth.tsso the sentence and the URL'sscopes=can't disagree.src/style.ts—hintsOptedOutsplits the explicitGIT_FI_NO_HINTSswitch out ofhintsEnabled. 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— awindows-installjob. The existing matrix runswindows-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, checksgit fireaches 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.mdINSTALL-01andAUTH-13.docs/quickstart.md— the migration callout now says whygem uninstallbeats deleting the files, and Windows guidance moved into its own callout.STATUS.md/SPEC.mdheadings — the ID prefix is now the heading with the category name beneath, so a section is shareable as#perfrather 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
postinstalloutput is hidden unless--foreground-scriptsis passed. A warning printed there would go where the existingCOMPLETE-07completion message already goes — nowhere. That's a separate pre-existing defect this turned up, and it needs its own fix.Only
--versionreports. A second git-fi onPATHisn't a fault on its own, and a line on every run would be noise to anyone who has a reason for it.--versionis 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
BRANCHis declared under both Commands and Default Branch Detection,MERGEunder both Merge Process and CI Integration, andCOMMANDacross 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 forAUTH-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 own —abortwraps what it's given in red, and an inner SGR reset ends that colour for every line after it.Windows naming order (a
.batsibling beating an extensionless launcher) is asserted from any host by passing the platform andPATHEXTin. 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
PATHseparator, and an explicitundefinedentry 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
Get-Command git-fi, gotC:\tools\ruby33\bin\git-fi, deleted it, andgit firesolved to npm'sgit-fi.ps1. Fixed.AUTH-13now puts in the error.windows-latestrunner (run 31668275278) established that a.batsibling wins over an extensionless launcher, thatGet-Commanddoes list an extensionless one, and that npm links its shims beforepostinstallruns.windows-installitself:while asking for POSIX resolution, soC:\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 defaultsErrorActionPreferenceto stop. Both fixed here.PATH: prints the version on stdout and the report on stderr, andGIT_FI_NO_HINTS=1suppresses it.