Let me be upfront about something: most Git tutorials are terrible.
They show you `git add`, `git commit`, `git push` - congratulations, you can now commit to a repo! - and then completely abandon you the moment things get complicated. Which they will. Fast.
I've watched junior devs freeze up during code reviews because they didn't know the difference between `revert` and `reset`. I've seen people nuke a shared branch with `--hard`. I've done some of those things myself.
So this isn't a cheat sheet. It's the explanation I wanted when I was learning - covering 17 commands with real context, real examples, and the "when do I actually use this?" answer that most posts skip entirely.
Before we dive in - a note on the order
I've deliberately ordered these by when you'd encounter them in a real project, not alphabetically. Config before init. Init before clone. That kind of thing. Makes more sense when you're learning.
Table of Contents
- git config
- git init
- git clone
- git remote
- git branch
- git checkout / git switch
- git status
- git diff
- git add
- git commit
- git push
- git fetch
- git pull
- git merge
- git rebase
- git revert
- git reset
- git stash
- git log
- Quick Decision Guides
- Full Workflow Example
- Mistakes I See All the Time
- FAQ
- Cheat Sheet
1. git config
Nobody talks about this one enough. It's boring, it's setup, it feels like homework - and yet skipping it means every commit you make has the wrong name attached to it forever.
`git config` sets your identity and preferences. Git stamps your name and email onto every single commit you make. It's permanent. Get it right first.
# Run these two before you do literally anything else
git config --global user.name "Your Name"
git config --global user.email "[email protected]"
# Tell Git to use VS Code for anything that needs an editor
git config --global core.editor "code --wait"
# I always set this - saves an annoying warning on every new repo
git config --global init.defaultBranch main
# Coloured output in the terminal (why is this not the default?)
git config --global color.ui auto
# See everything currently configured
git config --list
# Check a specific value
git config user.email
The `--global` vs `--local` thing: There are three scope levels and they nest. `--system` applies to every user on the machine and lives in `/etc/gitconfig`. `--global` applies to every repo under your user account and lives in `~/.gitconfig`. `--local` applies only to the current repository, stored in `.git/config`, and overrides everything above it.
That last one is useful when you contribute to open source with a personal email but need your work email on company repos:
# Inside a work project:
git config --local user.email "[email protected]"
2. git init
One command, one job: turns any folder into a Git repository.
cd my-project
git init
# Or skip the cd - create the folder and init at once
git init my-new-project
Git creates a hidden `.git` folder in your project root. That folder *is* the repository - the full history, all branches, all config. If you delete it, you lose everything. Don't delete it.
`git init` vs `git clone` - people sometimes confuse these. Use `git init` when you're starting a brand-new project from scratch and there's no existing remote repo to copy. Use `git clone` when the project already exists somewhere and you need a local copy. The other practical difference: after `git init` you have an empty repo with no remote configured - you'll need to add that yourself with `git remote add`. After `git clone`, the remote is already set up and pointing back to where you cloned from.
3. git clone
This is how you get an existing project onto your machine. It copies everything - files, full history, all branches.
# The everyday version
git clone https://github.com/facebook/react.git
# Clone into a different folder name
git clone https://github.com/facebook/react.git my-react-copy
# Shallow clone - only the latest snapshot, no history
# Much faster for large repos when you just need the code
git clone --depth 1 https://github.com/facebook/react.git
# Clone a specific branch and nothing else
git clone --branch develop --single-branch https://github.com/user/repo.git
# SSH clone (needs key setup, but preferred once you've done it)
git clone [email protected]:facebook/react.git
`--depth 1` is underused. If you're cloning something big just to run it or read the code, skip the 10-year commit history. Clones in seconds instead of minutes.
4. git remote
A "remote" is just a named shortcut to a URL. When you clone a repo, Git automatically creates one called `origin` pointing back to where you cloned from.
# See what remotes you have configured
git remote -v
# Add a remote (what you'd do after git init + creating a GitHub repo)
git remote add origin https://github.com/username/my-project.git
# Update the URL - e.g. after renaming the repo on GitHub
git remote set-url origin https://github.com/username/new-name.git
# Rename a remote
git remote rename origin old-origin
# Remove one you don't need
git remote remove upstream
The fork workflow - if you're contributing to open source, you'll typically have two remotes. `origin` points to your personal fork - this is where you push. `upstream` points to the original project - this is where you pull updates from.
git remote add upstream https://github.com/original-owner/original-repo.git
# Sync your fork with the original project
git fetch upstream
git merge upstream/main
5. git branch
Branches are one of Git's best features. They let you work on something in isolation without touching the main codebase - which means you can experiment freely, mess things up, and throw the branch away if needed.
git branch # List local branches (* = where you are)
git branch -a # List local + remote-tracking branches
git branch feature/login # Create a new branch
git branch -d feature/login # Delete (only if merged - safe)
git branch -D feature/login # Force-delete regardless
git branch -m main # Rename current branch to 'main'
Good branch names make PRs and `git log` dramatically easier to follow. The pattern most teams use: a type prefix, a slash, then a short description. Something like `feature/add-login-page`, `fix/navbar-overflow`, `hotfix/payment-null-error`, `release/v2.1.0`, or `chore/update-dependencies`. Simple, readable, instantly communicates what the branch is for.
6. git checkout / git switch
`git checkout` is one of those commands that does too many things. Switching branches, creating branches, restoring files - all the same command. Git 2.23 finally split this into `git switch` (branches) and `git restore` (files), which is much clearer.
Both still work. You'll see `checkout` everywhere in older docs and Stack Overflow answers, so know both:
# Switch to an existing branch
git checkout main
git switch main # same thing, clearer
# Create AND switch to a new branch
git checkout -b feature/shopping-cart
git switch -c feature/shopping-cart # same thing, clearer
# Discard all unsaved changes to a file
git checkout -- src/Header.js
git restore src/Header.js # same thing, clearer
# Restore a file to how it looked at a specific commit
git checkout a3f8c21 -- config/settings.py
One thing to watch out for: `git checkout
7. git status
Run this constantly. Seriously. Before `git add`. Before `git commit`. Whenever you're not sure what's happening. It costs nothing and has saved me from embarrassing commits more times than I can count.
git status # Full output
git status -s # Short format - more compact
What the output looks like:
$ git status
On branch feature/user-auth
Your branch is up to date with 'origin/feature/user-auth'.
Changes to be committed: ← staged, going into next commit
modified: src/auth/login.js
Changes not staged for commit: ← modified, but not staged yet
modified: src/auth/register.js
Untracked files: ← Git doesn't know about these yet
src/auth/forgot-password.js
Short format once you're familiar with it:
$ git status -s
M src/auth/login.js # green M = staged
M src/auth/register.js # red M = unstaged
?? src/auth/forgot-password.js # untracked
8. git diff
This shows you exactly what changed - line by line. It's how you review your own work before committing.
git diff # Unstaged changes vs last commit
git diff --staged # Staged changes vs last commit
git diff main feature/user-auth # Two branches against each other
git diff a3f8c21 b4e9d32 # Two commits against each other
git diff --name-only main # Just the filenames, no line diffs
Reading the output:
- return email.includes('@'); ← removed
+ const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; ← added
+ return regex.test(email); ← added
`--staged` is the one I use most - it shows exactly what's about to go into the next commit, so I can catch any stray `console.log` statements or half-finished code before it lands in the history.
9. git add
Staging. People find this confusing at first - why isn't commit enough? The staging area exists so you can carefully control what goes into each commit, even if you've made a bunch of unrelated changes at once.
git add src/auth/login.js # Stage one file
git add src/components/ # Stage everything in a folder
git add . # Stage everything (careful)
git add -p # Interactive - stage chunk by chunk
git add -A # Stage all changes including deletions
`git add -p` is worth learning properly. It shows each changed chunk and asks what to do. `y` stages it, `n` skips it, `s` splits it into smaller chunks, `e` lets you edit the chunk manually. It's slow at first but becomes second nature. Makes your commit history way cleaner when you're not dumping every change from the last two hours into one giant commit.
One warning about `git add .` - it stages everything, including files you didn't mean to touch. Always check `git status` first, and make sure your `.gitignore` is solid.
10. git commit
The actual save point. Every commit is a permanent snapshot you can always get back to.
git commit -m "Your message" # Standard
git commit # Opens editor for longer message
git commit -am "Your message" # Stage tracked files + commit together
git commit --amend # Fix the last commit
git commit --amend --no-edit # Add a forgotten file, keep same message
The `-am` shortcut only works for files Git is already tracking. New files still need `git add` first.
# Forgot to include a file? No need for a whole new commit
git add forgotten-file.js
git commit --amend --no-edit
# Typo in your last commit message? Fix it before pushing
git commit --amend -m "The correct message"
On commit messages - this matters more than most people realise early on. "fix bug" is useless six months from now. The Conventional Commits format is worth adopting:
feat(auth): add password reset flow via email token
Token expires after 1 hour. Uses existing mailer service.
Invalidated on use. Closes #142.
The format is `type(scope): short summary`, with an optional body explaining the *why*. Common types: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `perf`.
One important thing: `git commit` saves locally only. Nothing goes to the remote until you push.
11. git push
Sends your local commits up to the remote repository. This is how your work becomes visible to the rest of the team.
git push origin feature/user-auth # Push a specific branch
git push -u origin feature/user-auth # Push + set tracking (first push of a new branch)
git push # After tracking is set, just this
git push --all origin # Push all branches
git push origin --delete old-branch # Delete a remote branch
git push --force-with-lease # Safer force push
On `--force-with-lease` vs `--force`: `--force` overwrites whatever is on the remote, including work teammates have pushed since your last fetch. `--force-with-lease` first checks if anyone else pushed - if they did, it refuses. Always use `--force-with-lease` when you have to force push after a rebase or amend.
12. git fetch
Downloads everything that's changed on the remote - new commits, new branches, updates - but doesn't touch your working files. Nothing changes locally until you explicitly merge or rebase.
git fetch # Fetch from origin
git fetch --all # Fetch from all remotes
git fetch --prune # Also remove stale remote-tracking branches
The cautious workflow:
git fetch origin
git log HEAD..origin/main --oneline # See what changed before touching anything
git merge origin/main # Now integrate it
Use fetch over pull whenever you want to see what changed before committing to integrating it. Especially good first thing in the morning before you start working.
13. git pull
`git pull` is `git fetch` + `git merge` in one step. Grabs the remote changes and applies them to your current branch immediately.
git pull # Fetch + merge from tracked branch
git pull origin main # Pull from a specific remote/branch
git pull --rebase # Fetch + rebase instead of merge
`--rebase` keeps your history cleaner - your commits get replayed on top of the updated remote branch rather than creating a merge commit. Worth setting as the default:
git config --global pull.rebase true
Fetch vs pull in plain terms: fetch downloads but doesn't apply, so you can review what changed first. Pull downloads and applies immediately with no preview. Fetch is safer for important integrations; pull is fine for quick day-to-day syncing. The risk with pull is landing in a merge conflict you weren't expecting, whereas fetch lets you mentally prepare.
14. git merge
Integrates one branch into another. The most common use: you've finished a feature branch and want to bring it into `main`.
git checkout main
git merge feature/user-auth # Standard merge
git merge --no-ff feature/user-auth # Always create a merge commit
git merge --squash feature/user-auth # Squash all commits into one
git merge --abort # Bail out if there are conflicts
`--no-ff` is worth knowing. By default, if your feature branch is just ahead of main with no diverging commits, Git will "fast-forward" - move the pointer forward without creating a merge commit. That makes the branch look like it never existed in the history. `--no-ff` forces a merge commit, which preserves the fact that this was a separate line of work. Most teams prefer this.
Handling merge conflicts - when Git can't auto-merge, it pauses and adds conflict markers in the affected files:
<<<<<<< HEAD
const API_URL = 'https://api.staging.example.com';
=======
const API_URL = 'https://api.production.example.com';
>>>>>>> feature/env-config
Delete the markers, keep the correct code, then `git add
15. git rebase
Rebase replays your commits on top of another branch's tip. The result looks like you started your feature branch from the latest commit on main - even if you branched off weeks ago and main has moved on significantly.
git checkout feature/user-auth
git rebase main # Replay your commits on top of latest main
git rebase -i HEAD~4 # Interactive: clean up the last 4 commits
git rebase --continue # After resolving a conflict mid-rebase
git rebase --abort # Something went wrong, start over
Interactive rebase (`-i`) is where it gets powerful. You can squash ten WIP commits into one clean commit, reword messages, reorder commits, drop ones you don't want. Really useful before opening a PR so reviewers see a clean story rather than "WIP", "fix typo", "fix typo 2", "ok actually fix typo".
Merge vs rebase - the real answer:
Use `git merge --no-ff` when you're bringing a finished feature into `main`. Use `git rebase main` when you want to update your own feature branch with the latest changes from `main`. Use `git rebase -i` when you want to clean up messy commits before a PR. And if other people are actively working on the same branch as you - always merge, never rebase.
> The golden rule: never rebase commits that have already been pushed to a shared branch. Rebase rewrites history. If teammates have those commits, their local repos will diverge and things get painful fast.
16. git revert
Creates a new commit that undoes the changes from a previous commit. The key thing: it doesn't touch existing history - it just adds a new "undo" commit on top. That's what makes it safe to use on shared branches.
git log --oneline # Find the commit hash you want to undo
git revert a3f8c21 # Undo that commit (creates a new commit)
git revert HEAD # Undo the most recent commit
git revert --no-commit a3f8c21 # Stage the revert without committing yet
# Reverting a merge commit (-m 1 = parent 1 is the mainline)
git revert -m 1 <merge-commit-hash>
Revert vs reset: `git revert` adds a new "undo" commit without touching history, so it's safe on shared branches. `git reset` moves the branch pointer backwards, rewriting history - only safe for commits that haven't been pushed anywhere. If it's been pushed, always revert.
Think of it this way: `git revert` is like a newspaper printing a correction the next day. `git reset` is like going back and deleting the original article - fine if nobody read it yet, catastrophic if they did.
17. git reset
Moves your branch pointer to a previous commit. Unlike revert, it rewrites history - so it's only safe for commits that haven't been pushed anywhere yet.
git reset --soft HEAD~1 # Undo last commit, changes stay staged
git reset HEAD~1 # Undo last commit, changes stay in working dir
git reset --hard HEAD~1 # Undo last commit AND discard all changes - gone
git reset src/auth/login.js # Unstage a specific file
The three modes on a spectrum from careful to destructive:
- `--soft` - undoes the commit, keeps your changes staged and ready to recommit
- `--mixed` (default) - undoes the commit, puts changes back in the working directory unstaged
- `--hard` - undoes the commit and permanently deletes the changes. There is no undo for this.
# Undo the last 2 commits but keep all the code changes in the working directory
git reset HEAD~2
# Throw away everything and exactly match the remote
git reset --hard origin/main
> ⚠️ `git reset --hard` permanently deletes uncommitted changes. Double-check what you're resetting before you run it, and when in doubt use `git revert` instead.
18. git stash
Stash is like a temporary drawer for work you're not ready to commit. You're mid-feature, something urgent comes in, you can't commit half-finished code - stash saves your changes and gives you a clean working directory.
git stash # Stash current changes
git stash push -m "WIP: checkout form" # Stash with a label (always do this)
git stash list # See everything stashed
git stash pop # Apply most recent stash + remove it
git stash apply stash@{1} # Apply a specific one (keeps it in the list)
git stash drop stash@{1} # Delete a specific stash
git stash clear # Nuke all stashes
git stash push --include-untracked # Include new (untracked) files too
The workflow that comes up constantly:
# Mid-feature. Urgent bug reported.
git stash push -m "WIP: user profile redesign"
git switch main
git pull
git switch -c hotfix/payment-null-pointer
# Fix, commit, push
git commit -am "fix: handle null payment method"
git push -u origin hotfix/payment-null-pointer
# Back to your feature, right where you left off
git switch feature/user-profile
git stash pop
Always use `-m` with a description. "stash@{3}: WIP on main" is useless when you're staring at a list of five entries two days later.
19. git log
Your window into the project's history. Who changed what, when, and why. Also your best friend when you're hunting down the commit that introduced a bug.
git log # Full history (q to exit)
git log --oneline # One line per commit
git log --oneline --graph --all # Branch graph - use this one a lot
git log -n 5 # Last 5 commits only
git log --author="Jane" # Filter by author
git log --since="2 weeks ago" # Filter by date
git log --grep="payment" # Search commit messages
git log -- src/auth/login.js # History of a specific file
git log --stat # Show which files changed per commit
git log -S "validateEmail" # Find when a function was introduced
Set up this alias once and use it forever:
git config --global alias.lg "log --oneline --graph --all --decorate --color"
# Then just:
git lg
Quick Decision Guides
These three questions come up constantly. Bookmark this section.
Merge or rebase?
Is anyone else working on this branch?
├── YES → git merge. Never rebase shared branches.
└── NO → What are you trying to do?
├── Update feature branch with latest main → git rebase main
├── Merge finished feature into main → git merge --no-ff
└── Clean up messy commits before a PR → git rebase -i
Revert or reset?
Are the commits already pushed to a shared branch?
├── YES → git revert. Adds an undo commit, safe for everyone.
└── NO → Do you want to keep the code changes?
├── YES → git reset --soft (staged) or git reset (unstaged)
└── NO → git reset --hard (permanent - be sure)
Fetch or pull?
Do you want to see what changed before integrating it?
├── YES → git fetch → inspect → git merge or git rebase
└── NO → git pull (or git pull --rebase for cleaner history)
Full Workflow: Putting It All Together
Here's a realistic feature-to-production workflow using everything above.
# First time setup (once per machine)
git config --global user.name "Your Name"
git config --global user.email "[email protected]"
# Get the project
git clone https://github.com/team/project.git
cd project
# Always start from the latest main
git switch main
git pull
# Create your feature branch
git switch -c feature/user-profile
# ... write code ...
# Check your work constantly
git status
git diff
# Stage in logical chunks, not all at once
git add src/profile/
git commit -m "feat(profile): add avatar upload component"
git add src/api/
git commit -m "feat(api): add profile update endpoint"
# Before pushing, update with anything that landed on main while you worked
git fetch origin
git rebase origin/main
# Push the branch
git push -u origin feature/user-profile
# Oops - forgot a file in the last commit
git add src/profile/AvatarUpload.test.js
git commit --amend --no-edit
git push --force-with-lease
# Urgent bug report comes in while you're still working
git stash push -m "WIP: adding profile bio field"
git switch main && git pull
git switch -c hotfix/login-redirect
git commit -am "fix: correct post-login redirect URL"
git push -u origin hotfix/login-redirect
# Back to the feature
git switch feature/user-profile
git stash pop
# PR merged - but it introduced a bug. Revert it safely.
git log --oneline
git revert a3f8c21
# Clean up
git branch -d feature/user-profile
git push origin --delete feature/user-profile
Mistakes I See All the Time
Committing directly to `main` - always branch. Direct commits bypass review and can break deployments. Protect `main` in your repo settings.
Commit messages like "fix" or "update stuff" - your commit message is documentation. Write it for the person debugging this six months from now. Which might be you.
`git reset --hard` on pushed commits - once something's pushed, it's on a shared branch. Use `git revert`. Using reset here forces everyone else to deal with a diverged history, and they will not be happy.
Force pushing to `main` or `develop` - just don't. Protect these branches and use revert if something needs undoing.
Skipping `.gitignore` - set it up before your first commit. `node_modules`, `.env`, build folders, OS files - none of these should ever be committed. GitHub has [good templates](https://github.com/github/gitignore) for most project types.
Giant commits that do 10 things - "add login, fix navbar, update deps, refactor utils, fix typo" in one commit is a nightmare to review and impossible to revert selectively. Commit one logical thing at a time.
FAQ
What does HEAD mean?
HEAD points to your current position - usually the tip of your current branch. `HEAD~1` is one commit back. `HEAD~3` is three back.
What is `origin`?
Just a name. When you clone a repo, Git calls the source remote `origin` by convention. It's an alias for the URL. You can rename it or add more remotes with any names you want.
How do I undo a `git add` before committing?
`git restore --staged
`git pull` vs `git fetch` - the short answer?
Fetch downloads, doesn't apply. Pull downloads and applies immediately. When in doubt, fetch first and review.
`git reset` vs `git revert` - really quickly?
Reset rewrites history (local only). Revert adds a new undo commit (safe for shared branches). If it's been pushed, always revert.
When should I rebase instead of merge?
Updating your own feature branch with main's latest → rebase. Merging a finished feature into main → merge. Anything on a branch other people are using → always merge, never rebase.
How do I see what changed between two branches?
`git diff branch1..branch2` for line-by-line diffs. `git log branch1..branch2 --oneline` to see which commits are in branch2 but not branch1.
Cheat Sheet
git config --global user.name "Name" # Set your identity
git init # Start a new repo
git clone <url> # Copy a remote repo locally
git remote add origin <url> # Link to a remote
git branch <name> # Create a branch
git switch -c <name> # Create + switch to a branch
git status # See what's changed
git diff # Line-by-line changes
git diff --staged # What's about to be committed
git add -p # Stage interactively
git add . # Stage everything
git commit -m "msg" # Save a snapshot
git commit --amend # Fix the last commit
git push -u origin <branch> # Upload + set tracking
git fetch # Download remote changes (don't apply)
git pull --rebase # Download + rebase
git merge --no-ff <branch> # Merge with a merge commit
git rebase -i HEAD~n # Interactive rebase
git revert <hash> # Safe undo (adds new commit)
git reset --soft HEAD~1 # Undo commit, keep staged
git reset --hard HEAD~1 # Undo commit + discard changes
git stash push -m "desc" # Shelve current changes
git stash pop # Re-apply stash
git log --oneline --graph --all # Visual history
That's the lot. Honestly, you won't use all of these every day - most days are just `status`, `add`, `commit`, `push`, `pull`. But knowing the rest means you're not stuck when things get complicated. And they will get complicated.
Drop any questions in the comments. Happy to dig into anything here further.