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-backend process — just bucket credentials.
  • Air-gapped or self-hosted setups: when running gitbucket.wtf isn't desirable, gib is 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.

SchemeExampleEnv vars
s3://s3://my-bucket/repos/acme/widgetsS3_ACCESS_KEY, S3_SECRET_KEY, optional S3_ENDPOINT, S3_REGION
s3+https://s3+https://gateway.storjshare.io/my-bucket/repos/acme/widgetssame as s3:// (endpoint comes from the URL)
vercel-blob://vercel-blob://repos/acme/widgetsBLOB_READ_WRITE_TOKEN
file://file:///srv/git-bucket/acme/widgetsnone
bare path./repo or /tmp/reponone — 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: whatever HEAD points 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:

SymbolMeaningWhat to do
= up to datelocal and remote agreenothing
↑ aheadlocal has commits past remotegib push
↓ behindremote has commits past localgib pull
↕ divergedboth sides have unique commitsgib pull --force or merge manually
● local onlybranch exists locally, not in bucketgib push origin <branch>
○ remote onlybranch exists in bucket, not locallygib 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> — run gib remote add <name> <url> first.
  • S3 credentials missing — set S3_ACCESS_KEY and S3_SECRET_KEY (and optionally S3_ENDPOINT, S3_REGION).
  • local has diverged from remotegib pull --force (overwrites local) or do a manual merge.
  • remote ref is not an ancestor of local on push — gib push --force (overwrites remote) or pull and re-push.
  • empty repo (no branches yet) on clone — the bucket was created by gib init without --legacy-seed-commit and 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).