Skip to main content

Git Advanced Topics

Master sophisticated Git workflows, internals, and strategies used by professional development teams.

Interactive Rebase

Interactive rebase is the most powerful Git tool for manipulating history. Use it before submitting pull requests to create clean, logical commits.

Starting Interactive Rebase

git rebase -i main                 # Rebase and edit commits on main
git rebase -i HEAD~5 # Edit last 5 commits
git rebase -i --root # Edit all commits in repository

Interactive Commands

When the editor opens, you'll see:

pick a1b2c3d First commit
pick b3c4d5e Second commit
pick c5d6e7f Third commit

# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like squash, but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop = remove commit
# l, label = label current HEAD with a name
# t, reset = reset HEAD to a label
# m, merge = create a merge commit using the original merge message

Common Rebase Scenarios

Squashing commits:

pick a1b2c3d First feature commit
squash b3c4d5e Bug fix in feature
squash c5d6e7f Another improvement

Result: All three commits merged into one.

Reordering commits:

pick c5d6e7f Third commit
pick a1b2c3d First commit
pick b3c4d5e Second commit

Result: Commits reordered (useful for logical grouping).

Editing commit messages:

pick a1b2c3d First commit
reword b3c4d5e Fix typo in message
pick c5d6e7f Third commit

Editor opens for second commit message only.

Splitting commits:

git rebase -i HEAD~1
# Mark commit with 'edit'
# When rebasing stops at that commit:
git reset HEAD~1 # Undo the commit
git add <partial-files> # Stage part of changes
git commit -m "First part"
git add <remaining-files> # Stage remainder
git commit -m "Second part"
git rebase --continue

Autosquash

Mark commits with fixup/squash prefix, then use --autosquash:

git commit -m "fixup! Previous commit message"
git commit -m "squash! Another commit message"
git rebase -i --autosquash main
# Automatically moves and marks commits

Cherry-Pick

Apply specific commits from one branch to another without merging.

Basic Cherry-Pick

git cherry-pick a1b2c3d            # Apply single commit
git cherry-pick a1b2c3d b3c4d5e # Apply multiple commits
git cherry-pick a1b2c3d..c5d6e7f # Apply range (exclusive of start)
git cherry-pick a1b2c3d^..c5d6e7f # Apply range (inclusive of start)

Cherry-Pick with Modifications

git cherry-pick -x <commit>        # Add cherry-pick reference
git cherry-pick --no-commit <commit> # Stage changes without committing
git cherry-pick -e <commit> # Edit commit message
git cherry-pick --signoff <commit> # Add Signed-off-by line

Handling Cherry-Pick Conflicts

git cherry-pick <commit>
# Conflicts occur...
git status # See conflicted files
# Manually resolve conflicts
git add <resolved-files>
git cherry-pick --continue # Complete cherry-pick
git cherry-pick --abort # Cancel cherry-pick

Cherry-Pick Use Cases

  • Backporting: Apply bug fix to release branch
  • Selective merging: Cherry-pick features without full branch merge
  • Maintenance: Apply patches to multiple branches

Bisect: Finding Bugs

Binary search through commits to find where a bug was introduced.

Basic Bisect Workflow

git bisect start                   # Start bisect session
git bisect bad # Current commit is bad (has bug)
git bisect good v1.0 # Known good commit
# Git checks out middle commit
# Test the commit...
git bisect good # Or git bisect bad
# Continue until bug is found
git bisect reset # End bisect, return to original branch

Automated Bisect

git bisect start
git bisect bad HEAD
git bisect good v1.0
git bisect run npm test # Automatically run test script
# Git bisect runs test on each commit, reports when bug introduced
git bisect reset

Bisect Example

#!/bin/bash
# test-script.sh - Returns 0 if good, 1 if bad
npm run build || exit 1
npm test | grep "FAILED" && exit 1
exit 0
git bisect start
git bisect bad HEAD
git bisect good v1.0
git bisect run ./test-script.sh

Submodules

Include external repositories as subdirectories.

Adding Submodules

git submodule add <url> <path>     # Add submodule
git submodule add -b main <url> <path> # Add specific branch
git add .gitmodules <path>
git commit -m "Add: submodule"

This creates:

  • .gitmodules file tracking submodule info
  • Entry in .git/config
  • Submodule directory with separate .git file

Cloning with Submodules

git clone --recurse-submodules <url>  # Clone with submodules
git clone <url> # Clone without submodules
git submodule update --init # Initialize submodules after clone
git submodule update --init --recursive # Initialize recursive submodules

Updating Submodules

git submodule update                # Update to commit in .gitmodules
git submodule update --remote # Update to latest from remote
git submodule update --remote --merge # Update and merge
git submodule foreach git pull # Pull all submodules

