Advanced Git: Internals and Advanced Commands
The Git object model, advanced commands such as rebase, cherry-pick, and bisect, and how they work internally
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 type | Description | Stored contents |
|---|---|---|
| Blob | File contents (no name) | The file data itself |
| Tree | Directory structure | Filenames + Blob/Tree references |
| Commit | Snapshot metadata | Tree reference, parent commits, author, message |
| Tag | An annotated tag | Commit 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).
# 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 abc1234References
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
# 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
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
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)
git rebase -i HEAD~5pick 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| Command | Behavior |
|---|---|
pick | Use the commit as is |
reword | Edit only the commit message |
squash | Merge into the previous commit, combining messages |
fixup | Merge into the previous commit, discarding the message |
drop | Delete the commit |
edit | Amend the commit |
Which to use when
| Situation | Recommended strategy |
|---|---|
| Shared branches (main, develop) | Merge -- preserves history |
| Cleaning up a personal feature branch | Rebase -- clean history |
| Tidying commits before a PR | Interactive rebase -- squash, fixup |
| Commits already pushed | Merge -- 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
# 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 abc1234Where 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
Bisect (binary search)
Definition
A command that uses binary search to find the exact commit that introduced a bug.
Usage
# 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 resetAutomated bisect
# 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.shEfficiency
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
# 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 clearWhere 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
# 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=nowprunes 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
# 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 pruneWhere 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
| Comparison | Branch switching (checkout) | Worktree |
|---|---|---|
| Working directories | One | Several |
| Stash required | Yes (if there are changes) | No |
| Build cache | Invalidated | Kept independently |
| Concurrent work | Not possible | Possible |
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
# 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 pushHow 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.
# 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 --analyzeCaveats
- 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
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 gccreates and optimizes packfiles
# Create packfiles manually
git gc
# Check repository size
git count-objects -vH
# Packfile statistics
git verify-pack -v .git/objects/pack/pack-*.idx | head -20Monorepo optimization
Shallow clone
# 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 --unshallowSparse checkout
# 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 listPartial clone
# 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.gitCombining monorepo optimizations
# 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| Technique | Effect | Scenario |
|---|---|---|
| Shallow clone | Shrinks history | CI/CD, one-off builds |
| Sparse checkout | Shrinks the working directory | Working on one service inside a monorepo |
| Partial clone | Shrinks the initial clone | Very large repositories |
| Git LFS | Stores large files externally | Binary and media files |
Advanced command summary
| Command | Purpose | Risk |
|---|---|---|
rebase | Tidying and linearizing history | Medium (careful on shared branches) |
rebase -i | Squash, reword, drop commits | Medium |
cherry-pick | Applying selected commits | Low |
bisect | Finding the commit that introduced a bug | Low (read-only) |
stash | Shelving changes temporarily | Low |
reflog | Inspecting reference history and recovering | Low (read-only) |
worktree | Parallel working directories | Low |
filter-repo | Rewriting all of history | High (irreversible) |
gc | Creating packfiles, optimizing the repository | Low |
sparse-checkout | Partial checkout | Low |
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.