Skip to content

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):

$ git commit --amend -m "feat(auth): add JWT middleware with refresh tokens"

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
'