Managing Submodules

git submodule status               # Show submodule status
git submodule deinit <path> # Deregister submodule
git rm <path> # Remove submodule
git config --file=.gitmodules --remove-section submodule.<name> # Cleanup

Subtrees

Alternative to submodules: merge external repo as subdirectory.

Adding Subtrees

git subtree add --prefix=<path> <url> <branch>  # Add subtree
git subtree add --prefix=vendor/lib https://github.com/lib/repo main

Updating Subtrees

git subtree pull --prefix=<path> <url> <branch>  # Pull updates
git subtree push --prefix=<path> <url> <branch> # Push changes back

Subtree vs Submodule

FeatureSubtreeSubmodule
Learning CurveSteeperGentler
CloningAutomaticRequires init
Sharing ChangesPush back to originalCommit submodule pointer
Directory TrackingFiles in main repoSeparate .git file
Recommended ForExtracting external codeUsing external library

Git Hooks

Automate actions at specific Git events.

Hook Types

Pre-commit: Before committing

#!/bin/bash
# .git/hooks/pre-commit
npm run lint || exit 1
npm run format || exit 1

Pre-push: Before pushing

#!/bin/bash
# .git/hooks/pre-push
npm run test || exit 1

Commit-msg: Validate commit message

#!/bin/bash
# .git/hooks/commit-msg
if ! head -1 $1 | grep -qE "^(Add|Fix|Update|Remove):"; then
echo "Commit message must start with: Add, Fix, Update, or Remove"
exit 1
fi

Post-merge: After merging

#!/bin/bash
# .git/hooks/post-merge
npm install || exit 1

Setting Up Hooks

# Manual setup (one-time)
chmod +x .git/hooks/pre-commit

# Using Husky (NPM projects)
npm install husky --save-dev
npx husky install
npx husky add .husky/pre-commit "npm run lint"
npx husky add .husky/pre-push "npm run test"

# Commit hooks to repo
git add .husky .gitignore
git commit -m "Add: git hooks"

Common Hook Patterns

#!/bin/bash
# Prevent accidental commits to main
BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$BRANCH" = "main" ]; then
echo "Cannot commit directly to main. Use a feature branch."
exit 1
fi

# Prevent large files
MAX_SIZE=5242880 # 5MB
STAGED_FILES=$(git diff --cached --name-only)
for FILE in $STAGED_FILES; do
SIZE=$(stat -f%z "$FILE" 2>/dev/null || stat -c%s "$FILE" 2>/dev/null)
if [ $SIZE -gt $MAX_SIZE ]; then
echo "File $FILE is too large"
exit 1
fi
done

Git Internals

Understanding the plumbing beneath Git's porcelain.

Objects

Git stores four object types:

Blob: File contents

git hash-object <file>             # Calculate blob hash
git cat-file -p <hash> # View blob contents

Tree: Directory structure

git cat-file -p <tree-hash>        # View tree
# Shows: [mode] [type] [hash] [name]

Commit: Snapshot with metadata

git cat-file -p <commit-hash>      # View commit
# tree: <tree-hash>
# parent: <parent-hash>
# author: name <email> timestamp
# committer: name <email> timestamp
# message

Tag: Reference to commit

git cat-file -p <tag-hash>         # View tag

References

Branches and tags are simple references to commits:

cat .git/HEAD                      # Shows current branch
cat .git/refs/heads/main # Shows main branch commit
cat .git/refs/tags/v1.0 # Shows tag commit
git symbolic-ref HEAD # Show symbolic reference

Repository Structure

.git/
HEAD # Current branch reference
refs/
heads/ # Local branch references
remotes/ # Remote branch references
tags/ # Tag references
objects/
[hash]/ # Loose objects (blobs, trees, commits)
pack/ # Packfiles (compressed objects)
config # Repository configuration
hooks/ # Git hooks
index # Staging area
logs/ # Reflog

Signed Commits

Cryptographically sign commits with GPG.

Setup GPG

gpg --list-secret-keys --keyid-format long  # List keys
gpg --gen-key # Generate key
git config --global user.signingKey <KEY-ID> # Set default key
git config --global commit.gpgsign true # Always sign

Signing Commits

git commit -S                      # Sign commit
git commit -S -m "message" # Sign with message
git commit --gpg-sign=<key-id> # Sign with specific key

Verifying Signatures

git log --show-signature           # Show signatures in log
git verify-commit <commit> # Verify specific commit
git tag -v <tag> # Verify signed tag

Git LFS (Large File Storage)

Handle large files efficiently without bloating repository.

Setup

