gib — git in a bucket
CLI for the GitBucket.WTF platform. Push and pull git repositories directly to/from object storage with no server required.
gib and the GitBucket.WTF web app share a single engine (@gitbucket/storage's GitEngine), so a repo created by either tool is readable by the other.
When to use the CLI
- CI/CD without a git server: push build artifacts or generated repos straight to a bucket from a job runner. No SSH key, no HTTP auth, no
git-http-backendprocess — just bucket credentials. - Air-gapped or self-hosted setups: when running gitbucket.wtf isn't desirable,
gibis the whole story. - Local development against the platform: read/write repos in your dev bucket without going through the web app's HTTP routes.
If you have a running gitbucket.wtf instance and want standard git workflows, use git clone https://gitbucket.wtf/api/git/org/repo.git and skip the CLI.
Install
The CLI lives in the monorepo at apps/cli. From a clone of this repo:
pnpm install
pnpm --filter @gitbucket/gib build
# The binary is now at apps/cli/dist/index.js. Run it via node, or:
alias gib='node /absolute/path/to/apps/cli/dist/index.js'
For day-to-day development without a build step:
pnpm --filter @gitbucket/gib dev -- <args>
Storage URLs
gib accepts the same URL syntax everywhere a bucket is needed. Credentials always come from the environment — never the command line — so they don't end up in shell history.
| Scheme | Example | Env vars |
|---|---|---|
s3:// | s3://my-bucket/repos/acme/widgets | S3_ACCESS_KEY, S3_SECRET_KEY, optional S3_ENDPOINT, S3_REGION |
s3+https:// | s3+https://gateway.storjshare.io/my-bucket/repos/acme/widgets | same as s3:// (endpoint comes from the URL) |
vercel-blob:// | vercel-blob://repos/acme/widgets | BLOB_READ_WRITE_TOKEN |
file:// | file:///srv/git-bucket/acme/widgets | none |
| bare path | ./repo or /tmp/repo | none — treated as file:// |
On-disk layout
A repo is a key prefix inside a bucket. gib reads and writes:
{prefix}/HEAD symbolic ref → refs/heads/<default-branch>
{prefix}/objects/{hash[0:2]}/{hash[2:]} loose git objects (header + content)
{prefix}/objects/pack/pack-{sha}.pack pack files (downloaded by clone/pull)
{prefix}/objects/pack/pack-{sha}.idx matching pack indexes
{prefix}/refs/heads/{branch} ref → commit hash
{prefix}/refs/tags/{tag} ref → commit/tag hash
This is identical to the layout the GitBucket.WTF web app uses, so the two are fully interoperable.
Commands
gib init <url>
Initialize an empty repo at <url>.
gib init s3://my-bucket/repos/acme/widgets
gib init --branch trunk file:///srv/repos/widgets
-b, --branch <name>— default branch (default:main)--legacy-seed-commit— also write the legacy empty-tree initial commit. Only needed for backward compatibility with the gitbucket.wtf web app's pre-bare-init behavior; new repos shouldn't need it.
Without --legacy-seed-commit, init writes only HEAD, so a subsequent gib push of any local history works without --force.
gib remote add <name> <url> / list / rm <name>
Manage the bucket URLs gib remembers per local repo. Stored at .git/gib/config.json so they don't pollute your real git remotes.
gib remote add origin s3://my-bucket/repos/acme/widgets
gib remote list
gib remote rm origin
gib push [remote] [refspec]
Push local refs to a bucket remote.
gib push # push current branch to origin
gib push origin main # push main → main on origin
gib push origin main:trunk # push main → trunk
gib push --force origin main # allow non-fast-forward
Walks the local object graph from the source ref, uploads only objects the remote doesn't already have (one hasObject lookup per object), and refuses non-fast-forward updates unless --force is given.
gib clone <url> [dir]
Clone a repo from a bucket URL into [dir] (or a directory derived from the URL).
gib clone s3://my-bucket/repos/acme/widgets ./widgets
gib clone --branch release file:///srv/repos/widgets
-b, --branch <name>— branch to check out after clone (default: whateverHEADpoints to)
clone mirrors any pack files in the remote's objects/pack/ into the new local .git/objects/pack/ before walking loose objects, so isomorphic-git can resolve packed (including delta-compressed) objects transparently.
gib pull [remote] [refspec]
Fetch refs from a bucket remote and fast-forward the local branch.
gib pull # pull origin/<current-branch> → current-branch
gib pull origin main # pull origin/main → main
gib pull origin release:downstream # pull origin/release → local downstream
gib pull --force # overwrite divergent local history
Reports already up to date when local matches remote. Refuses non-FF updates without --force.
gib status [remote]
Compare every local branch against a bucket remote. Each branch is classified into one of six states:
| Symbol | Meaning | What to do |
|---|---|---|
= up to date | local and remote agree | nothing |
↑ ahead | local has commits past remote | gib push |
↓ behind | remote has commits past local | gib pull |
↕ diverged | both sides have unique commits | gib pull --force or merge manually |
● local only | branch exists locally, not in bucket | gib push origin <branch> |
○ remote only | branch exists in bucket, not locally | gib pull origin <branch> |
status walks locally for the ahead check (so unpushed commits are detected) and via the engine for the behind check (so unfetched commits in the bucket are detected).
Pack files
gib clone and gib pull download any pack files present in the remote and place them in local .git/objects/pack/. isomorphic-git transparently reads them on subsequent commands — including delta resolution.
gib push currently uploads only loose objects. Pack-on-push is a planned follow-up; it's blocked on adding pack-aware object reads to GitEngine so the web app can render objects the CLI packs.
Troubleshooting
unknown remote: <name>— rungib remote add <name> <url>first.S3 credentials missing— setS3_ACCESS_KEYandS3_SECRET_KEY(and optionallyS3_ENDPOINT,S3_REGION).local has diverged from remote—gib pull --force(overwrites local) or do a manual merge.remote ref is not an ancestor of localon push —gib push --force(overwrites remote) or pull and re-push.empty repo (no branches yet)on clone — the bucket was created bygib initwithout--legacy-seed-commitand nothing has been pushed yet. Push from any local repo to populate it.
Source
The CLI lives at apps/cli/ and the engine at packages/storage/. Both are covered by the workspace test suite (pnpm test).