Getting the most out of Git: Improving your developer experience

Thomas Siedenhans
Machine Learning Reply DACH
11 min readJan 26, 2023

Why you should care about advanced Git Features

Git is a powerful tool that is widely used in the software development industry for version control and collaboration. It allows developers to track and manage changes to their code and maintain a history of all versions of their code. One of the key benefits of using Git in software engineering is its ability to handle large and complex codebases. With Git, developers can easily manage multiple versions of a codebase, making it easier to track and resolve conflicts when working on a project with multiple team members.

You can think of Git as a tool for effective and efficient collaboration of code.
According to the latest StackOverflow Survey in 2022, more than 96% of professional Software developers are using Git already.

While working on different projects, I noticed that most developers are familiar with the basic concepts of Git, such as git fetch/pull/push/merge. These concepts are essential for working with Git and allow developers to collaborate on code with others in a very meaningful way already. However, it is an invaluable skill to gain a deeper understanding of Git and its advanced features.

What you could be missing out on

As most developers are already familiar with Git, it might be easy to get comfortable with the existing knowledge about the topic and not worry about other approaches or maybe more ideal workflows.
In the end, it is important to get to your goal quicker: Get the feature/bugfix/ etc. you worked on merged into the codebase. The following areas are where I identified the biggest gain to be had from improving knowledge about certain Git-Features:

Efficient Conflict Resolution: Conflict resolution strategies such as interactive rebasing are a huge help when contributing to a larger codebase and as a result, some people may struggle to resolve conflicts cleanly and effectively when not aware of this feature. As the interactive rebase is one of my favorite git features, I will go into more detail later in this article.

Better codebase Management: A lot of developers may not be aware of advanced features such as submodules, git-lfs and git-flow. Those could help them manage and organize their codebase more efficiently, and support the work with large files and multiple versions of the codebase. As these topics are more constrained towards larger and composed repositories, I will focus more on parts where developers can gain more universally applicable knowledge.

Improved Efficiency: Features such as stashing and bisecting, can save a lot of time by allowing you to quickly switch between different tasks, and help troubleshoot issues more efficiently. You will be pleased that we explore some of these concepts in the later parts of this article.

Better Code Auditing: One lesser-known concept is the idea that the Git SourceTree can be used to understand how the codebase has evolved over time, and to identify the source of bugs or issues. I have to mention one amazing talk from Adam Tornhill on this topic and other aspects of technical debt. He goes into further detail on how we can utilize Git for more efficient analysis of complex code pieces, which might be in the need to be refactored and also how to make the offboarding of a valuable team member a way better experience for the teams' efficiency. Amazing and valuable content!

Interactive Rebase

Personally, this is one of the features I enjoy the most while working with git. You get the possibility to rearrange, squash, drop, edit, … commits as you desire.

Assuming you have the following 4 commits in your branch:

6d9c37b HEAD@{0}: commit: badly worded refactoring
05f8e5a HEAD@{1}: commit: Important bugfix
a9f4c34 HEAD@{2}: commit: just an addition to the bugfix
29f2311 HEAD@{3}: commit: this commit can be dropped

By running the following command you can start the interactive rebase (in this case you want to rebase your existing branch on the dev branch)

git rebase -i origin/dev

The following “git-rebase-todo” file will open in your standard text editor (we will close it after inserting our commands):

pick 6d9c37b badly worded refactoring
pick 05f8e5a Important bugfix
pick a9f4c34 just an addition to the bugfix
pick 29f2311 this commit can be dropped

There is a multitude of very useful commands to use when altering these commits, but seeing the changes here I would resort to the following ones: The first commit is worded badly, so we can write “reword” instead of “pick” to get an opportunity to reword this. (Your coworkers and your future self will thank you for this 😊) We also notice that commits 2&3 are both the same bugfix, the 3rd commit is just a small addition we might have forgotten in the first commit. To fix this, I would use the “fixup” command instead of “pick” for the commit a9f4c34. This will merge that commit into the previous one, keeping only the commit message of that branch. If you want to keep the second commit message, use “squash” instead of “fixup”. Furthermore, we notice that the last commit was a failed try and we want to drop these changes without cluttering our commit history any further.
For that commit, we change “pick” to “drop” which removes that commit.
This is how our file will look after the adjustments:

reword 6d9c37b badly worded refactoring
pick 05f8e5a Important bugfix
fixup a9f4c34 just an addition to the bugfix
drop 29f2311 this commit can be dropped

Afterwards, save and close this git-rebase-todo file and the rebase will start. You will be prompted with an input to provide an alternative to the badly worded commit, where we stated that we wanted to rename it instead. After closing that file, we are done and have our branch tidied up and up to date with the dev branch!

We just rewrote Git-history!

That’s why we have to force-push our changes with

# "git push -f" works, if you are working in your branch on your own
# To be sure, we will use the safe option (while working in a team):

git push --force-with-lease

For that reason, some developers have a harsh stance on merging vs rebasing in Git to streamline different branches. After having engaged in a few of these conversations and having used both merging strategies, I adopted the following approach:
In a branch where you are working alone without others depending on your commit history (e.g. a feature or bugfix branch where you work and finish your task usually alone) it is much more comfortable to use the (interactive-)rebase to polish your commit history and to avoid having a merge commit in your own branch when resolving Conflicts with the parent branch.
But: As stated, rebasing is essentially “rewriting history”. I would not do it in a branch where other developers are basing their work off. A very good example of this is the development branch. I would not squash/drop/rewrite commits for that branch, as it would be confusing to other developers when they want to merge their changes back into this “common ancestor” branch.

Git reflog

The Git reflog command is a valuable tool for viewing the history of a Git repository. It shows a list of all the recent changes to the local repository, including branch updates, changes to the HEAD reference, and other actions. This can be useful for tracking down when a particular change was made, or recovering a lost commit.

For example; you have made some changes to your repository and you want to view the history of those changes. You could use the following command to view the reflog:

git reflog

This might produce output like the following:

6d9c37b HEAD@{0}: commit: Added a new feature
05f8e5a HEAD@{1}: commit: Made some changes to the code
a9f4c34 HEAD@{2}: commit: Fixed a bug

This shows that three commits were made recently (I hope you noticed that the commit messages were not ideally worded 😉), in the order shown. The HEAD@{0} entry refers to the most recent change, and each subsequent entry refers to an earlier change.

For example, if you wanted to revert to the “Fixed a bug” commit shown above, you could use the following command:

git reset — hard a9f4c34

This would reset the local repository to the state it was in when the “Fixed a bug” commit was made, and all the changes made since then would be discarded. This can be a useful way to undo a series of mistakes, e.g. to recover from a failed experiment.

Since the changes to the local repository get shown, this is also an amazing tool to recover lost commits, for example those of a deleted branch. In that case you would check which of the shown commits you want to recover and perform a cherry-pick:

Git cherry-pick

Git’s cherry-pick command is a useful tool for selectively applying changes from one branch to another. It allows you to choose specific commits from a branch and apply them to your current branch, without merging the entire branch. This can be useful in a variety of situations, such as when you want to cherry-pick only a bug fix from one branch without applying other changes to your current branch, or when you want to pick only a few important changes from a large branch and apply them to your current branch.

For example, suppose you have a Git repository with two branches: dev and feature. The feature branch contains several commits, including a commit that fixes a bug. You want to apply the bug fix to your dev branch, without merging the entire feature branch. You could do this using the cherry-pick command, like this:

# switch to the development branch
git checkout dev
# cherry-pick the commit that fixes the bug
git cherry-pick <commit-hash>

This would apply the changes from the specified commit to the development branch, without merging the entire feature branch. You could then continue working on the development branch, knowing that the bug has been fixed.

You can also use the cherry-pick command to pick multiple commits at once. For example, if you want to cherry-pick the last three commits from the feature branch, you could use the following command:

# switch to the dev branch
git checkout dev
# cherry-pick the last three commits from the feature branch
git cherry-pick feature~3..feature