git lfs install                    # Initialize LFS
git lfs track "*.psd" # Track file type
git add .gitattributes
git commit -m "Add: LFS tracking"

Usage

git add large-file.zip             # Added to LFS automatically
git push origin main # LFS file uploaded
git lfs ls-files # List tracked files
git lfs prune # Remove local LFS objects

Monorepo Strategies

Managing multiple projects in single repository.

Monorepo Structure

my-monorepo/
packages/
api/
package.json
src/
web/
package.json
src/
shared/
src/
.git
package.json (root)

Advantages

  • Atomic commits across projects
  • Shared dependencies
  • Simplified tooling
  • Easier refactoring

Challenges

  • Larger clone/fetch
  • Builds slower
  • Complex CI/CD

Tools for Monorepos

  • npm/yarn workspaces: Node.js packages
  • Lerna: Package management
  • Nx: Build system
  • Bazel: Google's build system
  • Turborepo: Fast monorepo build system

Git Workflows

Gitflow

Complex workflow with release and hotfix branches:

main (production)
├── hotfix-* (emergency fixes)
│ └── merge back to main and develop

develop (staging)
├── release-* (preparing release)
│ └── merge to main and develop

├── feature-* (feature development)
│ └── merge back to develop
└── bugfix-* (bug fixes)
└── merge back to develop

Best for: Teams with scheduled releases

Trunk-Based Development

Developers commit directly to main (or short-lived branches):

git checkout -b feature-x
git commit -m "Add feature"
git push origin feature-x
# PR review
git merge feature-x
  • Branches live days, not weeks
  • Frequent merges to main
  • CI/CD catches issues quickly

Best for: High-performing teams, DevOps

GitHub Flow

Simple workflow: main always deployable

main (always deployable)
├── feature-1 (short-lived)
├── feature-2 (short-lived)
└── hotfix-1 (short-lived)
git checkout -b feature-name
# Make changes
git push origin feature-name
# Create PR, review, merge

Best for: Continuous deployment, web apps

GitLab Flow

GitHub Flow plus environment branches:

main (staging)
├── production (merge main when ready)
├── feature-1
└── feature-2

Best for: Teams needing pre-production testing

Exercises

Exercise 1: Interactive Rebase and Squashing

# Create feature with multiple commits
git switch -c feature-squash
echo "feature 1" > f1.txt
git add f1.txt
git commit -m "Add feature part 1"

echo "feature 2" > f2.txt
git add f2.txt
git commit -m "Add feature part 2"

echo "bug fix" >> f1.txt
git add f1.txt
git commit -m "Fix bug in part 1"

# Interactive rebase to squash
git rebase -i main
# Change last two commits to 'squash'
# Save and edit final message

# Now feature has one clean commit
git log --oneline

Exercise 2: Cherry-Pick Across Branches

# Create commits on feature branch
git switch -c cherry-source
echo "commit A" > a.txt
git add a.txt
git commit -m "A"

echo "commit B" > b.txt
git add b.txt
git commit -m "B"

echo "commit C" > c.txt
git add c.txt
git commit -m "C"

# Cherry-pick only B to main
git switch main
git cherry-pick <hash-of-B>

# B is now on main, A and C not needed
git log --oneline

Exercise 3: Bisect a Bug

# Create history with bug
git switch -c buggy-history
for i in {1..10}; do
echo "version $i" > version.txt
git add version.txt
git commit -m "Version $i"
# Introduce bug in version 6
if [ $i -eq 6 ]; then
echo "BUG" >> version.txt
git add version.txt
git commit --amend -m "Version $i (buggy)"
fi
done

# Use bisect to find it
git bisect start
git bisect bad HEAD
git bisect good HEAD~10
# Test each commit, marking good/bad
# Git narrows down to buggy commit
git bisect reset

Exercise 4: Implement Pre-commit Hook

# Create hook
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/bash
CHANGES=$(git diff --cached --name-only)
for FILE in $CHANGES; do
if [[ $FILE == *.js ]]; then
echo "Linting $FILE..."
# Would run: npx eslint "$FILE" || exit 1
fi
done
exit 0
EOF

chmod +x .git/hooks/pre-commit

# Test it
echo "console.log('test')" > test.js
git add test.js
git commit -m "Add test" # Hook runs

Summary

Advanced Git mastery includes:

  • Interactive rebase: Clean, logical commit history
  • Cherry-pick: Selective commit application
  • Bisect: Debugging through binary search
  • Submodules/Subtrees: Managing external code
  • Hooks: Automating Git workflows
  • Internals: Understanding objects and references
  • Signed commits: Cryptographic verification
  • LFS: Handling large files
  • Workflows: Team collaboration patterns

These tools enable professional development practices and team workflows at scale.