Skip to content

01 Repository And Objects

A Graft repository is an ordinary worktree plus .graft/. Applications own the physical worktree files; .graft/ stores history, staged state, refs, SQLite page storage, and recovery records.

project/
app.sqlite
settings.json
attachments/report.pdf
.graft/
config.toml
HEAD
refs/
logs/
objects/
store/
index/
locks/
tmp/

A repository path is normalized UTF-8 relative to the worktree. Empty paths, ., .., absolute paths, and paths addressing .graft are invalid identities.

PathContentsRole
config.tomlformat, branch, files, merge, and remote settingsmutable metadata
HEADsymbolic branch ref or detached commitmutable pointer
refs/heads/*local branch repository commit IDsmutable pointers
refs/remotes/*last fetched remote positionsmutable pointers
refs/tags/*lightweight or annotated tagsmutable pointers
logs/HEAD, logs/refs/*ref update logappend-only recovery aid
objects/??/*blobs, trees, commits, tagscontent-addressed immutable data
objects/pack/*packed repository objectsimmutable data
store/fjall/SQLite volumes, logs, commits, segments, pagesstorage engine data
store/files/bytes for external artifactscontent-addressed payloads
index/state.tomlnormal and merge stagesmutable staged truth
index/worktree.tomldirty/deleted observationsrebuildable optimization
MERGE_HEAD, ORIG_HEADmerge target and original headrecovery metadata
locks/, tmp/, cache/coordination, replacement, cachesnon-canonical support state

Not everything inside .graft/ has the same status. Objects and snapshots have content identity; refs and the index are mutable control state; deleting a cache should only cost performance.

refs/heads/main
|
v
commit C2 ----parent----> commit C1
| |
tree tree
|
+-- app.sqlite ----------> sqlite-snapshot-v1 blob
+-- settings.json -------> file-blob-v2
+-- report.pdf ----------> large-file-pointer-v1

The canonical loose-object envelope is:

graft-object 1 <kind> <payload-length>\0<payload>

kind is blob, tree, commit, or tag. The BLAKE3 of the whole envelope is a 64-character object ID. Loose objects use two-character fan-out. Reads recompute the ID, and an existing object with the same content never needs to be overwritten.

Worktree contentTree modeBlobByte storage
SQLite database160000sqlite-snapshot-v1pages in store/fjall
inline text/small file100644file-blob-v2Base64 in the blob
external file100644large-file-pointer-v1content-hash fan-out under store/files/

An external pointer can exist while its bytes are not locally hydrated. Knowing an artifact’s identity and being able to read its bytes are separate states.

Objects are never modified in place. A commit writes new objects and then replaces refs/heads/main:

before: main -> C1
write: C2(parent=C1)
after: main -------------> C2

HEAD normally contains a symbolic ref such as refs/heads/main; a detached HEAD contains a commit ID. Before the first commit, that symbolic branch may not exist yet: an unborn branch.

Higher-level operations write immutable dependencies before moving a ref. Expected-value updates report stale state instead of overwriting a concurrent writer.

Outside a merge, the index stores only staged differences from HEAD:

  • a stage-0 object adds or replaces a path;
  • a stage-0 entry without an object deletes a path;
  • no entry means inherit the HEAD version.

During merge, one path can have:

StageMeaning
1Base
2Ours
3Theirs
0Resolved next-commit result

Any unresolved stage prevents a normal commit. A missing stage can mean deletion; it is not by itself corruption.

QuestionAuthority
What is the app reading now?physical worktree file
What will the next commit contain?HEAD plus stage-0 index overlay
What is in a historical version?commit → tree → blob graph
What bytes does a SQLite blob represent?descriptor → storage commits/pages
Where is the current branch?HEAD and refs/heads/*
What were both sides of a conflict?index stages plus merge journal