Machine Coding Problem

Git Version Control

maco30maco60macoAllinfrastructurecomposite-patternhashingdirected-acyclic-graph-(dag)
Commonly Asked By:GitHubGitLabAtlassianMicrosoft

Requirements & System Scope

The Git Version Control system must support:

  • Initializing a repository (init)
  • Adding files to a staging area (add)
  • Committing staged files with metadata (commit)
  • Viewing the commit history (log)

Class Diagram & Entity Relationships

Structural layout showing clean encapsulation of state and the DAG-based object model:

Loading...
  • Polymorphic Git Objects: All core version-controlled entities (Blob, Tree, Commit) inherit from the abstract GitObject base, enabling them to be stored uniformly in the ObjectDatabase key-value map.
  • Encapsulated Repo State: Repository isolates the staging index and active branches pointers from the immutable object store (odb).

Design Patterns & SOLID Principles

  • Composite Pattern: The Git Object model follows a composite pattern where a Tree contains references to Blobs and other Trees, forming a recursive file system directory structure.
  • Content-Addressable Storage (Flyweight): Objects are uniquely identified by their content's SHA-1 hash. Identical file contents naturally deduplicate and resolve to the exact same hash, saving massive amounts of storage space.
  • Directed Acyclic Graph (DAG): Commit objects form a DAG via parent hash references. This immutable graph structure elegantly enables branching, merging, and prevents infinite loops in history traversal.

Core Execution Workflows

1. Adding a File to Staging

  1. Read the file content from the local disk.
  2. Create a new Blob object and compute its SHA-1 hash based on its serialized content.
  3. Store the Blob in the Object Database (odb).
  4. Update the index (staging area) mapping the file path to the newly generated Blob's hash.

2. Committing Staged Files

  1. Iterate over the index to gather all staged file paths and their associated blob hashes.
  2. Create a Tree object representing this root directory structure, compute its hash, and store it in the odb.
  3. Retrieve the current HEAD commit hash to act as the parent for the new commit.
  4. Create a Commit object referencing the Tree hash, the parent hash, author metadata, and the commit message.
  5. Store the Commit in the odb.
  6. Update the active branch pointer to point to this newly created Commit hash.

Concurrency & Thread Safety Strategy

While a local Git implementation is typically single-user, simulating a central version control server (like GitHub) requires strict concurrency control to prevent branch corruption during simultaneous pushes.

  • Immutable Objects: Once any GitObject (Blob, Tree, Commit) is created and hashed, it is entirely immutable. This makes reading from the ObjectDatabase inherently thread-safe without requiring read locks.
  • Atomic Pointer Updates: Updating branch references (e.g., advancing HEAD or a specific branch pointer) is a critical section. Concurrent commits to the same branch could lead to a race condition (lost updates). This is mitigated using atomic reference updates or lock files (e.g., .lock files on branch refs in actual Git).
  • Concurrent Collections: In a multi-threaded JVM/in-memory model, the ObjectDatabase store and the branches mapping should use ConcurrentHashMap to handle concurrent read/write throughput securely.

Complete Clean Code Blueprint

Below is a clean, multi-language codebase blueprint showcasing our design patterns and thread-safety strategies in Java and Python:

// ─── JAVA BLUEPRINT ──────────────────────────────────────────────────────────
import java.security.MessageDigest;
import java.util.*;
import java.util.concurrent.*;

// 1. Core Git Objects (Composite Pattern)
abstract class GitObject {
    protected String hash;
    public String getHash() { return hash; }
    public abstract String type();
    public abstract String serialize();
    
