Skip to main content
RunBook Academy

Git, CI/CD & GitOpsII · Git ArchitectureArchitecture

The .git directory layout — what lives where under the repository

Intermediate⏱ ~20 mingit

What you'll learn

  • Map the .git directory layout and identify what each top-level entry stores
  • Read objects/ and explain how a SHA is mapped to a disk path
  • Distinguish loose refs from packed-refs and explain when Git uses each
  • Identify the role of config, hooks, and the HEAD ref file
  • Navigate the .git directory safely for forensics without corrupting the repository

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

The .git directory is the repository. Everything Git knows about your project — its history, its branches, its tags, its hooks, its configuration — lives under this single directory. The working tree is data; .git is the database. The first lesson of Git Architecture was the three areas; this lesson is the on-disk manifest of those areas, and the file you must be able to navigate to debug a broken repository, recover a corrupted pack, or write a script that acts on the repository without going through the porcelain.

The layout at a glance

flowchart TB
    subgraph dotgit[".git/"]
        OBJ["objects/\nloose objects and packs"]
        REFS["refs/\nheads, tags, remotes"]
        HEAD["HEAD\n(current ref)"]
        CFG["config\n(repo settings)"]
        HOOKS["hooks/\n(workflow glue)"]
        IDX["index\n(the staging file)"]
        LOG["logs/\n(reflog)"]
        PACKED["packed-refs\n(compressed refs file)"]
        INFO["info/\n(exclude, grafts)"]
        DESC["description\n(bare repo label)"]
    end
    OBJ --> OBJ1["objects/XX/YYYYYY...\nloose objects"]
    OBJ --> OBJ2["objects/pack/*.pack\nobjects/pack/*.idx\nobjects/pack/*.rev"]
    REFS --> REFS1["refs/heads/main\nrefs/tags/v3.0.0\nrefs/remotes/origin/main"]
    HEAD --> HEADN["(contents: ref: refs/heads/main)"]

The entries that matter for an infrastructure engineer:

  • objects/ — the content-addressed store.
  • refs/ — the named pointers (branches, tags, remotes).
  • HEAD — the current ref.
  • config — the repository configuration.
  • hooks/ — the workflow glue.
  • index — the staging file.
  • logs/ — the reflog.
  • packed-refs — a compact form of refs/ for large repositories.

objects/ — the content-addressed store

Every blob, tree, commit, and tag is stored in objects/. The directory layout is a function of the SHA-1 of the object: the first two hex characters of the OID are the directory name, and the remaining 38 characters are the file name:

ls .git/objects/05/
# 4a3b9c1d2e3f4g5h6i7j8k9l0m1n2o3p4q5r6s7t

Each file is a zlib-compressed object body. The format is <type> <size>\0<contents> compressed with zlib. Reading a loose object by OID is straightforward:

git cat-file -p 05a3b9c1d2e3f4g5h6i7j8k9l0m1n2o3p4q5r6s7t

For large repositories, objects are packed into .pack files under objects/pack/. The .idx file is the index into the pack, and the .rev file is the reverse index. Pack files are an optimisation: many small loose objects are compressed into a single large pack, with a delta encoding that stores only the differences between similar objects.

ls .git/objects/pack/
# pack-abc123.pack
# pack-abc123.idx
# pack-abc123.rev

refs/ — the named pointers

A ref is a file whose contents are the OID of a commit (or a tag, which in turn OIDs a commit). The standard layout:

ls .git/refs/heads/
# main
# feature/add-cache

cat .git/refs/heads/main
# 8a3f9d2a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e

Tag refs work the same way:

ls .git/refs/tags/
# v3.0.0

cat .git/refs/tags/v3.0.0
# f3a9c2b1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9

Remote-tracking refs live under refs/remotes/<remote>/:

ls .git/refs/remotes/origin/
# main
# HEAD

For repositories with many branches, Git stores refs in a single file .git/packed-refs for efficiency. The format is one line per ref: <oid> <refname>. When a ref exists both in packed-refs and as a separate file, the separate file wins.

cat .git/packed-refs
# 8a3f9d2a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e refs/tags/v2.0.0
# f3a9c2b1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9 refs/tags/v3.0.0

The on-disk format of refs is intentionally simple: a ref is a 40 hex character SHA followed by a newline. This is what makes git update-ref safe and scriptable — atomic write of a 41-byte file.

HEAD — the current ref

HEAD is a file whose contents are either ref: refs/heads/main (symbolic HEAD, the normal case) or a 40-char OID (detached HEAD):

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

