Git 101: Beauty and clarity: everyone will love your branches
This post is one of the Git 101: Conquer the GIT POWER series.
After many hours of developing and making fixes, your branch will have a lot of redundant and meaningless commits. In this post, we will learn how to reorganize your branches from this:
To this
Summary:
- Intro
- Check your git text editor
- Create a temporary Branch
- Find where to start
- Interactive Rebase
- Analyze commits and fixup them
- Push force the private branch
- Outro
1 - Intro
This post uses the repository https://github.com/ribas89/git-101-examples as an example. The branch main is the public branch, that everyone uses and can't be modified, and the branch organize-commits-1 is our private branch, that we create to code and do our things. You can only reorganize branches that only you use.
NEVER REORGANIZE A PUBLIC BRANCH
The entire team uses developer, master, and other public branches. Modify them WILL result in COMPLETE CHAOS.
2 - Check your git text editor
When organizing commits, we use some form of text editor. This command list the current editor:
git config --get core.editor
If you want to use another one, for example, the Notepad++:
git config --global core.editor "'C:/Program Files/Notepad++/notepad++.exe' -multiInst -notabbar -nosession -noPlugin"
3 - Create a temporary Branch
Let's create a temporary branch to organize the commits. I explained why this method helps in this post: https://ribas89.dev/git-101-how-to-stop-messing-up-with-branches-in-git#heading-3-when-in-doubt-use-a-temporary-branch
git checkout -B temp
This command creates a temporary branch named temp and resets with the commits of our organize-commits-1 private branch. We can see that with the git repository browser (gitk):
gitk
4 - Find where to start
In this exercise, the point where we want to start is when feature A was added. But as a general rule, we always start with the first commit from our private branch. That way, we don't have to overthink when to start. Just use the first commit on your private branch as a reference.
After finding the commit, copy its hash. The hash of the commit feat: add feature A is be8eb38c304d8605b2068465763c65d370fe6527. We will use this reference to find the commit before it, so with the tilde ~1 e execute this command:
git rebase -i be8eb38c304d8605b2068465763c65d370fe6527~1
If you are like me and don't like relative references, you can copy the hash directly but will have to analyze the branch first. So the first commit of our private branch is the feat: add feature A. What is behind it? The commit Initial commit. We copy the hash of this commit instead and execute the command like before:
git rebase -i 9bd8e179d0e41204ee19f2ffe84dbb92b2962ce8
After executing the git rebase command, you should get a list of the commits we will reorganize.
5 - Interactive Rebase
We are now rebasing our branch with git rebase -i. The list below contains our commits:
The first commit in the list is the first commit we pushed inside the branch. The last commit in this list was the last commit we pushed in the branch.
Every commit starts with the default action pick which means use this commit as is. The new git commit history will be exactly as this list. So when you change a commit from one line to another, the branch will have this new order when the rebase ends.
Without overthinking, let's reorder the commits by which feature the commit was about.
pick ef6bbf0 feat: add essential component for feature A
pick be8eb38 feat: add feature A
pick 3fefd23 fix: fix feature A
pick 2aba549 fix: fix feature A
pick 3ff5632 feat: add feature B
pick 93b4e36 fix: fix feature B
Save the file and close it.
After you close the file, the git rebase -i command finishes, and the commits inside the temporary branch are reordered. If you pay attention to the image, now the first commit has a different hash. It is not the be8eb38c304d8605b2068465763c65d370fe6527 we found before.
This is the expected behavior of the git rebase command. It changes the commit hashes. And that is the reason you NEVER rebase the public branches. Our branch has an entirely different commit history now. But that is okay because it is only our commits we are changing.
6 - Analyze commits and fixup them
The pick action is the most common action we will use to reorganize branches. The second is the fixup, and the reword is the third.
The fixup action means what it is supposed to be. When you mark something with the fixup action, that commit will combine with the upper commit, resulting in only one commit with both codes. Using this knowledge, let's rebase the branch and analyze some commits.
git rebase -i 9bd8e179d0e41204ee19f2ffe84dbb92b2962ce8
Now we have the new ordered list:
pick 5e2dcb2 feat: add essential component for feature A
pick 0c676d5 feat: add feature A
pick af31de5 fix: fix feature A
pick 7e2178d fix: fix feature A
pick ded69ef feat: add feature B
pick 521a4c5 fix: fix feature B
We can start by comparing these two commits:
pick 0c676d5 feat: add feature A
pick af31de5 fix: fix feature A
The first adds a feature, and the next is a fix for the same feature! It happens because we usually commit some feature, find a bug, and create another commit with the fix.
Well, since these commits are only in our private branch, we can combine them with the fixup command like that:
pick 0c676d5 feat: add feature A
f af31de5 fix: fix feature A
Changing pick to f will do the trick because f inside the interactive rebase is a shortcut to fixup.
Now we have the following commit list:
pick 5e2dcb2 feat: add essential component for feature A
pick 0c676d5 feat: add feature A
f af31de5 fix: fix feature A
pick 7e2178d fix: fix feature A
pick ded69ef feat: add feature B
pick 521a4c5 fix: fix feature B
If you look closely, the commit 7e2178d is another fix for feature A! When this happens, we can change it to fixup too!
pick 5e2dcb2 feat: add essential component for feature A
pick 0c676d5 feat: add feature A
f af31de5 fix: fix feature A
f 7e2178d fix: fix feature A
pick ded69ef feat: add feature B
pick 521a4c5 fix: fix feature B
Now both fixes will combine inside the commit, which adds feature A. But wait, there is another problem with feature A. The first commit, which adds an essential component to this feature, is loose in another commit. What should we do? Well, let's first combine feature A with this commit, using the fixup command again:
pick 5e2dcb2 feat: add essential component for feature A
f 0c676d5 feat: add feature A
f af31de5 fix: fix feature A
f 7e2178d fix: fix feature A
pick ded69ef feat: add feature B
pick 521a4c5 fix: fix feature B
In that configuration, all feature A commits will combine inside the feat: add essential component for feature A. But with this configuration, the commit will have the entire code necessary for feature A to work correctly, not just the essential component, which means this commit message is wrong now.
To rename this commit message, we can use the action reword with its shortcut r. That way, we have the following commit list:
r 5e2dcb2 feat: add essential component for feature A
f 0c676d5 feat: add feature A
f af31de5 fix: fix feature A
f 7e2178d fix: fix feature A
pick ded69ef feat: add feature B
pick 521a4c5 fix: fix feature B
Let's save those actions and start the rebase process by saving and closing this file. The rebase combine the commits, and another text file appears with the title COMMIT_EDITMSG that file contains only the commit we put the action reword:
Let's change it to feat: add feature A. Save and close the file.
Now let's see what it looks like in the repository browser:
All feature A commits is now one commit, with a good commit message! But it seems we forgot the feature B commits. That is okay because we can rebase the branch as many times as necessary to reorganize it:
git rebase -i 9bd8e179d0e41204ee19f2ffe84dbb92b2962ce8
That command now will result in a commit list with only three commits:
pick 5abcad5 feat: add feature A
pick 0b05ad9 feat: add feature B
pick 578fdfa fix: fix feature B
It should be easy by now, just fixup the commit fix for feature B, and that's it.
pick 5abcad5 feat: add feature A
pick 0b05ad9 feat: add feature B
f 578fdfa fix: fix feature B
Save and close the file. For the last time, let's check the repository browser:
And there is! The branch now appears much more clean and professional than before. We combine all commits related to each feature, and each commit is an independent executable point of our software, as it should be. Now there is only one thing left to do.
7 - Push force the private branch
Everything we did was inside the temporary branch. It is time to put our new, reorganized commits inside the private branch.
As we saw before, the commit history changed completely. That means we can not use the default git push, because hashes are different now. To make these new commits, the new private branch lets checkout -B it first:
git checkout -B organize-commits-1
That is the same command we used to make the temporary branch, but now we are erasing the private branch with our new commits.
We only changed the local branch. To change the remote, let's do a push force to change the remote branch too. The push force command follows the pattern:
git push origin +[BRANCH-NAME]
Where [BRANCH-NAME] is the name of the remote branch, in our case organize-commits-1
git push origin +organize-commits-1
The remote branch was forced to update with the new commits. Well done!
8 - Outro
The git rebase is a command that can be scary when you start using it. You can do just fine with temporary branches and some patience. Just remember that you should NEVER use this command in public branches, because you can mess with the entire team. For your local commits or private branches is fine and helps a lot when you create the push requests from your branch to develop or even master.