🌱 Introduction
Most devs say they know Git because they can push projects to github.
I’ve seen the opposite.
They know just ENOUGH git to break projects confidently.
A git add . here, a quick commit there… trusting AI agents and suddenly the project turns into something nobody wants to touch.
I’ve done that too, early on.
The difference is that you eventually get tired of fixing avoidable problems.
That’s when Git stops being a checklist of commands and starts becoming a way of working.
Most are trapped in so-called AI agents to clean their messes.
But their granny AI agent is confused too because their Git history looks like a clown show.
These 12 git commands I actually rely on when things get messy or when I want to keep things clean from the start.
Nothing fancy. Just practical stuff that saves time.
📘 Before we start (optional but important)
Git is a tool. it handles history. Clean Code handles everything inside that history. Developers don’t fail because of Git. They fail because the code itself becomes way too HARD to manage. If you fix that part early, Git becomes your oblige coworker.
use code: POWER40 40% OFF for one week.
Now put your seat belt on. Grab a black coffee and let's get serious 🚗
🎂 1) Rebasing
AI code reviewers and merge queues DO NOT babysit premature branches.
They expect discipline.
most so called git expert stack commits like this:
fix
fix again
testing
oops
final-final-v3
Then they open a PR (pull request) and hope it works.
Unfortunately it doesn’t. Machine has no emotion.
🍄🟫 What rebase actually is
Look at the image.
Bottom \= old commits
Top \= latest main
Dog \= your feature branch
Right now your branch is behind.
The dog is sitting somewhere in the middle.
But he wants the top.
So what does he do?
He cLimbs.
Step by step.
And then he sits on top of the latest main.
That’s rebase.
You don’t build sideways.
You don’t stay behind.
You move your work to the top.
Clean. Straight. Done.
🤔 You ask why?
Look, modern teams use AI merge queues, automated reviewers, async PR pipelines.
These systems are NOT your mother.
THey don’t review your failure.
AI pipelines don’t fix your alienated history.
They see this:
random commits
unclear changes
outdated branch
🙅♂️ And they reject it.
Not because you’re bad.
Because your git history is turmoil. 🧐🌡️
WitH rebase you fix everything before PR.
Now merge is simple.
🍨 Rebase workflow
This is what you actually do:
```
git switch feature-login
git fetch origin
git rebase origin/main
git add .
git rebase --continue
git push --force-with-lease
```
Enter fullscreen mode
Exit fullscreen mode
🏗️ Code demonstrations for beginners:
1) 🧗♂️ Switch to your feature branch \= git switch feature-login
You move into your own work area.
2) ⛅ Get latest updates from remote \= git fetch origin
You look at what changed on the server using fetch.
3) 🛟 Rebuild your work on top of latest main \= git rebase origin/main
This is the core moment.
Git takes your commits and replays them one by one on top of the newest main.
So instead of:
messy branch history
outdated base
You now get:
clean linear history
same work, new foundation
If conflicts happen, Git stops here.
4) 🚤 Fix conflicts (if any)
git add .
git rebase --continue
Enter fullscreen mode
Exit fullscreen mode
This means:
you fix the files
you tell Git: “okay continue”
Git keeps replaying your commits until everything is applied.
5) Push updated history \= git push --force-with-lease
Important part:
You are not just pushing code.
You are rewriting history.
So Git says:
“Only push if nobody else changed this branch while you were working.”
That’s what --force-with-lease protects you from.
It checks if someone else updated the branch.
If yes, it stops you.
So you don’t destroy their work.
again, rebase changes history.
That’s why you don’t just --force push blindly.
🪦 2) Reset, Revert, Restore
Mistakes will happen.
That’s normal.
Panicking is optional.
Some developers break things… then freeze.
Others fix it in 10 seconds and move on.
The difference is knowing which command to use.
Git gives you three weapons to reverse damage:
git revert HEAD
git reset --hard HEAD~1
git restore --staged app.js
git restore app.js
Enter fullscreen mode
Exit fullscreen mode
[ code demonstration is below ]
1) 🦺 git revert \| safe undo
You don’t delete anything.
git revert HEAD \= Makes a new commit that cancels the last one.
History stays clean.
Nothing is hidden.
This is what serious teams want.
2) 🗑️ git reset \| delete it completely
This one is different.
It moves your branch back.
The commit is gone.
Like it never happened.
git reset --hard HEAD~1 \= Deletes the last commit and your changes.
Fast. Clean. But dangerous.
If someone already pulled that commit… you just created a big mess.
Use it when you’re alone on the branch.
3) 🗃️ git restore \| fix files only
This doesn’t touch history.
It just fixes your working files.
git restore --staged app.js \= Removes app.js from staging (undo git add). Your edits stay untouched.
git restore app.js \= Deletes your changes. File goes back to last commit.
🧠 Concretely:
revert → undo safely
reset → erase history
restore → fix files
Three different jobs.
Where people mess up?
They use reset when they should use revert.
Now history is broken.
Team is confused.
Time is wasted.
🦐 2026 RRR Best Practices
git switch and git restore replaced the overloaded confusing git checkout. This isn’t optional anymore.
Modern teams would love to see you using them.
Old messy git checkout command did everything:
Switch branches. Restore files. Confuse everyone.
Now it’s split into two:
git switch: move between branches
git restore: fix files / undo file changes
ITs much cleaner and accurate.
That’s why you will never see me use checkout in this article.
It’s outdated.
Just like a serious dev who refuses to shallow AI hypes.
I will show you EXACTLY how they work in the next section (#3).
🫰3) Switch \& Restore
```
git switch main
git switch -c feature-payment
git restore README.md
git restore --staged utils.js
```
Enter fullscreen mode
Exit fullscreen mode
👉 git switch main
with that, you leave your current branch and return to mission control.
Before:
You’re on: bugfix/login-issue
You’ve got random work-in-progress edits.
After running:
git switch main
Enter fullscreen mode
Exit fullscreen mode
You are now on the main branch.
Your files instantly match the main branch’s state.
✍️ NOTE: Uncommitted edits may follow, unless switching would create conflicts, in which case Git stops you to avoid damage. That's protection.
either way, you’ve returned to home base. You’re back to clean ground..
👉 git switch -c feature-payment
Result:
A new branch is created which is feature-payment
And you immediately enter that branch.
Now your payment feature lives in isolation.
No risk of breaking main.
No overlap with other features.
🪜 One branch per mission.
👉 git restore --staged utils.js
This is your emergency fix when you accidentally run:
git add .
and stage files you didn’t want to include.
Fix it:
git restore --staged utils.js
Enter fullscreen mode
Exit fullscreen mode
Result:
utils.js is unstaged which is removed from the commit list.
But your edits remain safely in your working directory.
which means, you fix the mistake without losing a single line.
🐾 4) --amend Commits [Fix Without Noise]
You forgot a file?
You wrote a typo?
You missed a small edit?
You --amend.
🧱 What “--amend” actually DOES?
Amend means:
“Take my LAST commit and update it as if the mistake NEVER existed.”
It does NOT create a new commit. It replaces the previous one.
But most commits like this:
“fix again”
“final fix”
“real final fix”
Now history looks like garbage.
No worries.. you can time travel and --amend.
Real example (this happens all the time)
You accidentally committed .env
Sensitive keys. Big mistake.
You don’t say:
“oops let me fix it in next commit”
No.
You clean it properly:
git rm --cached .env
echo ".env" >> .gitignore
git commit --amend --no-edit
Enter fullscreen mode
Exit fullscreen mode
Done.
That commit never had .env.
⏹️ You remove the sensitive file
and rewrite the last commit like it never existed.
Let me explain..
git rm --cached .env 👉 This removes .env from Git tracking (not your computer)
Then:
echo ".env" >> .gitignore 👉 So you don’t repeat the same mistake
Now your changes are staged.
THEN you run:
git commit --amend --no-edit
🫡 This rewrites the last commit
👉 Same message
🦵 But WITHOUT the .env file
Why --no-edit?
Because you don’t want to waste time rewriting the message.
You’re saying:
“Same commit message. Just fix the mistake.”
🤔 What if you WANT to edit?
Then DO NOT use --no-edit.
Just do:
git commit --amend
Now Git opens the editor.
You can fix the message, explain what changed and make it more professional
Now let me illustrate an everyday use case scenario:
git commit --amend
git rebase -i HEAD~5
--amend → fix your last mistake instantly
rebase -i → squash, reorder, clean commits like a surgeon
Enter fullscreen mode
Exit fullscreen mode
This is how you turn:
“fixed stuff lol”
into [for example]:
“Refactor auth flow + fix token expiry bug”
Now your history makes sense.
⚠️ WARNING (read twice):
Only --amend local commits.
Never --amend commits already pushed to a team branch.
Why?
Because you rewrite history.
Automated pipelines, merge queues and AI agents HATE rewritten history.
🥍 Push amended commits ONLY if you're 100% sure NO ONE else has pulled them.
🏫 5) Git Log / Learn From History
Git doesn’t lie.
Your code can lie. Your memory can lie. Your teammates can lie.
nowadays devs zombie scroll through Git logs.
They don’t understand the POWER hidden in "history".
Good history \= easy debugging
Bad history \= endless suffering
⚖️ The 50/72 rule
Follow 50/72 Rule for efficient bug tracing AND debugging.
keep titles under 50 characters and wrap descriptions at 72.
write messages like "Add feature" instead of "Added feature".
and do not mix code refactors with new features in the same commit.
Bad example:
commit 1: "fixed stuff"
commit 2: "final update
commit 3: "changes lol.."
Good example:
commit 1: "fix(auth): resolve token expiry issue"
Now you know what happened without opening code.
Basic survival commands
git log --oneline
git log --oneline --graph --decorate
git log --oneline --author="Shahan"
git log --oneline -- path/to/file
What they really mean:
--oneline \= fast history scan + short, readable commits
--graph \= see branch chaos visually
--graph --decorate → visualize merges \& branches
--author \= see who broke what
-- path/to/file \= see only a single file’s history
This is how you stop guessing.
When things get serious you type
git log -p
git log -S "functionName"
git blame file.js
git reflog
Now you’re not reading history.
You’re hunting problems.
-p shows exact code changes
-S finds when a bug was introduced
git blame shows who touched each line
git reflog brings back “lost” commits
6) 🪝 Git Hooks to Automate Discipline
Junior devs hope code is fine.
Serious devs don’t hope anything.
They build systems that refuse bad code.
That’s what Git hooks are.
Hooks are simple:
They run automatically when you commit, push, or merge.
Without asking. Without reminders.
If something is wrong they stop you.
The idea is simple:
You try to commit.
Git says:
“Wait. Let me check that first.”
🔧 example of git hooks pre-commit (gatekeeper)
You create this file:
.git/hooks/pre-commit
Now you add rules:
```
!/usr/bin/env bash
echo "Running tests..."
npm test || {
echo "❌ Tests failed. Commit blocked."
exit 1
}
```
Enter fullscreen mode
Exit fullscreen mode
Then activate it:
chmod +x .git/hooks/pre-commit
Enter fullscreen mode
Exit fullscreen mode
What just happened?
You didn’t “add a tool.”
You added a guard at the door.
Now:
tests run automatically
failure \= commit rejected
broken code never enters history
Therefore, there's NO debate anymore.
No “I’ll fix it later” nap issue.
🪫2026 Git Hooks Reality
Nowadays most teams don’t hand-write hooks anymore.
They use tools:
they use Husky to manage hooks cleanly
or lint-staged to run checks only on changed files
Because raw hooks don’t scale well.
Tools make it consistent.
Important truth most tutorials skip
Local hooks are not secure.
Anyone can bypass them:
git commit --no-verify
And everything gets ignored.
So if you rely ONLY on local hooks you're mistaken.
So hooks alone are not enough.
Let me explain why..
Real system (what professionals use):
You combine two layers:
1. Local hooks
Fast feedback. Instant rejection.
2. CI/CD pipelines (GitHub Actions, etc.)
⚡Local \= speed
⚖️ Server \= enforcement
Local catches mistakes early.
Server enforces rules for everyone.
Together,nothing broken gets through.
You win.
🍋🟩🪞 7) Commit References
beginners copy full commit hashes like this:
a3f9c1d8b7e...
Long. Ugly. Useless in daily work.
You don’t need that noise.
🔧 Core Commands
git diff HEAD~2..HEAD
git rev-parse --short HEAD
git tag -a v2.0 -m "Release v2.0"
git push origin v2.0
Enter fullscreen mode
Exit fullscreen mode
-- HEAD~2..HEAD \= compare recent changes
-- rev-parse --short HEAD \= quick reference (human-friendly ID)
-- tag -a \= mark a stable checkpoint (this matters)
-- push origin v2.0 \= share that checkpoint with the world
🧠 Understand this properly
HEAD \= where you are
HEAD~1 \= one step back
HEAD~2 \= two steps back
You’re not memorizing hashes anymore.
You’re jumping up/down in a timeline. its super productive.
🆔 Why Tags Actually Matter??
Tags are not decoration.
They are anchors.
When you say:
git tag -a v2.0 -m "Release v2.0"
Enter fullscreen mode
Exit fullscreen mode
You’re declaring:
“This version is stable. This is deployable.”
Now your CI/CD pipelines + Deployment systems and AI tools
All know exactly what to ship.
NOTE: ⚠️ Fix this weak idea
“Full hashes are sh$t”
No.
Blind usage is actual sh$t.
Full hashes are for finding exact location, scripting and debugging critical issues,
Short refs are for speed.
Know both.
8) 🧳Stash [Pause Work Without Panic]
Interruptions are coming.
Boss calls. Bug report drops. Production is on fire.
And you’re sitting there with half-written code…
Now what?
Commit garbage?
Lose your work?
Panic?
NO.. professionals don’t panic
They stash.
What stash actually is?
Stash is simple:
Save everything I’m doing right now… and bring me back to a clean state.
No commit. No history pollution.
Just pause.
EXAMPLE:
Let say you’re working on a feature.
Suddenly you recieved a message from colleage:
“URGENT: Fix login bug NOW.”
You don’t argue.
You run:
git stash push -m "WIP: profile fix"
Enter fullscreen mode
Exit fullscreen mode
Everything disappears.
Not deleted.
Just stored safely.
Your workspace becomes clean again.
Now switch safely
git switch main
git pull
Enter fullscreen mode
Exit fullscreen mode
Now you fix the urgent issue without your half-work getting in the way.
See what you saved
git stash list
It’s like a small vault of paused work.
Nothing lost. Just parked.
Bring it back
When you’re ready:
git stash apply
Your work comes back exactly as it was.
Or:
git stash pop
Same thing, but removes it from stash after restoring.
Advanced move
git stash branch hotfix-auth stash@{0}
Enter fullscreen mode
Exit fullscreen mode
This does something smarter:
creates a new branch
restores your work there
keeps main branch clean
So you don’t mix emergency fixes with unfinished features.
No conflict with main work.
⚠️🛩️ Reality Check
Stash is NOT permanent storage.
It’s temporary.
Forget it… and it becomes a graveyard.🪦
So use it. Don’t hoard it.
When to use stash
Use it when:
you saves unfinished work safely and then switch tasks instantly
- anytime create a branch from stash (stash branch) for experiment
In summary.. need to switch tasks mid-flow? Stash temporarily saves your changes.
9) Bisect 🐞
Manual bug hunting? WEAK MOVE.
Manual bug hunting is slow.
And slow means you suffer.
Bisect \= sniper rifle for bug hunting.
Think now: something breaks in production.
Login stops working.
No logs. No clear reason.
Only one truth:
It worked yesterday. It’s broken today.
And now you have 50+ commits in between.
Amateurs go to sleep.
Professionals starts hunting.
So what's the solution? you divide the problem.
you use bisect:
git bisect start # Begin the hunt
git bisect bad # Current version is broken
git bisect good v2.1 # Last known good version
Enter fullscreen mode
Exit fullscreen mode
How the process works
You test the code.
Then you answer only two things:
works \= git bisect good
broken \= git bisect bad
That’s it.
Git keeps cutting the search space in half.
Again. And again.
Why this is powerful?
50 commits becomes
\~6 checks.
its called the power of binary search.
🔥 Now Finish the Bug Hunting mission
When the bug is found:
git bisect reset
Enter fullscreen mode
Exit fullscreen mode
Back to normal.
Bug identified.
Target eliminated. ☠️
💡Tips:
You can automate the entire hunt:
git bisect run npm test
Enter fullscreen mode
Exit fullscreen mode
Now Git runs your tests automatically on each step.
No manual checking.
No human error.
You just sit back and watch it find the culprit like quillbot ;).
NOTE:
Bisect only works if:
you hAve a known good version
your tests actually work
If your tests are broken, bisect becomes useless.
10) 🧑🦯➡️ Don’t Merge Blindly
You’ve got a feature-cat branch.
It “works on your machine.”
Now you’re about to merge into main.
And you’re thinking:
“Should be fine.”
That sentence along has destroyed more codebases than bugs ever did.
Rule #1) Check What files are changing
git diff --name-only main..feature-cat
Enter fullscreen mode
Exit fullscreen mode
This is your first reality check.
It shows exactly what will be affected.
Rule #2) What actually changed
git diff --color-words main..feature-cat
Enter fullscreen mode
Exit fullscreen mode
Now you see the real story.
Not “files changed”.
But what inside those files changed.
This is where bad merges get exposed.
Two ways to compare branches:
git diff main..feature-cat
git diff main...feature-cat
Enter fullscreen mode
Exit fullscreen mode
Two-dot vs Three-dot:
-- .. \= direct comparison (simple view)
-- ... \=true branch difference (REAL merge view)
Most people never learn this difference.
And that’s exactly why they merge wrong.
Before merge, you don’t ask:
“Will this work?”
You ask:
“What exactly am I about to change?”
To sum up, know the battlefield before the fight.
Compare first, merge second.
🫳 11) Cherry-Pick (pick ONLY What You Need)
Sometimes you only need ONE commit from another branch.
You Merging entire branches for one fix?
That’s how you import bugs you NEVER asked for.
lets cherry-pick..
Real Case scenario:
Your feature-login branch has 10 commits:
LoginUI.tsxredesign ❌authStyles.csschange UI ❌useAuth.tsrefactor ❌- ✅ Fix: token refresh bug (
authService.ts)
Production is breaking.
Users getting logged out every 5 minutes.
You don’t need UI changes.
You don't need refactor.
BUT you need that ONE fix: token refresh bug in authService.ts
💆 Step 1: Find the exact commit
git log --oneline feature-login
Enter fullscreen mode
Exit fullscreen mode
You see:
a1b2c3d fix: refresh token expiry bug in authService
f4e5d6c refactor: auth hooks
9a8b7c6 style: login UI update
Enter fullscreen mode
Exit fullscreen mode
You take the one that matters: 👉 a1b2c3d
🩺 Step 2: Surgical extraction
git checkout main
git pull
git cherry-pick a1b2c3d
Enter fullscreen mode
Exit fullscreen mode
Now only this change is applied:
authService.ts → FIXED
Everything else → untouched
Enter fullscreen mode
Exit fullscreen mode
No UI risk. No side effects.
🔨 Step 3: Clean it like a pro
git cherry-pick -e a1b2c3d
Enter fullscreen mode
Exit fullscreen mode
Update message:
fix(auth): resolve token refresh logout issue (hotfix)
Now your history makes sense to your team + your CI/CD pipeline
🙎♂️What just happened after that 3 steps workflow??
-- Bug reported in production
-- You located the fix commit
-- You shipped ONLY the fix
-- You avoided merging unstable work
This is how senior SWE teams move fast without breaking things.
But hey.. this is also where juniors get destroyed
Cherry-pick can bite you if you have:
-- Same fix exists in two branches later
-- Merge conflicts when syncing branches
-- Duplicate commits + confusing history
You didn’t remove the problem.
You copied the solution.
Big difference.
🧠 So when cherry-pick is the RIGHT move?
Use cherry-pick when:
- 🚨 Production hotfix
- 🎯 One isolated commit
- ⛔ Full merge is risky
Avoid when:
- 🌆 You need long-term branch sync
- ♾️ You’re patching randomly without plan
🧱 12) Git Add
If you think git mistakes happen during commits.
You're wrong.
They happen before that.
At staging.
You worked on:
login.jsprofile.jsutils.js
Only login.js is ready.
But only amateurs do this:
git add .
Enter fullscreen mode
Exit fullscreen mode
And suddenly:
/ half-finished code enters history
/ unrelated changes get bundled
/ reviews become useless
/ AI gets confused
/ debugging becomes harder later
That’s how conflicts starts.
🧑⚕️Professional workflow
You don’t git add . everything.
You choose what deserves to exist in history.
git add -p login.js
Enter fullscreen mode
Exit fullscreen mode
👉 Stage only specific parts of a file
👉 Build clean, intentional commits
🔍 Preview before committing
git add -n .
Enter fullscreen mode
Exit fullscreen mode
👉 Shows what would be staged
⚠️ Advanced control (use carefully)
git add --force .env
Enter fullscreen mode
Exit fullscreen mode
👉 Used only when you intentionally need ignored files
👉 Dangerous if you don’t understand why
Staging is not “pre-commit.”
It is decision-making.
You are deciding:
“What deserves to become history?”
✍️ NOTE:
Amateurs commit everything and fix later. Professionals stage with intention and never create havoc in the first place.
Conclusion 🏁
Git is not complicated.
Most of the commands above aren’t even complicated.
They just force you to slow down at the right moments.
It becomes messy when you stop paying attention.
After a while, you stop thinking in commands and start thinking in states:
what changed, what matters, and what should actually be saved.
That’s basically it.
Hope you find this article useful. let me know in the comments section what you have learned so far. Its great to hear. Cheers 🥂
SPONSOR:
Powered by Bright Data.
Git take care of your code.
Bright Data take care of your data:
proxies, scraping, data pipelines, all handled.
Just clean access to data pipelines that actually work when you need them for you apps or websites.
You build. It runs on your behalf.