jongkwan.dev
Development · Essay №014

Advanced Git: Internals and Advanced Commands

The Git object model, advanced commands such as rebase, cherry-pick, and bisect, and how they work internally

Jongkwan Lee2025년 7월 19일12 min read
Contents

A tour of Git's object model and of advanced commands such as rebase, bisect, and worktree, explained from how they work internally.

Using Git well requires going past surface-level commands to understand the internal data structures and how they behave. This note covers Git's internal object model, advanced commands, and monorepo optimization techniques.

The Git object model

Git is fundamentally a content-addressable file system. Every piece of data is stored as one of four object types.

The four object types

Object typeDescriptionStored contents
BlobFile contents (no name)The file data itself
TreeDirectory structureFilenames + Blob/Tree references
CommitSnapshot metadataTree reference, parent commits, author, message
TagAn annotated tagCommit reference, tag message

Inspecting objects directly

cat-file -p prints an object's contents in human-readable form, and -t returns only the object type (commit, tree, blob, or tag).

bash
# View a commit object
git cat-file -p HEAD
 
# View a tree object
git cat-file -p HEAD^{tree}
 
# View a blob object (a specific file)
git cat-file -p HEAD:src/app.ts
 
# Check the object type
git cat-file -t abc1234

References

Branches and tags in Git are nothing more than pointers (references) to commits:

  • HEAD: a symbolic reference pointing at the currently checked-out branch
  • Detached HEAD: the state where HEAD points directly at a specific commit rather than a branch
bash
# Check HEAD
cat .git/HEAD
# ref: refs/heads/main
 
# Check the commit a branch points to
cat .git/refs/heads/main
# a1b2c3d4e5f6...

Rebase vs merge

Merge

text
main:    A ─── B ─── C ──────── M (merge commit)
                      \        /
feature:               D ─── E
  • Preservation: every branch's history stays intact
  • 3-way merge: merges using the common ancestor (C) and the tip commits of both branches
  • Creates a merge commit (M)

Rebase

text
main:    A ─── B ─── C
                      \
feature:               D' ─── E' (new commit hashes)
  • Rewriting: the feature branch's commits are reapplied on top of main
  • Clean history: keeps history linear
  • Commit hashes change (new commits are created)

Interactive rebase (rebase -i)

bash
git rebase -i HEAD~5
text
pick   abc1234 feat: add user model
squash def5678 fix: typo in user model      # merge into the previous commit
pick   ghi9012 feat: add user repository
reword jkl3456 feat: add user service        # edit the message only
drop   mno7890 WIP: debugging                # delete the commit
CommandBehavior
pickUse the commit as is
rewordEdit only the commit message
squashMerge into the previous commit, combining messages
fixupMerge into the previous commit, discarding the message
dropDelete the commit
editAmend the commit

Which to use when

SituationRecommended strategy
Shared branches (main, develop)Merge -- preserves history
Cleaning up a personal feature branchRebase -- clean history
Tidying commits before a PRInteractive rebase -- squash, fixup
Commits already pushedMerge -- no rebase (force push is dangerous)

The golden rule: never rebase commits that have already been shared (pushed).

Cherry-pick

Definition

A command that picks a specific commit and applies it to the current branch. Only the commit's changes come along.

Usage

bash
# Cherry-pick a single commit
git cherry-pick abc1234
 
# Cherry-pick several commits
git cherry-pick abc1234 def5678
 
# Cherry-pick a range (abc1234 excluded, def5678 included)
git cherry-pick abc1234..def5678
 
# Stage the changes without committing
git cherry-pick --no-commit abc1234

Where it is used

  • Hotfix backports: applying a fix from the production branch to develop as well
  • Selective features: including only some commits from a feature branch in a release
  • Mistake recovery: moving work committed to the wrong branch onto the right one

Caveats

  • Cherry-pick creates a new commit (with a different hash)
  • The same change existing on two branches can cause conflicts at merge time later
  • Overuse makes history hard to trace

Definition

A command that uses binary search to find the exact commit that introduced a bug.

Usage

bash
# Start bisect
git bisect start
 
# Mark the current version as buggy
git bisect bad
 
# Mark a known good version
git bisect good v1.0.0
 
