Module 04

Inside the `.git` Folder

Developer Tools Git, GitHub & GitHub Copilot

Module 04 — Inside the .git Folder

Goal: Open Git’s hood and understand every file and folder it creates, how objects are stored, and how a “commit” really works on disk. This is the module that turns you from a Git user into someone who understands Git.

This is the longest and most important module. Take it slowly. Once it clicks, Git stops being magic and becomes obvious.


4.1 First look

Right after git init, list everything inside .git:

cd my-first-repo
ls -aF .git

You’ll see something like:

HEAD
config
description
hooks/
info/
objects/
refs/

After you make some commits, more appear:

HEAD
config
description
index
COMMIT_EDITMSG
ORIG_HEAD          (sometimes)
logs/
hooks/
info/
objects/
refs/

Let’s go through every one.


4.2 The files, one by one

A tiny text file that answers “where am I?” Open it:

cat .git/HEAD
# ref: refs/heads/main

It usually contains a symbolic reference — a pointer to a branch, not directly to a commit. So HEADrefs/heads/main → a commit hash. When you switch branches, this file is rewritten. In the rare “detached HEAD” state (when you check out a commit directly), this file contains a raw commit hash instead of a ref: line.

config

The local configuration for this repository (the --local level from Module 02). It holds repo-specific settings and, importantly, your remotes (where GitHub lives):

