Git Internals and History Editing

Last update: 2024-03-07 13:17:55 +0800 +0800
Available translations: 日本語 / 中文(简体) / 中文(繁體)

Git stores immutable objects and moves small references between them. Daily work should use porcelain commands, while the storage model explains why rewrites create new history and why recovery remains possible for a period.

Inspect the Repository Database #

The .git directory contains repository-local state:

PathPurpose
HEADSymbolic name of the checked-out branch, or a detached object ID
configRepository configuration
indexProposed snapshot for the next commit
objects/Loose objects and packfiles
refs/ and packed-refsBranch, tag, and remote-tracking names
logs/Local reflogs for references
hooks/Client-side hook samples and installed hooks
modules/Git directories for initialized submodules

A worktree holds checked-out files. The index records a candidate tree. A commit points to a tree together with parent IDs and metadata. A branch is a movable reference to a commit; it is not a mutable container of commits.

Inspect names and object types without decoding files manually:

sh
git rev-parse --git-dir
git show-ref --head
git cat-file -t HEAD
git cat-file -p HEAD
git count-objects -vH

Model Objects and Content Addressing #

Git identifies an object by hashing a canonical byte sequence:

text
<type><space><decimal-size><NUL><content>

SHA-1 repositories use a 160-bit object ID. New repositories can use SHA-256. The object ID identifies content and structure; it does not encode a time or a sequence number.

Git has four core object types:

  1. A blob stores file bytes without a filename.
  2. A tree maps path names and modes to blobs and subtrees.
  3. A commit names a root tree, zero or more parents, author and committer identities and times, and a message.
  4. An annotated tag stores tag metadata and points to another object.

A tree entry mode of 100644 is a normal non-executable file, 100755 is an executable file, 120000 is a symbolic link, and 160000 is a submodule gitlink. A gitlink points at a commit in another repository rather than embedding that repository’s files.

Create and inspect objects in a disposable repository:

sh
printf 'hello\n' | git hash-object -w --stdin
git cat-file -p <object-id>
git ls-tree HEAD
git ls-tree -r HEAD

Loose objects live under .git/objects/aa/bb..., where aa is the first byte of the hexadecimal ID. Git zlib-compresses their canonical representation. git cat-file is safer than manually adding compression headers or using ad-hoc decompression pipelines.

Understand Packs, Deltas, and Reachability #

Many loose objects cost filesystem metadata operations. git gc can collect reachable objects into packfiles in .git/objects/pack/. A .pack file holds object representations, an .idx file maps object IDs to pack offsets, and a .rev file can speed operations that need pack order.

Packfiles may delta-compress objects against other objects. Deltas are a storage optimization, not Git’s history model: every commit still names a complete root tree, and Git can reconstruct any referenced blob through its object graph. A commit does not store only the textual diff from its parent.

Reachability starts at references such as branches, tags, remote-tracking branches, and reflogs. Garbage collection retains reachable objects and can prune unreachable objects after configured expiration. git fsck reports connectivity and can locate unreachable objects:

sh
git fsck --full --no-reflogs --unreachable
git cat-file -p <unreachable-object-id>

Read References and the Index #

HEAD commonly contains ref: refs/heads/main. A detached checkout contains an object ID directly. Loose branch names are files under refs/heads/, while many references can be consolidated in packed-refs.

The index is not a list of changed files. It stores the path, mode, object ID, and stat cache for the next tree. During a merge conflict, the index can carry multiple stages for one path:

StageMeaning
1Merge base
2Current branch version
3Other branch version

Inspect index entries with:

sh
git ls-files --stage
git diff --cached

Resolving a conflict and running git add replaces stages 1–3 with one stage-0 entry.

Explain Why Rewriting Changes IDs #

A commit includes its parent object IDs. Changing its message, tree, author, committer, timestamp, or parent produces a different commit ID. Every descendant must be recreated because its recorded parent changes.

Rewrite local, unpublished work freely when it improves history. Coordinate before rewriting a shared branch. A forced update can discard a collaborator’s new remote commit; use --force-with-lease when a force push is necessary:

sh
git push --force-with-lease origin feature/topic

Amend the Latest Commit #

git commit --amend creates a replacement for HEAD and moves the current branch:

sh
git add README.md
git commit --amend --no-edit

Run git commit --amend without --no-edit to replace the message. The original commit remains recoverable through the reflog until expiry and pruning.

Edit Older Commits Interactively #

Interactive rebase rewrites the commits after the selected endpoint:

sh
git rebase -i HEAD~3

The command presents the last three commits. Its todo commands include:

CommandEffect
pickKeep a commit
rewordReplace its message
editStop after applying it
squashCombine it with the preceding commit and edit the message
fixupCombine it and discard its message
dropOmit it

To change an older commit’s contents, mark it edit, amend when the rebase stops, and continue:

sh
git add path/to/file
git commit --amend --no-edit
git rebase --continue

Resolve conflicts, stage the resolved paths, and continue. git rebase --abort returns the branch to its pre-rebase state. git rebase --edit-todo changes remaining instructions while a rebase is paused.

Recover with Reflogs #

Every local reference movement can have a reflog entry. Create an explicit backup name before a risky rewrite:

sh
git branch backup/before-rewrite

Then locate and name an earlier state:

sh
git reflog show --date=iso HEAD
git switch -c recovered-work HEAD@{1}

Reflogs are local implementation history, not a distributed backup. Publish important work, preserve remote backups, and avoid running destructive cleanup before recovery is complete.

Comments

Read or comment on GitHub (sign-in required to comment)