# Git automatically checks out a midpoint commit
# Test, then report the result
git bisect good   # this commit is fine
# or
git bisect bad    # this commit has the bug
 
# Git checks out another midpoint...
# Repeating pinpoints the offending commit in O(log n)
 
# Finish bisect
git bisect reset

Automated bisect

bash
# Automated bisect driven by a test script
# Script exit code: 0 = good, 1 = bad
git bisect start HEAD v1.0.0
git bisect run ./test-for-bug.sh

Efficiency

Even with 1,000 commits, roughly 10 tests are enough to pinpoint the offending commit (log2(1000) ≈ 10).

Stash

Definition

A command that temporarily shelves in-progress changes and returns you to a clean working directory.

Common usage

bash
# Shelve the current changes
git stash
 
# Save with a message
git stash save "WIP: payment integration"
 
# Include untracked files
git stash -u
 
# List stashes
git stash list
# stash@{0}: On feature/auth: WIP: payment integration
# stash@{1}: On main: debugging session
 
# Apply the most recent stash (keeping it)
git stash apply
 
# Apply the most recent stash and drop it
git stash pop
 
# Apply a specific stash
git stash apply stash@{2}
 
# Apply a stash onto a new branch
git stash branch new-branch-name
 
# Inspect a stash
git stash show -p stash@{0}
 
# Drop a specific stash
git stash drop stash@{0}
 
# Drop all stashes
git stash clear

Where it is used

  • Shelving work in progress to handle an urgent hotfix
  • Holding uncommitted changes before switching branches
  • Keeping experimental changes to reapply later

Reflog

Definition

A safety net that records every change to HEAD and branch references. It lets you recover commits you deleted or reset away by mistake.

Usage

bash
# Inspect HEAD movement history
git reflog
# a1b2c3d HEAD@{0}: commit: feat: add payment
# d4e5f6g HEAD@{1}: checkout: moving from main to feature
# h7i8j9k HEAD@{2}: reset: moving to HEAD~3
# l0m1n2o HEAD@{3}: commit: feat: add user model
 
# Reflog for a specific branch
git reflog show feature/auth
 
# Recover a deleted commit
git checkout HEAD@{3}
# or
git branch recovered-branch HEAD@{3}
 
# Undo a bad reset
git reset --hard HEAD@{2}

Caveats

  • The reflog exists only locally (it is not pushed)
  • Entries are kept for 90 days by default (gc prunes them)
  • git gc --prune=now prunes the reflog too, so use it carefully

Git worktree

Definition

A feature that lets one Git repository have multiple working directories at the same time. You can work on several branches concurrently without switching branches.

Usage

bash
# Create a worktree for an existing branch
git worktree add ../project-hotfix hotfix/payment-bug
 
# Create a worktree with a new branch
git worktree add -b feature/new-api ../project-new-api
 
# List worktrees
git worktree list
# /Users/jk/project          abc1234 [main]
# /Users/jk/project-hotfix   def5678 [hotfix/payment-bug]
# /Users/jk/project-new-api  ghi9012 [feature/new-api]
 
# Remove a worktree
git worktree remove ../project-hotfix
 
# Prune stale worktrees
git worktree prune

Where it is used

  • Parallel hotfixes: handling an urgent hotfix in another directory while working on main
  • Code review: checking out the PR under review in a separate worktree
  • Build comparison: building two versions at once to compare performance
  • Monorepos: working on another package's branch independently

Worktree vs branch switching

ComparisonBranch switching (checkout)Worktree
Working directoriesOneSeveral
Stash requiredYes (if there are changes)No
Build cacheInvalidatedKept independently
Concurrent workNot possiblePossible

Git LFS (Large File Storage)

Definition

An extension that stores large files (binaries, media, datasets) on a server outside the Git repository and commits only pointer files to Git.

Setup and usage

bash
# Install and initialize Git LFS
git lfs install
 
# Track large-file patterns
git lfs track "*.psd"
git lfs track "*.zip"
git lfs track "data/**"
 
# The patterns are recorded in .gitattributes
cat .gitattributes
# *.psd filter=lfs diff=lfs merge=lfs -text
# *.zip filter=lfs diff=lfs merge=lfs -text
 
# Commit .gitattributes
git add .gitattributes
git commit -m "chore: configure Git LFS"
 
