Sitemap
Naimur Rahman

Naimur Rahman

Learning in public. Writing about technology, productivity, and the lessons behind the code.

Git Workflow: Understanding the Git Commands That Look Almost the Same

14 min readSep 11, 2026

--

Without Git & GitHub, a day in software development is really hard.

Git is very easy to start using in a project, but it may become unclear when it comes to concepts like HEAD, Commit, Push, Pull, Rebase, Merge, Soft Reset, and Hard Reset. All of them look pretty similar, but it makes me think:

Press enter or click to view image in full size

Why are there so many commands if some of them look similar? Do they actually do the same thing?
Or, more importantly,
What exactly is Git doing when I run these commands? Let’s begin with some generalization before going into the main differences.

What is Git?

The common but important question here:

What is Git?
Git is a version control system that tracks changes in files inside a repository.
We can initialize Git in a directory using the CLI:

git init

After that, Git starts tracking changes in that repository. Even a simple change in a file can be detected. Sounds interesting, right? But most of us don’t think deeply about how Git actually works. Does it do some kind of magic?

For example, imagine I have a file: app.js

Then I change one line.
Does Git make another complete copy of app.js?
Then I change another line.
Does Git make another complete copy again?

We work with Git every day, and sometimes our repositories contain thousands of files. If Git really kept a complete copy of every file after every change, wouldn’t that consume a huge amount of storage?

So where does Git actually keep all these changes?
Another Git magic? We can explore instead of just overthinking.

A General Idea of How Git Works

Before talking about add, commit, stash, reset, revert, merge, or rebase. I think we should understand one simple idea.

Git mainly works with a repository and a set of objects that represent the history of that repository. When we initialize a repository by git init; Git creates a hidden .git directory.

So our project can look something like:

my-project/ 
│
├── src/
├── .gitignore
├── README.md
│
└── .git/

The .git directory is basically where Git keeps its internal information. But Git does not simply say:

“Here is a copy of your entire project. Let’s copy it again every time something changes.”

Git uses objects and references to represent the state and history of the repository. This is one reason Git can work efficiently with large project histories. And now the interesting part begins.
Because when we work with Git, we are actually moving changes between different states.
A very simple way to imagine the workflow is:

A general workflow of Git

This is where many Git commands start to look confusing. So let’s compare them one by one.

1. git add vs git commit vs git stash

These three commands are commonly used while working on changes. But they do completely different things. Let’s say I modify:

src/user.js

Now Git can see that something has changed.
If I run:

git status

Git may show:

modified: src/user.js

Now what happens when I use git add?

git add src/user.js

The file moves into the staging area.
Did I save the changes permanently in Git history? The Answer is No.

git add Basically tells Git:

“I want these changes to be part of my next commit.”

So we can think:

Staging after changes

Now comes: git commit

For example:

git commit -m "Update user validation"

A commit records the staged changes as a new point in the repository history. So:

Create record by Git commit

Then what is git stash?
This is where it becomes interesting.

Suppose I am working on a feature. I changed several files, but suddenly my manager says:

“Can you quickly fix this production issue?”

My current changes are not ready to commit. I don’t want to commit unfinished code just to switch branches. So I can use:

git stash

Git temporarily stores my uncommitted changes and gives me a clean working directory.

Later I can bring them back:

git stash pop
Stash working flow

So these commands have different purposes:
git add→ Prepare changes for the next commit

git commit→ Record staged changes in repository history

git stash→ Temporarily put unfinished changes aside

They may all appear during the same workflow, but they solve completely different problems.

2. git status vs git diff

Now we have another common confusion. I changed some files. I want to know:

What is happening in my repository?
The first command I usually think about is:git status
git status
gives me an overview. For example:

~$ git status
modified: src/user.js
modified: src/auth.js
untracked: test.js

It tells me the state of my working tree and staging area. But sometimes I don’t want to know only which files changed. I want to know:

What exactly did I change?
That’s where it git diffbecomes useful. For example:

~$ git diff
- const user = getUser();
+ const user = await getUser();

There is also an important detail. If I already staged a change:

git add src/user.js

then git diff does not normally show that staged change. To see the difference between the staged version and the last commit, I can use:

git diff --staged

So these commands' purposes:
git status→ Overview of repository state

git diff→ Unstaged changes

git diff --staged→ Staged changes

Again, they look similar because both help me inspect changes. But they answer different questions.

3. git fetch vs git pull

Now let’s move from local work to remote repositories. Most of us use GitHub as a remote repository. Suppose another developer pushed some new code to GitHub. My local branch doesn’t know about those changes yet. So I can run: git fetch

What does it do?
It downloads the latest information from the remote repository, but it does not automatically change my current working branch.
git fetch is more like:

“GitHub has some new information. Bring that information to me, but don’t change my current work yet.”

Then Comes: git pull
This is where things become different. A pull generally performs two steps:

git fetch
+
integrate the fetched changes

Depending on configuration, that integration may happen through a merge or rebase. So a simplified view is:

git fetch
→ Download remote changes
→ Don't integrate them into my current branch automatically

git pull
→ Fetch remote changes
→ Integrate them into my current branch

