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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@ To run the checkout as `git fi` in other repositories for a while:

```bash
npm run trial:on # build, npm link, load this copy's completion
npm run trial:off # unlink, restore the published version
npm run trial:off # unlink, restore the version it replaced
```

`trial:on` symlinks the checkout onto your `PATH`, so later `npm run build`s take effect without reinstalling. It also runs `install-completions --write .trial/completions` and appends a marked block to `~/.zshrc` that puts that directory on your `fpath` — the same command, writing the same two files, that a user installs with (`COMPLETE-06`), so a trial exercises the shipped install path rather than a shortcut around it. Only `~/.zshrc` is touched; under bash you get the linked binary and wire the completion yourself. `trial:off` deletes the block and `.trial/`, then reinstalls `@gettyimages/git-fi` from npm. Open a new terminal after either one.
`trial:on` symlinks the checkout onto your `PATH`, so later `npm run build`s take effect without reinstalling. It also runs `install-completions --write .trial/completions` and appends a marked block to `~/.zshrc` that puts that directory on your `fpath` — the same command, writing the same two files, that a user installs with (`COMPLETE-06`), so a trial exercises the shipped install path rather than a shortcut around it. Only `~/.zshrc` is touched; under bash you get the linked binary and wire the completion yourself. `trial:off` deletes the block and `.trial/`, then reinstalls the exact version the trial displaced, which `trial:on` recorded before linking over it (`BUILD-03`). Where that reference has been copied outside the marked block, `trial:off` names the lines rather than editing them, since the directory they point at is the one it just removed (`BUILD-04`). Open a new terminal after either one.

`git fi --version` is what says which build is in force: a trial answers `<version>-dev.g<sha>`, an installed copy answers the bare version (`BUILD-02`).

If `git fi` runs something other than what you expect, `which -a git-fi` lists every `git-fi` on your `PATH` in resolution order.

Expand Down
4 changes: 3 additions & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -754,10 +754,12 @@ Platform Compatibility

Build Provenance

A developer can put an unpublished checkout on PATH under the same `git fi` name the published install uses. These requirements keep the two tellable apart, so a bug report names the build it came from.
A developer can put an unpublished checkout on PATH under the same `git fi` name the published install uses. These requirements keep the two tellable apart, so a bug report names the build it came from, and govern what ending that trial hands back.

- `BUILD-01` git-fi shall treat itself as a dev build when a `.git` entry exists at its own package root. A published tarball carries none — npm's `files` list ships `dist`, `man`, `completions`, and the postinstall script — so the marker separates a linked checkout from an installed copy. It shall be read at the package root rather than the working directory, which is a git repository on every ordinary run.
- `BUILD-02` On a dev build, `--version` (`OPTION-05`) shall report the released version followed by `-dev.g<short-sha>`, naming the commit the build came from, and shall append `.dirty` when the checkout's tree differs from that commit. The `g` prefix is what keeps the identifier valid semver: a sha of all digits would otherwise read as a numeric identifier, which may not carry leading zeros. When no commit can be read, git-fi shall report `-dev` alone rather than dropping the marker.
- `BUILD-03` Ending a trial shall restore the exact published version the trial displaced. `npm link` overwrites the global install, so the version is read before the link and kept under `.trial/`; `trial:off` then installs that version rather than whatever is newest, because a trial borrows an install and should hand back the one it borrowed. Where no published version was recorded — the package was absent, or already linked to a checkout — the trial shall say which of the two it found and install the latest, rather than leave the developer with no `git fi` at all. Where a version was recorded, the trial shall install that version or fail saying so; a recorded version that no longer resolves is not grounds for substituting another. A recorded value that is not a bare version shall be refused rather than passed to the installer, which reads the wider `name@spec` grammar.
- `BUILD-04` Ending a trial shall remove only the rc-file block it wrote, delimited by its own markers, and shall report any remaining line naming the trial's completion directory by its literal path rather than editing it away. That directory is deleted as the trial ends, so a hand-copied `fpath` entry naming it would otherwise be left pointing at nothing, and a line the developer wrote is not the script's to remove. A reference spelled through `~` or a variable is not detected.

## `INSTALL`

Expand Down
8 changes: 5 additions & 3 deletions STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Tracks implementation status of each requirement in [SPEC.md](/SPEC.md).

**Last updated:** 2026-09-15
**Last updated:** 2026-09-17

Locations name the file and the enclosing symbol rather than a line range, so an
edit elsewhere in the same file leaves the row correct. `git grep` the symbol to
Expand All @@ -12,8 +12,8 @@ land on it.

| Status | Count |
|---------|-------|
| Covered | 134 |
| Total | 134 |
| Covered | 136 |
| Total | 136 |

## `PRE`

Expand Down Expand Up @@ -277,6 +277,8 @@ Build Provenance
|--------|--------------------------|---------|--------------------------------|
| BUILD-01 | Dev build detection | Covered | `src/build-info.ts` (`isDevBuild`) |
| BUILD-02 | `--version` names the commit | Covered | `src/build-info.ts` (`describeVersion`), `src/index.ts` (`parseArgs`) |
| BUILD-03 | Trial restores the version it displaced | Covered | `scripts/trial.sh` (`installed_state`) |
| BUILD-04 | Trial removes only its own rc block | Covered | `scripts/trial.sh` (`off`) |

