How Git is Different

TL;DR: Git differs fundamentally from traditional version control systems (like SVN) by storing data as complete snapshots of the project rather than file-based differences (deltas). Because Git is a local-first system, nearly every command runs instantly offline using a local database. Under the hood, Git uses content-addressable storage, where every file, directory tree, and commit is mapped via its unique cryptographic SHA-1 hash.


The Paradigm Shift: How Git Redefined Version Control

To understand Git, you must unlearn how traditional version control systems view the world. Developers transitioning from older systems like Subversion (SVN), Perforce, or CVS often struggle because they try to force Git into a centralized, delta-based mental model. Git is not just a faster SVN; it is built on a completely different set of computer science principles.

The Delta Model vs. The Snapshot Model

Traditional Centralized VCS (CVCS) systems use a Delta-based (or difference-based) storage model. They track files individually over time, recording the step-by-step modifications (lines added or removed) made to each file.

Centralized VCS (Delta Model):
File A: [V1] --------> [Delta A1] --------> [Delta A2]
File B: [V1] -----------------------------> [Delta B1]
File C: [V1] --------> [Delta C1] ------------------->

To reconstruct File A at Version 3, the system must read the base file [V1], apply [Delta A1], and then apply [Delta A2]. While this saves storage space, it makes operations like switching branches or viewing historical file states computationally expensive. The system must reconstruct the requested version on the fly by resolving the chain of changes.

Git rejects this approach. Instead of tracking file deltas, Git views its data as a stream of Snapshots of a miniature filesystem. Every time you record a commit, Git takes a picture of what all files in the project look like at that exact moment.

Git VCS (Snapshot Model):
Commit 1: [Blob A1]    [Blob B1]    [Blob C1]
             │                         │
Commit 2: [Blob A2]    [Blob B1]    [Blob C2]   (Blob B1 is reuse-linked)
             │            │            
Commit 3: [Blob A3]    [Blob B2]    [Blob C2]   (Blob C2 is reuse-linked)

To keep storage size small, Git does not duplicate unmodified files. If a file has not changed in a new commit, Git does not save it again. Instead, it stores a reference link pointing back to the pre-existing compression blob of that file.

Why the Snapshot Model Matters

  1. Instantaneous Branching and Tagging: Because a commit is a reference to a tree snapshot, creating a branch does not copy files. It simply writes a 41-byte text file representing a pointer to a specific commit.
  2. Speedy History Navigation: Checking out a historical version requires no complex delta arithmetic. Git simply swaps your current working files with the files listed in that historical snapshot.
  3. Robust Integrity: Because every file is addressed by its hash, Git detects corruption instantly.

What Happens Inside .git/ (Hands-on exploration)

To see the difference between Git’s architecture and other systems, we must explore its three-state system. The division between the Working Directory, the Staging Area (Index), and the Repository Database is the core operational flow of Git.

Let’s initialize a new repository and trace how Git’s files change under the hood as we transition a file through these three states.

Step 1: Setting up the environment

First, create a clean workspace and run git init to generate a local Git database:

$ mkdir git-difference-deepdive && cd git-difference-deepdive
$ git init
Initialized empty Git repository in /home/admin/Code/git-difference-deepdive/.git/

Right now, we have an empty Working Directory (our workspace where we edit files) and a pristine .git/ folder containing our Repository Database. The Staging Area does not exist yet; it is created dynamically when we stage our first file.

Step 2: Creating a File (The Working Directory State)

Create a simple text file:

$ echo "Git is a content-addressable database." > architecture.txt

At this stage, architecture.txt exists only in the Working Directory. Git’s index file and object database know nothing about it. If we run git status, Git alerts us that we have an “Untracked file”:

$ git status
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	architecture.txt

nothing added to commit but untracked files present (use "git add" to track)

Step 3: Staging the File (The Staging Area / Index State)

The Staging Area (historically called the Index) is a binary cache file that tracks the exact structure of files that will go into your next commit. Let’s stage the file using git add:

$ git add architecture.txt

Let’s look inside .git/ to see what changed:

$ ls -al .git/
total 28
drwxr-xr-x 7 admin admin 4096 Jul  7 10:24 .
drwxr-xr-x 3 admin admin 4096 Jul  7 10:24 ..
-rw-r--r-x 1 admin admin   23 Jul  7 10:24 HEAD
-rw-r--r-x 1 admin admin   92 Jul  7 10:24 config
-rw-r--r-x 1 admin admin   73 Jul  7 10:24 description
drwxr-xr-x 2 admin admin 4096 Jul  7 10:24 hooks
-rw-r--r-x 1 admin admin  137 Jul  7 10:24 index      # <-- The Staging Index!
drwxr-xr-x 2 admin admin 4096 Jul  7 10:24 info
drwxr-xr-x 6 admin admin 4096 Jul  7 10:24 objects   # <-- Storage folder modified!
drwxr-xr-x 4 admin admin 4096 Jul  7 10:24 refs

When we ran git add, two things happened:

  1. An object was written: Git compressed the contents of architecture.txt using zlib and wrote it to .git/objects/.
  2. The Index was updated: The binary .git/index file was updated to list the path architecture.txt and its corresponding object hash.

Let’s find the written object:

$ find .git/objects -type f
.git/objects/48/20067b0d7714856f776a3e5c9a1d82f7c0064d

The object hash is 4820067b0d7714856f776a3e5c9a1d82f7c0064d. Let’s use git ls-files --stage to inspect the contents of the binary .git/index file:

$ git ls-files --stage
100644 4820067b0d7714856f776a3e5c9a1d82f7c0064d 0	architecture.txt

This output shows that our staging index currently maps the filename architecture.txt directly to the object hash 4820067b0d7714856f776a3e5c9a1d82f7c0064d.

What happens if we modify the file in our working directory after adding it, but before committing? Let’s append a line:

$ echo "It calculates SHA-1 hashes of file contents." >> architecture.txt

If we run git status, we see that the file is in two states simultaneously!

$ git status
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
	new file:   architecture.txt

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   architecture.txt

This occurs because:

  • The Staging Area still contains the reference to 4820067b0d7714856f776a3e5c9a1d82f7c0064d (the first version of the file).
  • The Working Directory contains the modified file with the extra line. If we run git commit now, only the first version (which is staged in the index) will be recorded. Git commits the staging area, not the working directory! To update the staged state, we must run git add architecture.txt again. Let’s do that:
$ git add architecture.txt

Let’s check the staging index again:

$ git ls-files --stage
100644 5e0622ee1ee135cf5723ee6d2e0b57e0cf558eb6 0	architecture.txt

The hash in the index has changed to 5e0622ee1ee135cf5723ee6d2e0b57e0cf558eb6. Let’s check our objects database:

$ find .git/objects -type f
.git/objects/48/20067b0d7714856f776a3e5c9a1d82f7c0064d  # Old version blob
.git/objects/5e/0622ee1ee135cf5723ee6d2e0b57e0cf558eb6  # Staged version blob

Both blobs are now inside the repository objects database!

Step 4: Committing (The Repository State)

Let’s record the snapshot by running git commit:

$ git commit -m "feat: describe Git's database model"
[main (root-commit) b1871a2] feat: describe Git's database model
 1 file changed, 2 insertions(+)
 create mode 100644 architecture.txt

Our commit has been successfully written as commit object b1871a2 (the short hash). Let’s see all written objects now:

$ find .git/objects -type f
.git/objects/48/20067b0d7714856f776a3e5c9a1d82f7c0064d  # Initial staging blob
.git/objects/5e/0622ee1ee135cf5723ee6d2e0b57e0cf558eb6  # Committed blob
.git/objects/b1/871a2cd024bc6808ad6bfbfbfd63e9f45f7bbd  # Commit object
.git/objects/b7/cde210e30372df0d9f45efc1c5a938676f6259  # Tree object

Let’s inspect the tree and commit objects using git cat-file:

$ git cat-file -p b1871a2cd024bc6808ad6bfbfbfd63e9f45f7bbd
tree b7cde210e30372df0d9f45efc1c5a938676f6259
author Developer <dev@dotgit.dev> 1778153000 +0000
committer Developer <dev@dotgit.dev> 1778153000 +0000

feat: describe Git's database model

The commit object points to the tree b7cde210e30372df0d9f45efc1c5a938676f6259. Let’s inspect that tree:

$ git cat-file -p b7cde210e30372df0d9f45efc1c5a938676f6259
100644 blob 5e0622ee1ee135cf5723ee6d2e0b57e0cf558eb6    architecture.txt

The tree object maps our filename to the staged blob object 5e0622ee1ee135cf5723ee6d2e0b57e0cf558eb6.

graph TD
    Commit[Commit Object: b1871a2] -->|points to| Tree[Tree Object: b7cde21]
    Tree -->|maps filename to| Blob[Blob Object: 5e0622e]
    style Commit fill:#f9f,stroke:#333,stroke-width:2px
    style Tree fill:#bbf,stroke:#333,stroke-width:2px
    style Blob fill:#bfb,stroke:#333,stroke-width:2px

This layout highlights how Git’s snapshot model works: instead of storing lines of changes, Git creates a hierarchy of tree and blob references for every single commit.


Detailed Command Options & Advanced Usage

1. The Anatomy of Content-Addressable Storage

In most filesystems, you retrieve a file using its human-readable path name, such as /home/user/document.txt. The filesystem looks up the path in a directory table to locate the physical sectors on disk.

Git operates on a content-addressable basis. You retrieve files using the cryptographic hash of their content, regardless of their file path.

Let’s test this using Git’s low-level plumbing command, git hash-object. This command calculates the SHA-1 hash of a string, or writes it directly into Git’s object database:

# Calculate the hash of a string without writing it
$ echo "content-addressable" | git hash-object --stdin
2e4e73cc3514a387ee0cb95c80879ecfb5789f2d

# Write the object to the database
$ echo "content-addressable" | git hash-object -w --stdin
2e4e73cc3514a387ee0cb95c80879ecfb5789f2d

Let’s verify that the object was created in .git/objects/:

$ find .git/objects/2e/
.git/objects/2e/4e73cc3514a387ee0cb95c80879ecfb5789f2d

How does Git calculate this hash? It formats the content with a header in the pattern: blob <size>\0<content>. Let’s calculate the SHA-1 of this header+content representation in standard bash:

$ printf "blob 20\0content-addressable\n" | sha1sum
2e4e73cc3514a387ee0cb95c80879ecfb5789f2d  -

The output hash is identical! Since the hash is computed from the content and metadata header, two files with identical contents will share the exact same hash and be stored as a single blob in Git’s database, regardless of their filenames or locations in your directory tree.

2. Local-First Operations and Offline Capabilities

In traditional centralized VCS architectures (like SVN or CVS), the local workspace is a thin client. Most commands must query the remote central server to execute:

  • Running git log in Git reads the history directly from local files in .git/logs/ and .git/objects/. This takes milliseconds and works fully offline.
  • Running svn log sends a network request to the SVN server, parsing and streaming the history over HTTP or custom TCP protocols.

Because Git stores a full history clone, you can commit, branch, merge, stash, and rollback on a plane, in a train, or in areas with poor internet connectivity. You only connect to a server when pushing or fetching changes.

3. Loose Objects and Packfiles: The Efficiency Balance

Storing every file revision as a complete snapshot might seem like it would consume massive amounts of disk space. If you modify a single line of a 10MB file, Git creates a new 10MB blob object.

To prevent disk space issues, Git periodically runs a garbage collection process (git gc). This process converts individual, compressed Loose Objects (single files in .git/objects/) into a single compressed Packfile (.pack files inside .git/objects/pack/).

During this packaging process, Git performs Delta Compression. It searches for files with similar names and sizes, calculates the differences between them, and stores only the delta. However, unlike SVN, Git does this as a background optimization step rather than a core storage model. Under the hood, Git’s API continues to serve files as clean, absolute snapshots.