This difference becomes very useful when I want to inspect remote changes before deciding what to do with my local branch. For the case I use:

~$ git fetch origin

Then I can inspect the remote branch before integrating it. Instead of blindly saying git pull every time.

4. git reset vs git revert

Before talking about git reset and git revert, I think we need to understand one more thing.

What exactly is HEAD in Git?
We have already talked about commits and branches, but there is something that keeps appearing in Git commands: HEAD

At first, I also thought: Is HEAD another commit?
Or maybe it is the latest commit? Not exactly.

Let’s take a simple history:

A -> B -> C

Suppose I am currently working on the main branch. We can imagine it like this:

We can imagine it like this:

main
↓
C

And HEAD tells Git where I am currently positioned. A simplified way to visualize it is:

So HEAD usually points to the current branch, and the branch points to the current commit.

Now suppose I create another commit:

git commit -m "Add authentication"
The history becomes

And now:

Press enter or click to view image in full size
The head changes

The important thing here is that HEAD moves as I move through the repository history. If I switch to another branch: git switch feature

then HEAD moves to that branch

So I don’t think about HEAD as:

“The latest commit.”

Instead, I think about it as:

“Where am I currently positioned in Git?”

And once we understand that, git reset becomes much easier to understand.

Now, what does git reset actually do?

Suppose this is my current history

Suppose I realize:

“Wait, commit C was a mistake. I want my branch to go back to B.”

Get Naimur Rahman’s stories in your inbox

Join Medium for free to get updates from this writer.

I can run:

~$ git reset B

Or, more commonly:
~$ git reset HEAD~1

Because HEAD~1 means the commit immediately before the current HEAD.

The branch reference after reset

So what actually happened? The branch reference moved from C back to B. This is an important point. git reset is not simply:

"Delete a commit."

It primarily moves the current branch reference to another commit, while the effect on your working directory and staging area depends on the reset mode.
And this is where --soft, --mixed, and --hard come into the story.

git reset --soft

Let’s start with:git reset --soft HEAD~1

the branch moves back after reset

But the changes introduced by C are still staged.

So Git is basically saying:

“Okay, let’s move the branch back, but keep the changes ready for another commit.”

I can think about it like:

HEAD moves
↓
Commit disappears from current branch position
↓
Changes remain staged

This can be useful when I realize:

“My last commit message was wrong, or I want to modify the last commit before committing again.”

git reset --mixed

Now Let’s try:git reset --mixed HEAD~1.
This is actually the default behavior of git reset when no mode is specified.

But this time, the changes from C are no longer staged. They remain in my working directory as unstaged changes. So:

HEAD moves
↓
Changes are kept
↓
Changes become unstaged

Now I can modify those changes, stage only what I want, and create a new commit.

git reset --hard

And now comes the dangerous one: git reset --hard HEAD~1

Press enter or click to view image in full size
This is represetation of Hard reset

But this time, Git also makes the working directory and staging area match the target commit. So the changes introduced by C are discarded from my current working tree.

That means:

--soft
→ Move HEAD
→ Keep changes staged

--mixed
→ Move HEAD
→ Keep changes
→ Unstage them

--hard
→ Move HEAD
→ Discard changes from the working tree and staging area

This is why git reset --hard should be used carefully. If I have local changes that I have not committed or otherwise saved, I may lose them.
So now we can understand reset much better. But there is another question.

What if the commit has already been pushed to GitHub?
Should I still reset it? This is where it git revert becomes important.

git reset vs git revert

Suppose the history is: A → B → C

And C introduced a bug. If C only exists in my local branch, I might decide to reset: git reset --hard B

If C only exists in my local branch, I might decide to reset:

Now my branch becomes

But imagine C has already been pushed to GitHub and other developers may already have based their work on it. Now changing the existing branch history can create problems for everyone else. Instead, I can use:
git revert c

Git creates a new commit that reverses the changes introduced by C.

So instead A, B We get one Extra D.

Where D is the new commit that undoes the changes from C. The original commit C is still visible in the history. This is the important difference:

git reset
→ Move the branch to another point in history

git revert
→ Create a new commit that undoes an earlier commit

So if I think about it from a history perspective:

Press enter or click to view image in full size
Reset Vs Revert Visualization

And now the difference becomes much clearer.
reset changes where the branch points.
revert adds another commit to the history.

So when should I use which one?

From my understanding, a simple rule can be:

Reset vs Revert

Git Reset: When I need to move my branch backward and control what happens to the changes.

Git Revert: When I want to undo a change without rewriting the existing shared history.

Of course, Git situations can be more complicated than this simple rule, but this gives me a useful mental model. And now I finally understand why HEAD matters here. Because when I run:

git reset HEAD~1

I am not just telling Git:

“Delete my last commit.”

I am telling Git:

“Move my current branch reference away from where HEAD currently is, to its previous commit."

And whether the changes remain staged, unstaged, or are discarded depends on whether I use:

--soft
--mixed
--hard

That makes the whole thing much less mysterious. Git is not doing magic. It is moving references, changing states, and recording history.

5. git cherry-pick vs git merge vs git rebase

