Skip to content

Essential Git Commands: The Complete Developer Cheat Sheet

BackendBytes Engineering Team
BackendBytes Engineering Team
5 min read
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 ran git 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.

TL;DR

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:

CommandPurposeExample
git statusShow modified files and branch infogit status
git addStage changes for commitgit add . or git add file.txt
git commit -mCreate a snapshot with messagegit commit -m "Fix auth bug"
git pushSend commits to remotegit push origin main
git pullFetch and merge remote changesgit pull origin main
git branchList or create branchesgit branch -a or git branch feature-x
git checkout / git switchChange branches or restore filesgit switch -c feature-x
git mergeCombine branch historiesgit merge feature-x
git diffShow unstaged changesgit diff or git diff --staged
git logView commit historygit 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-edit

Branching 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 branch

Remote 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 main

Rule: 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 test

Tags & 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 resolutions

Undo & 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 --abort

When to use what

ScenarioUse ThisWhyDanger
Feature done, integrate to maingit merge --no-ff feature-xPreserves branch topology; clean "feature added" commit in historyPlain git merge loses branch shape; hard to see which commits belong together
Fix local mistake, not pushedgit reset HEAD~1Moves HEAD back, unstages changes; changes still in working dir for retrygit reset --hard loses the changes entirely; can't recover
Undo a push (public mistake)git revert abc123Creates a new commit that inverts changes; doesn't rewrite history; safe for shared branchesgit reset --hard on a shared branch will delete coworkers' work
Sync with main (non-destructive)git pull --rebaseReplays your commits on top of latest main; linear history; CI sees each commit in ordergit pull (merge) creates merge commits; clutters history
Copy 1-2 commits to another branchgit cherry-pick abc123Isolates exact changes; useful for backports or selective mergesCherry-picking across diverged branches causes conflict nightmares
Find breaking commit in 200+ commitsgit bisectBinary search; log(n) steps; finds culprit in ~8 commits instead of manual readingManual git log and git show — takes hours
Temporarily switch contextgit stash then git stash popSaves WIP without committing; clean working dir for switching branchesLeaving uncommitted changes; can cause merge conflicts when switching
Rewrite history before pushinggit rebase -i HEAD~3Squash/reword/drop; clean commit history; each commit is logically completeRewriting after pushing — rewrites shared history, breaks coworkers' branches
Force push after rebasegit push --force-with-leaseChecks if remote changed since your last fetch; fails safely if someone else pushedgit push --force — blindly overwrites remote, deletes coworkers' commits

Gotchas that bite in production

  1. git push --force on 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-includes for even safer behavior.
  2. git reset --hard on the wrong branch loses uncommitted work forever

    • You're on feature-x, run git reset --hard origin/main to "fix" a merge conflict, but actually you wanted to inspect the conflict first. 3 hours of work gone. reflog can recover it but you'll spend 30 minutes finding the commit hash.
    • Fix: Use git reset --soft or git reset HEAD (default, mixed) to move HEAD without losing changes. Only use --hard after confirming git status shows what you expect to lose. Alias reset to reset --soft to be safe by default.
  3. git rebase -i with wrong commit count rebases into shared history

    • You run git rebase -i HEAD~5 but 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 of HEAD~N. Lets Git calculate the correct range. Always check git log main..HEAD (commits to rebase) before starting.
  4. git stash on 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. Run git stash pop. Merge conflicts because the branch diverged. Untracked files are lost.
    • Fix: Run git add . then git stash to stash everything (tracked + untracked). Or use git stash branch feature-recovery to create a branch from the stash instead of popping it. Always verify git status is clean before switching branches.
  5. git cherry-pick with 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 maingit rebase main to linearize history and catch conflicts early
  • Squash messy commitsgit rebase -i main, change non-first commits to squash/fixup for clean history
  • Verify no secrets leakedgit diff main..HEAD and scan output for API keys, tokens, credentials
  • Run tests before push — local npm test or add a pre-push hook to block broken code
  • Use --force-with-lease if rebasedgit push --force-with-lease origin main is safe; --force deletes coworkers' commits
  • Tag releases semanticallygit tag -a v1.0.0 -m "Release: feature X, fix Y" then git push origin v1.0.0
  • Consider signing commitsgit 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 main

Partial 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 }}"
    fi

On 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.git

The --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

BackendBytes Engineering Team
BackendBytes Engineering Team

Engineering Team

A multidisciplinary team of backend engineers, architects, and DevOps practitioners shipping deep dives into distributed systems and production infrastructure.

Read Next