Over the past few years, Git has become one of the most used (if not THE most used) version control systems. Its success is probably due to being open source, resilient, distributed and highly supported in most continuous integration platforms. Most IDEs also integrate Git, and there are several GUIs out there that aim to help end-users with many daily basic operations. However, if you have been toying around with Git for a while, you have probably had to use the command line. If you are not familiar with it or the concepts behind Git, you certainly have experienced those difficult moments where you feel like you are not sure what you’re doing and fear performing irreversible things. Well good news : most of the time, as long as you have committed something, you probably will not lose work 🙂 But in order to get rid of those dreadful times where everything is unclear, we should start by understanding how Git works.
First of all, you have to understand that a central component in Git is the commit. A commit describes one or several changes made to the repository and should generally represent a “unit of work”. The definition of a unit of work will vary depending on your beliefs, your projects and your experience ; however, it is generally good practice to have small commits in order for you and your team to have a better control over the history of your repository (better readability, easier reverts, etc…).
Branches are mostly pointers and, if you are familiar with the concept of linked list, then it’s the best analogy you can make with them. Once you create a branch it will have its own history ; several branches can and will live in parallel until you merge or delete them. Tags are simply labels. They allow you to add some kind of alias to a commit, avoiding you the pain to deal with SHA-1 identifiers every time you want to navigate. Finally, HEAD is simply a pointer to the commit you are working on. If for some reason you happen to navigate to a previous or “unlinked” commit, Git will tell you that you are in “detached HEAD” mode, which means you can’t impact the branch you were previously working on. Branches, tags and HEAD are called refs, meaning they are humanly-readable.
That’s it, there is not much more to these concepts than that. In order to add some more visualization to all this. I strongly recommend you visit this website : https://learngitbranching.js.org/
Seriously, do it. And do the first few exercises it offers ; they are small, very well explained and let you visualize how every command you type works. It is the best tool I’ve found when I was looking to understand Git and I haven’t found a better one yet.
Now, a word about working with remote repositories. Most of the time, Git is used against a central server, usually your team’s continuous integration server. This is where build pipelines will be run on, making Git less decentralized and more centralized. This is not the optimal way of using it but is still perfectly viable.
When working with remote repositories, there will be a copy of the state of those repositories and of their branches stored locally on your hard drive. They are prefixed with “<name of the remote, usually origin>/”, e.g. “origin/main”. You can see these branches as some kind of official versions of what is currently on the remote server. When synchronizing your work, changes will be copied to these branches. This will not impact you when pushing your changes to the remote, but when fetching changes or when you want to undo some of them, this is very important to know.
The fetch command will download the latest changes from the remote into your local copy but will not merge those changes into your working copy of the branch. You will need to run an additional
git merge origin/<my branch> command for them to be merged. As a convenience, this is what the pull command does : it both fetches and merges the remote branch into your working copy.
And that wraps the basic commands and concepts that will be needed in order to understand what comes next. Again, if something still feels difficult to grasp, please do visit https://learngitbranching.js.org/ as it is way more complete and understandable than these few paragraphs.
Undoing committed changes
Let’s say you have performed several commits and one of them introduced a bug. You want to “rollback” this commit. How can you do that ? Well there are 2 easy ways : revert and reset.
Reverting a commit : creating its mirror
The first command, revert, will create a new commit that does the exact opposit of what was changed in the original commit. It is a gentle way of undoing as it will not change what was previously pushed to your teammates ; with Git, it is considered good practice to avoid modifying a history that was previously committed and integrated into a shared remote branch (e.g. the “main” branch).
So how does it work ? It is easy : list the latest commits using
git log, find the commit you want to revert, copy or write down its SHA-1 identifier and type
git revert <SHA-1>. Example :
Guillaume@Gron MINGW64 /i/Dev/git (main) $ git log commit 36e745cb672cde3a8513fb701252fdd92cefcd45 (HEAD -> main) Author: Guillaume Téchené Date: Mon Jun 7 18:58:28 2021 +0200 Commit that breaks something commit 68c8f391ff299b8aa124e6a349ef706e8371e46b Author: Guillaume Téchené Date: Mon Jun 7 18:58:14 2021 +0200 Some more work commit 1d18cd3364fc359d77fef2750ec63f99f4c6241c Author: Guillaume Téchené Date: Mon Jun 7 18:58:00 2021 +0200 Initial commit with a new file
Here we would like to revert the latest commit, with SHA-1 36e745cb672cde3a8513fb701252fdd92cefcd45. Hence the command :
git revert 36e745cb672cde3a8513fb701252fdd92cefcd45
Now if we run
git log again :
Guillaume@Gron MINGW64 /i/Dev/git (main) $ git log commit 91159413754b99ff8f7087d1b542a106de57f452 (HEAD -> main) Author: Guillaume Téchené Date: Mon Jun 7 18:58:51 2021 +0200 Revert "Commit that breaks something" This reverts commit 36e745cb672cde3a8513fb701252fdd92cefcd45. commit 36e745cb672cde3a8513fb701252fdd92cefcd45 Author: Guillaume Téchené Date: Mon Jun 7 18:58:28 2021 +0200 Commit that breaks something commit 68c8f391ff299b8aa124e6a349ef706e8371e46b Author: Guillaume Téchené Date: Mon Jun 7 18:58:14 2021 +0200 Some more work commit 1d18cd3364fc359d77fef2750ec63f99f4c6241c Author: Guillaume Téchené Date: Mon Jun 7 18:58:00 2021 +0200 Initial commit with a new file
Now we can see a new commit was created. If we run a
git diff between this commit and the previous one (or a
git show 91159413754b99ff8f7087d1b542a106de57f452), we will see that the changes introduced by the first one were cancelled by the second one without modifying the first commit, preserving the history for the whole team.
Resetting a commit : changing history
Now keeping a history clean can be a valuable intent as long as it does not impact everyone else. This is typically a good idea when the changes have not been pushed yet. Hence the reset command that will move the HEAD pointer to a previous commit. This erases one or more commits from the current branch ; the commits are still in the repository but are not considered part of the history anymore. The command to be run is simply
git reset to “erase” the last commit. If for some reason you want to erase more than one commit, you can use the ~ operator in combination with HEAD, for example
git reset HEAD~4 will remove the last 4 commits from the history.
This is a powerful command that helps cleaning histories but, as with all commands that manipulate the history, be careful when using them and make absolutely sure you will not impact your coworkers.
Cancelling all uncommitted changes
Then there might be some times when you start working hard, modify several files… and then realize you were completely off-track. You have several perfectly valid commits and do not want to lose them. Most Git GUI tools have an option to undo those changes but a simple command line can solve this :
git reset --hard. That’s it, nothing more. It will undo all uncommitted changes and leave HEAD unmodified at the latest commit.
--hard option can of course be used in combination with the previous paragraph :
git reset --hard HEAD~4 will remove the last 4 commits and will undo the uncommited changes.
WARNING : undoing uncommitted changes cannot be reverted ! You will lose all your changes and they will not be recoverable since you did not commit them. So be extra careful when using the
To be continued…
This article is getting a bit longer than expected. Git is a complex but powerful tool and it is mandatory to understand the basics in order to be able to use it properly 🙂
In the next article, I will introduce a few common problems you can run into : lost commits and how to find them back, how to narrow down a search for a faulty commit, modifying the history after having pushed modifications, etc… I will use simple examples just like in the git revert case to illustrate these points. Again, I cannot stress enough the value Learn Git Branching brought to me. I still refer to it from time to time, either for a refresher or to have a better understanding at what I do on a daily basis.