Git is an essential tool for developers to learn in order to effectively collaborate on source code. But certain Git concepts and commands are notoriously difficult for devs to learn.
Not only that, but Git commands often have nuances that lead to varying results on the state of your local repository. In addition, even once you learn a specific Git command, you might not use that command for a significant amount of time before needing it again.
This leads devs to repeatedly search Google or Stackoverflow for specific situational Git commands over and over again, praying that parallel results will occur when executing the command locally.
This commonly occurs with notoriously confusing Git commands such as
git merge, and
In this article, we'll provide an overview of these 3 commands using a command-line tool called Git-Sim to visually simulate Git operations in your local repo. This tool will enable us to easily create handy diagrams and animations illustrating how each Git command will affect the state of your own Git repo.
git reset – How to Reset Git HEAD while Managing Changes
The git reset command is one that I constantly find myself Googling before I need to use it.
This is partly because there are several commands that you can use to undo changes in Git, and I want to make sure I'm choosing the right one.
Also, any command that could accidentally lead to "losing work", like
git reset --hard is worth taking a few seconds beforehand to make sure you're confident in what you're about to do.
git reset do?
In a nutshell, git reset simply moves the tip of the current branch back to a previous commit. In that sense it can be used to "undo" the changes applied by commits between the current commit and the commit you want to reset back to.
Technically speaking, a Git branch is just a name (or label) that points to the newest commit in a chain of commits. This gets moved forward each time you make a new commit on a branch, and therefore always identifies what is known as the branch head commit.
git reset command moves this branch label back to a previous commit, which becomes the new branch head – effectively undoing all the changes in between.
Let's consider a simple example. Assume we have a repository which only tracks 1 file called
cheese.txt. The repo has 5 commits, each changing the text content within the file to a different cheese name.
We can illustrate this visually using Git-Sim by running the
$ git-sim log command to visually simulate the output of the
$ git log command:
As you can see, we have a
main branch pointing to the commit with ID
14c121... with commit message
Now let's say we want to undo the commits changing the content of
cheese.txt to "cheddar" and "Swiss". We can do this using the
git reset command to reset the
main branch back 2 commits, to "Gouda":
$ git reset HEAD~2
But if we are ever unsure about the impact this command will have, we can simulate it first using Git-Sim:
$ git-sim reset HEAD~2
Or if you prefer, the simulation can be rendered as a dynamic video animation using Git-Sim's
--animate flag as follows:
$ git-sim --animate reset HEAD~2
As you can see, the Git-Sim simulated output above shows that the
main branch and
HEAD pointer are reset backward 2 commits to the "Gouda" commit.
In addition, we can see that Git-Sim created a table with 3 columns:
- Changes deleted from
- Working directory modifications
- Staging area
By default, the
git reset command applies the
--mixed flag behavior, which moves any changes in reset commits into the working directory.
Git-Sim reflects this in the simulated output above by adding the file
cheese.txt into the Working directory modifications column.
Now that you've simulated the
git reset command using Git-Sim and can clearly see the expected result in the image output above, you can run the actual Git command
$ git reset HEAD~2 with confidence.
In addition to
--mixed, Git-Sim understands
--hard resets. The command
$ git-sim reset --soft HEAD~2 would keep the undone changes in the Staging area column, while the command
$ git-sim reset --hard HEAD~2 would place the undone changes in the Changes deleted from column since a hard reset discards changes to committed files.
git reset doesn't ever delete any commits (even when using the
--hard option), which means you don't lose any work at the time of executing it. But keep in mind that commits can become orphaned, meaning that certain commits become unreachable by any branch head. Git will eventually remove these via garbage collection, but they are retrievable for a while if you need them!
git merge – How to Merge Changes from Multiple Branches
The git merge command is another tricky one for beginner and intermediate Git users to wrap their heads around. There are several different types of merges that Git performs depending on the circumstances.
To provide a foundation, this sequence of two commands highlights how the merge syntax works:
$ git checkout <merge-into>
$ git merge <merge-from>
The first command shows that the
<merge-into> branch is the active branch that you currently have checked out in your working directory. Then when you execute the merge, you specify
<merge-from> as the branch that you want to merge changes from, into the active branch.
So for example, the following commands would merge changes from the
dev branch into
$ git checkout main
$ git merge dev
Git three-way merge
The most common merge case occurs when the history of two branches has diverged and the developer wishes to recombine the changes. In this scenario, Git performs something known as a three-way merge.
It is called a three-way merge because Git uses three commits to determine how to combine changes in your diverged branches:
- The commit at the tip of branch A
- The commit at the tip of branch B
- The merge-base of the two branches
We can use Git-Sim to simulate a three-way merge which merges the changes in two new commits on the
dev branch into
$ git checkout main
$ git-sim merge dev
In the example above, the tip of the
main branch before the merge is commit
14c121 ("Cheddar"), and the tip of the
dev branch is
f42fee ("Asiago"). The third commit used in Git's three-way merge algorithm is commit
640bb5 which is the merge base of the two branches. You can identify this by tracing back both branches until you find the first common ancestor.
Using the content in these 3 commits, Git creates a new merge commit (labelled
abcdef in the Git-Sim diagram above) which combines the changes from both branches.
Unlike regular commits, a merge commit has two parents instead of one, corresponding to the tips of the two branches that were merged together. These two parent relationships are denoted by the two dotted lines in the image above.
Git fast-forward merge
A fast-forward merge in Git occurs when the branch you are merging from is just further along the same line of development as the active branch. Consider the example below:
$ git-sim log
In this case, the
main branches have not actually diverged – they both lie along the same line of development. As a result, if you check out the
dev branch and try to merge in
main, Git doesn't even need to actually need to merge the content from the two branches.
Instead, Git can simply repoint the
dev branch ref to the same commit as
main points to, which is
14c121. (Remember a branch in Git is just a label pointing to a particular commit).
Git-Sim knows how to simulate this type of merge as well:
$ git checkout dev
$ git-sim merge main
Note that after checking out the
dev branch and merging
main into it, the
dev branch label was simply fast-forwarded to the same commit as
main, and no new merge commit was created.
However, in some cases you may want to force Git to create a merge commit even when doing a fast-forward merge.
This might be useful if you want to preserve the history of all merges so that your team knows when a merge was performed. You can do this using the
--no-ff flag, which is also supported by Git-Sim:
$ git checkout dev
$ git-sim merge --no-ff main
In the simulated output image above, Git created a new merge commit
abcdef which the
dev branch label now points to, even though it was possible to simply fast-forward to the commit pointed to by
Keep in mind that even when forcing a merge commit in this way, no divergence of branch history has taken place. Both branches still fall along a single line of development.
git rebase – How to Move the Current Commit onto a New Base Commit
Lastly, we'll briefly cover one specific scenario of the standard git rebase command. Instead of merging the contents of two branches,
git rebase <new-base> reapplies the commits reachable by the current branch onto a new base commit, specified by
git rebase runs in standard mode, which is what we'll be discussing here. The
-i flag allows rebase to run in interactive mode, but that is out of the scope of this article.
Using our previous example repo, let's note the 2 new commits added to the
ead5cc ("Fontina") and
Then we want to move these 2 new commits to the tip of the
main branch using the
git rebase command. This can be simulated using the commands:
$ git checkout dev
$ git-sim rebase main
In the simulated output above, the dotted lines show how the 2 new commits on the
dev branch are rebased onto the tip of
In a standard rebase, Git identifies the number of commits to rebase by walking back from the tip of the active branch until a common ancestor commit is found between the two branches.
Of course, this common ancestor already exists on the destination branch, so the rebase does not need to include it, and can end at that point.
In this case, the two commits on the
dev branch that don't yet exist on
ead5cc ("Fontina") and
f42fee ("Asiago"). These get rebased onto
main as shown by the dotted lines above. Since commit
640bb5 ("Gouda") already exists on
main, it is not included in the rebase.
Note that rebasing in Git actually rewrites commit history, since each commit ID depends on that commit's content and the history that comes before it.
Even if the content in the two rebased commits in the example above applies cleanly to the
main branch, new commit ID's must be calculated since their history now differs from when they were applied to the
For this reason, a good rule of thumb is to only rebase commits locally that haven't yet been pushed to a remote repository. This will ensure that you and your team maintain a consistent repository state for your project over time.
In this article, we use the Git-Sim tool to provide an overview of 3 notoriously confusing Git commands –
git merge, and
We illustrated how to reset Git's HEAD pointer to a previous commit while managing committed files, staged files, and working directory files as desired using the
Then we outlined the basic merge types in Git, including the 3-way merge and the fast-forward-merge.
Finally, we discussed rebasing branches, which means moving a set of commits onto a new base commit.
For each of these commands, we showed how the Git-Sim command-line tool can be leveraged to create unique, customized simulations of your Git command's effects before running the actual Git command itself.
If you're a visual learner and Git user, I encourage you to try out Git-Sim to simulate Git commands in your own local repos before running the actual commands.
This will give you the confidence to execute the actual Git commands, and even provide you with visual content to aid in documentation, content-sharing, and teaching others to use Git.
Git-Sim is a free and open-source tool written in Python, so if you're interested in contributing or if you find any bugs, feel free to check out the Git-Sim project on GitHub and/or contact me at firstname.lastname@example.org!