This would apply the changes from the last three commits on the feature branch to the development branch. You could then continue working on the development branch, with the changes from the feature branch incorporated.

It is also possible to cherrypick only the changes to a single file in a specific commit by specifying the filename with the -x flag, but this will most likely lead to merge conflicts at a later stage which have to be resolved manually.

Git bisect

Git bisect is a feature in Git that allows developers to find the commit that introduced a bug in the codebase by using a binary search algorithm. It works by allowing the developer to specify a “good” commit that doesn’t have the bug and a “bad” commit that has the bug, and then Git will automatically find the commit that introduced the bug by checking out the middle commit between the good and the bad commit and asking you to test if the bug is present in that commit.

If you have a rough understanding, that a specific but has been introduced between these 2 commits (which might have a multitude of commits between them):


6d9c37b Added a new feature # <- The bug is already here!
… (possibly 100+ commits merged overnight)
a9f4c34 HEAD@{2}: commit: Fixed a bug # <- You know the bug is NOT here!

To find the exact commit, which introduced the bug, you would type

# git bisect start <bad commit> <good commit>
git bisect start 6d9c37b a9f4c34

Following this command, git will select commits for you to test if the bug is present. If that is the case, type “git bisect bad”, if the bug is not present type “git bisect good”. This process will be repeated until you find the commit which introduced the bug you are hunting.

Thanks to the logarithmic scaling of the binary search, very large repositories with possibly over 1000 commits between the last “good” state can be searched quickly to find the commit that contains the bug.

Git Worktrees

Git worktrees are a feature of Git that allows you to have multiple working directories for a single Git repository. This allows you to work on multiple branches or versions of your code simultaneously, without having to switch between different local repositories or clone the repository multiple times.

To create a Git worktree, you can use the “git worktree” command. For example, if you want to create a new worktree for the “development” branch of your repository, you would run the following command:

git worktree add ../development development

This creates a new directory called “development” in the parent directory of your current repository and sets up a working tree for the “development” branch of your repository in that directory. You can now quickly check if some certain behavior is present on the “development” branch while continuing to work on e.g. the “feature/XXX” branch in your original repository. For me, this Git feature grew on me more and more, as I got to appreciate the time I saved by not having to either stash my changes and checkout (which can be a nuisance when going back and forth to check a multitude of changes) or clone and build the repository in another directory.

Git Aliases

Git aliases are shorthand commands that you can create to simplify and customize your Git workflow. They allow you to define a custom command that runs a series of Git commands or options under the hood.

For example, you like to look at your working directory diff by typing something like 
git diff --stat --color-words --ignore-all-space --find-renames --find-copies

To create a Git alias to prevent you from having to type all these flags every time, you can use the “git config” command. For example, to create the “dif” alias described above, you would run the following command:

git config --global alias.dif “diff --stat --color-words --ignore-all-space --find-renames --find-copies”

Once you have created an alias, you can use it just like any other Git command. In the example above, you would type “git dif” to run your command.

Git aliases are a convenient way to streamline and customize your Git workflow. You could also use them to execute multiple git commands in sequence. By using them, you can save time and effort and make your Git experience more efficient and enjoyable.

Where to go from here:

I am aware that there are many more features of Git, where I did not go into further detail, but my main goal is to provide solid groundwork for you as a developer to gain confidence in your abilities to work with advanced Git features. I hope you learned one or two things and maybe even picked up a cool Git-Feature you did not know about.

If I made you interested in discovering more about Git as a tool I can recommend you to have a look at the official documentation, where you can dive deeper into more of the commands mentioned and even check out some features which were left out: https://git-scm.com/docs .

For people who want to know more about the history of Git: There is an amazing Tech Talk from Linus Torvalds, who created Git for developers to have an easier time contributing to his Linux codebase, and especially for him to have an easier review process to code changes: https://www.youtube.com/watch?v=4XpnKHJAok8
This talk is very interesting, amusing and somewhat edgy in the good old Linus Torvalds fashion, which many of you will enjoy. I highly recommend giving it a listen!

--

--