    protected void computeHash() {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-1");
            byte[] hashBytes = digest.digest(serialize().getBytes());
            StringBuilder sb = new StringBuilder();
            for (byte b : hashBytes) {
                sb.append(String.format("%02x", b));
            }
            this.hash = sb.toString();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

class Blob extends GitObject {
    private final String content;

    public Blob(String content) {
        this.content = content;
        computeHash();
    }

    @Override public String type() { return "blob"; }
    @Override public String serialize() { return "blob " + content.length() + "\0" + content; }
    public String getContent() { return content; }
}

class TreeEntry {
    public final String mode;
    public final String name;
    public final String hash;
    public TreeEntry(String mode, String name, String hash) {
        this.mode = mode; this.name = name; this.hash = hash;
    }
}

class Tree extends GitObject {
    private final List<TreeEntry> entries = new ArrayList<>();

    public Tree(List<TreeEntry> entries) {
        this.entries.addAll(entries);
        this.entries.sort(Comparator.comparing(e -> e.name)); // Canonical sort
        computeHash();
    }

    @Override public String type() { return "tree"; }
    @Override public String serialize() {
        StringBuilder sb = new StringBuilder();
        for (TreeEntry e : entries) sb.append(e.mode).append(" ").append(e.name).append("\0").append(e.hash);
        return "tree " + sb.length() + "\0" + sb.toString();
    }
}

class Commit extends GitObject {
    private final String treeHash;
    private final String parentHash;
    private final String author;
    private final String message;
    private final long timestamp;

    public Commit(String treeHash, String parentHash, String author, String message) {
        this.treeHash = treeHash;
        this.parentHash = parentHash;
        this.author = author;
        this.message = message;
        this.timestamp = System.currentTimeMillis();
        computeHash();
    }

    @Override public String type() { return "commit"; }
    @Override public String serialize() {
        String content = "tree " + treeHash + "\n" +
                (parentHash != null ? "parent " + parentHash + "\n" : "") +
                "author " + author + " " + timestamp + "\n\n" +
                message + "\n";
        return "commit " + content.length() + "\0" + content;
    }
    public String getParentHash() { return parentHash; }
}

// 2. Object Database
class ObjectDatabase {
    private final Map<String, GitObject> store = new ConcurrentHashMap<>();

    public void store(GitObject obj) { store.put(obj.getHash(), obj); }
    public GitObject get(String hash) { return store.get(hash); }
}

// 3. The Repository Facade
class Repository {
    private final ObjectDatabase odb = new ObjectDatabase();
    private final Map<String, String> index = new ConcurrentHashMap<>(); // path -> blob hash
    private final Map<String, String> branches = new ConcurrentHashMap<>(); // branch -> commit hash
    private volatile String head = "main"; // current branch

    public Repository() {
        branches.put("main", null);
    }

    public synchronized void add(String path, String content) {
        Blob blob = new Blob(content);
        odb.store(blob);
        index.put(path, blob.getHash());
    }

    public synchronized String commit(String author, String message) {
        List<TreeEntry> entries = new ArrayList<>();
        for (Map.Entry<String, String> entry : index.entrySet()) {
            entries.add(new TreeEntry("100644", entry.getKey(), entry.getValue()));
        }
        Tree tree = new Tree(entries);
        odb.store(tree);

        String parentCommitHash = branches.get(head);
        Commit commit = new Commit(tree.getHash(), parentCommitHash, author, message);
        odb.store(commit);

        branches.put(head, commit.getHash());
        return commit.getHash();
    }

    public void log() {
        String curr = branches.get(head);
        while (curr != null) {
            Commit c = (Commit) odb.get(curr);
            System.out.println("commit " + c.getHash());
            System.out.println("Author: " + c.serialize().split("\n")[2]);
            System.out.println("\n    " + c.serialize().split("\n\n")[1]);
            curr = c.getParentHash();
        }
    }
}

// 4. Main Driver Class
public class GitRunner {
    public static void main(String[] args) {
        Repository repo = new Repository();
        
        System.out.println("--- Adding and Committing First File ---");
        repo.add("hello.txt", "Hello World!");
        repo.commit("Alice <alice@example.com>", "Initial commit: added hello.txt");
        
        System.out.println("\n--- Adding and Committing Second File ---");
        repo.add("main.java", "public class Main {}");
        repo.commit("Bob <bob@example.com>", "Added main.java class");
        
        System.out.println("\n--- Git Log ---");
        repo.log();
    }
}

Interactive Simulator

⚙️ Git Version Control Simulator

Simulate staging files, creating commit objects, and building the DAG history.

➕ git add <file>

Filename:
Content:

💾 git commit -m

Message:

📦 Staging Area (Index)

No files staged.

📜 git log (HEAD -› main)

No commits yet.
[05:37:29]Initialized empty Git repository.

💬Review

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...