Git for DevOps Engineers - Street Ops¶
What experienced engineers know about Git that you learn the hard way.
Recovery Playbooks¶
Recovering from a Bad Merge¶
Situation: You merged a branch into main and it broke everything.
Option 1: Revert the merge commit (safe for shared branches)
git log --oneline -5 # Find the merge commit hash
git revert -m 1 <merge-commit> # -m 1 keeps the main branch side
git push origin main
Note: If you later want to re-merge that branch, you need to
revert the revert first, or the changes won't appear in the diff.
Option 2: Reset (ONLY if no one else has pulled)
git reset --hard <commit-before-merge>
git push --force-with-lease origin main
--force-with-lease is safer than --force: it fails if someone
else pushed in the meantime.
Reflog Rescue¶
Situation: You ran git reset --hard and lost commits.
1. Find the lost commit:
git reflog
# Shows every HEAD movement. Find the commit before the reset.
# Output looks like:
# abc1234 HEAD@{0}: reset: moving to HEAD~3
# def5678 HEAD@{1}: commit: Add authentication module
# ghi9012 HEAD@{2}: commit: Update config parser
2. Recover:
git checkout def5678 # Detached HEAD at lost commit
git checkout -b recovered-branch # Create branch from it
# Or reset your branch to include it:
git reset --hard def5678
Reflog entries expire after 90 days by default. Don't wait.
Under the hood: The reflog is local-only — it is never pushed or pulled. Each clone has its own independent reflog. This means
git reflogonly saves you on the machine where the damage happened. If you destroyed commits on a shared branch, another team member's clone may still have the commits — ask them to push before they pull.
Bisect for Bug Hunting¶
Situation: Something broke, but you don't know which commit caused it.
1. Start bisect:
git bisect start
git bisect bad # Current commit is broken
git bisect good v1.2.0 # This tag was working
2. Git checks out a middle commit. Test it:
# Run your test/check
git bisect good # If this commit works
# OR
git bisect bad # If this commit is broken
3. Repeat until Git identifies the exact commit.
# Output: abc1234 is the first bad commit
4. Clean up:
git bisect reset
Automated bisect (much faster):
git bisect start HEAD v1.2.0
git bisect run ./test-script.sh
# Script must exit 0 for good, non-0 for bad
Cherry-Pick Safely¶
Situation: You need one specific commit from another branch,
not the whole branch.
1. Find the commit:
git log --oneline other-branch | head -20
2. Cherry-pick:
git cherry-pick <commit-hash>
3. If there's a conflict:
# Resolve conflicts, then:
git add <resolved-files>
git cherry-pick --continue
4. To cherry-pick a range:
git cherry-pick <oldest-hash>^..<newest-hash>
Gotcha: cherry-pick creates a NEW commit with a different hash.
If the original branch is later merged, Git usually handles the
duplicate gracefully, but it can cause confusion in the history.
Debug clue: If a cherry-pick causes unexpected conflicts, it is often because the commit depends on earlier commits in the source branch. Use
git log --oneline --graph <commit>~5..<commit>to see the commit's context. You may need to cherry-pick the prerequisite commits first, or manually resolve by understanding the missing context.
Undoing a Pushed Commit¶
Situation: You pushed a commit to a shared branch that needs to go.
Safe way (revert - creates a new commit that undoes the change):
git revert <commit-hash>
git push origin main
Unsafe way (rewrite history - ONLY for your own feature branch):
git reset --hard HEAD~1
git push --force-with-lease origin feature/my-branch
NEVER force-push to main/develop/shared branches unless you have
coordinated with every team member and understand the consequences.
Gotchas & War Stories¶
Secrets in Git history
Someone committed an AWS key. They deleted it in the next commit and thought they were safe. The key is still in the history. Tools like trufflehog and GitHub's secret scanning will find it. Fix: rotate the secret immediately, then use git filter-repo (successor to git filter-branch) to purge the file from all history. Force-push all branches. Every clone needs to re-clone.
# Find secrets in history
trufflehog git file://. --only-verified
# Or
git log --all --oneline -p | grep -iE "aws_secret|password|api_key"
Large files bloating the repo
Someone committed a 500MB database dump. Even after deleting it, the repo is huge because Git stores every version. Prevention: use .gitignore aggressively and set up a pre-commit hook to reject files over a threshold. For legitimate large files, use Git LFS.
# Find large objects in history
git rev-list --objects --all | \
git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | \
awk '/^blob/ {print $3, $4}' | sort -rn | head -10
Merge commits vs squash merge - Merge commit: preserves individual commits, two-parent merge commit in history - Squash merge: collapses all branch commits into one commit on main - Rebase and merge: replays commits on top of main (linear history)
Team decision. Squash is cleaner for main history but loses granular commit info. Choose one strategy and be consistent.
Detached HEAD panic You checked out a tag or a specific commit and now you're in "detached HEAD state." Your commits won't be on any branch. If you made changes you want to keep:
The .gitkeep convention
Git doesn't track empty directories. If you need an empty directory in the repo (e.g., logs/), add an empty .gitkeep file inside it. This is a convention, not a Git feature.
Remember:
--force-with-leasemnemonic: "force push, but only if my view of the remote is still current." It checks that the remote ref matches what you last fetched. If someone else pushed in the meantime, it fails safely. Always use--force-with-leaseinstead of--force— it is the seatbelt for force pushes.
Pre-commit hooks for infrastructure repos Set up hooks to catch problems before they're committed:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
hooks:
- id: check-yaml
- id: check-json
- id: detect-private-key
- id: no-commit-to-branch
args: ['--branch', 'main']
- repo: https://github.com/antonbabenko/pre-commit-terraform
hooks:
- id: terraform_fmt
- id: terraform_validate
Day-to-Day Git Workflow¶
# Starting work on a new feature
git checkout main
git pull origin main
git checkout -b feature/add-monitoring
# Working on changes
git add -p # Stage hunks interactively (review what you're committing)
> **One-liner:** `git add -p` is the most underused Git command. It lets you review every change before staging, catch accidental debug prints, and split a messy working tree into logical commits. The `s` (split) option breaks large hunks into smaller ones for surgical staging.
git commit -m "Add Prometheus scrape config for new service"
# Keeping up with main
git fetch origin
git rebase origin/main # Rebase your feature on latest main
# Resolve any conflicts
# Before creating a PR, clean up
git rebase -i origin/main # Squash/reword/reorder commits
git push origin feature/add-monitoring
# After PR is approved and merged
git checkout main
git pull origin main
git branch -d feature/add-monitoring # Delete local branch
Quick Reference¶
- Cheatsheet: Git