Do you know Git? Of course you do! Today, I’ve got a few of my favorite Git commands for you. When I was a junior developer, my tech lead taught me most of them (hi Wojtek, if you’re reading this 👋) and later, I started teaching the same commands to my own junior and mid-level developers.
I’ve actually been meaning to write this article for months. But you know how it is. It’s evergreen content, it requires a bit of work, and meanwhile I kept coming up with other ideas, so I kept postponing it. What finally motivated me was @francistrdev, who recently published his own article about Git commands.
My article will be a little different, though. If you’re completely new to Git and still learning the basics, check out Francis’s Git Gud! article first. But if add, commit, pull, and push are already second nature to you, this article is for you.
These are, let’s say, intermediate-level commands. Useful ones. The kind you actually need at work, but not yet senior-level Git magic like:
Type three lines into your terminal and a spaceship will appear in your repository.
If you have other useful commands at this level, please share them in the comments!
BTW, if you feel like it, you can also follow me on Instagram! I’m planning to post what could grandly be called “dev lifestyle” content there. 😅 I have four conferences coming up this fall, for example, so expect some behind-the-scenes photos from my little tour around Europe. I only have a handful of followers there so far and absolutely no idea what this experiment will turn into, but here you go: @sylwia.lask
All right, let’s start.
1. & 2. git merge vs git rebase
Haha, first point and we already have two commands. The reason I’m putting them together is that a lot of developers, even experienced ones, treat merge and rebase almost interchangeably.
And sure, their high-level purpose is similar: you want to integrate the history of one branch into another. Very often that means updating your feature branch with changes from main, develop, or another feature branch.
But the way they do it is quite different. Imagine you have this:
A---B---C main
\
D---E feature
You’ve been happily working on your feature branch, while someone else added commit C to main. Now you want to bring those changes into your branch.
Merge
If you run:
git merge main
while you’re on feature, Git combines both histories. You usually end up with something like this:
A---B---C------M
\ /
D---E---
M is a merge commit.
The original history stays intact. Git remembers that your feature branch diverged from main, both branches evolved independently for a while, and then they were joined together.
This can be a good thing because it preserves the real history of what happened. The downside is that if you merge main into your feature branch over and over again, your history can eventually start looking like spaghetti.
Rebase
Now let’s say instead you run:
git rebase main
Git takes commits D and E, temporarily removes them, moves your branch onto the latest main, and then reapplies your commits on top.
You get:
A---B---C---D'---E'
Much cleaner.
But notice the apostrophes. D' and E' are not technically the same commits as D and E. Git created new commits with new hashes. That means rebase rewrites history.
This is why a very useful rule of thumb is:
Rebase your own work. Be careful rebasing shared history.
If you’re working alone on a feature branch, rebasing it is usually perfectly fine. If three other developers have already based their work on your commits, rebasing those commits can make everyone’s life considerably more exciting. And not in a good way.
And what about conflicts?
Both merge and rebase can result in conflicts.
With a merge, Git tries to combine the two histories in one operation. You resolve the conflicting files, stage them:
git add .
and then continue:
git merge --continue
With rebase, things can be slightly more annoying because Git reapplies your commits one by one.
This means you may resolve a conflict, continue the rebase...
git add .
git rebase --continue
...and then get another conflict in the next commit. And another one. And another one. At which point you start questioning every career decision that led you here. 😅
But there’s also a small bonus. If you completely mess things up and just want to stop the entire circus, you can do:
git merge --abort
or:
git rebase --abort
Git will try to return you to the state from before the merge or rebase started. Extremely useful feature. 😁
3. git commit --amend
This is one of those commands I learned to love very quickly as a junior. Imagine you’ve just created a beautiful, perfectly round little commit. Then you look at the code and notice you forgot something.In my case, it was usually:
console.log("WTF");
You now have two options. Option one: make another glorious commit:
Remove console log
Which, let’s be honest, is not exactly the pinnacle of Git history aesthetics.
Or you can simply add the forgotten change to the previous commit. For example:
git add forgotten-file.ts
git commit --amend --no-edit
--no-edit means:
Add these changes to the previous commit, but keep the existing commit message.
If you want to change the commit message as well, just run:
git commit --amend
Git will open your configured editor and let you modify it.
There is one important detail here. amend doesn’t literally edit the existing commit. It creates a new commit containing the updated contents. And because commit hashes depend on the contents and metadata of the commit, the hash changes.
If you’re working locally and haven’t pushed the branch anywhere yet, no problem. But if the branch already exists on remote, your local history no longer matches the remote history. So when you try:
git push
you’ll probably get something like:
rejected (non-fast-forward)
Now, if you are absolutely sure nobody else is working on that branch, you could technically do:
git push --force
But what if you’re not sure?
And this brings us very smoothly to the next point.
4. git push --force-with-lease
After a rebase, amend, interactive rebase, squash, or anything else that rewrites your commits, your local branch history may no longer match the version stored on remote.
You can solve that with:
git push --force
But --force is basically saying:
My local version is the truth. Replace whatever is on remote with this.
If someone pushed something to that branch in the meantime, you can overwrite their work. They may get angry. And rightly so. 😏
A much safer option is:
git push --force-with-lease
In simplified terms, this tells Git:
Force-push my version, but only if the remote branch still looks the way I expect it to.
If the remote branch changed since the state your local Git knows about, for example because someone else pushed new commits, Git refuses the push instead of blindly overwriting them.
So instead of destroying someone’s afternoon, you get an error. I generally recommend:
--force-with-lease
over:
--force
whenever possible.
5. git rebase -i
For me, this is a very important command. Almost the king of Git. xDDDD Interactive rebase lets you do almost anything you want with your recent commits.
In other words, you can commit every single line individually, change everything ten times, create commits like:
Add validation
Fix validation
Actually fix validation
Fix validation again
Remove console.log
Please work now
and then later turn the whole mess into an elegant Git history.
Let’s say you want to edit your last five commits. You run:
git rebase -i HEAD~5
Git opens your configured editor — Vim, Nano, VS Code, or whatever you use — and shows something similar to:
pick a111111 Add login form
pick b222222 Add validation
pick c333333 Fix typo
pick d444444 Fix validation
pick e555555 Remove debug log
Now comes the fun part. You can replace pick with different commands.
pick
Keep the commit exactly as it is.
pick a111111 Add login form
reword
Keep the commit contents, but change its message.
reword a111111 Add beautiful login form
edit
Pause the rebase at that commit and let you modify it. Useful if you want to change the actual contents of an older commit.
squash
Combine the commit with the one before it.
For example:
pick a111111 Add login form
squash b222222 Add validation
squash c333333 Fix validation
Git turns those commits into one and lets you edit the final commit message.
So instead of:
Add login form
Add validation
Fix validation
you can end up with:
Add login form with validation
Beautiful. Round. Perfect.
fixup
fixup is similar to squash, but it throws away the commit message of the fixup commit.
For example:
pick a111111 Add login form
fixup b222222 Fix typo
fixup c333333 Remove console.log
You probably don’t care about preserving the historical significance of:
Remove console.log
So fixup is perfect here.
drop
Remove the commit completely.
drop c333333 Terrible idea
Goodbye.
Of course, there are different schools of thought about how much you should clean up your commits. Some senior developers say that submitting a nicely cleaned-up commit history for review is simply good manners. Others do not care in the slightest and think it’s unnecessary because GitHub, GitLab, or whatever platform you use can simply squash everything when merging the PR anyway.
But!!! What if you don’t want to click Squash and merge? What if your feature logically contains two commits and you actually want to preserve both? That happens.
So whether you use interactive rebase once a week or once every six months, I still think it’s worth knowing the king:
git rebase -i
6. git stash
This one comes in handy very often. You start working on a nice little feature. Or, these days, Kiro or Claude Code is working very hard while you watch. Suddenly someone reports a production bug.
Unfortunately, you now have to temporarily abandon your beautiful feature and switch to something else. The problem is that your current work is completely unfinished. You have debug logs everywhere, half the files are modified, the project doesn’t even build, and you definitely don’t want to commit this mess.
Although, of course, now you know about amend, so you could eventually clean it up.
But there’s a better solution:
git stash
Git temporarily stores your uncommitted changes and returns your working directory to a clean state.
Now you can switch branches:
git switch main
fix your production disaster, and later come back.
One important detail: by default, git stash stashes tracked files, but not new untracked files. If you also want to include newly created files, use:
git stash -u
or, even better, give your stash a useful name:
git stash push -u -m "WIP login feature"
Because once you have several stashes named basically “WIP”, future you will hate present you.
git stash list
To see all your stashes:
git stash list
You might get:
stash@{0}: On feature/login: WIP login feature
stash@{1}: On feature/cart: experiment
git stash show
To see what a particular stash contains:
git stash show stash@{0}
And if you want the full diff:
git stash show -p stash@{0}
git stash apply
To restore a stash:
git stash apply stash@{0}
The changes are restored, but the stash remains in the list.
git stash pop
You can also use:
git stash pop
This applies the stash and, if successful, removes it from the stash list.
So, roughly:
apply = restore
pop = restore + remove from stash
Very simple, very useful.
7. git cherry-pick
I have to admit this is one of the intermediate Git commands I use most often. Imagine there’s one specific commit on another branch that you want in your current branch.
You don’t want to merge the whole branch. You don’t want to rebase onto it. You literally need one thing. For example, in my project recently, I needed a commit containing configuration for newly created environments. Perfect use case.
You simply run:
git cherry-pick <commit-hash>
For example:
git cherry-pick a1b2c3d
Git takes the changes introduced by that commit and applies them to your current branch.
Important detail: it creates a new commit. So the resulting commit contains essentially the same changes, but it gets a new hash. Imagine this history:
main
A---B---C
feature
\
D---E---F
You’re on main, but you only want E.
After:
git cherry-pick E
you get something like:
A---B---C---E'
Simple.
But I’ll admit that, being a lazy creature, I use cherry-pick in slightly less noble ways too. Sometimes something gets completely messed up on my branch. The rebase goes badly, the history looks suspicious, conflicts start multiplying, and after a while I decide: I'll create a completely new branch from the correct place at the top of the tree. 😅
And because — thanks to the previous commands ☺️ — my commits are already nice and round, I simply cherry-pick them one by one onto the new branch.
I’m sure some Git pro is currently shaking their head at me, but let me put it this way: It works for me. 😀
8. git reset --soft, --mixed, --hard
Three ways of moving back in history, each with a different answer to the question:
What should happen to my changes?
Because how many times do we start working on something, write some code, and then realize: Nope. This whole idea was bad. Let’s go back.
The easiest way to understand reset is to think about three layers:
Commit history
Staging area
Working directory
Now we have three increasingly dramatic levels of reset. And every next one has greater potential to cause a small heart attack if you use the wrong one.
git reset --soft
Let’s say you want to undo the last commit:
git reset --soft HEAD~1
Git moves HEAD back by one commit, but all the changes from the removed commit remain staged.
So if your history was:
A---B
after the reset, your branch points to:
A
but the changes introduced by B are still ready to commit.
This is useful when you committed too early and want to recreate the commit differently.
git reset --mixed
Now:
git reset --mixed HEAD~1
or simply:
git reset HEAD~1
because --mixed is the default.
Again, Git moves back one commit.
But this time the changes are left in your working directory unstaged.
So nothing disappears from your files, but you need to git add things again before committing.
git reset --hard
And now we enter the danger zone:
git reset --hard HEAD~1
This moves HEAD back and updates both the staging area and working directory to match that commit. In other words, the changes disappear from your files too.
So:
--soft → keep changes staged
--mixed → keep changes unstaged
--hard → discard changes from the working tree
Use the last one with some awareness of what you’re doing. But what if you go one step too far and accidentally delete something you absolutely did not mean to delete?
And that brings us to...
9. git reflog
A command I use very rarely. But oh, how many times it has saved my ass.
For example, when I once ran git reset --hard and instead of removing my last changes, I basically removed the branch I had been working on for two days. 😀Amazing experience. Highly recommended.
The important thing to understand is that git log shows you commits reachable from the history you’re currently looking at. If you reset your branch backwards, some commits may disappear from git log.
That does not necessarily mean Git has immediately deleted them. Git also keeps a local log of movements of references such as HEAD. You can inspect that with:
git reflog
You may see something like:
e35fa12 HEAD@{0}: reset: moving to HEAD~2
821cd77 HEAD@{1}: commit: Add authentication
f992ab1 HEAD@{2}: commit: Add login page
Aha! There’s your missing commit.
Now you have several options. You could move the branch back to it:
git reset --hard 821cd77
But personally, if I’m already in panic-recovery mode, I prefer doing something safer first:
git branch rescue 821cd77
Now the commit is reachable from a branch again and I can calmly inspect what happened without immediately rewriting anything else.
The key difference is:
git log
shows your visible commit history.
git reflog
shows where your local references, especially HEAD , have pointed recently.
There is one important limitation, though. Reflog is not a magical backup of every character you’ve ever typed. If your changes were never committed, stashed, or otherwise stored as Git objects, reflog cannot magically resurrect them.
So yes, if you worked for six hours without committing anything and then destroyed those changes... Well. Maybe this will teach you to commit more often next time.
10. git revert
And finally:
git revert
Imagine you release a commit to production. Or, in the slightly less hardcore version of this scenario, to some shared develop branch.
Something goes very wrong. Everything breaks. So what now? Do you run:
git reset --hard
on the production branch?
Do you start performing an exorcism?
Fortunately, no. The elegant way to undo a commit while preserving a clear record of what happened in your repository history is:
git revert <commit-hash>
Imagine your history looks like this:
A---B---C
and C introduced the disaster.
You run:
git revert C
Git does not remove C.
Instead, it creates a new commit that applies the opposite changes:
A---B---C---D
You may end up with something like:
C: Add new payment logic
D: Revert "Add new payment logic"
This is extremely useful on shared branches because you are not rewriting public history. Everyone can clearly see:
- the original change happened,
- it caused trouble,
- it was reverted.
Compare that with resetting the branch and force-pushing it backwards, which rewrites history and can cause problems for everyone else working on it.
So, as a general rule:
Shared branch + bad commit → revert
is usually much safer than:
reset + force push
And that’s it!
As I said at the beginning, if you use other intermediate Git commands that regularly save you time or save your ass, please share them in the comments. Usually the comments under my articles end up being much better than the article itself, which continues to make me very happy.
Also, please note that last week’s article was a list. This week’s article is a list. And — spoiler alert — next week’s article will also be a list!
This is not because I smelled clicks and decided to turn into BuzzFeed. It just somehow happened that way. 😅
I hope you learned something new. And if you didn’t, I hope it was at least fun to read. 😁
Top comments (96)
Thanks for the mention and great detail on using Git :D
Haha never enough of Git 🙂
Im looking forward to the followup about git work-trees. With agentic now the mainstay, work-trees are becoming increasingly more invaluable
Haha, don’t tempt me! 😂
A best how to use git short document!
Ooooh, thank you so much! ☺️ Now I can just send this article to juniors instead of explaining all of this on a call every time. 😂
This is exactly how I started writing! So I didn't have to explain myself multiple times. 😂 Great article!
Thank you so much! 😄 Haha, I also wrote a few articles specifically for my juniors. But then somehow this whole writing thing took a rather unexpected turn. 😂
Sounds like we did much of the same thing!! It is a lot of fun. 🤩
Thats why is working so well
Hahahaha, should've written this a few years ago! 😅
Git commands are basic, but really important for every developer.
I think this will be a valuable article for many people! 😸
I use aliases in my
.zshrc, for example,gc="git commit -S".It makes my daily Git workflow a little bit faster.
Do you have any similar tricks or customizations in your setup? I'm curious! 😸
Haha, you know what? I actually don’t! 😄 I know about Git aliases, but I prefer typing the commands manually so I don’t forget them, partly because then I can recite them from memory when explaining Git to juniors. 😂
But we do have a very funny Polish repo with Git aliases called “git kurwa” (“kurwa” is a Polish swear word 😅). So apparently we’ve found a very Polish solution to Git frustration. 😂
Actually, I don’t remember all the Git commands either. 😹
I only make aliases for the ones I use all the time.
And I had no idea git kurwa existed! Thanks for introducing me!
github.com/jakubnabrdalik/gitkurwa
"Git, kurwa!" 😹
Hahaha, yes! 😂 It was super popular in Poland some time ago. I think pretty much every Polish developer had come across “Git, kurwa!” at some point.
Hi Sylwia! 😂
git reflogis basically Git’s emergency contact. You hope you never need it, but the moment you accidentally nuke two days of work, suddenly it’s your best friend.Now I’m curious: which Git command has saved you from the biggest disaster so far? 😄
Haha, that’s a good question! 😄 I’d say
git reverthas definitely saved me from the biggest disasters.git reflogcan save your work, sure, butgit revertcan save production. 😂Your article deserves to be shared with anyone who uses Git… and should be declared a public service. I can't even begin to tell you how many mistakes I would have avoided if you'd had me read it when I first started using Git!
Thank you so much, Pascal! Coming from you, that’s truly an honor! 😊
git refloghas rescued me from more than one moment of "well, that was unfortunate." 😂But your
git revertsection jumped out at me for a completely different reason. I've been writing a lot lately about durable AI memory and decision history, and Git keeps turning out to be a surprisingly good mental model.A revert doesn't pretend the original commit never happened. It preserves the original event and adds a new event saying, effectively, "this no longer governs the current state." That's very close to the distinction I've been working through between correction, supersession, and invalidation in AI memory.
Current state alone says what exists now. History tells you how it became that way.
Apparently the answer to half of my AI architecture questions continues to be: we solved a version of this problem decades ago with Git. 😄
Oh wow, that’s actually such a great observation! And yes, the more I work with AI agents, the more I feel like we keep rediscovering problems from good old software engineering. 😂
What makes it even more interesting is that in traditional software development, we’ve already solved many versions of these problems over the years. With agents, we’re suddenly looking at them from a new angle and still have so much room to figure out how those old lessons translate.
Alot of the ones that we rarely use are always the ones that end up saving our assess all the time. you think reflog might be useless? nah it just means you haven't screwed up enough lmao
when i started learning version control, i didnt think there was ever a UI and my default has always been through the CLI and i've always thought it was the default way to do version control for every developer... for large, complex changes, i rely on the Gitlens extensions.
until last year, i had a new member joining my team and he was using this blue UI thing i forgot the name of, i asked him what it was and he said it's just git commands with UI and i was like whatttt, they have that? lmao but hey, it gets the job done!
Hahaha, I wonder if that blue UI thing was SourceTree? 😄 I absolutely love SourceTree and I’m kind of addicted to it. 😂
To be clear, I run probably 99% of my Git commands from the CLI (although I’ll occasionally create a local branch from the GUI when I’m feeling lazy 😅). But I’m a very visual person, and SourceTree draws the branch and commit tree so beautifully that I can immediately see what’s going on in the project.
Yes!!! it was SourceTree!!! I've also thought about trying it out but then again, maybe I'm just lazy but I'm just not ready to try any new GUI for anything at the moment haha
Haha, I think if you don’t feel like you need it, there’s really no reason to force yourself to try it. 😄
Funny enough, SourceTree was actually introduced to me by a backend developer who also did pretty much everything from the CLI. He just liked having a visual representation of the branch tree too. 😂 So apparently there’s a whole secret society of “CLI for commands, GUI for looking at pretty trees” developers.
Great list! Though I can’t imagine
git merge,rebase, or evenstashbeing unknown to any developer on God's green earth. Those are basically the bread and butter of Git.Thanks! And that’s actually a very fair objection, one I’m happy to defend, because I do have a reason for each of those. 😄
First, junior developers often know
merge, but either haven’t heard much aboutrebase, or they’ve heard of it without really understanding how it works or how it differs from merging. That’s exactly why I devoted quite a bit of space to explaining the difference.Funny enough, recently even a senior developer on my team casually referred to “rebasing the branch,” and I was like, “But… we merged it there, didn’t we?”. Which actually mattered quite a lot in that particular situation. His answer was basically, “Yeah, I was just using it as shorthand.” I hope he wasn’t bluffing. 😂 But either way, imagine the confusion that kind of wording can create for juniors!
As for
stash, I also really hope everyone knows the basic command. 😄 That section was more about going beyond plaingit stashand showing things likestash list,apply, andpop.The merge vs rebase section is the one that finally made things click for me, I'd been using both without really understanding why I was choosing one over the other. The diagram of commits being replayed with new hashes is such a cleaner way to explain why rebasing shared history is risky.
git stash is probably the one I reach for most often in my day to day, that moment where you're halfway through something and a bug report lands and you just need to put everything down cleanly without committing a mess.
One I'd add at this level: git log --oneline --graph --all — not a command that does anything dramatic, but seeing the branch structure visually in the terminal helped me understand what merge and rebase were actually doing to my history way faster than any explanation did.
Thanks so much for the comment! 😄 And yes, --graph is great!
I have to admit that even though I type probably 90% of my Git commands manually, I still like using SourceTree, pretty much exclusively to look at the branch and commit tree. 😂 I’m definitely a visual learner, so being able to actually see the history makes everything much easier for me.
git reflog deserves more attention than it usually gets. Most Git commands are about changing the state of your repository, while reflog gives you a way to understand how you got there when something goes wrong. That makes it less of a daily workflow command and more of a recovery tool—and probably one of the best safety nets to learn before you actually need it.
Good framing, but worth to remember reflog lives inside .git, so it saves you only from yourself. When my SSD died the reflog died in same second, and what actually brought the thing back was a 13MB snapshot sitting on another machine. Local safety net and off machine copy are two different things, and most people have only first one.
That’s a very good point, thanks for the clarification! Of course, there are plenty of situations where reflog won’t save you: reflog entries expire (typically after 90 days for reachable entries, as I remember), the repository itself might be corrupted or lost, or the changes simply might never have been committed in the first place.
There are lots of nuances here, but if the repo is still alive and you’ve accidentally nuked something, it’s always worth trying your luck with git reflog. 😄
Yes, and expiry has funny detail in it. The 90 days is for reachable entries, unreachable ones go after 30 by default, and unreachable is exactly what you come to reflog for. So real window is shorter than people think, and it runs quietly while you postpone looking.
Exactly! Although my coworkers and I always joked that you usually have more than enough time anyway, because most of the time you reach for reflog about a minute after realizing you’ve just deleted your own work. 😂
Exactly! Especially when you accidentally click the wrong thing somewhere, or when a rebase goes very, very wrong. 😂 That’s when git reflog suddenly becomes your best friend.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.