git checkout 8a3f9d2
cat .git/HEAD
# 8a3f9d2a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e

The git switch and git checkout commands both update HEAD by writing to this file. git symbolic-ref HEAD refs/heads/<branch> is the lowest-level way to change it from a script.

config — the repository configuration

.git/config is an INI file with the repository’s settings. It is the local-scope file in Git’s three-level config cascade (system, global, local — covered in lesson II-05). Common sections:

[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
[remote "origin"]
    url = git@github.com:acme/infrastructure.git
    fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
    remote = origin
    merge = refs/heads/main

Edit this file with git config <key> <value> (the safe way) or directly with an editor (only if you know the INI format). Git will rewrite the file in place on every git config operation to preserve formatting.

hooks/ — the workflow glue

.git/hooks/ contains executable scripts that Git runs at specific points in the workflow. The shipped defaults are non-executing .sample files (pre-commit.sample, post-commit.sample, etc.) — copy and chmod +x the ones you want to use.

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

The full hook list is in githooks(5). Client-side hooks (the full list — pre-commit, commit-msg, pre-push, etc.) are bypassable by anyone with shell access. Server-side hooks (pre-receive, update, post-receive) are the only hooks that enforce policy on push — they live on the receiving server, not in the client’s .git/hooks/.

logs/ — the reflog

.git/logs/ records every movement of HEAD and every ref. This is the recovery surface for git reset --hard mistakes, for commits made in detached HEAD, and for branches that were deleted: the commits are still in the object store, and the reflog still has their OIDs.

git reflog
# 8a3f9d2 (HEAD -> main) HEAD@{0}: commit: add logging bucket
# f3a9c2b HEAD@{1}: reset: moving to main
# 5e7d8c9 HEAD@{2}: commit: (detached) debug cache layer

The reflog is the single most useful forensic tool in Git. It is the journal of every commit made to this clone, whether the commit is on a branch now or not.

flowchart LR
    A["Need to inspect\nan object"] --> B["git cat-file -p <oid>"]
    C["Need to read\na ref"] --> D["git rev-parse <ref>"]
    E["Need to recover\na commit"] --> F["git reflog"]
    G["Need to edit\na ref"] --> H["git update-ref"]
    I["Need to edit\nconfig"] --> J["git config <key> <value>"]

The rule is: prefer the plumbing commands, never edit .git/objects/ directly, and treat .git/ as a database that Git owns. For scripts that need to interact with the repository, the plumbing commands from lesson II-03 are the right interface.

Production discipline

  1. Never hand-edit .git/objects/. The SHA-1 in the filename is the OID of the contents. Any drift silently breaks the object. Use git hash-object and git update-ref from scripts.
  2. Treat the reflog as a 30-day safety net. Reflog entries expire after roughly 30 days (90 for reachable commits). If a commit has been lost for more than a month and is not on any branch, treat it as gone.
  3. Back up .git/, not the working tree. The working tree is a checkout of a commit in .git/. A disaster recovery procedure that backs up only the working tree gives you a snapshot without history; a backup that includes .git/ gives you history, refs, the index, and the reflog.

Cross-course references

  • Linux for Production Sysadmins - Part IX (Filesystem) covers the inode/directory entry model, which is the closest filesystem analogue to the .git/objects/ directory layout.
  • Ansible for Production Sysadmins - Part XXXVII (RepoArch) describes the analogous layout for Ansible collections: the roles/, playbooks/, and inventory/ directories play roles similar to objects/, refs/, and HEAD.
  • Terraform for Production Sysadmins - Part IX (State) describes the layout of a Terraform state file, which is a serialised form of the same content-addressed model.

Quiz

Knowledge check · 4 questions

  1. Q1. A loose object is stored at `.git/objects/05/4a3b9c1d2e3f4g5h6i7j8k9l0m1n2o3p4q5r6s7t`. What is the full OID of this object?

  2. Q2. Pack files in `.git/objects/pack/` are an alternative storage format that can be safely read and edited by humans.

  3. Q3. Name the file inside `.git/` that records every move of HEAD and every ref, and explain what it is used for.

  4. Q4. Diagnose a missing branch and recover the commits that were on it.

    An engineer ran `git branch -D feature/add-cache` to delete a feature branch that had not been merged. The next day, the team decides the branch was needed and asks for the commits back. The branch is gone from `refs/heads/`, gone from `packed-refs`, and the engineer cannot remember the tip OID.

Passing score: 75%. Answers are checked in this browser.