Real-World Scenario: Crafting Clean History

Suppose you have spent the last three hours modifying files to resolve a bug. In the process, you also cleaned up some code styling and updated the README documentation.

If you were using SVN, you would commit everything at once, resulting in a single commit containing unrelated changes. This makes the commit message vague and makes future rollbacks difficult.

With Git’s staging area, you can split these changes into separate, logical commits.

  1. Check the modified files:

    $ git status
    On branch main
    Changes not staged for commit:
      modified:   src/auth.js
      modified:   src/helper.js
      modified:   README.md
  2. Stage and commit the bugfix first: You want to commit only the changes in src/auth.js and src/helper.js that resolve the bug. You stage them explicitly:

    $ git add src/auth.js src/helper.js
    $ git commit -m "fix(auth): prevent session timeout overflow"
  3. Stage and commit the documentation update separately: Now, your staging area is clean, and only README.md remains unstaged:

    $ git status
    On branch main
    Changes not staged for commit:
      modified:   README.md
    
    $ git add README.md
    $ git commit -m "docs: update setup instructions in README"

By separating your changes, you create a clear, readable commit log that makes tracking modifications straightforward.


Common Pitfalls and Mistakes

1. Forgetting to Stage Modified Files

The Mistake: You edit index.js, run git commit -m "update index", and realize the changes were not committed because you did not run git add first. The Fix: Always run git add to stage your changes before committing, or use the shortcut git commit -am "message" (which stages and commits modified, tracked files in a single step).

2. Accidental Detached HEAD State

The Mistake: Running git checkout <commit-hash> to look at an old version. You are no longer on a branch; you are in a “detached HEAD” state. Any commits made here will not update your branches and can easily be lost. The Fix: To return to safety, switch back to your branch:

$ git switch main

If you want to make changes based on a historical commit, create a new branch from it:

$ git switch -c feature/historical-fix <commit-hash>

3. Running destructive checkout commands

The Mistake: Running git checkout -- <file> or git restore <file> to discard changes in your working directory. This command is destructive and unrecoverable; since the discarded changes were never staged or committed, Git cannot recover them. The Fix: Double-check your local changes with git diff before discarding them.


Command Quick Reference

CommandPurposeGit-Specific Difference
git add -pStages changes interactively in chunks (patches)Allows splitting modifications in a single file into different commits
git ls-files --stageDisplays files in the staging area with their SHA-1 hashesDirectly inspects the binary .git/index file
git cat-file -t <hash>Returns the type of a Git object (blob, tree, commit, tag)Inspects Git’s local database
git cat-file -p <hash>Pretty-prints the contents of a Git objectInspects Git’s local database
git hash-object -w <file>Computes the SHA-1 hash of a file and writes it to the databaseLow-level plumbing command
git gcCleans up loose objects and compiles packfilesCompresses local database using delta compression

FAQ (People Also Ask)

Why does Git use snapshots instead of deltas?

Git uses snapshots to make key operations like branching, merging, and checkout instantaneous. Because Git stores files as complete snapshots (compressed blobs), it doesn’t need to compute chain deltas to check out a version. This simplifies Git’s architecture and makes it structurally faster than delta-based systems.

What are the three states in Git?

The three states are:

  1. Working Directory: The local filesystem folder containing your files that you edit.
  2. Staging Area (Index): A binary file tracking the changes prepared for the next commit snapshot.
  3. Repository (.git): The local database containing all committed object snapshots and branch references.

How does Git maintain cryptographic integrity?

Git hashes all contents (blobs, trees, commits) using SHA-1. Every commit hash is calculated from the file contents, directory structures, committer metadata, and the hash of the parent commit. This creates a chain of references where changing any character in a historical file alters every subsequent hash, making history tamper-proof.

Does Git branch creation duplicate my files?

No. Creating a branch in Git does not copy your project files. It simply writes a 41-byte text file inside .git/refs/heads/ containing the 40-character SHA-1 hash of the commit you are branching from. This makes branching in Git cheaper and faster than in systems like SVN.


Test Your Knowledge

Loading quiz…