libvctrl
A robust, content-addressed version control engine for arbitrary data, designed for embedding into applications.
libvctrl provides the core data model, storage abstractions, hashing, encoding, commands, diffing, and three-way merging needed to build version control functionality directly into applications -- without shelling out to an external VCS or depending on a CLI tool. It is a library only and does not ship a binary.
Table of Contents
- Overview
- Architecture
- Installation
- Dependencies
- Quick Start
- Domain Model
- Storage
- Hashing
- Encoding
- Commands
- Diffing
- Merging
- Error Handling
- Module Reference
- Testing
- Build and Lint
- Security Considerations
- Limitations
- Roadmap
- License
Overview
libvctrl implements a content-addressed version control engine similar in principle to Git's object model, but with key differences:
- SHA-512 hashes instead of SHA-1, providing 256 bits of collision resistance.
- Trait-based abstractions for storage, hashing, encoding, diffing, and merging, allowing custom backends and algorithms without modifying the core.
- Command pattern for all operations, providing a uniform interface that
accepts mutable references to an
ObjectStoreand aRefStore. - Embedded design -- no CLI, no subprocess calls, no filesystem
assumptions beyond what the storage backend requires. The provided
MemoryStoreandMemoryRefStorerequire no filesystem at all.
Architecture
src/
lib.rs Crate root, re-exports all modules
error.rs VctrlError enum
codec/ Encoding format
mod.rs Encoder trait and re-exports
binary.rs BinaryEncoder implementation
command/ Command pattern operations
mod.rs Command trait and re-exports
branch.rs CreateBranch, DeleteBranch, GetBranch, SetHead
checkout.rs Checkout (recursive tree materialization)
create_commit.rs CreateCommit
log.rs Log (commit history traversal)
merge.rs MergeCommand
diff/ Tree diffing
mod.rs TreeDiff trait, DiffKind, DiffEntry
tree_diff.rs TreeDiffer implementation
domain/ Core domain types
mod.rs Re-exports all domain types
blob.rs Blob (content-addressed data)
commit.rs Commit (snapshot record)
hash.rs Hash (64-byte SHA-512), HashError
object.rs Object enum (Blob, Tree, Commit)
tree.rs Tree, TreeEntry, EntryKind, TreeError
user.rs UserInfo (name + email)
hashing/ Hashing trait and implementation
mod.rs Hasher trait and re-exports
sha512.rs Sha512Hasher
merge/ Three-way merge
mod.rs ThreeWayMerge trait
resolver.rs ConflictResolver trait
three_way.rs ThreeWayMerger implementation
storage/ Storage backends
mod.rs Re-exports
traits.rs ObjectStore, RefStore traits
memory.rs MemoryStore, MemoryRefStore
Installation
There are several ways to add libvctrl to your Rust project:
1. Using cargo add
This command will automatically add the dependency line to Cargo.toml.
2. Adding Manually in Cargo.toml
Add the following line to the [dependencies] section:
[]
= { = "https://github.com/mroczect/libvcrtl.git" }
3. Clone the repository and use it as a local dependency (path)
If you want to develop or modify the library alongside your project, clone the repository first:
Then, in your project's Cargo.toml, navigate to the cloned path:
[]
= { = "../libvcrtl" } # adjust the directory location
With this method, changes you make to the library will be immediately reflected in the main project upon compilation.
4. Fork and use it as a Git dependency from your fork
You can fork the repository to your own GitHub account, then use it the same way as method 1 or 2, just replace the URL to your fork repository:
[]
= { = "https://github.com/your-username/libvcrtl.git" }
Toolchain Requirements
This library uses Rust edition 2024. Make sure your toolchain supports that edition (Rust 1.85.0 or later). To check the installed Rust version:
If your toolchain is older, update it with:
If for some reason you need to use an older edition (e.g., 2021), you can change the edition line in libvctrl's Cargo.toml from "2024" to "2021". However, keep in mind that some syntactic features may not be available.
Dependencies
libvctrl depends on the following crates (handled automatically by Cargo):
chrono0.4.45 (serdefeature)serde1.0.229 (derivefeature)serde_json1.0.151sha20.11.0thiserror2.0.19
All public types and traits are exported directly in the crate root, so you can import them easily:
use ;
See the tests/ directory in the repository for complete usage examples.
Dependencies
| Crate | Version | Purpose |
|---|---|---|
| chrono | 0.4.45 | Timestamps for commits (with serde feature) |
| serde | 1.0.229 | Serialization framework (with derive feature) |
| serde_json | 1.0.151 | JSON serialization |
| sha2 | 0.11.0 | SHA-512 digest computation |
| thiserror | 2.0.19 | Error derive macro |
Quick Start
use ;
// Set up storage
let mut store = new;
let mut refs = new;
// Create a blob and store it
let blob = new;
let hasher = Sha512Hasher;
let blob_hash = hasher.hash_blob;
store.put.unwrap;
// Create a tree with one entry
let entry = new;
let tree = new.unwrap;
let encoder = BinaryEncoder;
let mut buf = Vecnew;
encoder.encode_tree;
let tree_hash = hasher.hash_tree_encoded;
store.put.unwrap;
// Create a branch and set HEAD
let author = new;
CreateBranch
.execute.unwrap;
SetHead
.execute.unwrap;
// Create a commit
let commit_hash = CreateCommit .execute.unwrap;
println!;
Domain Model
Blob
A content-addressed data container. The inner data field is private to
enforce encapsulation.
Methods:
| Method | Signature | Description |
|---|---|---|
new |
(data: Vec<u8>) -> Self |
Construct from raw bytes |
as_bytes |
(&self) -> &[u8] |
Borrow the inner bytes |
into_bytes |
(self) -> Vec<u8> |
Consume and return the inner bytes |
Blob implements Debug, Clone, Serialize, and Deserialize.
Hash
;
A 64-byte (512-bit) SHA-512 hash value. The inner array is private. Hash
is Copy because 64 bytes is small enough for stack allocation.
Construction methods:
| Method | Signature | Description |
|---|---|---|
from_bytes |
(bytes: [u8; 64]) -> Self |
Construct from a fixed-size array (const) |
from_slice |
(&[u8]) -> Result<Self, HashError> |
Construct from a slice; fails if length is not 64 |
from_hex |
(&str) -> Result<Self, HashError> |
Construct from a 128-character hex string |
Access methods:
| Method | Signature | Description |
|---|---|---|
as_bytes |
(&self) -> &[u8; 64] |
Borrow the inner byte array |
to_hex |
(&self) -> String |
Produce a 128-character lowercase hex string |
Trait implementations:
| Trait | Behavior |
|---|---|
Debug |
Formats as Hash(<hex>) |
Display |
Formats as the bare hex string |
FromStr |
Parses from hex via from_hex |
Serialize |
Serializes as a hex string |
Deserialize |
Deserializes from a hex string with validation |
Clone, Copy, PartialEq, Eq, Hash |
Standard |
Tree and TreeEntry
A Tree represents a directory listing: an ordered collection of named
references to child objects (blobs or subtrees). Entries are sorted
lexicographically by name on construction and duplicate names are rejected.
Tree methods:
| Method | Signature | Description |
|---|---|---|
new |
(entries: Vec<TreeEntry>) -> Result<Self, TreeError> |
Construct, sort by name, detect duplicates |
entries |
(&self) -> &[TreeEntry] |
Borrow the sorted entries |
into_entries |
(self) -> Vec<TreeEntry> |
Consume and return the entries |
is_empty |
(&self) -> bool |
Check if there are no entries |
TreeEntry::new:
TreeError:
Because entries are sorted on construction, the hash of a tree is deterministic regardless of the input order. This is critical for content-addressed storage: two trees with the same entries in different orders produce the same hash.
Commit
A snapshot record linking a tree to metadata.
Fields:
| Field | Type | Description |
|---|---|---|
tree |
Hash |
Hash of the root tree this commit captures |
parents |
Vec<Hash> |
Zero or more parent commit hashes (empty for root commits) |
author |
UserInfo |
The original author of the change |
committer |
UserInfo |
The identity that created this commit object |
timestamp |
DateTime<Utc> |
Automatically set to Utc::now() on construction |
message |
String |
Commit message |
signature |
Option<Vec<u8>> |
Optional cryptographic signature bytes |
Commit::new:
The timestamp is always Utc::now() at construction time. There is no way to
set a custom timestamp through new. This ensures that commits created
through the API always have a valid, recent timestamp.
UserInfo
Author or committer identity. No validation is performed on the name or
email fields. The struct is a plain data holder.
Object
Tagged union of all storable object types. Commit is boxed to avoid
infinite type recursion (a Commit contains UserInfo which contains
String, and the overall size is large enough that boxing reduces stack
pressure).
Object::obj_type:
Returns "blob", "tree", or "commit".
Storage
ObjectStore Trait
Trait for content-addressed object storage. Implementations store and
retrieve Object values keyed by their Hash.
put-- Store an object. If an object with the same hash already exists, the implementation may overwrite it or silently ignore the duplicate (the memory backend overwrites).get-- Retrieve an object. ReturnsOk(None)if the hash is not present.exists-- Check whether a hash is present without retrieving the object.
RefStore Trait
Trait for named reference and HEAD management.
set_ref/get_ref/delete_ref-- Manage named references (branches, tags, etc.) that map string names toHashvalues.set_head-- Set the HEAD pointer. Thetargetis a string that can be either a symbolic reference (starting withrefs/) or a direct hex hash.head-- Resolve HEAD to aHash. If HEAD is a symbolic reference, the implementation resolves it throughget_ref. If HEAD is a direct hash, it is parsed from hex.head_ref_name-- Return the symbolic reference name that HEAD points to, orNoneif HEAD is a direct hash or unset. Used byCreateCommitto update the current branch after creating a commit.
MemoryStore
In-memory ObjectStore backed by HashMap<Hash, Object>. Provides new()
and implements Default. All objects are kept in memory for the lifetime of
the store. Suitable for testing, prototyping, and transient computations.
MemoryRefStore
In-memory RefStore backed by HashMap<String, Hash> for references and
Option<String> for HEAD.
HEAD resolution logic in head():
- If
headisNone, returnOk(None). - If
headisSome(target)andtargetstarts with"refs/", resolve by callingget_ref(target). - If
headisSome(target)and does not start with"refs/", parse it as a 128-character hex hash viaHash::from_hex. ReturnsErr(VctrlError::Hash)if the hex is invalid.
head_ref_name() logic:
- If
headisSome(target)andtargetstarts with"refs/", returnOk(Some(target.clone())). - Otherwise, return
Ok(None).
Hashing
Hasher Trait
Trait for content-addressed hashing. Each method takes raw or encoded data
and produces a Hash. The separate methods allow implementations to include
a type prefix in the hash input, preventing cross-type hash collisions.
Sha512Hasher
;
SHA-512 implementation of Hasher.
Hash format: Each method computes SHA-512(prefix || length_be || 0x00 || data):
| Method | Prefix | length_be |
|---|---|---|
hash_blob |
"blob " |
data.len() as u64, big-endian |
hash_tree_encoded |
"tree " |
data.len() as u64, big-endian |
hash_commit_encoded |
"commit " |
data.len() as u64, big-endian |
The prefix includes a trailing space (for example, b"blob "). The null
byte 0x00 separates the header from the data. This format is modeled after
Git's object hashing and ensures that two different object types with
identical content produce different hashes.
Example:
let hasher = Sha512Hasher;
let blob_hash = hasher.hash_blob;
let other_hash = hasher.hash_blob;
assert_eq!; // deterministic
let different_hash = hasher.hash_blob;
assert_ne!;
Encoding
Encoder Trait
Trait for serializing domain objects to a byte buffer. The buffer is appended to (not cleared), allowing multiple objects to be encoded into the same buffer.
BinaryEncoder
;
Binary format implementation of Encoder.
Binary Format Specification
Tree encoding:
| Offset | Size | Value |
|---|---|---|
| 0 | 1 | Version byte: 0x01 |
| 1 | 4 | Entry count (big-endian u32) |
| 5 | ... | For each entry: |
Per entry:
| Offset | Size | Value |
|---|---|---|
| +0 | 2 | Name length (big-endian u16) |
| +2 | name_len | Name bytes (UTF-8) |
| +2+name_len | 1 | Kind: 0x00 = Blob, 0x01 = Tree |
| +3+name_len | 64 | Hash bytes (raw, 64 bytes) |
Commit encoding:
| Offset | Size | Value |
|---|---|---|
| 0 | 1 | Version byte: 0x01 |
| 1 | 64 | Tree hash |
| 65 | 4 | Parent count (big-endian u32) |
| 69 | 64 * n | Parent hashes |
| ... | ... | Author (see below) |
| ... | ... | Committer (see below) |
| ... | 8 | Timestamp seconds (big-endian i64) |
| ... | 4 | Timestamp sub-second nanoseconds (big-endian u32) |
| ... | 4 | Message length (big-endian u32) |
| ... | msg_len | Message bytes (UTF-8) |
| ... | 4 | Signature length (big-endian u32); 0 if None |
| ... | sig_len | Signature bytes (if present) |
User (author/committer) encoding:
| Offset | Size | Value |
|---|---|---|
| +0 | 2 | Name length (big-endian u16) |
| +2 | name_len | Name bytes (UTF-8) |
| +2+name_len | 2 | Email length (big-endian u16) |
| +4+name_len | email_len | Email bytes (UTF-8) |
Commands
Command Trait
All operations implement this trait. Each command takes mutable references to
an object store and a reference store, and returns a typed output or a
VctrlError. The command itself is consumed by reference (&self), not by
value, allowing it to be reused.
Branch Operations
CreateBranch
Creates or updates a named reference. The name must start with
"refs/heads/". Returns Err(VctrlError::InvalidRef) otherwise. On
success, returns Ok(()).
DeleteBranch
Deletes a named reference. The name must start with "refs/heads/".
Returns Err(VctrlError::InvalidRef) otherwise. On success, returns
Ok(()). Deleting a nonexistent reference silently succeeds (the memory
backend's HashMap::remove behavior).
GetBranch
Retrieves the hash associated with a named reference. The name must start
with "refs/heads/". Returns Ok(Some(hash)) if the reference exists,
Ok(None) if it does not, or Err(VctrlError::InvalidRef) if the name is
invalid.
SetHead
Sets the HEAD pointer. The target must be either:
- A branch reference starting with
"refs/heads/", or - A valid 128-character hexadecimal hash.
Returns Err(VctrlError::InvalidRef) if the target is neither. On success,
returns Ok(()).
CreateCommit
Creates a commit object, stores it, and updates the current branch reference.
Execution steps:
- Construct a
Commitwithtimestamp: Utc::now()andsignature: None. - Encode the commit using the provided
encoder. - Hash the encoded bytes using the provided
hasher. - Store the commit as
Object::Commit(Box::new(commit)). - If HEAD points to a symbolic reference (via
refs.head_ref_name()), update that reference to the new commit hash. - Return the commit hash.
The encoder and hasher are boxed trait objects, allowing the caller to inject custom implementations.
Log
;
Traverses the commit history starting from HEAD, following the first parent of each commit.
Execution steps:
- Resolve HEAD. If HEAD is unset, return an empty vector.
- Starting from the HEAD hash, look up the commit object.
- Push the commit into the result vector.
- Follow
commit.parents[0]to the next commit. - Repeat until a commit with no parents is reached or an object is not found.
The result is ordered from most recent to oldest (newest commit first).
Limitation: Only follows the first parent. Merge commits' second and subsequent parents are ignored. This produces a linear history view.
Checkout
Recursively materializes a tree into a flat list of file paths and their contents.
Output: Vec<(String, Vec<u8>)> where each tuple is (path, data).
Execution steps:
- Look up the tree object by
tree_hash. - For each entry in the tree:
- If
EntryKind::Blob: look up the blob, prepend the current path prefix, and add(path, blob.into_bytes())to the result. - If
EntryKind::Tree: recurse into the subtree, extending the path prefix with"{prefix}/{name}".
- If
- Return the flat list.
Depth limit: Recursion is capped at 1000 levels. If exceeded, returns
Err(VctrlError::Other("max checkout depth exceeded")).
Error: Returns Err(VctrlError::NotFound("tree not found")) if the
hash does not resolve to a tree object.
MergeCommand
Executes a three-way merge. Delegates to the provided ThreeWayMerge
implementation with the base, ours, and theirs tree hashes. Returns
the hash of the merged tree on success, or Err(VctrlError::MergeConflict)
on unresolvable conflicts.
Diffing
TreeDiff Trait
DiffKind and DiffEntry
| Variant | Meaning |
|---|---|
Added |
Entry exists in new_tree but not in old_tree |
Removed |
Entry exists in old_tree but not in new_tree |
Modified |
Entry exists in both but with different hashes; captures both old and new hashes |
TreeDiffer
;
Implementation of TreeDiff. Converts both trees to BTreeMap<String, TreeEntry>,
collects the union of keys, and classifies each entry by comparing presence
and hash equality. Entries present in both trees with the same hash are
omitted from the result (no diff). The output is ordered by key name because
BTreeSet iteration is sorted.
Merging
ThreeWayMerge Trait
ConflictResolver Trait
Called when both ours and theirs have modified the same blob relative to
base. Returns Some(resolved_data) to resolve the conflict, or None to
fail with VctrlError::MergeConflict.
The resolver receives the raw bytes of the base, ours, and theirs blobs. It does not receive the entry name or path. Resolvers that need path context must capture it through other means (closures, environment variables, etc.).
ThreeWayMerger
;
Full three-way merge implementation. Handles all nine combinations of entry presence in (base, ours, theirs):
| base | ours | theirs | Result |
|---|---|---|---|
| - | O | - | Added by ours: keep O |
| - | - | T | Added by theirs: keep T |
| B | - | T | Removed by ours, modified by theirs: keep T |
| B | O | - | Modified by ours, removed by theirs: keep O |
| B | - | - | Removed by both: omit |
| B | O | T (O==T) | Same modification: keep O |
| B | O | T (O==B) | Ours unchanged, theirs modified: keep T |
| B | O | T (T==B) | Theirs unchanged, ours modified: keep O |
| B | O | T (all differ) | Conflict: call resolver |
When all three versions differ and the entries are blobs, the
ConflictResolver is called. If it returns Some(data), a new blob is
created and stored. If it returns None, VctrlError::MergeConflict is
returned.
When all three versions differ and the entries are trees, the merger
recurses into the subtrees. When the entry kinds differ (one is Blob, the
other is Tree), VctrlError::MergeConflict is returned with reason
"type mismatch".
Depth limit: Recursion is capped at 1000 levels. If exceeded, returns
Err(VctrlError::Other("max merge depth exceeded")).
After merge: The merged tree is constructed via Tree::new (which sorts
and validates entries), encoded, hashed, and stored. The hash of the merged
tree is returned.
Error Handling
VctrlError
| Variant | Source | When produced |
|---|---|---|
Hash |
HashError |
Invalid hash length or hex string |
Tree |
TreeError |
Duplicate tree entry name |
NotFound |
String | Object or tree not found in store |
InvalidRef |
String | Branch name lacks refs/heads/ prefix, or HEAD target is invalid |
MergeConflict |
entry, reason | Unresolvable conflict during three-way merge |
| 5 | Io |
std::io::Error |
Serialization |
String | Serialization failure |
Backend |
String | Backend-specific error |
Other |
String | Catch-all for uncategorized errors |
VctrlError implements std::error::Error (via thiserror), Debug, and
Display. The Hash, Tree, and Io variants implement From for
automatic conversion with the ? operator.
HashError
TreeError
Module Reference
| Module | Status | Description |
|---|---|---|
codec |
Implemented | Encoder trait and BinaryEncoder |
command |
Implemented | Command trait and all command implementations |
diff |
Implemented | TreeDiff trait, DiffKind, DiffEntry, TreeDiffer |
domain |
Implemented | Blob, Hash, Tree, TreeEntry, Commit, UserInfo, Object |
error |
Implemented | VctrlError, HashError, TreeError |
hashing |
Implemented | Hasher trait and Sha512Hasher |
merge |
Implemented | ThreeWayMerge trait, ConflictResolver trait, ThreeWayMerger |
storage |
Implemented | ObjectStore and RefStore traits, MemoryStore, MemoryRefStore |
Testing
22 tests across 7 test files:
blob_test (2 tests)
| Test | Verifies |
|---|---|
blob_new_and_access |
Blob::new and as_bytes() round-trip |
blob_into_bytes |
into_bytes() returns original data |
branch_test (3 tests)
| Test | Verifies |
|---|---|
branch_create_get_delete |
Create, get, and delete branch round-trip |
branch_invalid_name |
Name without refs/heads/ prefix returns InvalidRef |
set_head_works |
SetHead resolves HEAD to the branch's hash |
checkout_test (4 tests)
| Test | Verifies |
|---|---|
checkout_flat_tree |
Flat tree materializes to expected file paths |
checkout_recursive |
Nested tree produces paths with / separators |
checkout_empty_tree |
Empty tree produces empty file list |
checkout_nonexistent_tree_error |
Non-existent tree returns NotFound |
commit_test (3 tests)
| Test | Verifies |
|---|---|
create_commit_and_log |
Create a commit and retrieve it via Log |
commit_chain_log |
Two-commit chain produces correct history order |
commit_getters |
Commit struct field access and default timestamp |
diff_test (2 tests)
| Test | Verifies |
|---|---|
diff_added_removed_modified |
Added, removed, and modified entries detected |
diff_no_changes |
Identical trees produce empty diff |
merge_test (3 tests)
| Test | Verifies |
|---|---|
merge_no_conflict |
Non-conflicting changes merge correctly |
merge_conflict_blob |
Blob conflict returns MergeConflict |
merge_resolved |
KeepOursResolver resolves conflict and produces correct result |
tree_test (5 tests)
| Test | Verifies |
|---|---|
tree_new_sorts_entries |
Entries are sorted by name on construction |
tree_duplicate_entries_error |
Duplicate names return TreeError::DuplicateEntry |
tree_hash_deterministic |
Different input order produces same hash |
tree_empty |
Empty tree is valid and reports is_empty() |
tree_into_entries |
into_entries() returns the entries |
Run the test suite:
Build and Lint
The project includes a Makefile. Run make ci for the full CI pipeline
(format check, clippy, and tests).
Security Considerations
- SHA-512 collision resistance. The hashing scheme uses SHA-512, which provides 256 bits of collision resistance. This is sufficient for all practical purposes and is stronger than Git's SHA-1.
- Type-prefixed hashing. Each hash includes the object type (
blob,tree,commit) as a prefix. This prevents cross-type hash collisions where a blob and a tree with the same content would produce the same hash. - Content-addressed integrity. Objects are stored and retrieved by their content hash. Any corruption of the stored data will result in a hash mismatch when the object is re-read and verified by the caller.
- No cryptographic signing. The
Commit::signaturefield is an opaqueOption<Vec<u8>>. libvctrl does not create, verify, or interpret signatures. Applications must implement signing and verification themselves. - No encryption. libvctrl does not encrypt objects. If confidentiality is required, the application must encrypt data before storing it as a blob.
- Depth limits.
CheckoutandThreeWayMergerenforce a maximum recursion depth of 1000. This prevents stack overflow from maliciously crafted deeply nested trees. - Memory store has no persistence.
MemoryStoreandMemoryRefStoreexist only in RAM. Data is lost when the process exits. Applications requiring persistence must implement a filesystem or database backend.
Limitations
- Only an in-memory storage backend is provided.
Logonly follows the first parent, producing linear history.Checkoutproduces in-memory file lists, not filesystem writes.- Branch names must start with
refs/heads/. Tags and remote references are not supported. MergeCommandproduces a merged tree but does not create a merge commit.ConflictResolverreceives blob data only, without path context.Commit::newalways setstimestamptoUtc::now(). Custom timestamps are not supported.- The binary encoding does not include a CRC or integrity checksum beyond the SHA-512 content hash.
Roadmap
- Filesystem storage backend.
- Tag support (
refs/tags/). - Remote reference namespace (
refs/remotes/). - Full ancestry traversal (all parents, not just first).
- Merge commit creation as part of
MergeCommand. - Path-aware conflict resolver.
- Custom commit timestamps.
- Streaming encoding and decoding for large objects.
- Pack format for efficient storage.
- crates.io publication.
License
This project is licensed under the MIT License. See the LICENSE file in the repository for the full text.