Now we understand commits, branches, HEAD, reset, and revert. But there is another situation that happens almost every day when working with a team.

Multiple branches have different histories.

Now I have two different lines of development.

At some point, I need the changes from my feature branch to come back into main. And this is where three Git commands start looking very similar:

git merge
git rebase
git cherry-pick

All of them can somehow bring changes from one place to another. But again, they don’t actually do the same thing. Let’s understand them one by one.

git merge

Let’s start with the most straightforward one. Suppose my history looks like:

Head on main branch

And I want to bring the feature branch into main.I can run:

git merge feature

Git combines the histories of the two branches. Depending on the situation, Git may be able to perform a fast-forward, or it may create a merge commit. For example, with a merge commit:

Press enter or click to view image in full size
(E + F ) represents the merged commit

What I like about merge is that it keeps the branch history visible. I can look at the history and understand:

“These commits were developed on another branch, and then that branch was merged into main.”

So I think about merge like this:

git merge
→ Combine two lines of history

It doesn’t take my commits and pretend they were created somewhere else. It connects the histories.

git rebase

Now let’s look at rebase. Suppose I have the same branch state: My feature branch was created from C. But main has now moved forward to F.

Current state of changes

I want my feature branch to be based on the latest main. I can use:

git switch feature
git rebase main

Git takes my feature commits and reapplies them on top of the current main. So the history can become:

Press enter or click to view image in full size
History Representation After Rebase

Notice something interesting here. The original D and E are not simply moved. Git creates new commits containing the changes, so they get new commit identities.

The changes may be similar, but they are new commits. And now the history looks much more linear. There is no merge commit in this example. This can make the project history easier to read. But there is an important rule to understand.

“Rebase rewrites commit history.”

So if other developers are already working with the commits I am rebasing, rewriting those commits can create problems for them. That’s why rebasing is generally safer for my own unpublished feature work than for shared history. So:

git rebase
→ Reapply commits on top of another base
→ Creates new commit identities
→ Can produce a cleaner, linear history

But then, what is git cherry-pick?

Now let’s imagine something slightly different. Suppose my feature branch has:

Press enter or click to view image in full size

But I don’t want the entire feature branch. I only want the authentication fix. So merging the entire branch doesn’t make sense. Rebasing the branch doesn’t make sense either.

I only want one specific commit. That’s where: git cherry-pick becomes useful. For example:

git cherry-pick <commit-id>

Git takes the changes introduced by that particular commit and applies them to my current branch. Suppose my main branch is:

Press enter or click to view image in full size

Again, notice the difference. The changes from F were applied, but Git creates a new commit on the current branch. So F have different commit identities.

This is why cherry-pick is useful when I need a specific change rather than an entire branch. For example, maybe a bug was fixed in a development branch, but I need that same fix in a release branch. I don’t necessarily want all the unfinished development work. I only want that particular fix.

git cherry-pick
→ Take a specific commit
→ Apply its changes to the current branch
→ Create a new commit

Now, how are these three actually different?

At this point, all three commands may still sound similar. They all somehow bring changes from somewhere else. But the intention behind them is different.

I think about them like this:
git merge
→ “I want to combine these branch histories.”

git rebase
→ “I want to replay these commits on a different base.”

git cherry-pick
→ “I only want this particular commit.”

And now I know why Git needs all three. They may produce similar results in some situations, but they communicate different intentions and affect history differently.

Conclusion

After going through all these commands, Git may still look like a lot of things to remember. But I don’t think we need to remember everything as a separate command. The important thing is to understand what we are trying to do.

If I change something, I can use git status and git diff to understand my changes.
If I am ready to save those changes into history, I use git add and git commit.
If my work is not ready but I need a clean working directory, I can use git stash.
If someone else has pushed changes, I can use git fetch to see them first, or git pull to fetch and integrate them.
If I made a mistake in my local history, git reset can move my branch back.
If that change is already shared with others, git revert can undo it by creating a new commit instead of rewriting the existing history.
And when different branches need to come together, I have merge, rebase, and cherry-pick, depending on what I actually want to bring into my branch.
So, instead of remembering Git like this:

add
commit
stash
fetch
pull
reset
revert
merge
rebase
cherry-pick

I prefer to remember the workflow:

Change
↓
Inspect
↓
Stage
↓
Commit
↓
Share
↓
Synchronize
↓
Combine or Undo when needed

And there is one thing I learned from exploring these commands:

Git is not difficult because there are too many commands.
It feels difficult when we don’t understand what each command is changing.

Once we understand the relationship between the working directory, staging area, commits, branches, HEADand remote repository, the commands start to make much more sense.
We don’t need to memorize every command before using Git. We just need to ask one simple question before running one:

“What do I want to happen to my code and its history?”

I think that question is enough to make Git feel a lot less like magic.

Thank you for reading this article. This article reflects my understanding based on learning and working with Git. If you find any inaccuracies or have additional insights, I’d love to hear your feedback. Feel free to leave a comment or ask any questions.

--

Naimur Rahman
Naimur Rahman

Written by Naimur Rahman

Learning in public. Writing about technology, productivity, and the lessons behind the code.