Skip to content

Essential Git Commands: The Complete Developer Cheat Sheet

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

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. 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]

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/>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-edit

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

Remote 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 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; .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 resolutions

Undo & 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 --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 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.
  2. git reset --hard on the wrong branch destroys uncommitted work

    • Committed work survives in reflog; uncommitted changes are simply gone.
    • Fix: prefer --soft / plain reset (mixed); run git status and confirm what you're about to lose before any --hard.
  3. git rebase -i HEAD~N with 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 with git log main..HEAD.
  4. git stash only stashes tracked files

    • Untracked files stay behind, then get lost in the branch-switch shuffle.
    • Fix: git stash -u (include untracked), or git stash branch feature-recovery instead of popping into a diverged branch.
  5. git cherry-pick conflicts 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.

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..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 rebased — git push --force-with-lease origin main is safe; --force deletes coworkers' commits
  • Tag releases semantically — git tag -a v1.0.0 -m "Release: feature X, fix Y" then git 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 main

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

A 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

Sources

  1. 1.Git Reference Manual — git-scm.com, 2026
BackendBytes Engineering Team
BackendBytes

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