A Git-inspired version control system built from scratch in C++20.
Mini Git is an educational systems-programming project designed to explore how modern version control systems work internally.
Rather than simply using Git's commands or libraries, the project progressively implements the fundamental mechanisms behind a version control system, including hashing, content-addressable storage, objects, trees, commits, staging, repository state, references, HEAD, and history.
Note: Mini Git is an educational project inspired by Git. It is not intended to replace Git and does not aim for full Git compatibility.
The primary goals of Mini Git are to understand and implement the core concepts behind modern version control systems:
- Content-addressable storage
- Cryptographic hashing
- Object databases
- Blob objects
- Tree objects
- Commit objects
- Staging areas
- Working Tree state
- Repository state
- References
HEAD- Branches
- Commit history
- Diffs
- Merging
- Repository integrity
- Basic version-control workflows
The project also emphasizes professional C++ development practices:
- C++20
- CMake
- Modular architecture
- Separation of concerns
- RAII
- Const correctness
- Error handling
- Automated testing
- Documentation
- Git-based development workflow
- Integration testing
Mini Git is being developed incrementally through multiple implementation phases.
- C++20 project structure
- CMake build system
- Command-line executable
- Git-based development workflow
- Documentation structure
- Automated testing through CTest
mini-git --versionmini-git init- Repository initialization
.mini-git/metadata directoryobjects/directoryrefs/heads/directory- Initial
HEADreference HEADpointing tomain- Basic
Repositoryabstraction - Repository path access through
Repository
- SHA-256 hashing
- OpenSSL integration
- OpenSSL EVP-based hashing
- Deterministic hash generation
- Hexadecimal hash representation
- Binary-data hashing support
- Known SHA-256 test vectors
- Common
Objectabstraction BlobobjectsTreeobjectsCommitobjects- Object serialization
- Tree entries
- Commit parent relationships
- Initial commits without parents
- Binary-safe file reading
FileReaderabstraction- Reading arbitrary file contents
Blob::from_file()- File contents represented as Blob objects
- Blob serialization with object type and content size
- SHA-256 object identifiers for serialized Blobs
- File → Blob → Object ID workflow
The current Blob representation is:
blob <size>\0<content>
The serialized Blob is hashed with SHA-256 to produce its object identifier.
The complete file-to-storage pipeline is:
File
│
▼
FileReader
│
▼
Blob
│
▼
serialize()
│
▼
SHA-256
│
▼
Object ID
│
▼
Object Database
│
▼
.mini-git/objects/<object-id>
ObjectDatabaseabstraction- Persistent object storage
- Object identifiers used as storage keys
- Binary-safe object writing
- Binary-safe object reading
- Object existence checking
- Duplicate-object detection
- Object retrieval by object ID
- Persistent storage under
.mini-git/objects/ mini-git hash-object <file>
The object database stores serialized objects using their SHA-256 object identifiers.
The current storage model is:
.mini-git/
└── objects/
└── <object-id>
Identical serialized objects produce the same object identifier.
If an object with that identifier already exists, Mini Git reuses the existing object instead of writing another copy.
- Tree object representation
- Tree entries for files and directories
- Deterministic Tree serialization
- Recursive directory traversal
- File-to-Blob conversion during Tree construction
- Directory-to-Tree conversion
- Nested Tree support
- Empty directory support
.mini-gitexclusion- Persistent Tree storage through the Object Database
- Automated TreeBuilder tests
A directory such as:
project/
├── README.md
├── main.cpp
├── src/
│ ├── App.cpp
│ └── Utils.cpp
└── empty/
can be represented as:
Root Tree
├── README.md → Blob
├── main.cpp → Blob
├── src → Tree
│ ├── App.cpp → Blob
│ └── Utils.cpp → Blob
└── empty → Empty Tree
TreeBuilder recursively constructs this object hierarchy and stores the resulting Trees and Blobs in the Object Database.
Mini Git's own .mini-git/ directory is excluded from generated working-tree representations.
IndexabstractionIndexEntryrepresentation- Repository-relative staged file paths
- Staged file → Blob object ID mapping
- Adding new staged entries
- Updating existing staged entries
- Duplicate path prevention
- Checking whether a path is staged
- Accessing staged entries
- Persistent Index storage
- Index serialization
- Index loading
.mini-git/indexmini-git add <file>
The Index represents the staged state between the Working Tree and the future commit system.
The current simplified Index representation is:
path<TAB>object-id
For example:
main.cpp abc123...
README.md def456...
src/App.cpp ghi789...
The staging workflow is:
Working Tree
│
│ mini-git add <file>
▼
FileReader
│
▼
Blob
│
▼
Object Database
│
▼
Object ID
│
▼
Index
│
▼
.mini-git/index
The Index does not store file contents themselves.
It stores references to immutable Blob objects in the Object Database.
Phase 9 introduced repository state inspection:
mini-git statusThe implementation compares the Index against the Working Tree.
It can identify:
- Modified tracked files
- Deleted tracked files
- Untracked files
- Nested untracked files
- Clean Working Tree state
- Internal
.mini-git/files that must not appear as untracked files
The current status model is:
Repository
│
▼
Index
│
│ compare
▼
Working Tree
│
┌──────────┼──────────┐
▼ ▼ ▼
Modified Deleted Untracked
For a tracked file:
Index object ID
│
│ compare
▼
Current file contents
│
▼
Blob serialization
│
▼
SHA-256
│
▼
Current object ID
If the current object ID differs from the Index entry, the file is reported as modified.
If the file no longer exists, it is reported as deleted.
Files present in the Working Tree but absent from the Index are reported as untracked.
The .mini-git/ directory is excluded from recursive untracked-file detection.
Phase 10 connects the existing Index, Tree, Commit, and Object Database components into the first complete repository snapshot workflow.
The implemented command is:
mini-git commit -m "message"The commit pipeline is:
Working Tree
│
│ mini-git add
▼
Index
│
│ mini-git commit
▼
TreeBuilder
│
▼
Tree
│
▼
Commit
│
▼
Object Database
│
▼
Commit Object ID
The Phase 10 implementation establishes the connection between staged files and repository-level commits.
A commit is created from the currently staged state.
For example:
mini-git add main.cpp
mini-git commit -m "Update main file"The commit process creates and stores the corresponding Tree and Commit objects.
A commit represents a snapshot of the staged repository state together with commit metadata and its parent relationship.
The initial commit has no parent.
Subsequent commits can reference the previous commit as their parent.
Conceptually:
Commit C
│
│ parent
▼
Commit B
│
│ parent
▼
Commit A
The resulting structure forms the foundation of Mini Git's commit history.
Phase 10 also establishes the basis for future HEAD-aware repository state and history inspection.
The commit workflow has been manually tested using isolated test repositories.
Example workflow:
mini-git add main.cpp
mini-git commit -m "Update main file"and:
printf 'A\n' > a.txt
mini-git add a.txt
mini-git commit -m "Add a"These tests verify that staged files can progress through:
Working Tree
↓
Index
↓
Tree
↓
Commit
↓
Object Database
The current test suite covers:
- Hash unit tests
- Object serialization tests
- FileReader tests
- Blob integration tests
- Object database tests
- TreeBuilder tests
- Index tests
- Status tests
- Known SHA-256 test vectors
- Determinism tests
- Different-input tests
- Binary-data tests
- Missing-file error tests
- Object storage tests
- Object retrieval tests
- Object existence tests
- Duplicate-object tests
- Tree serialization tests
- Deterministic Tree ordering tests
- Recursive Tree construction tests
- Empty-directory tests
.mini-gitexclusion tests- Index insertion tests
- Index update tests
- Multiple Index entries
- Index persistence tests
- Index update persistence tests
- Clean Working Tree tests
- Modified-file tests
- Deleted-file tests
- Untracked-file tests
- Nested untracked-file tests
.mini-gitstatus exclusion tests- CTest integration
Run the complete test suite with:
ctest --test-dir build --output-on-failureThe following major subsystems are planned for future phases:
- Repository discovery
mini-git add .- File deletion staging
- Full HEAD-aware
mini-git status - Staged-vs-HEAD status
- Complete reference management
mini-git log- Branch management
- Checkout
- Diff
- Merge
- Conflict handling
- Tags
- Repository integrity checking
- Garbage collection concepts
- Performance benchmarking
- Extensive repository-level integration testing
- Advanced index formats
- Full Git-compatible object formats
Mini Git is designed to go beyond simply copying Git commands.
A major goal of the project is to make the internal mechanisms of version control understandable and observable.
Planned educational commands include:
mini-git inspect
mini-git graph
mini-git explain
mini-git stats
mini-git fsck
These commands are intended to expose the internal architecture of Mini Git.
For example:
mini-git explain add main.cppcould eventually explain the internal process:
Working Tree
│
▼
Read file
│
▼
Create Blob
│
▼
Calculate SHA-256
│
▼
Store Object
│
▼
Update Index
Similarly:
mini-git explain commit -m "message"could eventually show:
Index
│
▼
Build Tree
│
▼
Store Tree
│
▼
Create Commit
│
▼
Store Commit
│
▼
Update Reference
This educational layer is an important part of the project's purpose.
The goal is not only to reproduce commands, but to expose the systems concepts underneath them.
Mini Git is being developed as a layered version-control system.
The current architecture is:
mini-git CLI
│
▼
Command Layer
│
┌────────────────┼────────────────┐
▼ ▼ ▼
Working Tree Index Repository
│ │ │
▼ │ ▼
FileReader │ Object Database
│ │ │
▼ │ ▼
Blob │ Objects
│ │ ┌────┼────┐
│ │ ▼ ▼ ▼
└────────────────┴──────── Blob Tree Commit
The intended complete version-control workflow is:
Working Tree
│
│ add
▼
Index
│
│ commit
▼
Tree
│
▼
Commit
│
▼
Reference
│
▼
HEAD
The currently implemented commit pipeline reaches:
Working Tree
│
│ add
▼
Index
│
│ commit
▼
TreeBuilder
│
▼
Tree
│
▼
Commit
│
▼
Object Database
Reference and HEAD integration will continue to be developed in later phases.
The Working Tree is the collection of files and directories currently present on disk.
Working Tree
├── main.cpp
├── README.md
└── src/
└── App.cpp
It represents the current state of the project files.
The Working Tree is the source of file contents when files are staged or inspected by status.
The Index is the staging area between the Working Tree and the repository.
The workflow is:
Working Tree
│
│ mini-git add
▼
Index
│
│ commit
▼
Tree
│
▼
Commit
Mini Git's Index stores simplified entries:
path → Blob object ID
For example:
main.cpp → abc123...
README.md → def456...
The Index is persisted at:
.mini-git/index
The Index is mutable.
Re-staging the same path replaces its staged object ID rather than creating a duplicate Index entry.
The status subsystem inspects the relationship between the Index and the current Working Tree.
Currently:
Index
│
│ compare
▼
Working Tree
A tracked file is modified when its current contents produce a different Blob object ID from the ID stored in the Index.
Indexed Blob ID
│
│ !=
▼
Current Blob ID
A tracked file is deleted when it exists in the Index but no longer exists in the Working Tree.
A file is untracked when it exists in the Working Tree but has no corresponding Index entry.
The status scanner recursively searches directories so nested untracked files can also be detected.
The .mini-git/ directory is excluded from this scan because repository metadata is not part of the Working Tree.
Full Git-like status requires three states:
HEAD
│
▼
Index
│
▼
Working Tree
The current status subsystem primarily compares:
Index ↔ Working Tree
HEAD-aware staged and unstaged comparisons will be developed as reference and history functionality is completed.
Mini Git represents repository data using three primary object types:
Object
├── Blob
├── Tree
└── Commit
A Blob represents file contents.
File
│
▼
FileReader
│
▼
File Contents
│
▼
Blob
A Blob does not need to know the filename associated with its contents.
Identical file contents can therefore correspond to the same object.
Mini Git currently serializes a Blob as:
blob <size>\0<content>
The serialized representation is subsequently hashed to produce the object's identifier.
A Tree represents directory structure.
Tree
├── main.cpp → Blob
├── README.md → Blob
└── src/ → Tree
│
└── App.cpp → Blob
Trees connect filenames and directory structure to object identifiers.
Trees can recursively contain other Trees, allowing complete directory hierarchies to be represented.
A Commit represents a repository snapshot together with metadata and history.
A simplified commit contains:
Commit
├── tree
├── parent
├── author
└── message
Commits can reference previous commits:
Commit C
│
▼
Commit B
│
▼
Commit A
This forms the foundation of the commit history graph.
Phase 10 connects these Commit objects to the Index and Tree-building pipeline.
The Phase 10 commit architecture is:
Working Tree
│
│
▼
Index
│
│ commit
▼
TreeBuilder
│
▼
Tree
│
▼
Commit
│
▼
Object Database
The Index represents the desired snapshot.
TreeBuilder converts the staged state into a Tree representation.
The Tree becomes the root snapshot referenced by the Commit.
The Commit then contains metadata such as:
- Tree object ID
- Parent commit information
- Author information
- Commit message
The resulting Commit is stored in the Object Database.
Mini Git separates filesystem operations from object representation.
FileReader is responsible for reading the exact bytes of a file.
Files are opened in binary mode so that arbitrary binary data can be represented without text-mode transformations.
Conceptually:
Filesystem
│
▼
FileReader
│
▼
Raw File Contents
│
▼
Blob
This separation allows the Blob object to remain focused on representing file contents rather than performing filesystem operations itself.
Trees provide the connection between individual objects and complete directory structures.
The current workflow is:
Directory
│
▼
TreeBuilder
│
├── File
│ │
│ ▼
│ FileReader
│ │
│ ▼
│ Blob
│
└── Directory
│
▼
TreeBuilder
│
▼
Tree
For example:
project/
├── main.cpp
├── README.md
└── src/
├── App.cpp
└── Utils.cpp
is represented conceptually as:
Root Tree
├── main.cpp → Blob ID
├── README.md → Blob ID
└── src → Tree ID
├── App.cpp → Blob ID
└── Utils.cpp → Blob ID
Tree serialization is deterministic.
Entries are sorted by name before serialization so that equivalent directory contents produce the same serialized Tree regardless of filesystem traversal order.
Empty directories are represented by empty Tree objects in Mini Git.
This is an educational simplification; real Git does not normally track empty directories as independent repository objects.
Mini Git uses SHA-256 to generate deterministic object identifiers.
The hashing layer is isolated behind the Hash abstraction.
The current hashing pipeline is:
Data
│
▼
Hash::sha256()
│
▼
OpenSSL EVP
│
▼
SHA-256
│
▼
64-character hexadecimal string
SHA-256 produces:
256 bits
↓
32 bytes
↓
64 hexadecimal characters
For example:
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
The resulting hash can serve as an object identifier.
Mini Git is designed around content-addressable storage.
The basic concept is:
Object Content
│
▼
SHA-256
│
▼
Object ID
│
▼
Object Database
The same serialized content produces the same object identifier.
Therefore:
same content
↓
same serialization
↓
same SHA-256
↓
same object ID
Mini Git uses the object ID as the key for persistent object storage:
Object Content
│
▼
SHA-256
│
▼
Object ID
│
▼
.mini-git/objects/<object-id>
This provides the foundation for:
- Object identity
- Persistence
- Deduplication
- Immutable object storage
The Object Database is responsible for persistent storage and retrieval of serialized repository objects.
Its responsibilities include:
- Storing serialized objects
- Generating object identifiers
- Checking whether objects already exist
- Reading objects by object identifier
- Avoiding duplicate object storage
The current implementation stores each object as a file named after its object ID:
.mini-git/
└── objects/
├── <object-id-1>
├── <object-id-2>
└── <object-id-3>
The Object Database is intentionally separated from the Repository abstraction.
Repository manages repository-level information such as the location of .mini-git.
ObjectDatabase manages the contents of .mini-git/objects.
This separation allows object storage to be tested independently.
The Index is the bridge between the Working Tree and commits.
Its simplified representation is:
IndexEntry
├── path
└── object_id
For example:
main.cpp → abc123...
README.md → def456...
src/App.cpp → ghi789...
When a file is staged:
mini-git add main.cppMini Git performs:
main.cpp
│
▼
FileReader
│
▼
Blob
│
▼
ObjectDatabase
│
▼
Blob ID
│
▼
Index
│
▼
.mini-git/index
The Index is stored at:
.mini-git/index
The current educational format is:
path<TAB>object-id
For example:
main.cpp abc123...
README.md def456...
The Index can be saved and loaded so that staged state survives program termination.
The Index does not duplicate Blob contents.
It references objects already stored in the Object Database.
A Mini Git repository contains a hidden .mini-git directory:
project/
├── .mini-git/
│ ├── objects/
│ ├── refs/
│ │ └── heads/
│ ├── HEAD
│ └── index
│
├── source files...
└── other project files...
The .mini-git directory contains repository metadata and internal version-control information.
Project files remain in the Working Tree.
The index file is created when staging information is first persisted.
A repository can currently be initialized with:
mini-git initThis creates:
.mini-git/
├── objects/
├── refs/
│ └── heads/
└── HEAD
The initial HEAD contains:
ref: refs/heads/main
This means that HEAD symbolically refers to the main branch.
The repository can subsequently receive staged files and commits.
The objects/ directory is initially empty.
Objects are added when commands such as hash-object, add, and commit store serialized objects in the repository.
The Index is created separately when staging information is saved.
The implementation includes a hash-file command for inspecting the file-to-Blob-to-hash pipeline:
./build/mini-git hash-file hello.txtFor example:
printf 'Hello Mini Git!' > hello.txt
./build/mini-git hash-file hello.txtRunning the command multiple times without changing the file should produce the same object identifier.
Changing the file contents should produce a different identifier.
The hash-file command is retained as an educational/debugging interface.
It calculates the object identifier without relying on persistent object storage.
Mini Git can create a Blob from a real file, calculate its object identifier, and persist the serialized object in the repository's Object Database:
./build/mini-git hash-object hello.txtThe command performs:
File
│
▼
Blob::from_file()
│
▼
Blob
│
▼
ObjectDatabase::store()
│
▼
SHA-256
│
▼
Object ID
│
▼
.mini-git/objects/<object-id>
Running the command again without changing the file produces the same object identifier.
Changing the file contents produces a different identifier while preserving the previously stored object.
Mini Git's staging command is:
./build/mini-git add hello.txtThe command performs:
hello.txt
│
▼
FileReader
│
▼
Blob
│
▼
ObjectDatabase
│
▼
Blob ID
│
▼
Index
│
▼
.mini-git/index
The Index contains the path and corresponding Blob object ID.
For example:
hello.txt 7c8f...
If the same file is staged again after being modified, its existing Index entry is updated rather than duplicated.
The Object Database may contain both the old and new Blob objects because objects are immutable and content-addressed.
Mini Git can inspect the relationship between staged files and the Working Tree:
./build/mini-git statusFor example:
./build/mini-git add main.cpp
./build/mini-git statusThe Working Tree should initially be clean:
On branch main
Working tree clean.
If the file is modified afterward:
On branch main
Changes not staged for commit:
modified: main.cpp
If a tracked file is deleted:
On branch main
Deleted files:
deleted: main.cpp
If a new file is created without staging it:
On branch main
Untracked files:
notes.txt
Nested untracked files are also detected:
Untracked files:
src/notes.txt
The internal .mini-git/ directory is ignored by the untracked-file scanner.
Mini Git's commit command is:
./build/mini-git commit -m "Initial commit"A typical workflow is:
./build/mini-git add main.cpp
./build/mini-git commit -m "Add main file"The commit pipeline is:
Working Tree
│
│ add
▼
Index
│
│ commit
▼
TreeBuilder
│
▼
Tree
│
▼
Commit
│
▼
Object Database
The staged Index represents the snapshot that should be committed.
TreeBuilder constructs the Tree representation.
The Tree object is stored in the Object Database.
A Commit object is then created referencing the Tree.
The Commit is also stored in the Object Database.
Subsequent commits can reference previous commits as parents.
Conceptually:
Commit C
│
│ parent
▼
Commit B
│
│ parent
▼
Commit A
This establishes the foundation for future history traversal.
A complete basic workflow currently looks like:
mkdir mini-git-test
cd mini-git-test
/path/to/mini-git/build/mini-git init
printf 'Hello\n' > main.cpp
/path/to/mini-git/build/mini-git add main.cpp
/path/to/mini-git/build/mini-git commit -m "Initial commit"After modifying a file:
printf 'Updated\n' > main.cpp
/path/to/mini-git/build/mini-git add main.cpp
/path/to/mini-git/build/mini-git commit -m "Update main file"The repository now contains multiple Commit objects connected through parent relationships.
Mini Git uses CMake as its build system.
From the project root:
cmake -S . -B build
cmake --build buildThe main executable is generated at:
build/mini-git
The project uses OpenSSL for SHA-256 hashing.
On systems where CMake does not automatically locate the Homebrew installation, configure the project with:
cmake -S . -B build \
-DOPENSSL_ROOT_DIR=/opt/homebrew/opt/openssl@3Then build:
cmake --build buildRun Mini Git with:
./build/mini-gitExpected output:
Mini Git
./build/mini-git --versionExpected output:
mini-git version 0.1.0
Create a separate directory for testing:
mkdir mini-git-test
cd mini-git-testThen run Mini Git:
/path/to/mini-git/build/mini-git initThe result should be:
mini-git-test/
└── .mini-git/
├── objects/
├── refs/
│ └── heads/
└── HEAD
The HEAD file should contain:
ref: refs/heads/main
Running init again should report that a repository already exists.
Mini Git uses automated tests to validate individual components and their interactions.
The current test suite includes:
tests/
├── HashTests.cpp
├── ObjectTests.cpp
├── FileReaderTests.cpp
├── BlobTests.cpp
├── TreeBuilderTests.cpp
├── IndexTests.cpp
└── StatusTests.cpp
The hash tests verify:
- Known SHA-256 values
- Deterministic hashing
- Different inputs producing different hashes
- Binary data handling
The object tests verify:
- Blob serialization
- Tree serialization
- Deterministic Tree ordering
- Commit serialization
- Initial commits without parents
- Object database storage
- Object existence checking
- Object retrieval
- Duplicate-object detection
The FileReader tests verify:
- Text file reading
- Binary file reading
- Preservation of null bytes
- Missing-file error handling
The Blob tests verify:
- Creating Blobs from real files
- Correct Blob serialization
- Binary-file Blob handling
The TreeBuilder tests verify:
- Building Trees from directories
- Converting files into Blobs
- Recursive nested directory handling
- Empty directory handling
- Tree persistence
.mini-gitexclusion
The Index tests verify:
- Adding entries
- Detecting staged paths
- Updating existing entries
- Preventing duplicate paths
- Multiple staged entries
- Index persistence
- Loading staged entries
- Persistence of updated entries
The Status tests verify:
- Clean Working Tree detection
- Modified tracked files
- Deleted tracked files
- Untracked files
- Nested untracked files
.mini-gitexclusion- Recursive directory scanning
Repository-level commit tests and broader integration tests will continue to expand as later phases are implemented.
CTest is used to execute the complete automated test suite.
Run:
ctest --test-dir build --output-on-failureMini Git itself is version-controlled using Git.
The development workflow is:
Modify Code
↓
Build
↓
Run Tests
↓
Inspect Behavior
↓
Update Documentation
↓
Commit Changes
↓
Push to GitHub
Git manages the source code of Mini Git.
Mini Git is separately used to experiment with the concepts that Git itself implements.
This creates an interesting development relationship:
Git
│
│ manages Mini Git source code
▼
Mini Git
│
│ experiments with version control concepts
▼
Test Repositories
Mini Git is intentionally built incrementally.
Instead of immediately implementing a large collection of commands, the project first establishes the internal mechanisms required to support those commands.
The fundamental version-control workflow is:
Working Tree
│
│ add
▼
Index
│
│ commit
▼
Tree
│
▼
Commit
│
▼
Branch
│
▼
HEAD
The object model underneath this workflow is:
Files
│
▼
Blobs
│
▼
Trees
│
▼
Commits
│
▼
References
│
▼
HEAD
Each subsystem is implemented and tested before more complex functionality is built on top of it.
Git is commonly used as a command-line tool without requiring users to understand its internal implementation.
Building a simplified version from scratch provides an opportunity to understand:
- How files become versioned objects
- How content-addressable storage works
- How cryptographic hashes identify data
- How objects can be reused
- How directory structures are represented
- How staging works
- How the Index references immutable objects
- How repository status can be determined
- How commits reference previous commits
- How branches are represented
- How
HEADworks - How commit history forms a graph
- How version-control operations manipulate repository state
- How a systems-oriented C++ application can be designed and tested
Mini Git is therefore both a software-engineering project and a systems-programming learning project.
C++20
CMake
OpenSSL 3
The project currently uses OpenSSL's EVP interface for SHA-256 hashing.
- Git
- GitHub
- CMake
- C++ standard library
- Unix / Linux concepts
The project currently uses and plans to use facilities including:
std::filesystemstd::stringstd::vector- File streams
- Error handling facilities
- Other C++ standard-library components as required
The current source tree is:
mini-git/
├── CMakeLists.txt
├── README.md
├── LICENSE
├── .gitignore
│
├── include/
│ ├── Repository.hpp
│ ├── Hash.hpp
│ ├── Object.hpp
│ ├── Blob.hpp
│ ├── Tree.hpp
│ ├── Commit.hpp
│ ├── FileReader.hpp
│ ├── ObjectDatabase.hpp
│ ├── TreeBuilder.hpp
│ ├── Index.hpp
│ └── Status.hpp
│
├── src/
│ ├── main.cpp
│ ├── Repository.cpp
│ ├── Hash.cpp
│ ├── Blob.cpp
│ ├── Tree.cpp
│ ├── Commit.cpp
│ ├── FileReader.cpp
│ ├── ObjectDatabase.cpp
│ ├── TreeBuilder.cpp
│ ├── Index.cpp
│ └── Status.cpp
│
├── tests/
│ ├── HashTests.cpp
│ ├── ObjectTests.cpp
│ ├── FileReaderTests.cpp
│ ├── BlobTests.cpp
│ ├── TreeBuilderTests.cpp
│ ├── IndexTests.cpp
│ └── StatusTests.cpp
│
└── docs/
└── architecture.md
As the project grows, additional modules and dedicated test files will be introduced when their responsibilities become necessary.
Each component should have a clear responsibility.
For example:
Hash
→ generates object identifiers
Object
→ defines the common object interface
FileReader
→ reads raw file contents
Blob
→ represents file contents
Tree
→ represents directory structure
TreeBuilder
→ converts filesystem directories into Trees and Blobs
Commit
→ represents snapshots and history
ObjectDatabase
→ stores and retrieves objects
Repository
→ manages repository-level state
Index
→ manages staged file state
Status
→ compares repository staging state with the Working Tree
Components should not unnecessarily take responsibility for unrelated operations.
This separation becomes increasingly important as commands such as branch, checkout, diff, and merge are added.
Mini Git primarily uses the C++ standard library.
External dependencies are introduced only when they provide a meaningful advantage.
OpenSSL is currently used for cryptographic hashing rather than implementing SHA-256 manually.
Resources should be managed through C++ lifetime semantics wherever practical.
This includes:
- File streams
- Memory
- Locks
- Other resources introduced later
The codebase will be progressively reviewed for safer and clearer resource management.
Functions that do not modify an object should be marked const where appropriate.
For example:
std::string serialize() const;This communicates that serialization should not modify the object.
Operations that can fail should detect and report errors clearly.
Examples include:
- Repository already exists
- Repository does not exist
- File cannot be opened
- Object does not exist
- Invalid object
- Invalid reference
- Invalid command
- Invalid filesystem path
- Invalid Index data
- Invalid commit operation
The CLI should provide useful error messages instead of silently failing.
Mini Git is intentionally much smaller than Git.
It does not attempt to reproduce every Git feature.
Full compatibility with real Git is not the primary objective.
The priorities are:
Understanding
+
Correct Implementation
+
Clean Architecture
+
Testing
+
Documentation
The project uses simplified internal representations where appropriate for educational purposes.
For example, Mini Git currently uses:
- SHA-256 rather than Git's historical default SHA-1
- A simplified object serialization format
- A flat object-storage layout
- A simplified text-based Index format
- A simplified Tree representation
- A simplified Commit representation
- Simplified repository status semantics
These differences are intentional and will be documented rather than hidden.
The current Index format also has limitations around filenames containing whitespace because the first implementation uses a simple text representation.
A more robust path encoding or binary Index format can be introduced later if needed.
The current add implementation is also still limited to individual files:
mini-git add <file>Directory-wide staging with:
mini-git add .has not yet been implemented.
The current status implementation primarily compares the Index against the Working Tree.
Full HEAD-aware status will be introduced as commit references and repository state management mature.
Understand the fundamental concepts behind version control and Git.
Establish the C++20 project, CMake, Git workflow, testing structure, and documentation.
Implement:
mini-git initCreate the initial .mini-git repository structure and HEAD.
Implement deterministic cryptographic hashing using SHA-256 and OpenSSL.
Implement the foundational object types:
- Blob
- Tree
- Commit
and their serialization interfaces.
Connect Blobs to real file contents and establish the first complete object-identity workflow.
File
↓
FileReader
↓
Blob
↓
Serialization
↓
SHA-256
↓
Object ID
Implement persistent object storage and retrieval.
Implemented:
File
↓
Blob
↓
Serialization
↓
SHA-256
↓
Object ID
↓
Object Database
↓
.mini-git/objects/<object-id>
The Object Database supports:
- Storing objects
- Reading objects
- Checking object existence
- Reusing existing objects with the same object ID
The hash-object command provides a command-line interface for creating and storing Blob objects from files.
Construct Tree objects from repository directory structures.
Implemented:
- File-to-Blob conversion
- Directory-to-Tree conversion
- Recursive Tree construction
- Nested directories
- Empty directories
- Deterministic Tree serialization
.mini-gitexclusion- Persistent Tree storage
Implement the staging area between the Working Tree and the commit system.
Implemented:
IndexIndexEntry- Staged path → Blob object ID mapping
- Index updates
- Duplicate path prevention
- Index persistence
- Index loading
.mini-git/indexmini-git add <file>
The staging workflow is:
Working Tree
│
│ mini-git add <file>
▼
Blob
│
▼
Object Database
│
▼
Blob ID
│
▼
Index
Implement:
mini-git statusImplemented:
- Modified tracked files
- Deleted tracked files
- Untracked files
- Nested untracked files
- Recursive Working Tree scanning
.mini-gitexclusion- Clean Working Tree detection
- Automated Status tests
Current comparison:
Index ↔ Working Tree
The complete three-state model will eventually be:
HEAD
│
▼
Index
│
▼
Working Tree
Implemented.
Connect the Index, Trees, Commit objects, and Object Database to implement:
mini-git commit -m "message"The commit pipeline is:
Index
↓
TreeBuilder
↓
Tree
↓
Commit
↓
Object Database
Phase 10 establishes:
- Creating commits from staged state
- Building a repository Tree from staged information
- Creating Commit objects
- Connecting Commit objects to Trees
- Storing Trees in the Object Database
- Storing Commit objects in the Object Database
- Commit parent relationships
- Initial commits without parents
- Basic commit workflow testing
This is the first phase where Mini Git can turn staged repository state into a persistent repository snapshot.
Implement commit history inspection:
mini-git logPlanned functionality:
- Read the current commit
- Traverse parent commits
- Display commit IDs
- Display commit messages
- Display author information
- Display commit order
- Provide readable history output
Implement repository references and HEAD management.
Planned functionality:
- Reading references
- Writing references
- Resolving
HEAD - Updating branch references
- Connecting commits to branch pointers
Implement branch creation and management.
Planned functionality:
mini-git branch
mini-git branch <name>Branches will become references pointing to commits.
Implement switching between repository states.
Planned functionality:
- Checkout branches
- Restore files from Trees
- Update
HEAD - Update Working Tree
- Update Index
Compare repository states and Working Tree changes.
Planned functionality:
- Working Tree vs Index
- Index vs HEAD
- Commit vs commit
- File-level differences
- Human-readable diff output
Implement simplified merging.
Planned functionality:
- Fast-forward merging
- Basic three-way merge
- Merge-base discovery
- Combining repository histories
Detect and represent merge conflicts.
Planned functionality:
- Conflict detection
- Conflict reporting
- Conflict markers
- Conflict resolution workflow
Implement lightweight tags.
Planned functionality:
- Create tags
- Resolve tags
- Inspect tagged commits
Explore repository maintenance concepts.
Planned functionality:
- Object reachability
- Reachable vs unreachable objects
- Unused object detection
- Garbage-collection concepts
- Repository maintenance analysis
Expand testing coverage.
Planned testing includes:
- Unit tests
- Integration tests
- Repository-level tests
- Edge-case tests
- Failure tests
- Multi-commit workflows
- Branch tests
- Checkout tests
- Diff tests
- Merge tests
- Conflict tests
Improve validation, error handling, and failure recovery.
Focus areas:
- Invalid repositories
- Corrupt objects
- Invalid references
- Invalid commit data
- Filesystem failures
- Interrupted operations
- Better CLI error messages
- Defensive validation
Benchmark important repository operations and identify performance bottlenecks.
Potential benchmarks include:
- Hashing
- Blob creation
- Object storage
- Tree construction
- Status scanning
- Commit creation
- History traversal
Review the architecture and modern C++ practices.
Focus areas:
- RAII
- Const correctness
- Ownership
- Interfaces
- Testability
- Separation of concerns
- Error handling
- Maintainability
- Modern C++20 practices
Document:
- Architecture
- Internal object formats
- Index format
- Repository layout
- Commit format
- Reference management
- Design decisions
- Architectural tradeoffs
- Simplifications compared with Git
- Implementation details
Compare Mini Git with real Git feature-by-feature.
The comparison will identify:
- Similarities
- Differences
- Simplifications
- Architectural choices
- Object-format differences
- Storage differences
- Index differences
- Reference differences
- History differences
- Limitations
The purpose is educational rather than compatibility-focused.
Build the unique educational features that distinguish Mini Git from a basic Git clone.
Planned commands include:
mini-git inspect
mini-git explain
mini-git graph
mini-git stats
mini-git fsck
Expose internal repository objects and metadata.
Example:
mini-git inspect <object-id>Potential output:
Object ID: abc123...
Type: Blob
Size: 42 bytes
Stored at:
.mini-git/objects/abc123...
Explain what Mini Git is doing internally.
Example:
mini-git explain commit -m "Initial commit"Potential output:
1. Read Index
2. Build Tree
3. Store Tree
4. Create Commit
5. Store Commit
6. Update reference
Visualize the commit history as an ASCII graph.
Example:
* Commit C
|
* Commit B
|
* Commit A
Eventually this can become a richer representation of branches and merges.
Expose repository statistics.
Potential statistics include:
- Number of objects
- Number of Blobs
- Number of Trees
- Number of Commits
- Repository object storage size
- Number of tracked files
- Number of branches
- Commit counts
- Object reuse
Inspect repository integrity.
Potential checks include:
- Missing objects
- Invalid object IDs
- Invalid Trees
- Invalid Commits
- Broken parent references
- Broken Tree references
- Invalid references
- Unreachable objects
These features are intended to make the internals of Mini Git observable and understandable.
Prepare the project for professional presentation.
Deliverables:
- GitHub repository
- High-quality README
- Architecture documentation
- Technical documentation
- Resume description
- LinkedIn description
- Interview explanation
- Technical discussion points
- Demonstration workflow
- Project screenshots / terminal demonstrations
- Clean commit history
- Professional repository organization
Current phase: Phase 10 — Commits
The core file-to-object pipeline is implemented:
Working Tree
│
▼
FileReader
│
▼
Blob
│
▼
SHA-256
│
▼
Object ID
│
▼
Object Database
The staging system is implemented:
Working Tree
│
│ add
▼
Index
│
▼
.mini-git/index
The status subsystem can compare:
Index
│
│ compare
▼
Working Tree
│
┌──────────┼──────────┐
▼ ▼ ▼
Modified Deleted Untracked
The object hierarchy is implemented:
Filesystem
│
├── Files
│ ↓
│ Blobs
│
└── Directories
↓
Trees
↓
Commits
The Phase 10 commit pipeline is now implemented:
Working Tree
│
│ add
▼
Index
│
│ commit
▼
TreeBuilder
│
▼
Tree
│
▼
Commit
│
▼
Object Database
Mini Git can now turn staged files into persistent repository commits.
The next major subsystem is:
Phase 11 — Log
which will traverse the commit parent chain and expose the repository's history.
The long-term architecture is:
mini-git CLI
│
▼
Command Layer
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
Working Tree Index Repository
│ │ │
▼ │ ▼
FileReader │ References
│ │ │
▼ │ ▼
Blob │ HEAD
│ │ │
└──────────┬─────┘ │
│ │
▼ │
Object Database ◄────────────┘
│
┌──────────┼──────────┐
▼ ▼ ▼
Blob Tree Commit
│
▼
History
│
┌────────┼────────┐
▼ ▼ ▼
Branch Merge Tags
The educational layer will eventually sit above the repository architecture:
mini-git CLI
│
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
Normal Commands Educational Tools Diagnostics
│ │ │
│ ├── inspect ├── fsck
│ ├── explain └── stats
│ └── graph
│
▼
Version Control Engine
This is intended to make Mini Git not merely a small Git imitation, but a systems-programming project for understanding how version control works internally.
This project is provided under the terms of the license included in the repository.