Git
Package git is Git hosting for your org: create repos, clone, push, and see what they cost.
Package git is Git hosting for your org: create repos, clone, push, and see what they cost.
| Base URL | https://api.hanzo.ai |
| Operations | 36 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
git
POST /v1/git/{org}/{project}/{repo}/git-receive-pack
Accept a push, and turn it into a build
The pack-transfer phase of a push, and the point at which a push becomes an EVENT. NEVER ANONYMOUS: a push always requires an authenticated org, and the org in the path must equal it.
Once the pack is on disk the repository's storage usage is metered and a build is fired for every branch whose tip actually moved, computed from the before/after branch diff rather than from what the client claimed. That runs on a cancel-immune context, so a client that hangs up the moment its push lands still gets its build, and it runs even when git itself exited non-zero — the refs on disk are the ground truth. Repacking housekeeping is detached and never blocks the response.
A Content-Type other than application/x-git-receive-pack-request is 400. Addressed under the API prefix, with the PROJECT as a middle path segment: project scope otherwise rides a header a git client cannot send, so this path is the only usable remote for a project-scoped repository. This is git's own wire protocol, not an API call to make by hand: point a git client at the clone URL and it makes this request itself.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
org | path | string | yes | |
project | path | string | yes | |
repo | path | string | yes |
POST /v1/git/{org}/{project}/{repo}/git-upload-pack
Serve a clone or fetch
The pack-transfer phase of a clone or fetch: the request and the response are git's binary pack protocol, streamed straight through git itself — request body to git's stdin, git's stdout to the response — so a multi-gigabyte clone never lands in this process's memory.
A PUBLIC repository is fetched anonymously; a private one requires its own org, and a wrong or absent org is 404 rather than a hint that the repository exists. A Content-Type other than application/x-git-upload-pack-request is 400. Addressed under the API prefix, with the PROJECT as a middle path segment: project scope otherwise rides a header a git client cannot send, so this path is the only usable remote for a project-scoped repository. This is git's own wire protocol, not an API call to make by hand: point a git client at the clone URL and it makes this request itself.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
org | path | string | yes | |
project | path | string | yes | |
repo | path | string | yes |
GET /v1/git/{org}/{project}/{repo}/info/refs
Advertise a repository's refs to a git client
The ref-advertisement phase of git's smart-HTTP protocol — the first request a clone, a fetch and a push all make. ?service= selects which: git-upload-pack advertises for a fetch, git-receive-pack for a push, and any other value is 400.
ANONYMOUS ONLY FOR FETCH, AND ONLY ON A PUBLIC REPOSITORY. The push advertisement always requires an authenticated org, and where a path org is present it must equal the authenticated one. A private repository reached without its org is 404, indistinguishable from one that does not exist. Addressed under the API prefix, with the PROJECT as a middle path segment: project scope otherwise rides a header a git client cannot send, so this path is the only usable remote for a project-scoped repository. This is git's own wire protocol, not an API call to make by hand: point a git client at the clone URL and it makes this request itself.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
org | path | string | yes | |
project | path | string | yes | |
repo | path | string | yes |
POST /v1/git/{org}/{repo}/git-receive-pack
Accept a push, and turn it into a build
The pack-transfer phase of a push, and the point at which a push becomes an EVENT. NEVER ANONYMOUS: a push always requires an authenticated org, and the org in the path must equal it.
Once the pack is on disk the repository's storage usage is metered and a build is fired for every branch whose tip actually moved, computed from the before/after branch diff rather than from what the client claimed. That runs on a cancel-immune context, so a client that hangs up the moment its push lands still gets its build, and it runs even when git itself exited non-zero — the refs on disk are the ground truth. Repacking housekeeping is detached and never blocks the response.
A Content-Type other than application/x-git-receive-pack-request is 400. Addressed under the API prefix, so git clone https://<host>/v1/git/<org>/<repo>.git works on any host the binary serves. This is git's own wire protocol, not an API call to make by hand: point a git client at the clone URL and it makes this request itself.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
org | path | string | yes | |
repo | path | string | yes |
POST /v1/git/{org}/{repo}/git-upload-pack
Serve a clone or fetch
The pack-transfer phase of a clone or fetch: the request and the response are git's binary pack protocol, streamed straight through git itself — request body to git's stdin, git's stdout to the response — so a multi-gigabyte clone never lands in this process's memory.
A PUBLIC repository is fetched anonymously; a private one requires its own org, and a wrong or absent org is 404 rather than a hint that the repository exists. A Content-Type other than application/x-git-upload-pack-request is 400. Addressed under the API prefix, so git clone https://<host>/v1/git/<org>/<repo>.git works on any host the binary serves. This is git's own wire protocol, not an API call to make by hand: point a git client at the clone URL and it makes this request itself.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
org | path | string | yes | |
repo | path | string | yes |
GET /v1/git/{org}/{repo}/info/refs
Advertise a repository's refs to a git client
The ref-advertisement phase of git's smart-HTTP protocol — the first request a clone, a fetch and a push all make. ?service= selects which: git-upload-pack advertises for a fetch, git-receive-pack for a push, and any other value is 400.
ANONYMOUS ONLY FOR FETCH, AND ONLY ON A PUBLIC REPOSITORY. The push advertisement always requires an authenticated org, and where a path org is present it must equal the authenticated one. A private repository reached without its org is 404, indistinguishable from one that does not exist. Addressed under the API prefix, so git clone https://<host>/v1/git/<org>/<repo>.git works on any host the binary serves. This is git's own wire protocol, not an API call to make by hand: point a git client at the clone URL and it makes this request itself.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
org | path | string | yes | |
repo | path | string | yes |
DELETE /v1/git/keys/{id}
Removes a registered SSH key, scoped to the caller's org: an org can only delete its own, and a key id it does not own is not found. Answers 204 with no body. Once removed the key no longer authenticates any SSH git access.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | ID is the key's identifier ("gitkey_…"), from the :id path segment. |
GET /v1/git/keys
Returns the SSH public keys registered to the caller's org — the keys
that authenticate git clone git@<host>:<org>/<repo>.git. Keys are org-scoped
on read even though the fingerprint index is global, so one org never sees
another's.
POST /v1/git/keys
Registers an SSH public key so it can authenticate git clone git@<host>:<org>/<repo>.git for the caller's org. The key line is parsed and
canonicalized before storage, its SHA256 fingerprint becomes the auth lookup
handle, and the full public key round-trips (it is public). Answers 201.
Fingerprints are globally unique, so a key already registered — to this org or
any other — is a 409: one key belongs to exactly one org.
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
publicKey | string | — | PublicKey is one OpenSSH authorized-key line ("ssh-ed25519 AAAA… you@host"). |
title | string | — | Title labels the key in the console. |
GET /v1/git/repos/{name}/blob
Returns one file's bytes at one revision. Text comes back verbatim, binary comes back base64, and a file past the 1 MiB view cap comes back marked truncated with NO content — the client is expected to clone instead.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
name | path | string | yes | Name is the repo to read, from the :name path segment. |
ref | query | string | — | Ref is a branch, tag or commit; empty means the repo's HEAD. |
path | query | string | — | Path is repo-relative; empty is the tree root. |
GET /v1/git/repos/{name}/commits
Walks a ref's history newest first, or one path's history when a
path is given. There is no cursor: the page is the newest limit commits.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
name | path | string | yes | Name is the repo to read, from the :name path segment. |
ref | query | string | — | Ref is the branch, tag or commit to walk back from; empty means HEAD. |
path | query | string | — | Path narrows the history to commits touching it; empty walks the whole ref. |
limit | query | integer | — | Limit caps the page. |
GET /v1/git/repos/{name}/files
Returns every file a glob selects at one revision, WITH its bytes and the revision they came from. It is the read a delivery generator makes: one call answers "what is the inventory at this commit, and what does it say", where listing and then fetching would be a request per file.
Returning the resolved revision matters as much as the bytes. A generator that
lists at main and then reads at main can straddle a push and assemble half
its inventory from one commit and half from the next; resolving once makes the
whole read consistent by construction.
A file past the read cap comes back Truncated with no content rather than being dropped. A caller building a desired set has to know the difference between "this file is empty" and "this file was not read" — silently omitting it is how a pruning reconcile deletes what the missing file declared.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
name | path | string | yes | Name is the repo to read, from the :name path segment. |
ref | query | string | — | Ref is a branch, tag or commit; empty means the repo's HEAD. |
glob | query | string | — | Glob selects files, matched segment by segment so * never crosses a /. |
POST /v1/git/repos/{name}/gc
Repacks a repo into one bitmapped pack and rewrites its commit-graph, so the next clone reuses the bitmap instead of walking the whole object graph. Idempotent, and safe to interrupt — git swaps both artifacts atomically. It runs under one pack slot with the same memory bounds as a clone, so it can block behind heavy pack traffic rather than compete with it. Storage usage is re-measured afterwards, since a repack reclaims space.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
name | path | string | yes | Name is the repo's org-unique handle, from the :name path segment. |
POST /v1/git/repos/{name}/mirror
Imports an external git repository into the caller's repo, provisioning it on first use. Fetch is FORCED and covers every ref, so a first call clones the source and a repeat call re-syncs it — the endpoint is idempotent by mirror semantics. Mirrored bytes are metered exactly like a push, and a push.landed event is emitted for the default branch so the code index picks the repo up.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
name | path | string | yes | Name is the local repo to mirror into, from the :name path segment. |
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
name | string | — | Name is the local repo to mirror into, from the :name path segment. |
project | string | — | Project is the sub-scope to land the repo in; empty uses the caller's own, exactly as a create would. |
source | string | — | Source is the http(s) git URL to fetch from. |
DELETE /v1/git/repos/{name}/mirrors/{id}
Removes one outbound mirror target; later pushes stop being forwarded to it. Answers 204 with no body. Nothing is done to the downstream remote itself — only this repo's intent to push there is dropped.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
name | path | string | yes | Name is the repo, from the :name path segment. |
id | path | string | yes | ID is the row to remove, from the :id path segment. |
GET /v1/git/repos/{name}/mirrors
Returns a repo's outbound mirror targets — the downstream remotes the mirror reactor pushes to whenever a push lands here.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
name | path | string | yes | Name is the repo's org-unique handle, from the :name path segment. |
POST /v1/git/repos/{name}/mirrors
Registers a downstream remote the repo's advanced refs are pushed to whenever a push lands here. Answers 201. The URL must be https to a host on the mirror allowlist (github.com / gitlab.com): the same set the mirror credential may be sent to, so a target can never capture the shared token or point the push at an internal service. Any embedded userinfo is stripped — credentials ride env-only at push time and never enter the stored URL. One mirror per host per repo; a second is a 409.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
name | path | string | yes | Name is the repo whose advanced refs are pushed downstream, from the :name path segment. |
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
host | string | — | Host is an optional assertion of the target's hostname. |
name | string | — | Name is the repo whose advanced refs are pushed downstream, from the :name path segment. |
url | string | — | URL is the downstream https git remote. Must be https to an allowlisted host (github.com / gitlab.com); any embedded… |
POST /v1/git/repos/{name}/push
Lands a set of files as one commit without a git client — the
hanzo.app builder's push. The repo is CREATED on first push, the files are
merged onto the branch tip (unlisted files survive), and the same
push-to-deploy hook a real receive-pack fires is fired, so downstream this is
indistinguishable from a git push.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
name | path | string | yes | Name is the repo to push into, from the :name path segment. |
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
branch | string | — | Branch to advance; empty means "main". |
files | pushFile[] | — | Files are added to or overwritten on the branch tip — files already there and not listed SURVIVE. |
message | string | — | Message is the commit message; empty gets a generated one. |
name | string | — | Name is the repo to push into, from the :name path segment. |
GET /v1/git/repos/{name}/readme
Returns the README at the tree root as plain text — unrendered, so the caller decides how to present it. A repo with no README is not found.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
name | path | string | yes | Name is the repo to read, from the :name path segment. |
ref | query | string | — | Ref is a branch, tag or commit; empty means the repo's HEAD. |
GET /v1/git/repos/{name}/refs
Lists a repo's branches, tags and default branch — what a branch picker needs in one call. Unlike the other read ops it tolerates a repo with no commits: the ref sets come back empty and the default branch is still named.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
name | path | string | yes | Name is the repo's org-unique handle, from the :name path segment. |
DELETE /v1/git/repos/{name}/subscriptions/{id}
Removes one Slack subscription from a repo; the notifier stops posting that repo's events to that channel. Answers 204 with no body. An id that is not this repo's subscription is not found.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
name | path | string | yes | Name is the repo, from the :name path segment. |
id | path | string | yes | ID is the row to remove, from the :id path segment. |
GET /v1/git/repos/{name}/subscriptions
Returns a repo's Slack subscriptions — which channels the lifecycle notifier posts this repo's push and deploy events to.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
name | path | string | yes | Name is the repo's org-unique handle, from the :name path segment. |
POST /v1/git/repos/{name}/subscriptions
Binds a Slack channel to a repo, so the lifecycle notifier posts that repo's push and deploy events there. Answers 201. The same channel twice on one repo is a 409; a repo outside the caller's scope is a 404, exactly as reading it is.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
name | path | string | yes | Name is the repo to subscribe, from the :name path segment. |
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
channel | string | — | Channel is the Slack channel the notifier posts to — an id (C…/G…), a #name, or a bare name. |
events | string[] | — | Events narrows delivery to these lifecycle kinds (push.landed, deploy.live, deploy.failed). |
name | string | — | Name is the repo to subscribe, from the :name path segment. |
GET /v1/git/repos/{name}/tree
Lists the immediate children of one directory at one revision, directories before files. It does not recurse — walk down a level at a time.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
name | path | string | yes | Name is the repo to read, from the :name path segment. |
ref | query | string | — | Ref is a branch, tag or commit; empty means the repo's HEAD. |
path | query | string | — | Path is repo-relative; empty is the tree root. |
GET /v1/git/repos/{name}
Returns one repo with its live ref state: every branch name and the resolved HEAD commit. Both are read from the object store on each call, so an empty repo reports no branches and an empty head rather than failing. A repo outside the caller's scope is not found.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
name | path | string | yes | Name is the repo's org-unique handle, from the :name path segment. |
PATCH /v1/git/repos/{name}
Flips a repo's public bit, the one mutable repo setting today. Public grants ANONYMOUS fetch only; push and the whole control plane stay org-authed. Returns the updated repo.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
name | path | string | yes | Name is the repo to update, from the :name path segment. |
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
name | string | — | Name is the repo to update, from the :name path segment. |
public | boolean | — | Public flips anonymous read access. |
DELETE /v1/git/repos/{name}
Removes a repo's metadata and purges its storage. Answers 204 with no body. The metadata row is the source of truth for existence, so a storage purge that fails is logged and the delete still succeeds — and a second call is a 404, not a second delete.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
name | path | string | yes | Name is the repo's org-unique handle, from the :name path segment. |
GET /v1/git/repos
Returns the repos in the caller's scope, most recently updated first. The scope is the request principal's — the gateway-minted org and its optional project — never anything off the wire, so a caller only ever sees its own. Rows carry no branches or HEAD; read one repo for those.
POST /v1/git/repos
Provisions an empty bare repository in the caller's scope and returns it with its clone URLs. Answers 201. The name must be unique within the scope — a repeat is a 409, never a silent overwrite of an existing repo. The org comes from the validated principal, so a repo is always born owned by the caller's own tenant.
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
description | string | — | Description is a free-form blurb, max 4KiB. |
name | string | — | Name is the repo's handle, unique within the scope, and the last segment of both clone URLs. |
project | string | — | Project narrows the repo to a sub-scope of the org. |
public | boolean | — | Public grants ANONYMOUS read (fetch) only; push and the whole control plane stay org-authed. |
GET /v1/git/usage
Returns per-repo and total storage bytes for the caller's org — the queryable, per-tenant number commerce and o11y meter on. It spans EVERY project sub-scope, unlike the repo list, so a billing consumer sees the whole tenant footprint in one call. Sizes are last-measured values (create, push, mirror and gc each re-measure), not a live walk of the disk.
POST /v1/git/webhook
Retired — forge pushes build via platform.hanzo.ai
GONE (410). This was the canonical forge's push-to-deploy door, and it never dispatched a build in its life.
It handed each verified push to cloud.OnGitPush, a single-registrant seam whose only registrant lives in apps/platform. cloud runs each app as its own OS process, so in the git process that builder is nil forever — and this handler answered 204 either way. Delivered, signature valid, green on the forge's hook page, and nothing built.
Push-to-deploy now belongs to POST https://platform.hanzo.ai/v1/git-webhook, which owns the build system-of-record and dispatches BuildKit Jobs. git.hanzo.ai delivers there through ONE forge-wide system webhook covering every repository; a repo opts in by committing hanzo.yml, not by owning a hook of its own.
The route is kept, and answers 410 naming that address, precisely so a misdirected delivery says what is wrong. Deleting it would 404, and a 404 here reads as 'the API is switched off' — the wrong conclusion this estate has already drawn twice.
POST /v1/git/zap/createRepo
Create a repository over the ZAP transport
Creates a repository in the caller's org and project scope and answers with its record. name is required and description is optional; project narrows the scope within the org. A name already taken in that scope is a 409 envelope and an invalid name a 400.
A ZAP PROCEDURE, not a REST resource. It answers the bridge's {status, msg, data} envelope rather than the raw view the /v1 route returns — which is a wire shape a typed op cannot produce, and the reason this stays a raw handler — and it calls the SAME core function the REST route calls, so the two transports cannot diverge in behaviour. Org and project scope come from the request identity and NEVER from the body: the body cannot widen the caller's scope. Without a validated org the answer is a 403 envelope.
Request body — application/json
| Field | Type | Required | Description |
|---|---|---|---|
description | string | — | |
name | string | — | |
project | string | — |
POST /v1/git/zap/deleteRepo
Delete a repository over the ZAP transport
Deletes the repository named by name and answers with the deleted name. A repository outside the caller's org and project scope is a 404 envelope, so a delete can never reach another tenant's repository.
A ZAP PROCEDURE, not a REST resource. It answers the bridge's {status, msg, data} envelope rather than the raw view the /v1 route returns — which is a wire shape a typed op cannot produce, and the reason this stays a raw handler — and it calls the SAME core function the REST route calls, so the two transports cannot diverge in behaviour. Org and project scope come from the request identity and NEVER from the body: the body cannot widen the caller's scope. Without a validated org the answer is a 403 envelope.
Request body — application/json
| Field | Type | Required | Description |
|---|---|---|---|
description | string | — | |
name | string | — | |
project | string | — |
POST /v1/git/zap/getRepo
Read one repository over the ZAP transport
Answers a single repository's record, named by name. A repository outside the caller's org and project scope is a 404 envelope, the same answer one that does not exist gets.
A ZAP PROCEDURE, not a REST resource. It answers the bridge's {status, msg, data} envelope rather than the raw view the /v1 route returns — which is a wire shape a typed op cannot produce, and the reason this stays a raw handler — and it calls the SAME core function the REST route calls, so the two transports cannot diverge in behaviour. Org and project scope come from the request identity and NEVER from the body: the body cannot widen the caller's scope. Without a validated org the answer is a 403 envelope.
Request body — application/json
| Field | Type | Required | Description |
|---|---|---|---|
description | string | — | |
name | string | — | |
project | string | — |
POST /v1/git/zap/listRepos
List your repositories over the ZAP transport
Answers every repository in the caller's org and project scope. It reads NO body — the scope is entirely the caller's identity — so a request with an empty object is correct.
A ZAP PROCEDURE, not a REST resource. It answers the bridge's {status, msg, data} envelope rather than the raw view the /v1 route returns — which is a wire shape a typed op cannot produce, and the reason this stays a raw handler — and it calls the SAME core function the REST route calls, so the two transports cannot diverge in behaviour. Org and project scope come from the request identity and NEVER from the body: the body cannot widen the caller's scope. Without a validated org the answer is a 403 envelope.
POST /v1/git/zap/usage
Report your org's git storage footprint over the ZAP transport
Answers every repository in the caller's org with its size in bytes, plus the org's total — what git storage is actually being used, and by which repository. It reads NO body, and it is scoped to the caller's own org, so it is that org's footprint and never the fleet's.
A ZAP PROCEDURE, not a REST resource. It answers the bridge's {status, msg, data} envelope rather than the raw view the /v1 route returns — which is a wire shape a typed op cannot produce, and the reason this stays a raw handler — and it calls the SAME core function the REST route calls, so the two transports cannot diverge in behaviour. Org and project scope come from the request identity and NEVER from the body: the body cannot widen the caller's scope. Without a validated org the answer is a 403 envelope.
How is this guide?