🔀 Command Comparisons
The commands that look similar but do very different things — explained side by side.
clone vs
fetch vs
pull
Getting code
All three bring code from a remote server to your machine — but at completely different stages of the journey.
origin) automatically.origin/main). Your working files are not touched at all.git fetch followed immediately by git merge (or rebase). Downloads new commits and updates your current branch.git fetch + review over git pull when you're on a feature branch. git pull auto-merges — which can create an unexpected merge commit. Fetch first, then consciously choose to merge or rebase.In practice
Day 1 — you just joined the team: Use git clone. It downloads everything and sets up the folder. You never clone again on this machine.
Day 15 — you're mid-feature and a teammate tells you they pushed a fix you need: Use git fetch origin then git rebase origin/main. Fetch checks what arrived; rebase pulls it under your work without creating a stray merge commit.
Monday morning — you just want the latest before starting the day: On main, git pull is fine. It's your own branch, fast-forward only, no surprise merge commits.
# Day 1 — clone once
git clone https://gitlab.company.com/team/MyApp.git
# Mid-feature — check what's new without touching your files
git fetch origin
git log origin/main --oneline -5 # see the incoming commits
git rebase origin/main # apply when ready
# Morning catch-up on main
git switch main
git pull
merge vs
rebase
Combining branches
Both bring commits from one branch into another. The difference is how they do it — and what the history looks like afterwards.
main via an MR. The merge commit is a clear record of when the feature landed.main while you work. Keeps history clean and linear.| merge | rebase | |
|---|---|---|
| History shape | Non-linear, shows branches | Linear, as if no branching happened |
| Merge commit | Yes | No |
| Safe on shared branches? | Yes | No — rewrites commit hashes |
| Conflict handling | One conflict resolution session | May pause once per replayed commit |
| Common use | MR / PR final merge into main | Keeping feature branch in sync |
In practice
You're building a login feature. While you were working, Alice pushed 3 commits to main that include a shared config change you need.
Use rebase to pull Alice's changes under yours: git fetch origin then git rebase origin/main. Your feature commits land on top of Alice's — clean, linear history. When your MR is reviewed, the diff is only your changes.
Your feature MR is approved and ready to land in main.
GitLab does a merge — it creates a merge commit that says "feature/login merged into main at 14:32". The branch history is preserved, and it's clear exactly when and what landed.
The rule of thumb: rebase to sync, merge to land.
reset vs
revert vs
restore
Undoing things
Three commands with "undo" in their DNA — but they operate at completely different levels.
git reset has three modes — all of them move HEAD backwards, but differ in what happens to the files:
| Flag | Commits | Staging area | Working files | Use for |
|---|---|---|---|---|
--soft | Removed | Changes kept staged | Unchanged | Re-commit with a better message / split into multiple commits |
--mixed (default) | Removed | Changes unstaged | Unchanged | Re-stage selectively before re-committing |
--hard | Removed | Cleared | Deleted | Completely throw away work — destructive, no undo |
git reset --hard permanently deletes your uncommitted changes. There is no undo for this. For commits already pushed to a shared branch, always use git revert instead.In practice
You typed a commit message with a typo and immediately regret it — not pushed yet:
git commit --amend -m "Fix: correct message" — rewrites just the last commit message. Or git reset --soft HEAD~1 to fully undo the commit and re-stage everything.
You accidentally committed your .env file — not pushed yet:
git reset HEAD~1 (mixed, the default) — puts the commit back as staged changes. Remove the file, update .gitignore, then re-commit.
You pushed a broken commit to main an hour ago and other people have already pulled it:
git revert a1b2c3d then git push — creates a new commit that undoes the damage. Nobody's history breaks. This is the only safe option once others have the commit.
You edited a file and want to throw away all your changes before committing:
git restore src/config.js — resets the file to the last committed state. No commits involved at all.
switch vs
checkout
Branch switching
git checkout used to do everything — switch branches, restore files, detach HEAD. It was confusing. In 2019, Git split it into two focused commands.
| Task | Old way (checkout) | New way (switch / restore) |
|---|---|---|
| Switch to a branch | git checkout main | git switch main |
| Create and switch to a new branch | git checkout -b feature/x | git switch -c feature/x |
| Discard changes to a file | git checkout -- file.js | git restore file.js |
| Check out a specific commit (detached HEAD) | git checkout a1b2c3d | git switch --detach a1b2c3d |
git switch and git restore for new work. When reading old docs or Stack Overflow answers, you'll see git checkout — it still works, just mentally map it to the newer commands.In practice
You find a Stack Overflow answer from 2018 that says git checkout -b feature/x — does it still work?
Yes. The old command still works. The modern equivalent is git switch -c feature/x. Both do exactly the same thing. Use switch going forward so your brain isn't confused between "switch branch" and "restore file" — two very different operations that checkout used to handle with almost identical syntax.
The most common mistake with old checkout: people would run git checkout filename.js expecting to switch to a branch, but instead silently discarded all their unsaved changes in that file. git switch refuses to accept a filename — it only takes branch names — which makes this mistake impossible.
push vs
push --force vs
push --force-with-lease
Pushing
Push sends your commits to GitLab. Force push is needed after rewriting history — but the type of force push matters a lot.
--force-with-lease instead.--force, but first checks that nobody else has pushed to the branch since you last fetched. If they have, Git stops and warns you instead of overwriting their work.main, develop, or any branch others are working on. Force push is only appropriate on your own feature branch, after a rebase, before others have pulled it.In practice
You ran git rebase origin/main on your feature branch and now git push is rejected:
Rebase rewrites commit hashes, so your local history no longer matches what's on GitLab. GitLab rejects the push to protect you. This is the one situation where force-push is correct — run git push --force-with-lease origin feature/my-branch. The --force-with-lease flag checks nobody else pushed to your branch while you were rebasing.
Why not just use --force?
Imagine you rebased, then your teammate (not knowing you were rebasing) pushed one commit to your branch. git push --force silently deletes their commit — they lose their work with no warning. git push --force-with-lease detects this and stops, letting you fetch and reconcile first.
# Normal daily push
git push
# First push of a new branch (sets up tracking)
git push -u origin feature/my-branch
# After a rebase — use force-with-lease, never --force
git push --force-with-lease origin feature/my-branch