Skip to content

Git index ingest

The data plane knows only content hashes. The Git index is the layer that puts paths, commits, branches and tags back on top, and these endpoints are how it gets populated.

In practice you don’t call them by hand — cav repo index and the pre-push hook do. This page is for anyone writing their own indexer or debugging one.

Terminal window
export API=https://cavsnode.com/api/v1
export REPO=<repository-uuid>
export H="Authorization: Bearer $CAVS_TOKEN"
export J="Content-Type: application/json"

Every endpoint requires repository.push.

GET …/index/state what the Hub already knows
POST …/index/sessions open a session, learn the page limits
POST …/index/sessions/{sessionID}/commits paged, ≤ 500 per request
POST …/index/sessions/{sessionID}/tree paged, ≤ 5000 entries per request
POST …/index/sessions/{sessionID}/finalize refs + tags, atomic generation swap

What the Hub currently knows, so an incremental indexer can compute a delta rather than re-walking history.

Terminal window
curl -sS "$API/repositories/$REPO/index/state" -H "$H"
{
"indexed": true,
"refs": [
{ "name": "main", "kind": "branch", "head_sha": "9a3f21c…" },
{ "name": "v1.4.0", "kind": "tag", "head_sha": "7c1e88b…" }
]
}

indexed is false when no refs are known yet — that’s the signal to do a full backfill (cav repo index --full).

POST /repositories/{repoID}/index/sessions

Section titled “POST /repositories/{repoID}/index/sessions”
Terminal window
curl -sS -X POST "$API/repositories/$REPO/index/sessions" \
-H "$H" -H "$J" -d '{"client_version": "cav/1.2.0"}'
201 Created
{
"session_id": "5c8a…",
"limits": {
"commits_per_page": 500,
"tree_entries_per_page": 5000
}
}

POST /repositories/{repoID}/index/sessions/{sessionID}/commits

Section titled “POST /repositories/{repoID}/index/sessions/{sessionID}/commits”

Upload a page of commits. Maximum 500 per request.

Terminal window
curl -sS -X POST "$API/repositories/$REPO/index/sessions/$SESSION/commits" \
-H "$H" -H "$J" \
-d '{
"commits": [
{
"sha": "9a3f21c4b5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0",
"parents": ["7c1e88b…"],
"author_name": "You",
"author_email": "[email protected]",
"authored_at": "2026-07-28T11:58:00Z",
"committed_at": "2026-07-28T11:58:00Z",
"message": "Retrain on the augmented set",
"files_changed": 3,
"artifacts": [
{
"path": "checkpoints/model.safetensors",
"status": "M",
"oid": "9f2c8b0e…",
"prev_oid": "1a2b3c4d…",
"size": 104857600,
"prev_size": 104857600
}
]
}
]
}'
Field Notes
sha Full Git commit SHA. Invalid values give 400.
parents Parent SHAs
author_name, author_email, authored_at, committed_at RFC 3339 timestamps
message Full commit message. Indexed for text search.
files_changed Count from the diff
artifacts[] The LFS objects this commit touched — this is what links commits to content
artifacts[].status A, M, D — added, modified, deleted
artifacts[].oid / prev_oid New and previous content hashes
200 OK
{ "accepted": 1 }

Upserts are keyed by (repository, sha), so re-sending a page is safe and idempotent. An empty commits array returns {"accepted": 0}.

POST /repositories/{repoID}/index/sessions/{sessionID}/tree

Section titled “POST /repositories/{repoID}/index/sessions/{sessionID}/tree”

Upload the file-tree snapshot for a ref. Maximum 5000 entries per request; page as needed.

Terminal window
curl -sS -X POST "$API/repositories/$REPO/index/sessions/$SESSION/tree" \
-H "$H" -H "$J" \
-d '{
"ref": "main",
"entries": [
{ "path": "checkpoints/model.safetensors", "oid": "9f2c8b0e…", "size": 104857600 },
{ "path": "data/train.parquet", "oid": "aa11bb22…", "size": 524288000 }
]
}'
Field Notes
ref Branch or tag name. Invalid values give 400.
entries[].path Repository-relative path
entries[].oid The object’s content hash — for LFS-tracked files
entries[].size Logical size

Entries accumulate into a new generation, which replaces the previous snapshot atomically at finalize. Readers never see a half-written tree.

POST /repositories/{repoID}/index/sessions/{sessionID}/finalize

Section titled “POST /repositories/{repoID}/index/sessions/{sessionID}/finalize”

Commit the session: point refs at their heads, record tags (which become releases), swap the tree generation and sweep the old one.

Terminal window
curl -sS -X POST "$API/repositories/$REPO/index/sessions/$SESSION/finalize" \
-H "$H" -H "$J" \
-d '{
"refs": [
{ "name": "main", "kind": "branch",
"head_sha": "9a3f21c…", "head_committed_at": "2026-07-28T11:58:00Z" }
],
"deleted_refs": ["experiment/old-idea"],
"tags": [
{ "tag": "v1.4.0", "target_sha": "9a3f21c…",
"message": "Vision stack 1.4.0", "tagged_at": "2026-07-28T12:00:00Z" }
],
"stats": { "commits": 1, "files": 2 }
}'
Field Notes
refs[] Branches and tags to point at their heads. kind is branch or tag.
deleted_refs[] Refs removed since the last index
tags[] Tags to materialise as releases
stats Advisory counters for the activity feed

Each array has its own size cap; exceeding one gives 400 bad_request — “too many refs in one finalize”. Split across sessions.

Finalize emits repository.index.finalized (public webhook event repository.push.indexed) and audits repository.index.

  1. GET …/index/state for the known heads.

  2. Walk your refs. For each, compute the commits between the known head and the new head (git rev-list <known>..<new>), or everything for a full backfill.

  3. Batch commit metadata with git log --stdin and per-commit LFS diffs with git diff-tree --stdin -z --first-parent. Parse LFS pointer files (they’re ≤ 1024 bytes) to get each object’s oid and size.

  4. POST …/commits in pages of ≤ 500. Retry 429 and 5xx with backoff.

  5. git ls-tree -r -l -z <ref> for the tree, and POST …/tree in pages of ≤ 5000.

  6. POST …/finalize with the refs, deleted refs and tags.

The reference implementation is cav’s git.rs and lfs.rs. Rather than reimplementing, consider shelling out to cav repo index:

Terminal window
cav repo index --full # backfill everything
cav repo index # incremental
cav repo index --ref main --ref dev # specific refs
cav repo index --dry-run # inspect what would be sent
Endpoint Depends on
GET …/tree, …/file, …/file/history tree entries + commits
GET …/commits, …/commits/{sha} commits
GET …/branches refs
GET …/releases tags
GET …/artifacts/{oid}, …/timeline commits + tree entries
GET …/storage/folders tree entries
GET /search?type=files|commits tree entries + commits

All of them degrade gracefully with "indexed": false rather than erroring.

  • Best-effort by design. The pre-push hook never blocks or fails a push. A missing index degrades the dashboard; it never loses data.
  • Idempotent. Commit upserts key on (repository, sha); tree snapshots swap wholesale by generation.
  • No bytes. Only metadata travels — paths, hashes, sizes, messages. Object content goes through the data plane.
  • Metadata is metadata. Commit messages and paths are stored so they can be shown and searched. If yours contain secrets, skip indexing (cav repo connect --skip-index).