cat .git/config
[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
[remote "origin"]
    url = git@github.com:you/my-first-repo.git
    fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
    remote = origin
    merge = refs/heads/main

description

Only used by an old web interface called GitWeb. For everyday GitHub work, ignore it. It exists for historical reasons.

index

This file is the staging area from Module 03. It’s a binary file listing every tracked file, its hash, and metadata. When you run git add, Git updates index. When you commit, Git turns index into a permanent snapshot. You won’t read it directly (it’s binary), but now you know: “staging area” and “the index” both mean this file.

git ls-files --stage     # a human-readable view of what's in the index

COMMIT_EDITMSG

A scratch file holding the message of your most recent commit. When you run git commit without -m, Git opens your editor pointed at this file. Harmless to ignore.

ORIG_HEAD

A safety bookmark. Before doing something big and potentially destructive (a merge, rebase, reset, or pull), Git saves where HEAD was into ORIG_HEAD. If things go wrong you can recover with git reset --hard ORIG_HEAD. It’s Git quietly leaving you a breadcrumb.

FETCH_HEAD / MERGE_HEAD (appear temporarily)

  • FETCH_HEAD records what was just downloaded by git fetch (Module 06).
  • MERGE_HEAD exists only during a merge that’s in progress, recording the other commit being merged in. It disappears once the merge completes or is aborted.

packed-refs (appears later)

For efficiency, Git sometimes compresses many branch/tag pointer files into a single packed-refs text file instead of keeping thousands of tiny separate files. It’s just an optimization; the meaning is identical to the loose refs described below.


4.3 The folders, one by one

hooks/

Contains hook scripts — programs Git runs automatically at certain moments (before a commit, after a merge, before a push, etc.). On init, Git fills this with .sample files like pre-commit.sample. They do nothing until you remove the .sample extension and make them executable. Hooks let you, for example, run tests or linters before every commit. We use them in Module 09.

ls .git/hooks
# pre-commit.sample  pre-push.sample  commit-msg.sample  ...

info/

Holds an exclude file — like .gitignore, but personal and not committed. Use it to ignore files only on your machine without changing the shared .gitignore.

logs/

Records the history of where your references have pointed — this powers the reflog, your ultimate safety net (Module 08). Open it:

cat .git/logs/HEAD

Every time HEAD moves (commit, checkout, reset, merge), a line is appended here. Even if you “lose” a commit, its hash usually still lives in this log, so you can recover it. This is why it’s hard to truly lose work in Git.

refs/

The heart of branches and tags. Inside:

  • refs/heads/ — one tiny file per local branch. The file is named after the branch and contains a single commit hash.
  • refs/tags/ — one file per tag.
  • refs/remotes/ — your last-known positions of branches on remotes (e.g. refs/remotes/origin/main).

Prove that a branch is just a file with one hash inside:

cat .git/refs/heads/main
# a1b2c3d4e5f6...     <- that's it. A branch is a 40-char pointer.

This is the big reveal: a branch is not a copy of your code. It’s a 40-byte file naming one commit. That’s why creating a branch is instant and free.

objects/

The actual database — where all your content lives. This deserves its own section.


4.4 The object database — how Git really stores everything

Everything Git stores permanently is an object, addressed by its hash. There are four types:

ObjectStoresAnalogy
BlobThe raw contents of one fileA file’s data (no name, no path)
TreeA directory listing: names → blob/tree hashes + permissionsA folder
CommitA snapshot pointer: the top tree + parent(s) + author + messageA labeled photo
TagAn annotated tag object pointing to a commitA named bookmark with metadata

Here’s the beautiful part — they nest:

commit  ──► tree (root folder)
                ├──► blob   (README.md contents)
                ├──► tree (src/ folder)
                │       └──► blob (app.js contents)
                └──► blob   (.gitignore contents)

   └──► parent commit ──► its own tree ──► ...

So a commit points to a tree (the root folder), which points to blobs (file contents) and other trees (subfolders). Walk that structure and you can reconstruct your entire project at that moment. That’s a snapshot, made real.

Seeing it with your own eyes

# What type is this object, and what's inside it?
git cat-file -t HEAD        # commit
git cat-file -p HEAD        # prints the commit: its tree, parent, author, message

Example output of git cat-file -p HEAD:

tree 9a8b7c6d...
parent 0f1e2d3c...
author Your Name <you@example.com> 1719500000 +0000
committer Your Name <you@example.com> 1719500000 +0000

Add login feature

Now follow the tree it points to:

git cat-file -p 9a8b7c6d
# 100644 blob e83c5163...  README.md
# 040000 tree 4d5e6f7a...  src

You’re literally browsing Git’s database by hand. Every blob hash there is a file’s content; every tree is a folder.

Why identical files are stored once

Because an object’s address is the hash of its content, two identical files anywhere in your project (or across all of history) produce the same blob and are stored only once. This content-addressed design is why Git is so space-efficient despite “snapshotting everything.”

Loose objects vs. packfiles

  • A fresh object is written as a loose object: a single compressed file at .git/objects/ab/cdef... (the first 2 hash characters become a subfolder, the rest the filename).
  • Over time Git runs garbage collection (git gc) and bundles many loose objects into a compressed packfile in .git/objects/pack/. This saves huge amounts of space and is where Git does finally store differences between similar objects, behind the scenes.
ls .git/objects            # 'ab', 'cd' folders (loose) plus 'pack' and 'info'
ls .git/objects/pack       # .pack and .idx files after gc

4.5 What actually happens when you commit (the full story)

Now we can narrate git commit precisely:

  1. You run git add file.js. Git compresses file.js’s contents, computes its hash, and writes a blob object into .git/objects/. It records that blob’s hash + filename in the index (.git/index).
  2. You run git commit. Git:
    • builds tree objects from the index (one per folder), writing them to .git/objects/;
    • creates a commit object pointing to the root tree, to the current HEAD commit as its parent, and stamps it with your author info and message;
    • writes that commit object to .git/objects/ and gets its hash;
    • updates the current branch file (e.g. .git/refs/heads/main) to contain the new commit’s hash;
    • appends a line to .git/logs/HEAD (the reflog).

That’s the whole mechanism. Commits, branches, history — all of it — are objects in objects/ plus pointer files in refs/, coordinated by HEAD and index. There is no hidden magic.


4.6 Bare repositories (a quick aside)

A normal repo has your files plus a .git folder. A bare repository has only the .git contents and no working files — it exists purely to be pushed to and pulled from. Servers (including, conceptually, GitHub) store your project as bare repos, because nobody edits files directly on the server. You create one with git init --bare. You rarely need this as a beginner, but it explains what a “server-side” repo really is.


Try it yourself

  1. In a repo with a couple of commits, run cat .git/HEAD and follow the trail: cat .git/refs/heads/main to get the commit hash.
  2. Run git cat-file -p HEAD and find the tree line. Then git cat-file -p <that tree hash> to list your files.
  3. Pick a blob hash from that tree and run git cat-file -p <blob hash> — you’ll see a file’s exact contents.
  4. Run cat .git/logs/HEAD and watch your every move recorded.
  5. Run git count-objects -v to see how many loose objects you have.

Key takeaways

  • .git is the repository — a database of objects plus pointer files.
  • Four object types: blob (file content), tree (folder), commit (snapshot), tag. They nest to reconstruct any snapshot.
  • Objects are content-addressed by hash, stored in objects/ (loose, then packed); identical content is stored only once.
  • A branch is a one-line file in refs/heads/ holding a commit hash; HEAD points to the current branch.
  • The index is the staging area; logs/ powers the reflog, your safety net.
  • A commit just writes objects and moves one pointer — no magic.

Next: Module 05 — Branching, Merging & Rebasing