Skip to content
Baptiste Clavié edited this page Jan 31, 2025 · 1 revision

How to Git Good

Git is the tool we’re using to keep a version of the code at all time and also contribute to it. But it also has a few quirks to use it properly, so let’s see that in this document.

I won’t do git’s history, as multiple sources already does it kinda well (such as the official website, the git pro book, github, … pick one), the goal here is to establish a git workflow that won’t hinder us while working and developing the apps we’re aiming to develop.

If you have any questions, feel free to reach out to me, I’d be happy to answer anything related to that (and other stuff as well but let’s keep in the topic !).

Good practices

Commit

A good git commit is a commit that does one and only one thing (eg Patch the test env). It is really important that they do, and not do some kind of shopping list (such as done this, this and that) . They are called Atomic commits.

Even better, think of the git commit message as a headline for an article in ideally less than 50 characters. You can also be more detailed in the body of the git commit message (lines ≤ 70 characters). To take the patch env test commit :

Patch the test env

The .env was not properly loaded, making running tests on a local env
not working if you do not have the same postgres setup.

An amelioration would be to properly use the .env loading, but that
will be for another time. :)

As you can see, I summarized the commit in a few words for the title, and detailed in the body why this was done, with improvements to do if we want to do them. If you can’t summarize your commit in a few words with a proper sentence, that probably means your commit is doing too much.

Managing branches

Branches have two origins ; local and remote. Remote branches are what you pull (e.g the current state they have on Github in our case), while local branches are the ones you are currently working on in your environment. It’s actually a bit more complicated than that, but let’s stick to this definition for now (see the Git Book if you’re curious).

To update the local branches with the remote branches, there are several ways to do it ;

  • git pull, which will pull the remote branch, and try to apply it to the current branch. I strongly recommend to use the --rebase option with it, so that your commits stay on top. Even if this command is practical, I also strongly recommend not to use this method, as it can be hell to manage whenever you get a conflict.
  • In two steps, which is my preferred way :
    • git fetch -apPt origin. If you’re confused by the options, what it basically does is to fetch all the remote branches, tags (as fetch doesn’t do it by default), and prune branches and tags that are not existing anymore on the remote. This command allows you to be sure to have updated remote branches.
    • git rebase origin <branch name> to update your branch with the remote. We’ll discuss the rebase command in the builtin tools, as it is a bit particular as it rewrite the history, so it needs to be a bit careful, but shouldn’t pose any problems if your branch is clean.

Unless the branch is a big feature integration and you know multiple people will work on said branch, each and every branch should be personal and having one and only one person on it. If multiple person are working on a feature, each one of them should have a branch to be merged into the feature’s main branch. This is pretty important for the next parts, and will be detailed in the workflow section. There are of course a few exceptions, but then it can get rock n’ roll. :D

In this regard, I’d advise you to not make local version of remote branches when you don’t need to actively work on them ; use the remote branch when you need to merge or rebase branches into your own branch instead.

Merge vs Rebase

A few builtin tools exist to manage the git tree. I won’t detail all the tools, but the main ones I think will be pretty useful, especially for the proposed workflow : git merge, git rebase being a few of them. We’ll see how and in which context we should use either one of these two commands.

Git merge

While merging a child branch into a parent branch is admitted as being git merge command main purpose, some may be tempted in using the command to update child branch through git merge parent , which can be a bad idea.

Why is this a bad idea ? It may avoid you conflicts (and solve all of them in one go), but then you will need to solve conflicts on things you didn’t work on, so you won’t have a clue on how to solve them. And the more merges you make, the more the probability of having conflicts on how you solved them before will rise. The same goes when updating your local branch with a remote one.

Another good practice for merge workflows is the --no-ff option, which keeps a merge commit and allows to separate the commits merged from the ones in the branch receiving the merge and keep a clean history.

So please, keep your merges from child to parent, and use git rebase instead when you can, which I will explain right now. :)

Git rebase

