Skip to content

v0.3.0

Choose a tag to compare

@philiptaron philiptaron released this 01 Oct 18:57
· 22 commits to main since this release
c1c25fb

h gains a shared object store that keeps the upstream history of big projects in one place, clones that understand GitHub forks and submodules, container clones for projects worked on only through worktrees, and hooks that give Claude Code's agents worktrees from the store. The shell functions work as before.

Commands

  • h is a set of commands. h [--root DIR] [--store DIR] <command> takes go, resolve, store and hook, with the code root and store also read from $H_CODE_ROOT and $H_STORE. Scripts and agents can run it without the shell function:
    • h go <term> jumps to the project, cloning it first if needed. It's what the shell function runs.
    • h resolve <term> prints an existing project's directory. It never clones and never contacts GitHub.
    • Git options for every clone go after --. The old h --resolve <root> <term> form still works.
  • h-shell-init --store DIR makes the shell function's clones borrow from that store. Its store commands also run in place. h store on its own, or followed by anything other than a store command, still jumps to a project named store.

Cloning

  • Submodules are cloned by default, unless an option such as --no-recurse-submodules, --recurse-submodules=<pathspec>, --bare or --mirror says otherwise. The identity in the git options (-c credential.username=..., user.*) reaches the submodules too, both when their credentials are looked up and in their configuration.
  • Forks:
    • When GitHub says a repository is a fork, the clone gets an upstream remote. It can be fetched but not pushed to, and the fork's own remote becomes the default push target, even when --origin or clone.defaultRemoteName gives it another name.
    • The upstream is fetched with the clone's own history limits (--depth, --shallow-since, --shallow-exclude, --filter). A single-branch clone gets only the upstream's default branch.
    • The upstream is added before any submodules, even when one of them fails.
    • A submodule given relative to the project, such as ../lib.git, comes from your fork's copy when one exists, and otherwise from beside the upstream. Git alone would only look beside the fork.
  • Container clones: h <term> --container clones into <dir>/.bare, with a .git file beside it and no working tree, for projects worked on only through git worktree add.
    • origin has ordinary remote-tracking branches, and there are no local branches.
    • HEAD is detached at origin/HEAD, and moved there again each time h goes to the container, so a new worktree branch starts from the latest fetched default branch.
    • Worktrees link to the container by relative paths, so the container can be moved with them.
    • The clone takes the remote's object format, SHA-1 or SHA-256.
    • --no-tags lasts for later fetches.
    • Options that git fetch doesn't share are refused, as are remotes with no default branch.
    • A failed clone leaves nothing behind.
  • GitHub casing:
    • h <user>/<repo> finds an existing checkout in any casing, without the network.
    • GitHub is asked for the canonical casing, and whether the repository is a fork, only when there's something to clone.
    • The first request goes without a token, so public repositories never touch git's credential helpers. After a 404 (as for a private repository) or a rate limit, h asks git credential fill for the token git keeps for github.com, under each identity's own credential.username, and tries again.
    • Nothing ever prompts, and the token is only ever sent to the host it was stored for.

The object store

  • One bare repository holds many upstreams as remotes. Each is named by the path h would clone it to, such as github.com/NixOS/nixpkgs.

    • Branches live under refs/remotes/<name>/ and tags under refs/tags/<name>/, so nothing collides, and <name>/HEAD follows each upstream's default branch.
    • When one upstream's name extends another's (gitlab.com/g/proj and gitlab.com/g/proj/sub), the outer one's refs move under <name>/-/.
    • Refs are kept in a reftable.
    • The store is never pushed to and never pruned.
  • Clones borrow from it:

    • Clones made with a store borrow its objects, and so do their submodules, so a clone of a fork costs only the fork's own commits.
    • A clone with its full history puts its upstream, and a fork's parent, into the store first, and its submodules afterwards, so nothing is downloaded twice. Clones limited by --depth, --shallow-* or --filter stay out of it.
    • Clones tell the server only about the store history they share with it, and store fetches negotiate with git's skipping algorithm, so a store full of unrelated projects doesn't slow either down.
  • Commands:

    Command Does
    h store init Create the store, with the identity from the git options
    h store add <term>... Add and fetch upstreams. Every term is checked first, and one whose first fetch fails is taken out again
    h store fetch [-q] [<term>...] Fetch everything, or the named upstreams, best-effort: each one that fails is named, the rest are still fetched, and the exit status is nonzero
    h store remove <term>... Forget upstreams and their refs. Their objects stay
    h store list The upstreams' names
    h store path The store's directory
    h store remote <term> The store's name for an upstream
    h store show <term> <ref>[:<path>] A commit or a file, from a branch, tag or commit hash, with no checkout
    h store worktree <term> <ref> [DIR] A cheap clone of one upstream with the names an ordinary clone has
    h store ingest [DIR] Put an existing checkout under the store
    h store maintain [hourly|daily|weekly] Run git's maintenance tasks, none of which deletes objects
    • h store worktree's clone has origin/* branches and plain tags, so git describe and build tooling behave as usual. git fetch refreshes it from the store, it can't push to the store, and rm -rf cleans it up.
    • h store ingest covers a clone made before the store, or one whose submodules changed in a pull. Its remotes and every submodule's repository, at every level, become upstreams. Missing submodules are cloned from the store, and local copies of the store's objects are dropped. Objects the store lacks, reachable or not, are always kept.
  • Terms: store commands take the names h store list prints, URLs, user/repo, or a bare name that's unambiguous in the store. A term that isn't in the store is reported as such.

  • Unaffected by global settings: fetch.pruneTags=true in your global configuration doesn't touch the store's tags. Neither does SHA-256 as the default for new repositories, since the store always uses SHA-1.

Claude Code worktrees

h hook worktree-create and h hook worktree-remove work with Claude Code's WorktreeCreate and WorktreeRemove hooks. See the README for the settings.json configuration.

  • worktree-create adds a worktree to the clone the session is in, at <clone>/.claude/worktrees/<name>, on the branch worktree-<name>.
    • The branch starts from the store's copy of the upstream's default branch (a fork's parent's) as of the last fetch, without touching the network.
    • Each clone uses the store it borrows from.
    • Its submodules come from the store.
  • worktree-remove commits anything uncommitted to the worktree's branch, gives a detached HEAD with commits of its own a branch, and only then removes the worktree. Branches are never deleted.

Fixes

  • h ignores GIT_DIR, GIT_WORK_TREE and git's other repository variables from its environment. Run from inside a git hook, it no longer works on the hook's repository instead of its own.
  • A failed or rate-limited GitHub lookup no longer clones a second copy of a repository under different casing.
  • up counts a .git file as a project root, so it works in worktrees, submodules and container clones.

Requirements

  • Reading a store's refs, including from the git of a clone that borrows from it, needs git 2.45 or later.
  • A container's worktrees need git 2.48 or later.

Under the hood

  • Tests: 231 unit and end-to-end tests. They run real git against local repositories, with a mock GitHub API and fake credential helpers, and never touch the network.