Git Advanced - Street-Level Ops¶
Real-world Git operations for situations that go beyond add-commit-push. Includes emergency recovery scenarios (merged from git-save-your-ass).
Emergency Recovery Scenarios¶
"I just destroyed my branch"¶
You ran git reset --hard or git checkout . or git branch -D and your work is gone.
# Step 1: Find the commit in reflog
git reflog
# Step 2: Find the commit hash where your work existed
# Look for entries like "commit: my important work"
# Step 3: Recover
git checkout -b recovery-branch <hash>
# or
git reset --hard <hash>
The reflog retains entries for 90 days (HEAD reflog) or 30 days (branch reflogs) by default.
"I force-pushed and overwrote the remote branch"¶
# Step 1: Find the pre-push commit
git reflog
# Step 2: Force push the correct commit
git push --force origin <correct-hash>:branch-name
# Prevention -- safe force push:
git push --force-with-lease origin branch-name
"I committed to the wrong branch"¶
# Option A: Move last commit to correct branch
git branch correct-branch # Create branch at current commit
git reset --hard HEAD~1 # Remove commit from current branch
git checkout correct-branch # Go to correct branch
# Option B: Cherry-pick to correct branch
git checkout correct-branch
git cherry-pick <hash>
git checkout wrong-branch
git reset --hard HEAD~1
"I accidentally staged/committed a secret"¶
# If not pushed yet:
git reset HEAD~1 # Undo commit, keep changes
# Remove the secret from files
git add .
git commit -m "clean commit"
# If pushed:
# 1. Rotate the secret immediately (it is compromised)
# 2. Remove from history:
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch path/to/secret' HEAD
# Or use BFG Repo-Cleaner (faster):
bfg --delete-files secret.env
git push --force --all
Key point: If a secret was ever pushed, consider it compromised.
"I have merge conflicts and I'm lost"¶
# Abort and start over
git merge --abort
# or
git rebase --abort
# Accept all theirs (their version wins)
git checkout --theirs .
git add .
# Accept all ours (our version wins)
git checkout --ours .
git add .
Task: Recover from a Bad Rebase¶
You rebased onto main, resolved conflicts wrong, and now tests fail. The old state is gone from your branch. The reflog has it.
# See where your branch was before the rebase
$ git reflog
a1b2c3d HEAD@{0}: rebase (finish): returning to refs/heads/feature/auth
e4f5a6b HEAD@{1}: rebase (pick): feat: add auth middleware
7c8d9e0 HEAD@{2}: rebase (start): checkout main
f1a2b3c HEAD@{3}: commit: feat: add token refresh # ← this is pre-rebase
# Reset your branch to the pre-rebase state
$ git reset --hard f1a2b3c
HEAD is now at f1a2b3c feat: add token refresh
# Verify your branch is back to its original state
$ git log --oneline -5
f1a2b3c feat: add token refresh
d4e5f6a feat: add auth middleware
a7b8c9d initial setup
If you already pushed the bad rebase, you need --force-with-lease (not --force) to update the remote, and you must coordinate with anyone else working on the branch.
Task: Undo the Last Commit (Keep Changes)¶
You committed too early, or the commit message is wrong, or you forgot to add a file.
# Undo the commit but keep all changes staged
$ git reset --soft HEAD~1
# Now your changes are staged, ready to be recommitted
$ git status
Changes to be committed:
modified: src/auth/middleware.py
new file: src/auth/tokens.py
# Add the forgotten file and recommit
$ git add src/auth/config.py
$ git commit -m "feat: add auth middleware with token support"
If you only need to fix the commit message (and haven't pushed):
Task: Squash Commits Before Merge¶
Your feature branch has 12 commits including "wip", "fix typo", and "actually fix it this time." Clean them up before merging.
# Count commits on your branch that aren't on main
$ git log --oneline main..HEAD
f1a2b3c fix: typo in test
d4e5f6a wip: still debugging
a7b8c9d fix: handle nil pointer
e1f2a3b feat: add user endpoints
c4d5e6f feat: add user model
# Interactive rebase the 5 commits
$ git rebase -i main
In the editor, reorganize:
pick c4d5e6f feat: add user model
fixup a7b8c9d fix: handle nil pointer
fixup d4e5f6a wip: still debugging
pick e1f2a3b feat: add user endpoints
fixup f1a2b3c fix: typo in test
Result: 2 clean commits instead of 5 messy ones.
Using fixup commits (cleaner workflow)¶
# While developing, create fixup commits that reference the commit to fix
$ git commit --fixup=c4d5e6f
# Creates: "fixup! feat: add user model"
# When ready, autosquash will put fixups in the right place
$ git rebase -i --autosquash main
Task: Cherry-Pick a Fix from Another Branch¶
A teammate fixed a bug on the release branch. You need that same fix on main.
# Find the commit on the release branch
$ git log --oneline release/v2.1
abc1234 fix: prevent race condition in session store
def5678 chore: bump version to 2.1.1
# Cherry-pick it onto your current branch (main)
$ git cherry-pick abc1234
# If there's a conflict
$ git status
Unmerged paths:
both modified: src/session/store.go
# Resolve it, then continue
$ vim src/session/store.go
$ git add src/session/store.go
$ git cherry-pick --continue
Task: Bisect to Find the Bug-Introducing Commit¶
Production is broken. It worked two weeks ago. 87 commits happened since then.
# Start bisect
$ git bisect start
# Current HEAD is broken
$ git bisect bad
# Tag v2.3.0 from two weeks ago was good
$ git bisect good v2.3.0
Bisecting: 43 revisions left to test after this (roughly 6 steps)
[abc123def] feat: add caching layer
# Git checks out the midpoint. Run your test:
$ make test-integration
# Tests pass
$ git bisect good
Bisecting: 21 revisions left to test after this (roughly 5 steps)
# ...repeat until Git narrows it down...
$ git bisect bad
abc123def456 is the first bad commit
commit abc123def456
Author: Bob <bob@example.com>
Date: Mon Mar 10 14:22:00 2026 -0500
feat: add caching layer
# Done — reset to get back to your branch
$ git bisect reset
Fully automated bisect¶
# Write a script that exits 0 if good, non-zero if bad
$ cat > /tmp/test-bug.sh << 'SCRIPT'
#!/bin/bash
make build 2>/dev/null && python -c "
from app.session import SessionStore
s = SessionStore()
assert s.get('nonexistent') is None, 'Bug: should return None'
"
SCRIPT
$ chmod +x /tmp/test-bug.sh
# Let Git run it automatically
$ git bisect start HEAD v2.3.0
$ git bisect run /tmp/test-bug.sh
# Git tests each commit automatically and reports the first bad one
Task: Clean Up Branches (Local and Remote)¶
After months, you have dozens of stale branches.
# List local branches merged into main
$ git branch --merged main
feature/auth
feature/old-api
fix/header-parsing
* main
# Delete them all (except main)
$ git branch --merged main | grep -v '^\*\|main' | xargs git branch -d
Deleted branch feature/auth (was abc1234).
Deleted branch feature/old-api (was def5678).
Deleted branch fix/header-parsing (was 789abcd).
# Prune remote tracking branches that no longer exist on origin
$ git fetch --prune
# See which remote branches are gone
$ git branch -vv | grep ': gone]'
feature/auth abc1234 [origin/feature/auth: gone] feat: add auth
fix/old-bug def5678 [origin/fix/old-bug: gone] fix: race condition
# Delete local branches whose remote is gone
$ git branch -vv | grep ': gone]' | awk '{print $1}' | xargs git branch -d
# List remote branches (to find stale ones)
$ git branch -r --merged origin/main | grep -v main
origin/feature/auth
origin/feature/old-api
Task: Reduce Repo Size (Remove Large Files from History)¶
Someone committed a 500MB database dump three months ago, then deleted it. The file is gone from the working tree but still in pack history, making clones slow.
# Find the largest objects in the repo
$ git rev-list --objects --all | \
git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | \
awk '/^blob/ {print $3, $4}' | sort -rn | head -10
524288000 data/prod-dump.sql
15728640 vendor/huge-binary.tar.gz
# Option 1: git-filter-repo (recommended, installable via pip)
$ pip install git-filter-repo
$ git filter-repo --path data/prod-dump.sql --invert-paths
# Rewrites entire history without that file
# Option 2: BFG Repo Cleaner (Java-based, simpler syntax)
$ java -jar bfg.jar --delete-files prod-dump.sql
$ git reflog expire --expire=now --all
$ git gc --prune=now --aggressive
# After cleanup, force push (coordinate with team!)
$ git push --force-with-lease --all
Task: Git Hooks for CI Enforcement¶
Set up a pre-push hook that runs tests before pushing:
# Create the hook
$ cat > .git/hooks/pre-push << 'HOOK'
#!/bin/bash
echo "Running tests before push..."
# Run fast tests only
if ! make test-quick 2>/dev/null; then
echo "Tests failed. Push aborted."
echo "Run 'make test-quick' to see failures."
exit 1
fi
# Check for secrets in staged files
if git diff --cached --diff-filter=ACM -z --name-only | \
xargs -0 grep -lE '(AKIA[0-9A-Z]{16}|sk-[a-zA-Z0-9]{48}|-----BEGIN.*PRIVATE KEY)' 2>/dev/null; then
echo "Potential secrets detected. Push aborted."
exit 1
fi
HOOK
$ chmod +x .git/hooks/pre-push
For team-wide hooks, use a hooks manager committed to the repo:
# With pre-commit framework (Python)
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: check-yaml
- id: detect-private-key
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.0
hooks:
- id: ruff
args: [--fix]
# Install hooks from config
$ pre-commit install
Task: Resolve Merge Conflicts¶
A real conflict in a frequently-edited file:
# Attempt the merge
$ git merge feature/api-v2
Auto-merging src/router.py
CONFLICT (content): Merge conflict in src/router.py
Automatic merge failed; fix conflicts and then commit the result.
# See which files have conflicts
$ git diff --name-only --diff-filter=U
src/router.py
# Open the file — conflict markers show both versions:
# <<<<<<< HEAD
# app.get("/users", endpoint=list_users_v1)
# =======
# app.get("/users", endpoint=list_users_v2)
# app.get("/users/search", endpoint=search_users)
# >>>>>>> feature/api-v2
# After manually resolving:
$ git add src/router.py
$ git merge --continue
# To abort and go back to pre-merge state:
$ git merge --abort
Merge strategies for specific situations¶
# Accept all of theirs for a specific file
$ git checkout --theirs src/generated/schema.py
$ git add src/generated/schema.py
# Accept all of ours for a specific file
$ git checkout --ours package-lock.json
$ git add package-lock.json
# Use a 3-way merge tool
$ git mergetool
# Launches configured tool (vimdiff, meld, kdiff3)
Task: Find Who Changed a Line and Why¶
Production code has a weird constant. You need to know who set it and why.
# Find who last changed line 42
$ git blame -L 42,42 src/config.py
a1b2c3d4 (Bob Smith 2025-11-03 14:22:00 -0500 42) MAX_RETRIES = 1
# That just shows the last change. Find the full history of that line:
$ git log -S "MAX_RETRIES = 1" --oneline -- src/config.py
a1b2c3d fix: reduce retries to prevent cascade failure
f4e5d6c feat: add retry configuration
# See the actual diff where it was introduced
$ git show a1b2c3d -- src/config.py
# Search for when MAX_RETRIES was ever set to 5 (the old value)
$ git log -S "MAX_RETRIES = 5" --oneline -- src/config.py
f4e5d6c feat: add retry configuration
Task: Stash Workflows for Context Switching¶
You are mid-feature, a P1 comes in, you need to switch context fast.
# Stash everything including untracked files
$ git stash push -u -m "feature/billing: halfway through invoice calc"
Saved working directory and index state On feature/billing: halfway through invoice calc
# Switch to main, create hotfix
$ git checkout main
$ git checkout -b hotfix/payment-crash
# ... fix the bug ...
$ git commit -am "fix: handle nil payment method"
$ git push origin hotfix/payment-crash
# Switch back and restore your work
$ git checkout feature/billing
$ git stash pop
# All your changes are back, including untracked files
# If pop causes conflicts, the stash is NOT dropped
# Resolve conflicts, then manually drop:
$ git stash drop stash@{0}
Task: Worktrees for Parallel Reviews¶
You need to review a PR while still working on your branch. Worktrees let you have both checked out simultaneously.
# Create a worktree for the PR branch
$ git fetch origin pull/142/head:pr-142
$ git worktree add ../review-pr-142 pr-142
# Now you have two working directories:
# /home/dev/project → your feature branch
# /home/dev/review-pr-142 → PR #142
# Review in the second directory
$ cd ../review-pr-142
$ make test
$ grep -r "TODO" src/
# When done, clean up
$ cd ../project
$ git worktree remove ../review-pr-142
$ git branch -d pr-142
Unconventional Uses of Git¶
Git's content-addressable storage, DAG history, and diff machinery make it useful well beyond source code.
Git as a Configuration Database¶
Track infrastructure config changes with full audit history:
# Set up a config repo for /etc (etckeeper does this automatically)
$ cd /etc
$ sudo git init
$ sudo git add -A
$ sudo git commit -m "Initial /etc snapshot"
# After every config change, commit with context
$ sudo git commit -am "Enable TLS 1.3 in nginx — ticket INFRA-2847"
# Who changed sshd_config and when?
$ git log --oneline -- ssh/sshd_config
a1b2c3d 2025-11-15 Disable password auth — security audit
f4e5d6c 2025-09-03 Allow port forwarding for dev team
8a9b0c1 2025-06-12 Initial sshd hardening
# What exactly changed?
$ git diff f4e5d6c..a1b2c3d -- ssh/sshd_config
# Roll back a bad config change
$ git checkout f4e5d6c -- ssh/sshd_config
$ sudo systemctl restart sshd
Also useful for: tracking DNS zone files, firewall rules, cron tabs, and application configs across a fleet.
Git as a Deployment Mechanism¶
Many production systems deploy by pulling a git ref:
# Deployment by tag (common in small-to-mid orgs)
$ ssh deploy@prod-web-01 'cd /srv/app && git fetch && git checkout v2.4.1'
# Capistrano-style: deploy to timestamped directory, symlink
$ DEPLOY_DIR="/srv/releases/$(date +%Y%m%d%H%M%S)"
$ git clone --depth 1 --branch v2.4.1 git@github.com:org/app.git "$DEPLOY_DIR"
$ ln -sfn "$DEPLOY_DIR" /srv/app/current
# Rollback: point symlink to previous release
$ ln -sfn /srv/releases/20250310143000 /srv/app/current
# GitOps: push to a deploy branch, ArgoCD/Flux picks it up
$ git push origin main:deploy/production
# ArgoCD watches deploy/production and applies manifests automatically
Git as a Document/Knowledge Base¶
# Track meeting notes, runbooks, postmortems with full history
$ git init ops-wiki && cd ops-wiki
$ mkdir -p runbooks postmortems decisions
# Every document change is tracked, attributed, and searchable
$ git log --all --oneline -- runbooks/database-failover.md
$ git log -S "connection_pool" --oneline # Find when a term was introduced
# Use git blame to find who wrote each section
$ git blame -L 20,40 runbooks/database-failover.md
# Diff between two versions of a runbook
$ git diff HEAD~5..HEAD -- runbooks/database-failover.md
Git for Data Pipeline Versioning¶
# Track ML model configs, data schemas, and pipeline definitions
$ git tag -a model-v3.2 -m "XGBoost with new feature set, AUC=0.94"
# Reproduce any historical model run
$ git checkout model-v3.1
$ python train.py --config config.yaml
# Track schema migrations alongside code
$ git log --oneline -- migrations/ | head -10
# Each migration is a commit — you can bisect schema bugs
Git Bundles for Offline Transfer¶
# Create a self-contained bundle (useful for air-gapped environments)
$ git bundle create repo.bundle --all
# Transfer repo.bundle via USB, scp, or any file transfer
# Clone from a bundle
$ git clone repo.bundle my-repo
# Incremental bundle (only new commits since a tag)
$ git bundle create update.bundle v1.0..main
# On the other side:
$ git fetch update.bundle main:main
Git Worktrees for CI/CD Parallelism¶
# Run tests against multiple branches simultaneously
$ git worktree add ../test-main main
$ git worktree add ../test-release release/v2.4
$ git worktree add ../test-feature feature/new-api
# Each worktree is a full working directory sharing the same .git
# CI can test all three in parallel without cloning three times
$ parallel 'cd {} && make test' ::: ../test-main ../test-release ../test-feature
# Clean up
$ git worktree remove ../test-main ../test-release ../test-feature
Common Production Workflows¶
Real patterns teams use daily that go beyond textbook Git.
Hotfix While Deep in a Feature¶
# You're mid-feature, a P0 comes in. Don't stash — use a worktree.
$ git worktree add ../hotfix main
$ cd ../hotfix
# Fix the bug on main
$ vim src/auth/session.py
$ git add -p && git commit -m "fix: prevent session fixation on token refresh"
$ git push origin main
# Back to your feature, no context lost
$ cd ../project
$ git worktree remove ../hotfix
# Pull the fix into your feature branch
$ git fetch origin && git rebase origin/main
Release Train with Cherry-Picks¶
# Cut a release branch
$ git checkout -b release/v2.5 main
# Only specific commits go into the release (not everything on main)
$ git cherry-pick abc1234 # bugfix: connection pool leak
$ git cherry-pick def5678 # feat: add health check endpoint
# Skip commit 789abcd (too risky for this release)
# Tag and deploy
$ git tag -a v2.5.0 -m "Release 2.5.0"
$ git push origin release/v2.5 --tags
Monorepo Sparse Checkout for Service Owners¶
# Clone the monorepo but only check out your service
$ git clone --filter=blob:none --sparse git@github.com:org/monorepo.git
$ cd monorepo
$ git sparse-checkout set services/auth shared/proto shared/config
# You only see your service + shared code on disk
$ ls
services/auth/ shared/proto/ shared/config/
# Pull updates (only fetches blobs for your sparse set)
$ git pull
Automated Changelog from Conventional Commits¶
# If your team uses conventional commits (feat:, fix:, chore:, etc.)
# Generate a changelog between releases:
$ git log v2.4.0..v2.5.0 --pretty=format:'%s' | \
awk '/^feat/ {print "- " $0}' > /tmp/features.txt
$ git log v2.4.0..v2.5.0 --pretty=format:'%s' | \
awk '/^fix/ {print "- " $0}' > /tmp/fixes.txt
# Or use git-cliff / conventional-changelog for full automation
$ git cliff --latest --output CHANGELOG.md
Blame-Ignore for Formatting Commits¶
# After a bulk formatting pass (e.g., running black/prettier across the repo),
# git blame shows the formatting commit on every line. Fix that:
# Create a blame-ignore file
$ echo "abc1234 # bulk format with black" >> .git-blame-ignore-revs
$ git config blame.ignoreRevsFile .git-blame-ignore-revs
# Now git blame skips the formatting commit and shows the real author
$ git blame src/auth/middleware.py
# Shows meaningful commits, not "style: run black"
Multi-Remote Workflow (Fork + Upstream)¶
# Common in open-source: your fork + upstream repo
$ git remote add upstream git@github.com:original/project.git
$ git remote -v
# origin git@github.com:you/project.git (push)
# upstream git@github.com:original/project.git (fetch)
# Keep your fork up to date
$ git fetch upstream
$ git rebase upstream/main
$ git push origin main
# Work on a feature, PR goes to upstream
$ git checkout -b feature/fix-auth
$ git push origin feature/fix-auth
# Create PR from you/project:feature/fix-auth → original/project:main
Signed Releases for Supply Chain Security¶
# Tag releases with GPG or SSH signatures
$ git tag -s v2.5.0 -m "Release 2.5.0 — signed"
# Verify a tag before deploying
$ git verify-tag v2.5.0
# gpg: Good signature from "Release Bot <release@company.com>"
# Require signed commits on protected branches (GitHub/GitLab setting)
# CI verifies: git verify-commit HEAD
# SSH signing (simpler than GPG, Git 2.34+)
$ git config --global gpg.format ssh
$ git config --global user.signingkey ~/.ssh/id_ed25519.pub
$ git tag -s v2.5.1 -m "Release 2.5.1 — SSH signed"
Git Bisect with Infrastructure Tests¶
# Terraform plan started failing but tests pass. Find the commit:
$ git bisect start HEAD v2.4.0
$ git bisect run bash -c '
cd devops/terraform/modules/vpc && \
terraform init -backend=false -input=false 2>/dev/null && \
terraform validate
'
# Git pinpoints the commit that broke terraform validate
# Same pattern for Helm:
$ git bisect run bash -c '
helm lint devops/helm/myapp -f devops/helm/values-dev.yaml
'