Essential Git Commands: The Complete Developer Cheat Sheet
Key Takeaways
- →`git bisect` binary-searches through 200+ commits to find a breaking change in 4 minutes — mark current as bad, last week's deploy as good, let Git narrow it down
- →`git push --force-with-lease` is safe; `git push --force` overwrites coworkers' commits if they pushed during your rebase — always use `--force-with-lease`
- →Interactive rebase (`git rebase -i HEAD~5`) squashes messy commits into clean ones before pushing — rewrite history only on branches you own, never on shared main
- →`git reflog` recovers lost commits after accidental reset — every branch movement is logged; you can find and restore work that looks gone
- →`git stash` saves work-in-progress without committing; `git stash pop` applies it later — cleaner than temp commits when context-switching
A subtle bug caused sporadic payments to fail in production. Somewhere in 200+ commits merged that week, a
<=became<in price rounding. One engineer rangit bisect start, marked the current commit bad and last week's deploy good, then let Git binary-search. Seven test runs later—about 4 minutes—bisect found the exact commit. Without it, half a day of manual diff reading.
Every Git command moves code between four places: working directory (your files) → staging area (git add) → local repository (git commit) → remote server (git push). Master the 10 core commands for daily work and six advanced operations for when things go wrong. Use --force-with-lease instead of --force to protect coworkers' commits. Keep branches short-lived, squash messy commits before pushing, and use git bisect to find breaking changes in minutes instead of hours.
- 10 core commands: status, add, commit, push, pull, branch, checkout, merge, diff, log
- 6 advanced: rebase, cherry-pick, stash, reflog, bisect, tag
- Recovery tools:
git reset(undo locally),git revert(undo publicly),git reflog(find lost commits)
Pick the Right Recovery Path
Most production git emergencies are "I broke history — what's safe?" The wrong undo command corrupts shared branches and forces other engineers to re-clone. Route by what you need to undo:
graph TD
Start[I broke something] --> Where{Where did<br/>the change go?}
Where -->|Working dir<br/>not staged| WD[git restore file<br/>git restore --source HEAD~1]
Where -->|Staged<br/>not committed| Stg[git restore --staged file<br/>git reset HEAD]
Where -->|Committed locally<br/>not pushed| Local{Keep changes<br/>or discard?}
Where -->|Pushed to remote<br/>shared branch| Remote[git revert SHA<br/>creates new commit<br/>NEVER force-push]
Where -->|Lost commit<br/>cannot find SHA| Reflog[git reflog<br/>git checkout SHA<br/>git cherry-pick]
Local -->|Keep changes| Soft[git reset --soft HEAD~1]
Local -->|Discard changes| Hard[git reset --hard HEAD~1<br/>destructive — verify first]
style Remote fill:#fdd
style Hard fill:#fdd
style Reflog fill:#dfd
style Soft fill:#dfd
style WD fill:#dfd
style Stg fill:#dfd
The diagram is the audit trail for every git emergency: never --hard reset shared history, never force-push main. Recovery via reflog saves more careers than any other git feature[Git docs].
The 10 essential commands
[Git docs]Every Git workflow is built from these. Commit them to muscle memory:
| Command | Purpose | Example |
|---|---|---|
git status | Show modified files and branch info | git status |
git add | Stage changes for commit | git add . or git add file.txt |
git commit -m | Create a snapshot with message | git commit -m "Fix auth bug" |
git push | Send commits to remote | git push origin main |
git pull | Fetch and merge remote changes | git pull origin main |
git branch | List or create branches | git branch -a or git branch feature-x |
git checkout / git switch | Change branches or restore files | git switch -c feature-x |
git merge | Combine branch histories | git merge feature-x |
git diff | Show unstaged changes | git diff or git diff --staged |
git log | View commit history | git log --oneline --graph |
Setup and core workflow
The git data flow — every command moves files between four locations:
graph LR
WD[Working directory<br/>your edited files] -->|git add| Stage[Index aka<br/>staging area]
Stage -->|git commit| Local[Local repo<br/>.git/objects]
Local -->|git push| Remote[Remote repo<br/>origin/main]
Remote -->|git fetch| Local
Local -->|git checkout / restore| WD
Stage -->|git restore --staged| WD
Local -.->|git stash| Stash[Stash stack<br/>WIP shelf]
Stash -.->|git stash pop| WD
Local -.->|git reflog<br/>90-day safety net| Recovery[Recovery via<br/>git checkout SHA]
style WD fill:#fdd
style Stage fill:#ffd
style Local fill:#dfd
style Remote fill:#dfd
style Recovery fill:#dfd
The diagram is the entire git mental model: every command is an arrow between four storage locations + the stash and reflog escape hatches.
# Initial setup (one-time)
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"
# Clone a repository
git clone https://github.com/org/repo.git
# Daily workflow
git status # Check what changed
git add . # Stage all changes
git commit -m "Descriptive message" # Create snapshot
git push origin main # Send to remote
# Undo the last commit (keep changes)
git reset HEAD~1
# Discard all unstaged changes
git restore .
# Amend the last commit
git add forgotten-file.txt
git commit --amend --no-editBranching and merging
[Git docs]# Create and switch to a new branch
git switch -c feature-x # Modern (Git 2.23+)
git checkout -b feature-x # Legacy
# List branches
git branch -a # All (local + remote)
git branch --merged # Safe to delete
git branch --no-merged # Not yet merged
# Merge a branch into current
git merge feature-x # Creates merge commit
git merge --no-ff feature-x # Preserve branch topology (recommended)
# Delete a branch
git branch -d feature-x # Safe (only if fully merged)
git branch -D feature-x # Force delete
# Switch to remote branch
git switch --detach origin/feature-x # Detached HEAD at the remote commit
git switch --track origin/feature-x # Create local tracking branchRemote operations
[Git docs]# List remotes
git remote -v
# Add a remote
git remote add origin https://github.com/org/repo.git
git remote add upstream https://github.com/original/repo.git # Fork workflow
# Fetch and pull
git fetch origin # Download without merging
git pull origin main # Fetch + merge (or rebase if configured)
# Push
git push origin main # Send commits to remote
git push -u origin feature # Set upstream tracking
git push origin --all # Push all branches
# Force push (after rebase/amend)
git push --force-with-lease origin feature # Safe: checks if remote changed
git push --force-with-lease --force-if-includes origin feature # Safest (Git 2.30+)Never use git push --force—it overwrites coworkers' commits if they pushed while you rebased.
History rewriting (rebase & cherry-pick)
[Git docs]# Interactive rebase: squash, reword, or drop commits
git rebase -i HEAD~5 # Rebase last 5 commits
# Actions: pick (keep), reword (edit message), squash/fixup (merge),
# drop (delete)
# Autosquash: auto-apply fixup commits
git commit --fixup=abc123 # Create "fixup! ..." commit
git rebase -i --autosquash main # Auto-squash into target
# Cherry-pick: copy specific commit(s)
git cherry-pick abc123 # Copy single commit
git cherry-pick abc123..def456 # Copy range (after abc123 to def456)
git cherry-pick --abort # Cancel in-progress cherry-pick
# Rebase onto main (linearize history)
git rebase mainRule: only rebase commits not yet pushed, or branches where you're the sole contributor. Rewriting shared history causes merge conflicts for coworkers.
Stash, inspection & bisect
# Stash (temporarily shelve changes)
git stash push -m "WIP: feature-x" # Save with a message
git stash list # List all stashes
git stash apply # Apply most recent (keep it)
git stash pop # Apply and remove
git stash apply stash@{2} # Apply specific stash
git stash branch feature-x stash@{0} # Create branch from stash
# Inspect history
git log --oneline --graph # Compact history with branches
git log --stat # Show which files changed
git log --grep="keyword" # Search commit messages
git log -S "string" # Find when string was added/removed
git shortlog -sn # Commits per author
# Show differences
git diff # Unstaged changes
git diff --staged # Staged changes
git diff HEAD~1 HEAD # Compare commits
git blame filename.txt # Who changed each line
# Binary search for the breaking commit
git bisect start
git bisect bad HEAD
git bisect good v1.0
git bisect good # or "git bisect bad" as you test commits
# Automated: git bisect run npm testTags & advanced features
# Tagging (for releases)
git tag -a v1.0.0 -m "Release 1.0.0" # Annotated (recommended)
git tag v1.0.0-rc1 # Lightweight
git tag -l "v1.*" # List matching pattern
git push origin v1.0.0 # Push specific tag
git push origin --tags # Push all tags
# Git hooks (auto-run at workflow points; must be in .git/hooks/)
# pre-commit: lint/test before commit
# pre-push: run tests before push
# commit-msg: enforce message format
# Use husky for team-wide shared hooks
# Configuration defaults
git config --global pull.rebase true # Rebase on pull
git config --global push.autoSetupRemote true # Auto-track branches
git config --global rerere.enabled true # Remember conflict resolutionsUndo & recovery
[Git docs]# Undo changes
git reset --soft HEAD~1 # Undo commit, keep changes staged
git reset HEAD~1 # Undo commit, unstage changes
git restore filename.txt # Discard unstaged changes (one file)
git restore . # Discard all unstaged changes
git restore --staged filename.txt # Unstage (keep in working dir)
# Merge conflicts
git status # See conflicted files
# Edit files, remove <<<<<<< / ======= / >>>>>>> markers
git add conflicted_file.txt
git commit # Complete merge
# Recovery (reflog shows all HEAD movements)
git reflog # List all commits (including deleted branches)
git checkout -b recovered abc123 # Create branch from "lost" commit
git clean -fdn # Dry run: show untracked files
git clean -fd # Delete untracked files/dirs
# Abort in-progress operations
git merge --abort
git rebase --abort
git cherry-pick --abortWhen to use what
| Scenario | Use This | Why | Danger |
|---|---|---|---|
| Feature done, integrate to main | git merge --no-ff feature-x | Preserves branch topology; clean "feature added" commit in history | Plain git merge loses branch shape; hard to see which commits belong together |
| Fix local mistake, not pushed | git reset HEAD~1 | Moves HEAD back, unstages changes; changes still in working dir for retry | git reset --hard loses the changes entirely; can't recover |
| Undo a push (public mistake) | git revert abc123 | Creates a new commit that inverts changes; doesn't rewrite history; safe for shared branches | git reset --hard on a shared branch will delete coworkers' work |
| Sync with main (non-destructive) | git pull --rebase | Replays your commits on top of latest main; linear history; CI sees each commit in order | git pull (merge) creates merge commits; clutters history |
| Copy 1-2 commits to another branch | git cherry-pick abc123 | Isolates exact changes; useful for backports or selective merges | Cherry-picking across diverged branches causes conflict nightmares |
| Find breaking commit in 200+ commits | git bisect | Binary search; log(n) steps; finds culprit in ~8 commits instead of manual reading | Manual git log and git show — takes hours |
| Temporarily switch context | git stash then git stash pop | Saves WIP without committing; clean working dir for switching branches | Leaving uncommitted changes; can cause merge conflicts when switching |
| Rewrite history before pushing | git rebase -i HEAD~3 | Squash/reword/drop; clean commit history; each commit is logically complete | Rewriting after pushing — rewrites shared history, breaks coworkers' branches |
| Force push after rebase | git push --force-with-lease | Checks if remote changed since your last fetch; fails safely if someone else pushed | git push --force — blindly overwrites remote, deletes coworkers' commits |
Gotchas that bite in production
-
git push --forceon a shared branch overwrites coworkers' commits silently- You rebase to fix conflicts locally. You run
git push --force origin main. Meanwhile, a teammate pushed a critical hotfix while you were rebasing. Your force push deletes it. Nobody notices for 10 minutes. - Fix: Always use
git push --force-with-lease origin branch. It checks if the remote changed since your last fetch — fails safely if someone else pushed. Git 2.30+: use--force-with-lease --force-if-includesfor even safer behavior.
- You rebase to fix conflicts locally. You run
-
git reset --hardon the wrong branch loses uncommitted work forever- You're on feature-x, run
git reset --hard origin/mainto "fix" a merge conflict, but actually you wanted to inspect the conflict first. 3 hours of work gone.reflogcan recover it but you'll spend 30 minutes finding the commit hash. - Fix: Use
git reset --softorgit reset HEAD(default, mixed) to move HEAD without losing changes. Only use--hardafter confirminggit statusshows what you expect to lose. Aliasresettoreset --softto be safe by default.
- You're on feature-x, run
-
git rebase -iwith wrong commit count rebases into shared history- You run
git rebase -i HEAD~5but actually your 3 unpushed commits are buried 7 commits deep. You start rebasing committed history that teammates are building on. Their pulls fail with "force update required". Team loses 30 minutes untangling. - Fix: Use
git rebase -i main(rebase onto a branch name) instead ofHEAD~N. Lets Git calculate the correct range. Always checkgit log main..HEAD(commits to rebase) before starting.
- You run
-
git stashon a dirty index causes you to lose files if you switch branches- You have unstaged changes. Run
git stash(only stashes tracked files). Switch branches. Realize you need those files. Rungit stash pop. Merge conflicts because the branch diverged. Untracked files are lost. - Fix: Run
git add .thengit stashto stash everything (tracked + untracked). Or usegit stash branch feature-recoveryto create a branch from the stash instead of popping it. Always verifygit statusis clean before switching branches.
- You have unstaged changes. Run
-
git cherry-pickwith conflicts during peak deployments blocks the release- You cherry-pick a fix from dev to main. 3-way merge conflict in a file that's wildly different on both branches. You're resolving it under time pressure, make a mistake, the hotfix contains the wrong code. Deploys in 2 minutes. Catches production.
- Fix: Cherry-pick to a temp branch first (
git cherry-pick abc123 -b temp-cherry), test locally, then merge to main. Or just merge the whole feature branch if it's ready. Cherry-pick is for surgical backports, not large merges.
Production checklist
Before pushing to main or creating a release, run through these steps to prevent broken deploys and security leaks:
- Rebase onto latest main —
git rebase mainto linearize history and catch conflicts early - Squash messy commits —
git rebase -i main, change non-first commits to squash/fixup for clean history - Verify no secrets leaked —
git diff main..HEADand scan output for API keys, tokens, credentials - Run tests before push — local
npm testor add a pre-push hook to block broken code - Use
--force-with-leaseif rebased —git push --force-with-lease origin mainis safe;--forcedeletes coworkers' commits - Tag releases semantically —
git tag -a v1.0.0 -m "Release: feature X, fix Y"thengit push origin v1.0.0 - Consider signing commits —
git config --global commit.gpgsign true(with GPG or SSH key) for verified badges
Git in CI: caching, partial clone, and shallow trade-offs
CI runners clone your repo on every job. For a 200 MB monorepo with 50k commits and 10k refs, a naive git clone burns 30-60 seconds per job before any test runs. Across a 40-job pipeline that is 30 minutes of pure clone wait per pull request. The fix is not "throw a faster runner at it" — it is matching the clone strategy to what each job actually needs.
Shallow clone is the lazy fix. It downloads only the most recent commits, skipping history:
# Default GitHub Actions / GitLab CI behaviour: shallow with depth=1
git clone --depth=1 https://github.com/org/repo.git
# Need merge-base with main for a diff-based test selector? depth=1 fails
git fetch --unshallow # convert to full clone (slow, undoes the win)
git fetch --deepen=50 # extend by 50 commits (cheaper)Shallow clone breaks anything that walks history: git blame, git log, git merge-base origin/main HEAD, semantic-release version inference, and changelog generators. If your CI runs git diff main...HEAD to decide which packages to rebuild, depth=1 silently returns the wrong diff because main is not in the clone.
Partial clone is the better default for medium and large repos. It downloads commit and tree objects but defers blob download until a file is actually read:
# Skip all blobs at clone time — fetch on demand
git clone --filter=blob:none https://github.com/org/repo.git
# Skip blobs above a size threshold (good for repos with mixed binary + code)
git clone --filter=blob:limit=1m https://github.com/org/repo.git
# Combine with sparse-checkout for monorepos: clone metadata, check out one path
git clone --filter=blob:none --no-checkout https://github.com/org/monorepo.git
cd monorepo
git sparse-checkout init --cone
git sparse-checkout set services/payments libs/shared
git checkout mainPartial clone keeps full history available — git log and git blame still work — but only fetches blobs lazily when you cat a file or run git checkout against a path. For a CI job that compiles one service in a monorepo, this can cut clone time from 45s to 4s and disk usage from 800 MB to 60 MB. The trade-off: every blob access becomes a network round trip, so test runs that read many files pay incremental latency. Pair it with sparse-checkout to bound the working tree.
Caching the .git directory is the highest-leverage CI optimization most teams skip. GitHub Actions' actions/checkout@v4 re-clones every run; instead, cache .git/ keyed by the previous commit and run git fetch on hit:
# .github/workflows/ci.yml — cache .git across runs
- uses: actions/cache@v4
with:
path: .git
key: git-${{ github.sha }}
restore-keys: git-
- name: Fast clone via cache
run: |
if [ -d .git ]; then
git fetch --no-tags --filter=blob:none origin "${{ github.sha }}"
git reset --hard "${{ github.sha }}"
git clean -fdx
else
git clone --filter=blob:none --no-tags \
https://github.com/${{ github.repository }}.git .
git reset --hard "${{ github.sha }}"
fiOn a cache hit this fetches only the new commits since the previous run — typically a few hundred kilobytes — instead of cloning the entire repo. On a cache miss it falls back to a partial clone. Result: median checkout time drops from 38s to 3s. The git clean -fdx line is critical; without it, leftover artifacts from the cached run (built binaries, node_modules, generated files) corrupt the next build.
Reference repos are the nuclear option for self-hosted runners. Maintain a single up-to-date mirror on each runner host, then have every job clone with --reference:
# One-time setup on the runner host
git clone --bare https://github.com/org/repo.git /opt/git-mirror/repo.git
# Cron: keep the mirror fresh
0 * * * * git -C /opt/git-mirror/repo.git fetch --all --prune
# In every CI job
git clone --reference /opt/git-mirror/repo.git --dissociate \
https://github.com/org/repo.gitThe --reference flag tells the new clone to read shared objects from the mirror's objects/ directory, dropping clone time to single-digit seconds even for full history. The --dissociate flag copies referenced objects into the new clone after fetch so the working clone is self-contained — safer if the mirror is deleted mid-build.
Decision rule for picking a strategy: if your job runs git log, git blame, or computes a merge-base, do not use shallow. Use partial clone (--filter=blob:none) by default for any repo over 50 MB. Add sparse-checkout for monorepos where each job touches one subtree. Cache .git/ across runs once the partial-clone strategy is stable. Reach for reference repos only on self-hosted runners where you control the host.
The cost is real on hosted runners too: a team running 200 PRs/day with a 60-job pipeline spending 30s extra per job on naive cloning burns 100 hours of compute monthly — at GitHub Actions rates, real money. Tuning the clone is one of the cheapest CI wins available[Git docs].
Frequently Asked Questions
How do I undo a push?
If you pushed commits you want to remove, don't panic. Create a new commit that reverts the changes: git revert abc123 (creates inverse commit). Or rebase and git push --force-with-lease origin main if the branch allows it. Never use --force — always use --force-with-lease.
What's the difference between git reset and git revert?
git reset moves HEAD back and rewrites history (use before pushing). git revert creates a new commit that inverts changes (use after pushing). For shared branches, always revert.
How do I find which commit introduced a bug?
Use git bisect: mark current state as bad, a known-good commit as good, then Git binary-searches. For automated testing: git bisect run npm test. Finds the culprit in log(n) steps.
Why should I use --force-with-lease instead of --force?
--force blindly overwrites the remote branch. If a coworker pushed 3 commits while you were rebasing, --force deletes their work. --force-with-lease checks if the remote changed—it fails safely if someone else pushed.
How do I stage only some changes in a file?
Use git add -p (interactive hunk staging). Git shows each change and asks: [y]es / [n]o / [s]plit / [e]dit / [?]help. Perfect for creating focused, single-purpose commits.
Keep Reading
- Essential Linux Commands Cheat Sheet — The shell commands, text pipelines, and process management tools you use alongside Git every day
- Essential Docker Commands Cheat Sheet — Container lifecycle and image builds that git hooks and CI pipelines trigger from your commits
- Go Testing Best Practices — The test suites that
git bisect runautomates to find breaking commits - Essential Kubernetes Commands Cheat Sheet — Workload triage commands that complete the cheat-sheet trifecta with git and Linux
- Terraform in Production — When the diff is "delete production database," reflog-style safety nets become state-locking + plan-review
Engineering Team
A multidisciplinary team of backend engineers, architects, and DevOps practitioners shipping deep dives into distributed systems and production infrastructure.
Read Next
Essential Docker Commands: The Complete Cheat Sheet
Docker reference: container lifecycle, image management, volumes, networking, and debugging tools for production systems.
Essential Kubernetes Commands: The Complete kubectl Cheat Sheet
Definitive kubectl reference: pod debugging, deployments, StatefulSets, RBAC, scheduling, Helm, and production troubleshooting flowcharts.
Essential Linux Commands: A Backend Engineer's Cheat Sheet
60+ Linux commands for production debugging: processes, networking, kernel tuning, and the gotchas that trip up engineers.