## `INSTALL`

Expand Down
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ verify:
trial-on:
npm run trial:on

# Unlink the checkout and reinstall the published @gettyimages/git-fi
# Unlink the checkout and restore the @gettyimages/git-fi version it replaced
trial-off:
npm run trial:off

Expand Down
92 changes: 89 additions & 3 deletions scripts/trial.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,57 @@ set -euo pipefail

root="$(cd "$(dirname "$0")/.." && pwd)"
compdir="$root/.trial/completions"
restore="$root/.trial/replaced-version"
zshrc="${HOME}/.zshrc"
marker="git-fi-trial"
pkg="@gettyimages/git-fi"

# What the global install is right now: a version for a registry install,
# `linked` where it already points at a checkout, or empty where the package is
# absent. `npm link` overwrites that install, so the answer has to be taken
# before it runs and kept for `off` to restore. `npm ls` exits non-zero on some
# tree problems while still printing usable JSON, so its status is discarded
# rather than aborting the run under `pipefail`.
installed_state() {
{ npm ls -g --depth=0 --json "$pkg" 2>/dev/null || true; } | node -e '
const raw = require("fs").readFileSync(0, "utf8");
try {
const dep = (JSON.parse(raw).dependencies ?? {})[process.argv[1]];
if (!dep) process.exit(0);
// A linked checkout reports `resolved` as a file: URL; only a
// registry install names a version to come back to.
if (String(dep.resolved ?? "").startsWith("file:")) process.stdout.write("linked");
else if (dep.version) process.stdout.write(dep.version);
} catch (e) {
process.stderr.write(`could not read the global ${process.argv[1]}: ${e.message}\n`);
}
' "$pkg"
}

case "${1:-}" in
on)
cd "$root"
npm run build

mkdir -p "$root/.trial"
if [ ! -f "$restore" ]; then
state="$(installed_state)"
case "$state" in
linked)
: >"$restore"
echo "$pkg already points at a checkout, so the version it displaced is gone; trial:off will install the latest. Pin by hand if you need a particular one."
;;
"")
: >"$restore"
echo "No published $pkg is installed; trial:off will install the latest."
;;
*)
printf '%s\n' "$state" >"$restore"
echo "Trial will restore $pkg@$state"
;;
esac
fi

npm link

node "$root/dist/index.js" install-completions --write "$compdir"
Expand All @@ -47,15 +91,57 @@ on)
echo "Open a new terminal, then try: git fi <TAB> git fi -a <TAB> git-fi <TAB>"
;;
off)
version=""
[ -f "$restore" ] && version="$(cat "$restore")"

# `npm i -g <pkg>@<spec>` takes far more than a version — a URL, a git ref,
# a `file:` path, an `npm:` alias — and this value comes off disk. Anything
# that isn't a bare version is refused rather than fetched.
case "$version" in
"") ;;
-* | *[!0-9A-Za-z.+-]*)
echo "! $restore does not hold a version: $version" >&2
echo " Refusing to install it. Delete the file and re-run to get the latest." >&2
exit 1
;;
esac

if [ -f "$zshrc" ]; then
tmp="$(mktemp)"
sed "/# BEGIN ${marker}/,/# END ${marker}/d" "$zshrc" >"$tmp" && mv "$tmp" "$zshrc"

# Only the marked block is ours to remove, so a hand-copied reference to
# the same directory survives it — and `.trial/` goes on the next line,
# leaving that fpath entry pointing at nothing. Name it rather than
# delete a line the user wrote.
if grep -qF "$compdir" "$zshrc"; then
echo "! $zshrc still names $compdir outside the ${marker} block:" >&2
grep -nF "$compdir" "$zshrc" >&2
echo " That directory is about to go; remove those lines by hand." >&2
fi
fi

rm -rf "$root/.trial"
npm rm -g @gettyimages/git-fi || true
npm i -g @gettyimages/git-fi
state="$(installed_state)"

if [ -n "$version" ]; then
# The exact version the trial displaced, not whatever is newest: a trial
# should hand back the install it borrowed.
npm rm -g "$pkg" || true
npm i -g "$pkg@$version"
elif [ -n "$state" ] && [ "$state" != "linked" ]; then
# Nothing is linked, so no trial is running and this install is someone's
# own choice. Reinstalling here would replace it with whatever is newest,
# which is the outcome the recorded version exists to prevent.
echo "No trial to revert; $pkg@$state is installed from the registry. Leaving it alone."
else
npm rm -g "$pkg" || true
echo "No recorded version to come back to; installing the latest $pkg."
npm i -g "$pkg"
fi

echo
echo "Reverted. Open a new terminal."
echo "Reverted. Open a new terminal, then \`git fi --version\` to confirm."
;;
*)
echo "usage: npm run trial:on | npm run trial:off" >&2
Expand Down
Loading