0034: Site changes from the installed Git

On this page

Status: accepted. Its read-only rule is superseded by 0081; its rules for reading stay.

Context

Each site gets a Changes page: the files changed since the last commit and their diffs, often to review what a coding agent did. Reading Git means picking a library or the git program. Rust's Git libraries either lag on submodules, worktrees and a stable API (gitoxide) or are GPL with a linking exception and need a C toolchain on Windows (libgit2). A repository is also untrusted input: its config can name programs that Git runs while it compares files, such as an fsmonitor hook, clean and process filters, textconv and an external diff. On macOS, /usr/bin/git is a stand-in that opens Apple's installer when the Command Line Tools are missing.

Decision

  • Short Leash runs the installed git (leash_core::changes), so renames, conflicts, line endings and worktrees mean what they mean to the person's own Git. On macOS it uses /usr/bin/git only when xcode-select -p names a developer folder with Git in it, and runs that Git directly. Otherwise the page says Git isn't installed.
  • Every command runs with:
    • --no-optional-locks and GIT_OPTIONAL_LOCKS=0, so Short Leash never takes Git's index lock.
    • --no-pager.
    • -c core.fsmonitor=false and -c diff.external=, with --no-ext-diff --no-textconv on diffs.
    • Fixed output settings: prefixes, relative paths, quoting and color.
    • GIT_TERMINAL_PROMPT=0, GIT_CEILING_DIRECTORIES set to the home folder (so a home folder kept in Git doesn't claim every site), and the GIT_* variables that point Git elsewhere removed.
    • --ignore-submodules=all and -c submodule.recurse=false, so no command goes into a submodule.
    • Only the absolute folders of the PATH, so a program Git starts is never found inside the repository.
    • GIT_NO_LAZY_FETCH=1, --no-lazy-fetch on Git 2.44 and later, and GIT_ALLOW_PROTOCOL set and empty, so a read never reaches a remote. A partial clone would otherwise download the objects it left on its remote, running the upload-pack, SSH command or credential helper the repository's config names. A diff that needs such an object says the repository is a partial clone and names the command to run once in a terminal to download what's missing: git diff for uncommitted changes, git diff <base>...HEAD for This branch. The base is named only when it holds nothing but ASCII letters, digits, ., - and / (and doesn't start with -); otherwise the command is git diff <merge base id> HEAD, which downloads the same. A branch name comes from the repository, so it's never quoted for one shell: what's shown means the same in sh, PowerShell and cmd.exe.
  • Filter drivers defined in the repository's own config (git config --local) are turned off. Filters in the person's own config, such as Git LFS, stay on; turning them off would show every file they filter as changed.
  • Git's own safety check (safe.directory) is respected: a refusal is a state on the page with the command to run, never an override.
  • The page is read-only: no stage, commit or discard. It compares the working tree and index with HEAD only.
  • A diff is asked for per file as it scrolls near and must be one of the files Git lists for the site. Untracked files are read by Short Leash, regular files only and at most 1 MB, never pipes or devices. At most three of these Git commands run at once.
  • Diffs are drawn by Short Leash's own React code, with words marked from a longest common sequence of tokens and syntax colors from the Lezer parsers the scratchpad editor already ships. No diff library is added.
  • The page asks Git again every 10 seconds while it's in view and when the window comes back; there's no file watcher.

Consequences

  • People need Git installed; the page says so and how to get it.
  • A Git command per refresh costs a process, more on Windows. Large repositories with ignored folders are fast; one with an unignored node_modules stops at 10,000 files and names the folders.
  • Whatever a filter in the person's own config does still runs, as it would in their own terminal.
  • Changes can be up to 10 seconds old while the page is open; a watcher can come later without changing the API.
  • Discarding, comparing against another branch and checkpoints for agents are left for later.