# From then on, normal add/commit/push is handled by LFS automatically
git add design.psd
git commit -m "feat: add design asset"
git push

How it works

git filter-repo (history rewriting)

Definition

A tool that rewrites a Git repository's entire history. It is the faster, safer replacement for git filter-branch.

Use cases

--path names the target path, and --invert-paths inverts the meaning so that only that path is removed (everything else is kept). --path-rename moves paths, --email-callback rewrites per-commit email addresses in code, and --analyze only reports on size without rewriting anything.

bash
# Delete a file from all of history (a secret key committed by mistake, for example)
git filter-repo --path secrets.env --invert-paths
 
# Keep only one directory and delete the rest (splitting a monorepo)
git filter-repo --path src/auth-service/ --path-rename src/auth-service/:
 
# Rewrite email addresses in bulk
git filter-repo --email-callback '
    return email.replace(b"old@company.com", b"new@company.com")
'
 
# Identify large files
git filter-repo --analyze

Caveats

  • Every commit hash changes (history is rewritten)
  • Every team member has to force-clone
  • Back up before running it

Packfiles and delta compression

How Git makes storage efficient

Git initially stores each object individually as a loose object, but under certain conditions it compresses them into a packfile.

How packfiles work

text
Loose objects (initially)
.git/objects/
├── a1/b2c3d4...  (blob)
├── d4/e5f6g7...  (blob)
└── ...
 
Packfile (after compression)
.git/objects/pack/
├── pack-abc123.pack  (object data)
└── pack-abc123.idx   (index -- fast lookup)

Delta compression

  • Stores only the difference (delta) between similar objects
  • A small change to a large file does not cause the whole file to be stored again
  • git gc creates and optimizes packfiles
bash
# Create packfiles manually
git gc
 
# Check repository size
git count-objects -vH
 
# Packfile statistics
git verify-pack -v .git/objects/pack/pack-*.idx | head -20

Monorepo optimization

Shallow clone

bash
# Clone only the latest commit (when history is not needed)
git clone --depth 1 https://github.com/org/monorepo.git
 
# Fetch the rest of the history when needed
git fetch --unshallow

Sparse checkout

bash
# Initialize sparse-checkout
git sparse-checkout init --cone
 
# Check out only the directories you need
git sparse-checkout set services/auth-service shared/common
 
# Add another
git sparse-checkout add services/payment-service
 
# Show the current configuration
git sparse-checkout list

Partial clone

bash
# Clone without blobs (downloaded on demand)
git clone --filter=blob:none https://github.com/org/monorepo.git
 
# Exclude trees as well for a minimal clone
git clone --filter=tree:0 https://github.com/org/monorepo.git

Combining monorepo optimizations

bash
# Best combination: partial clone + sparse checkout
git clone --filter=blob:none --sparse https://github.com/org/monorepo.git
cd monorepo
git sparse-checkout set services/my-service shared/utils
TechniqueEffectScenario
Shallow cloneShrinks historyCI/CD, one-off builds
Sparse checkoutShrinks the working directoryWorking on one service inside a monorepo
Partial cloneShrinks the initial cloneVery large repositories
Git LFSStores large files externallyBinary and media files

Advanced command summary

CommandPurposeRisk
rebaseTidying and linearizing historyMedium (careful on shared branches)
rebase -iSquash, reword, drop commitsMedium
cherry-pickApplying selected commitsLow
bisectFinding the commit that introduced a bugLow (read-only)
stashShelving changes temporarilyLow
reflogInspecting reference history and recoveringLow (read-only)
worktreeParallel working directoriesLow
filter-repoRewriting all of historyHigh (irreversible)
gcCreating packfiles, optimizing the repositoryLow
sparse-checkoutPartial checkoutLow

Summary

All Git data reduces to four objects, Blob, Tree, Commit, and Tag, and branches and tags are merely references pointing into them. Once you see that structure, why rebase changes commit hashes and why cherry-pick creates new commits are explained at the object-model level rather than as command behavior. Bisect, reflog, and worktree simplify debugging, recovery, and concurrent work through binary search, reference history, and parallel working directories respectively. filter-repo carries the highest risk because it rewrites history wholesale, and in monorepos a combination of shallow, sparse, and partial clone reduces clone cost. The next post applies these commands to team collaboration workflows and branching strategies.