Repository navigation
v0.3.0
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
his a set of commands.h [--root DIR] [--store DIR] <command>takesgo,resolve,storeandhook, with the code root and store also read from$H_CODE_ROOTand$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 oldh --resolve <root> <term>form still works.
h-shell-init --store DIRmakes the shell function's clones borrow from that store. Its store commands also run in place.h storeon its own, or followed by anything other than a store command, still jumps to a project namedstore.
Cloning
- Submodules are cloned by default, unless an option such as
--no-recurse-submodules,--recurse-submodules=<pathspec>,--bareor--mirrorsays 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
upstreamremote. It can be fetched but not pushed to, and the fork's own remote becomes the default push target, even when--originorclone.defaultRemoteNamegives 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.
- When GitHub says a repository is a fork, the clone gets an
- Container clones:
h <term> --containerclones into<dir>/.bare, with a.gitfile beside it and no working tree, for projects worked on only throughgit worktree add.originhas ordinary remote-tracking branches, and there are no local branches.- HEAD is detached at
origin/HEAD, and moved there again each timehgoes 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-tagslasts for later fetches.- Options that
git fetchdoesn'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,
hasksgit credential fillfor the token git keeps for github.com, under each identity's owncredential.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
hwould clone it to, such asgithub.com/NixOS/nixpkgs.- Branches live under
refs/remotes/<name>/and tags underrefs/tags/<name>/, so nothing collides, and<name>/HEADfollows each upstream's default branch. - When one upstream's name extends another's (
gitlab.com/g/projandgitlab.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.
- Branches live under
-
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--filterstay 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 initCreate 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 listThe upstreams' names h store pathThe 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 hasorigin/*branches and plain tags, sogit describeand build tooling behave as usual.git fetchrefreshes it from the store, it can't push to the store, andrm -rfcleans it up.h store ingestcovers 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 listprints, 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=truein 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-createadds a worktree to the clone the session is in, at<clone>/.claude/worktrees/<name>, on the branchworktree-<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-removecommits 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
hignoresGIT_DIR,GIT_WORK_TREEand 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.
upcounts a.gitfile 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.