Rust · content-addressed · git-shaped
Timelace is a tiny content-addressed version control system in Rust, small enough to read in one sitting. Blobs, trees, and commits, each one addressed by the SHA-256 hash of its own bytes. Here is the real object model, running in your browser.
Add a file, then add another with identical content and compare blob ids below.
| Name | Content | Blob id |
|---|
A commit hashes a tree of the working files above, plus the current HEAD as its parent.
HEAD
(no commits yet)
Walked from HEAD by following each commit's parent link. Check out an earlier commit to restore its snapshot into the working files above.
Every blob, tree, and commit ever created in this session, addressed only by the hash of its own bytes. Identical blob content always lands on the same id, so it is stored once.
real SHA-256 hashing, in your browser, nothing touches a network
Every object's name is a hash of its own content. That single rule is what makes content addressing work, and it is the entire idea Timelace is built to make visible.
The bytes of one file, nothing else. Two files with identical content share one blob, automatically.
Names mapped to blob or tree ids, sorted before hashing so identical contents always produce the same id.
A tree id, an optional parent, a message, and a timestamp. Follow parent links to walk the whole history.
The full workflow, initialize a repository, stage a file, commit it twice, walk the history, and check an earlier snapshot back out. This is real output captured from the CLI.
$ timelace init
initialized empty timelace repository in ./.timelace
$ echo "hello" > notes.txt
$ timelace add notes.txt
$ timelace commit -m "first commit"
committed a3dd0375a9657697e621c10312a6f272b1bb8fdd97dacb8da2f6a4f3bd932e35
$ echo "hello again" > notes.txt
$ timelace add notes.txt
$ timelace commit -m "second commit"
committed a36723d63290713b16b3e4590038a3dfe37a7d76b03f81dcc81aa0769b019d05
$ timelace log
commit a36723d63290713b16b3e4590038a3dfe37a7d76b03f81dcc81aa0769b019d05
timestamp 1788785036
second commit
commit a3dd0375a9657697e621c10312a6f272b1bb8fdd97dacb8da2f6a4f3bd932e35
timestamp 1788785034
first commit
$ timelace checkout a3dd0375a9657697e621c10312a6f272b1bb8fdd97dacb8da2f6a4f3bd932e35
$ cat notes.txt
hello
Because an object's id is the hash of its own bytes, deduplication needs no bookkeeping. Stage two different files with identical content, and the tree records one blob for both.
$ printf 'hello\n' > a.txt
$ printf 'hello\n' > b.txt # identical content, different name
$ timelace add a.txt
$ timelace add b.txt
$ timelace commit -m "two identical files"
committed c5461d0b23fd0081cc9a6a16be0a76b4ddcb695672165f38cd3f229424a032cd
$ find .timelace/objects -type f | wc -l
3 # one commit, one tree, one shared blob
Timelace is not a git replacement. It is the same object model git is built on, implemented directly and left readable, with the layers that are not the point stripped away.
Content-addressed blobs, trees, and commits are exactly how git stores history underneath. The idea is small, but the real implementation buries it under packfiles, refs, the index cache, and decades of features.
The same content addressing done directly: SHA-256 over a framed object, a two-level object directory, a HEAD pointer, and a staging index. No branches, remotes, merges, or packfiles, so the whole model fits in one file you can read in five minutes.
init, add, commit, log, status, and checkout, with automatic deduplication and exact working-tree restoration, all covered by an integration suite. Read src/objects.rs and you have read the entire model.
Objects live in a two-level directory split by the first two characters of their id, the same trick git uses to keep any one directory small.
A CLI over the object store, a Rust library underneath it, and an integration suite that pins the behavior down.
init, add, commit, log, status, and checkout. A file argument to add can be a single path or a whole directory.
The object model in objects.rs and the store in store.rs are a plain Rust library. The only dependencies are sha2 for hashing and clap for argument parsing; the model itself is from scratch.
cargo test runs 15 tests: initialization, staging and committing, parent-chain linking, log ordering, content deduplication, exact checkout restoration, and the error paths for operating outside a repository, unknown ids, and an empty commit.
$ cargo test
Running unittests src/lib.rs
running 6 tests
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running tests/integration.rs
running 9 tests
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Not a git replacement. A way to actually see how content-addressed version control works, with no packfiles, no plumbing and porcelain split, no thousands of files standing between you and the idea. Read src/objects.rs and you have read the whole model.
Source, tests, and design notes are public on GitHub. The object model, the on-disk layout, and the commit and checkout algorithms are documented in DESIGN.md.