Essential Git Commands: The Complete Developer Cheat Sheet
Picture a subtle bug making payments fail sporadically. Somewhere in 200+ commits that week, a
<=became<in price rounding. Rungit bisect start, mark the current commit bad and last week's deploy good, and Git binary-searches: about eight test runs to the exact commit, instead of 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. reflog keeps lost local commits recoverable for 30 days by default[1].
The 10 essential commands
Every Git workflow is built from these. Commit them to muscle memory:[1]
| 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/>30-day net for<br/>lost commits| 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 "[email protected]"
# 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
Source[1]# 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
Source[1]# 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)
Source[1]# 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; .git/hooks/ or core.hooksPath)
# 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
Source[1]# 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 silently deletes coworkers' commits- A hotfix pushed while you rebased is gone the moment your force-push lands.
- Fix: always
--force-with-lease(fails safely if the remote moved); Git 2.30+ adds--force-if-includes.
-
git reset --hardon the wrong branch destroys uncommitted work- Committed work survives in
reflog; uncommitted changes are simply gone. - Fix: prefer
--soft/ plainreset(mixed); rungit statusand confirm what you're about to lose before any--hard.
- Committed work survives in
-
git rebase -i HEAD~Nwith the wrong N rewrites shared history- Miscount and you're rebasing commits teammates already built on.
- Fix:
git rebase -i main(branch name — Git computes the range); preview withgit log main..HEAD.
-
git stashonly stashes tracked files- Untracked files stay behind, then get lost in the branch-switch shuffle.
- Fix:
git stash -u(include untracked), orgit stash branch feature-recoveryinstead of popping into a diverged branch.
-
git cherry-pickconflicts under deploy pressure ship the wrong resolution- Fix: pick onto a temp branch first —
git switch -c temp-cherry && git cherry-pick abc123— test, then merge. Cherry-pick is for surgical backports, not large merges.
- Fix: pick onto a temp branch first —
Production checklist
Before pushing to main or creating a release, run through these steps to prevent broken deploys and security leaks:
- Rebase onto main —
git rebase main: linear history, early conflicts - 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
For a 200 MB monorepo with 50k commits, 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:
# actions/checkout default: depth=1 (GitLab CI defaults new projects to 20)
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 history-walkers: git blame, git log, merge-base computation, semantic-release inference, and changelog generators. If your CI runs git diff main...HEAD to decide which packages to rebuild, depth=1 makes it fail: the clone lacks the history to find a merge base.
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 — git log/git blame work — fetching blobs lazily when git checkout, git show or git blame needs them[1]. For a CI job that compiles one service in a monorepo, no historical file versions are downloaded unless a command asks for them. 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 turns most checkouts into an incremental fetch. On hosted runners, actions/checkout 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 }}"
fiA cache hit fetches only new commits; a miss falls back to a partial clone. 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.git--reference reads shared objects from the mirror's objects/ directory — clone time drops 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: jobs running git log, git blame, or merge-base computations must 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.
On hosted runners the cost compounds: 200 PRs/day x 60 jobs x 30s of naive cloning is 100 hours of compute a day — over a 22-day working month that's 2,200+ hours, at GitHub Actions rates, real money. Tuning the clone is one of the cheapest CI wins available.
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
Sources
- 1.Git Reference Manual — git-scm.com, 2026
Engineering Team
An independent engineering publication covering distributed systems, databases, and production infrastructure. Every factual claim is cited to a primary source or removed.
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.