Most developers know enough Git to survive: add, commit, push, repeat. Then a merge conflict shows up, or someone force-pushes over your work, or you need to find which of 400 commits introduced a production bug — and suddenly Git feels like a black box you're negotiating with instead of a tool you control.
This guide closes that gap. We're going to walk through every command you'll actually use in a professional engineering career — not as a dry reference, but with the real-world scenario that explains why it exists and when to reach for it. By the end, you won't just know the commands. You'll know which one to use at 2 AM when a hotfix is burning and your branch is a mess.
Every Git workflow starts with configuration. Get this right once and you save yourself hours of friction — misattributed commits, painful repeated typing, and merge tools that don't launch.
git config — Identity, Defaults, and Aliases
git config writes settings to one of three scopes: --system (every user on the machine), --global (every repo for your user), or --local (just the current repo, the default). Local always wins over global, which wins over system.
# Identity — required before your first commit
git config --global user.name "Jane Doe"
git config --global user.email "jane@company.com"
# Per-repo override — common when work and personal emails differ
cd ~/work/client-project
git config user.email "jane@client.com"
# Set your default branch name for new repos
git config --global init.defaultBranch main
# Make `git log`, `diff`, etc. colorized
git config --global color.ui auto
# View everything currently in effect, with the file each value came from
git config --list --show-originIndustry teams almost always set a per-repo user.email for client or open-source work — mixing your personal GitHub identity into a client's commit history is a common and embarrassing mistake.
Aliases — Shave Seconds Off Every Command
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.st status
git config --global alias.cm "commit -m"
# A genuinely useful one: a readable, graph-based log
git config --global alias.lg "log --oneline --graph --decorate --all"
# Now you can just run:
git lggit init and git clone
git init turns any directory into a Git repository by creating a .git folder. git clone does the same, but also downloads the full history and remote tracking config from an existing repo — it's how you get a working copy of a project that already lives on GitHub.
# Start a brand-new project
mkdir my-app && cd my-app
git init
# Get an existing project — the everyday case
git clone https://github.com/company/backend-api.git
# Clone into a custom folder name
git clone https://github.com/company/backend-api.git api-service
# Clone only recent history — much faster for huge, old repos
git clone --depth 1 https://github.com/torvalds/linux.gitgit status — Read the Room First
Before every add, commit, or branch switch, professionals run git status. It tells you exactly what's staged, what's modified but unstaged, and what's untracked — the single most-run command in Git for a reason.
git status
# Compact, one-line-per-file view — great once you're fluent in the symbols
git status -sgit add — Staging With Precision
git add moves changes into the staging area — the draft of your next commit. Staging specific files (not everything at once) is what lets you write commits that do one logical thing, which is the foundation of a clean, reviewable history.
# Stage one file
git add src/auth/login.ts
# Stage a whole directory
git add src/components/
# Stage everything tracked and modified
git add -u
# The power move: interactively stage hunks within a file
git add -p src/auth/login.tsgit rm and git mv — Do It the Git Way
Deleting or renaming a file with your OS file manager (or rm/mv in the shell) leaves Git seeing a deletion plus an untracked new file — it has to guess it was a rename. Using Git's own commands stages the change correctly and preserves rename detection in history.
# Remove a file from disk AND stage the deletion
git rm old-config.json
# Stop tracking a file but keep it on disk (e.g. you forgot to .gitignore it)
git rm --cached secrets.env
# Rename/move a file — stages as a single tracked rename
git mv src/utils/helper.js src/utils/formatters.jsgit commit — Snapshots With Meaning
# Short message
git commit -m "Fix null pointer in checkout flow"
# Stage all tracked changes and commit in one step (skips untracked files)
git commit -am "Update pricing calculation"
# Open your editor for a multi-line message — subject + body
git commitCommit Message Best Practices
- Subject line under ~50 chars, imperative mood: 'Add retry logic' not 'Added' or 'Adds'
- Blank line, then a body explaining why, not what — the diff already shows what
- One logical change per commit — this is what makes git revert and git bisect actually useful later
- Reference ticket IDs when your team uses them: 'Fix race condition in cache writer (JIRA-4521)'
git commit --amend — Fixing the Last Commit
Realized you forgot a file, or typo'd the message, seconds after committing? Don't make a new 'fix typo' commit — amend the one you just made.
# Fix just the message
git commit --amend -m "Fix null pointer in checkout flow"
# Forgot to include a file — stage it, then fold it into the last commit
git add forgotten-file.ts
git commit --amend --no-editgit log — Reading the Story of a Project
# The default — verbose, one commit per screen
git log
# What every senior dev actually runs
git log --oneline --graph --decorate --all
# Filter by author or date range
git log --author="Jane" --since="2 weeks ago"
# See which files changed in each commit
git log --stat
# Search commit messages for a keyword
git log --grep="payment"git diff — Staged vs. Unstaged
# Unstaged changes — what's different from the last commit but not yet added
git diff
# Staged changes — what will actually go into the next commit
git diff --staged
# Compare two branches
git diff main..feature/new-checkout
# Just the filenames that changed, not the full diff
git diff --name-only main..feature/new-checkoutgit show — Inspect One Commit
# Full diff of a specific commit
git show a1b2c3d
# Just show a file as it existed at that commit
git show a1b2c3d:src/config.tsgit blame — Find the Author Without Being Toxic About It
# Who touched each line of this file, and in which commit
git blame src/payments/stripe.ts
# Only a specific line range
git blame -L 40,60 src/payments/stripe.ts
# Ignore whitespace-only changes and formatting commits when attributing
git blame -w --ignore-rev <formatting-commit-sha> src/payments/stripe.tsgit branch — Create, List, Delete
# List local branches
git branch
# List local and remote-tracking branches
git branch -a
# Create a branch without switching to it
git branch feature/user-profiles
# Delete a branch that's been fully merged (safe)
git branch -d feature/user-profiles
# Force-delete a branch even if unmerged (you know what you're doing)
git branch -D experimental/spikegit checkout vs. git switch
git checkout is the historical Swiss-army knife — it switches branches, restores files, and creates branches, all through slightly different flag combinations, which makes it easy to mistype and lose changes. Modern Git split its responsibilities into git switch (branches) and git restore (files). Use the modern commands in new work; recognize the old ones because plenty of scripts and older teammates still use them.
# --- Old way (still everywhere) ---
git checkout feature/dashboard # switch branches
git checkout -b feature/dashboard # create + switch
git checkout -- src/index.ts # discard local changes to a file
# --- Modern way (Git 2.23+) ---
git switch feature/dashboard # switch branches
git switch -c feature/dashboard # create + switch
git switch - # jump back to the previous branchgit worktree — The Industry Productivity Secret
Normally, switching branches means your working directory changes underneath you — you can't have main and feature/hotfix checked out at the same time in one folder. git worktree lets you attach a second (or third) working directory to the same repository, each checked out to a different branch, sharing the same .git history.
# You're deep in feature work with a dirty working directory,
# and a critical hotfix just came in. Don't stash — spin up a worktree.
git worktree add ../myapp-hotfix hotfix/prod-crash
# Now you have two folders:
# myapp/ -> still on feature/big-refactor, untouched
# myapp-hotfix/ -> a clean checkout of hotfix/prod-crash
cd ../myapp-hotfix
# fix, commit, push, open PR — without touching your feature branch state
# When done, clean it up
git worktree remove ../myapp-hotfix
# See all active worktrees
git worktree listgit remote — Managing Remote URLs
# List configured remotes
git remote -v
# Add a remote (common when forking)
git remote add upstream https://github.com/original-org/project.git
# Change a remote's URL — e.g. after migrating to SSH
git remote set-url origin git@github.com:company/project.gitgit fetch — Why It's Safer Than Pull
git fetch downloads new commits and branches from the remote into your local remote-tracking branches (origin/main, etc.) but does not touch your working directory or current branch. It lets you look before you leap — review what changed on main before merging it into your work.
# Download remote changes without merging anything
git fetch origin
# See what's new on origin/main before touching your branch
git log HEAD..origin/main --oneline
# Now merge deliberately, once you know what's coming
git merge origin/maingit pull — And Why --rebase Is Often Better
git pull is just git fetch followed by git merge — convenient, but it creates a merge commit every time your local branch has diverged from the remote, even for trivial syncs. git pull --rebase instead replays your local commits on top of the latest remote commits, keeping history linear.
git pull
# Cleaner history — no noisy 'Merge branch main into main' commits
git pull --rebase
# Make rebase the default behavior for pulls, repo-wide
git config pull.rebase truegit push — Upstreams and Safe Force Pushing
# First push of a new local branch — links it to origin for future plain pushes
git push -u origin feature/dashboard
# After the upstream is set, this is enough
git push
# After an interactive rebase or amend, you MUST force push —
# but never with plain --force on shared branches
git push --force-with-leasegit merge — Fast-Forward vs. No-FF
A fast-forward merge happens when the target branch hasn't moved since you branched off — Git just slides the pointer forward, no merge commit needed. A no-ff merge always creates a merge commit, even when a fast-forward is possible, preserving the fact that a feature branch existed.
# Fast-forward if possible (default behavior)
git checkout main
git merge feature/login
# Force a merge commit, keeping the feature branch visible in history
git merge --no-ff feature/logingit rebase — Rewriting History for Clarity
Rebasing takes your branch's commits and replays them on top of another branch's tip, producing a linear history with no merge commit. Interactive rebase (-i) goes further, letting you edit, squash, reorder, or reword commits before they land.
# Replay your feature branch's commits on top of the latest main
git checkout feature/checkout-redesign
git rebase main
# Interactive rebase — clean up the last 5 commits before opening a PR
git rebase -i HEAD~5pick a1b2c3d Add checkout skeleton
squash b2c3d4e fix typo
squash c3d4e5f address review comment
reword d4e5f6g Add discount code field
pick e5f6g7h Add unit tests
# Commands:
# p, pick = keep commit as-is
# r, reword = keep commit, edit its message
# s, squash = combine with the commit above, merge messages
# f, fixup = like squash, but discard this commit's message
# d, drop = remove the commit entirelyA conflict happens when Git can't automatically reconcile two changes to the same lines — say, you and a teammate both edited the same function on diverging branches. Git pauses the merge or rebase and asks you to resolve it by hand. This is completely normal; the goal is to handle it calmly and correctly.
Step 1: Trigger and Identify
git merge feature/pricing-update
# Auto-merging src/pricing.ts
# CONFLICT (content): Merge conflict in src/pricing.ts
# Automatic merge failed; fix conflicts and then commit the result.
# See exactly which files are conflicted
git statusStep 2: Read the Conflict Markers
function calculateDiscount(total: number): number {
<<<<<<< HEAD
return total * 0.9; // your branch: 10% discount
=======
return total * 0.85; // incoming branch: 15% discount
>>>>>>> feature/pricing-update
}Reading the Markers
- <<<<<<< HEAD — everything below this, down to the =======, is what's currently on YOUR checked-out branch
- ======= — the dividing line between the two versions
- >>>>>>> feature/pricing-update — everything above this, up from the =======, is what's coming IN from the branch you're merging
- Your job: edit the file to keep the correct logic (one side, the other, or a manual blend) and delete all three marker lines
Step 3: Resolve and Complete
# After manually editing the file to the correct final state:
git add src/pricing.ts
# For a merge conflict
git commit
# For a rebase conflict, use --continue instead
git rebase --continueUsing a Merge Tool
For anything beyond a trivial one-line conflict, a visual merge tool (VS Code's built-in 3-way merge editor, or a dedicated tool like meld / kdiff3) beats reading raw markers in a plain editor.
# Configure VS Code as your merge tool once
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'
# Launch it on the current conflicts
git mergetoolBest Practices for Resolving Conflicts
- Pull/rebase frequently — small, frequent conflicts are far easier than one giant conflict after a two-week-old branch merges
- Never blindly 'accept incoming' or 'accept current' without reading both sides — you can silently delete a teammate's fix
- When unsure what the correct merged logic should be, ask the author of the other side rather than guessing
- After resolving, run the test suite before committing the merge — conflict resolution can compile fine while being logically wrong
- Use
git diffafter staging the resolved file, before committing, as a final sanity check of exactly what you're about to merge
Every developer breaks something in Git eventually. The difference between a five-second recovery and a bad afternoon is knowing exactly which undo command applies to your situation.
git restore — Unstage or Discard File Changes
# Unstage a file, keeping your edits in the working directory
git restore --staged src/config.ts
# Discard local edits entirely, back to the last commit — DESTRUCTIVE
git restore src/config.tsgit clean — Removing Untracked Files
# ALWAYS preview first — dry run, shows what would be deleted
git clean -n -fd
# Actually delete untracked files (-f) and directories (-d)
git clean -fdgit reset — --soft, --mixed, --hard
git reset moves your branch pointer (and HEAD) to a different commit. The three modes differ in how much they touch the staging area and working directory — this is the single most misunderstood command in Git, so here's exactly what each one does.
| Mode | Commit History | Staging Area | Working Directory |
|---|---|---|---|
| --soft | Moved to <commit> | Unchanged — old changes stay staged | Unchanged |
| --mixed (default) | Moved to <commit> | Reset — changes become unstaged | Unchanged |
| --hard | Moved to <commit> | Reset | Reset — all local edits are destroyed |
# Undo the last commit, but keep everything staged and ready to re-commit
# Scenario: you committed too early, want to add one more file to the SAME commit
git reset --soft HEAD~1
# Undo the last commit and unstage everything, but keep the edits in your files
# Scenario: you want to reorganize which files go into which commit
git reset --mixed HEAD~1 # --mixed is the default, so `git reset HEAD~1` also works
# Nuclear option: throw away the last commit AND all uncommitted changes
# Scenario: this branch is garbage, you want it to look exactly like an earlier commit
git reset --hard HEAD~1git revert — The Safe Undo for Shared Branches
Instead of rewriting history like reset, git revert creates a new commit that reverses the changes of a previous one. History stays intact and forward-moving — which is exactly why it's the only acceptable way to undo something on main or any branch other people are working from.
# Undo a specific commit by creating an inverse commit
git revert a1b2c3d
# Revert a merge commit — you must specify which parent is 'mainline'
git revert -m 1 a1b2c3d
# Revert multiple commits without committing each one individually
git revert --no-commit HEAD~3..HEAD
git commit -m "Revert last 3 commits — broke checkout flow"git reflog — The Ultimate Safety Net
Git almost never truly deletes anything immediately. Every time HEAD moves — commits, resets, checkouts, rebases, even branch deletions — it's logged in the reflog for about 90 days by default. If you think you've lost a commit or an entire branch, the reflog is where you find it.
# See the history of everywhere HEAD has pointed
git reflog
# Example output:
# a1b2c3d HEAD@{0}: reset: moving to HEAD~1
# e5f6g7h HEAD@{1}: commit: Add discount code field
# Recover from an accidental hard reset
git reset --hard HEAD@{1}
# Recover an entire branch you deleted by mistake
git branch recovered-branch e5f6g7hgit stash — Pause Work Without Committing
Scenario: you're mid-feature, working directory is a mess of half-finished changes, and a production hotfix lands on your desk. You can't commit half-finished work, and you don't want to lose it either. git stash shelves your changes and gives you a clean working directory instantly.
# Shelve everything (tracked, modified) with a label
git stash push -m "WIP: user profile validation"
# Now your working directory is clean — go handle the hotfix
git switch main
git switch -c hotfix/prod-crash
# ...fix, commit, push...
# Back to feature work — see what's stashed
git stash list
# stash@{0}: On feature/profiles: WIP: user profile validation
# Restore it and remove it from the stash list
git stash pop
# Restore it but KEEP it in the stash list too (rarely needed)
git stash apply
# Discard a stash you no longer need
git stash drop stash@{0}git cherry-pick — Move One Commit, Not a Whole Branch
Scenario: a critical bug fix landed as one commit deep inside a long-running feature branch that isn't ready to merge. You need that fix on main right now, without pulling in the rest of the unfinished feature.
git switch main
# Apply just that one commit's changes onto main as a new commit
git cherry-pick a1b2c3d
# Cherry-pick a range of commits
git cherry-pick a1b2c3d..e5f6g7h
# If it conflicts, resolve like a merge conflict, then:
git cherry-pick --continuegit bisect — Binary Search Your History for a Bug
Scenario: a bug exists today, you know it didn't exist a month and 300 commits ago, and no one remembers which change introduced it. Instead of manually checking out commits one by one, git bisect performs a binary search — it can find the culprit in ~9 steps out of 500 commits.
git bisect start
# Mark the current commit as broken
git bisect bad
# Mark a known-good commit further back (e.g. last month's release tag)
git bisect good v2.4.0
# Git checks out a commit halfway between good and bad — test it, then tell it:
git bisect good # if this commit works
# or
git bisect bad # if this commit is also broken
# Repeat — Git narrows the range each time — until it prints:
# a1b2c3d is the first bad commit
# Return to your original branch when done
git bisect resetgit tag — Marking Releases
# Lightweight tag — just a name pointing at a commit, no metadata
git tag v1.2.0
# Annotated tag — stores tagger, date, and a message; what you want for real releases
git tag -a v1.2.0 -m "Release 1.2.0: new checkout flow"
# Tags don't push automatically — push them explicitly
git push origin v1.2.0
# Push all local tags at once
git push origin --tagsgit submodule — Repos Inside Repos
A submodule links another Git repository into yours at a specific commit — common for sharing an internal component library across multiple projects without duplicating code.
# Add a submodule
git submodule add https://github.com/company/shared-ui-kit.git libs/ui-kit
# Cloning a repo that already has submodules? Don't forget --recurse-submodules
git clone --recurse-submodules https://github.com/company/main-app.git
# Pull the latest changes for existing submodules
git submodule update --remoteThe gap between fearing Git and commanding it isn't more memorization — it's knowing that every situation has a deliberate, correct tool. Messy working directory and an urgent hotfix? stash or worktree. Need one fix from a stalled branch? cherry-pick. Lost a commit? reflog. History has a bug hiding somewhere in 300 commits? bisect. A conflict isn't a crisis — it's just <<<<<<<, =======, and >>>>>>> asking you to make one decision at a time.
The Mental Model to Keep
- Never rebase or force-push public history — merge or revert instead
- Prefer --force-with-lease over --force, always
- git reflog means almost nothing in Git is ever truly, unrecoverably gone
- Small, focused commits make revert, bisect, and cherry-pick dramatically more useful later
- When in doubt, git status and git diff --staged before you commit, every time
Git rewards precision. Once these commands are muscle memory, you stop treating version control as a hazard to route around and start using it as what it actually is: the most powerful safety net and collaboration tool in your entire stack.