Rebase is a tool to get the tip of the origin branch specified in the command (git rebase <tip>, such as git rebase origin/main ), and re-apply the commits you have since the start at the top of the commits of the specified branch. This is what we call rewriting history.

This command allows you to keep a clean history within your branch, and also staying up to date with a parent branch. But this comes with a few caveats, such as the mentioned history rewriting, and if you already pushed your branch, having to push-force it through the git push -f origin my-branch command.

Handling conflicts with rebase

When doing a rebase, and that’s where making git atomic useful is pretty much mandatory, you may have conflicts. What you need to do in that case to make solving them a non-problem, is to know what your commit was intending to do at the time. For example, let’s say you have the following commit (note, this is a real commit but I truncated some parts on purpose) :

commit 74d2046c0ffced82f7609f78aa5a84f943073708
Author: Taluu <me@localhost>
Date:   Thu Jan 23 12:35:18 2025 +0100

    Fix naming for types
    
diff --git a/protobuf/types.proto b/protobuf/types.proto
index 2f55b5de..3adde8d2 100644
--- a/protobuf/types.proto
+++ b/protobuf/types.proto
@@ -59,7 +59,7 @@ enum MatchQueryCompany {
 enum ObjectType {
     TYPE_UNKNOWN = 0;
 
-    TYPE_ACCOUNT = 1;
+    TYPE_CONTACT = 1;
     TYPE_COMPANY = 2;
 }

So now, let’s say there is a conflict on the TYPE_ACCOUNT value, so that it’s 0 instead of 1 for example. When rebasing, git will complain that it has a commit, and ask us what to do. So, this commits tells me my goal was to use the CONTACT term instead of ACCOUNT ; even if further down the line, I may or may not have changed the value for whatever reason, or the name, it doesn’t matter ; what matters is that at this point in time, you just wanted to change the ACCOUNT name. So the proper way to handle this conflit would be to change the name, and just the name. Rather than bothering with “now it looks like this”, and this is pretty important, you should bother with “it should look like this after this commit was applied”.

Interactive Rebase

Another tool that is available with git rebase is the interactive git rebase, accessible with the -i option : git rebase -i origin/main for example, that allows you to either pick (reapply) commit, fixup commits (merge two commits in one, dropping the message for the fixed commit), squash commits (same thing but keep the 2 commit messages) or drop commits. Other commands are available as well, but let’s keep to the most useful ones.

git rebase -i origin/main-branch

pick 9223c69b Add proto for search messages
squash 90287abc Import types proto
pick 74d2046c Use "contact" instead of "account" in object type
fixup 85a286ef "contact", not "kontakt"
drop 920b27cf rename other stuff

# Rebase 1bc51eb7..742292b8 onto 1bc51eb7 (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
#                    commit's log message, unless -C is used, in which case
#                    keep only this commit's message; -c is same as -C but
#                    opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
#         create a merge commit using the original merge commit's
#         message (or the oneline, if no original merge commit was
#         specified); use -c <commit> to reword the commit message
# u, update-ref <ref> = track a placeholder for the <ref> to be updated
#                       to this position in the new commits. The <ref> is
#                       updated at the end of the rebase
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.

As you can see, in the rebase prompt, I chose to squash a commit, fixup one and drop another. Once git goes through each command, if it needs information (such as for the squash command), it will stop and ask you what to do : in this case, we want to merge the two commits, so we may need to alter the first commit message in Add proto for search feature. The fixup command just takes the commit it is fixed up onto (in this case, Use "contact" instead of "account" in object type , as there’s no need to know that we made a typo somewhere). And as stated in the rebase prompt, the drop is not even necessary, deleting the line is enough but it’s there for example’s sake. If you want to push even further, you can also edit commits (e.g split them apart), rename them, …

As with the non-interactive command, if it encounters a conflict, it will notify it and ask you to take an action if it can’t solve it himself.

A workflow proposal : The feature branch workflow

Sources