DEV Community

Josef Andersson
Josef Andersson

Posted on • Updated on

A clean Git history with Git Rebase and Conventional Commits

Other Git related articles on DEV from me:
Git signing and signoff like a champ
The "linear-git-commit-history-with-signing-in-github-ui"-workaround

Introduction

As developers we always strive to improve and simplify our processes. One important motivator is less time spent on future guesswork. We also would like that new developers in our projects will be able to quickly focus on their task instead of diving into a mess of lasagna and spaghetti code.
And we all know that the "new developer" is in many cases you, a half year later when you are wondering what the hell you were thinking about many commits earlier.

One part of making life simpler for everyone is by keeping a clean Git history. Now, What is a clean Git History? Well, you can find different opinions on that, but for me (and quite a few projects) a nice Git history:

  • tells the reader something in an expected understandable way
  • makes a feature coherent.
  • makes it easy to revert or cherry pick a feature
  • tells the reader that the former contributors cared about the project and new contributors

So, caring about your Git history is a well spent investment for your projects, and surely worth the small extra time it might take to learn.

This article will give you a short opinionated lightweight view of how to achieve that.

The basic rules:

  • The commit that goes into the main (or develop etc) branch should aim to be atomic, aiming to contain only the feature you are working on
  • While working in your feature branch, your commit messages can look in whatever way you wish: wip and so on. But, the important thing is that when you are done with your feature and ready to go for a Pull Request you first rebase your feature branch and clean up your commits.
  • The feature commit should have a clear defined message - Don't re-invent here - There exists a fairly used and accepted convention called Conventional Commits, so we are going to use that.

Rebase and avoiding merge commits

Rebasing might sound scary if you are not familiar with it, (overwriting history, yikes?), but it is really not after you got the hang of where to, and how to. It puts your commits on top of whatever you rebase against.

Instead of when you do Pull Requests with merge:

Image description "Merging main into the feature branch" by Atlassian is licensed under CC BY 2.5 AU

You will do this with rebase:

Image description "Rebasing the feature branch onto main" by Atlassian is licensed under CC BY 2.5 AU

And in practice, the history of your commit graph will look like the one to the right, instead of the one to the left:
Image description"Messy left vs linear right" by Unknown is licensed under Unknown

Much cleaner to follow and understand. And, just ignore the commit messages themselves for now - we will improve upon that soon.

Basically you will use rebase in a few different ways.
One is when you pull from the current branch. Always use pull with --rebase flag.

git pull --rebase 
Enter fullscreen mode Exit fullscreen mode

Another case is when you pull your changes from main/develop into your feature branch


git rebase --interactive origin/main

Enter fullscreen mode Exit fullscreen mode

(instead of doing git merge )

Finally, the last case is when you squash all your features in your feature branch - before submitting your Pull/Merge-Request.


git rebase --interactive <sha-with-the-commit-you-are-squashing-from>

Enter fullscreen mode Exit fullscreen mode

This article wont go into every detail about learning you how to rebase, as there are many good articles about that already - I recommend that you read more about it in the following links:
Merge vs Rebase
Git Rebasing

And, then practice. Set up a git project and add a few commits, and a branch or two and start experimenting. If you don't try it for real, you will never feel comfortable.

So, now we now how to get a linear, easy to follow Git graph. But if you look at the earlier comparison picture above, there is still much to improve upon regarding the commit messages themselves.

Enter..

Conventional Commits

It is a simple convention that is used in quite a few Open Source projects. I think it originated in the Angular community (not sure here).

Image description

The Conventional Commit format looks roughly like this:

<type(optional scope): imperative of what it does

So a practical example message could then look like this:

fix: add a validator for the fluxcapacitor class

But, for the real format read more about Conventional Commits.

The great thing of not re-inventing things like commit message standards is that you

  • bypass endless organisation and team discussions about how to do things in certain way. Instead you can agree, avoid distractions and focus on the important work - making a good product
  • make it easier for new people to dive in to the project
  • can use tools written for that standard - in the case of Conventional Commits, autogenerate changelogs and more.
  • write better commit messages, as you have to give them a bit of thought

A rebase/conventional commit workflow example in practice

Lets walk through the life of fictional feature branch.

Start: You start your feature-branch from a branch, here main, (but might as well be develop or however you are working with branches).

git checkout -b feature/my-branch
Enter fullscreen mode Exit fullscreen mode

working....

taking a long break so

git commit -m 'chore: a commit'

git push -u origin feature/my-branch
Enter fullscreen mode Exit fullscreen mode

working ...

git commit -m 'wip gotta go'

git push
Enter fullscreen mode Exit fullscreen mode

Ah, meanwhile, new features (commits) was added to main by collegue Knuth, so you update your feature branch:


git rebase -i origin/main
Enter fullscreen mode Exit fullscreen mode

back to working...

git commit -m 'docs: last docs update'

git push
Enter fullscreen mode Exit fullscreen mode

Finally, ready to proudly clean up our feature commit history, before submitting a Pull/Merge Request:

git rebase -i <sha-to-latest-commit-before-the-feature-branch>
Enter fullscreen mode Exit fullscreen mode

OR, if you read my article about signoff and signing :) :)


git rebase -i --signoff --gpg-sign <sha-hash-of-the-commit-before the-the-feature-branch>


Enter fullscreen mode Exit fullscreen mode

Now you will enter Git's interactive rebasing mode, which shows a list of the commits (from the hash you choose), and you get a list of options too.

Default 'pick' just takes the commit as is. Most often I do r (reword) for the top commit (oldest) and f (fixup) for the others. f means the commit contents will be squashed into the previous commit, and so we will end up with a single commit that we got the chance to reword. But see the example in next section.

So, With a lot of in work-in-progress commits on the feature-branch, you want to squash every commit with 'f' so there only will be one commit on the end. Except the last one that you want to reword with 'r'.

Image description

After saving, you are given the dialouge to reword the commit you choose 'r' for. So just rewrite the commit header message as you please, here as a Conventional Commit:

Image description

Finally, after saving, and rebase signals done succesfully, you do a quick sanity check that everythings seems fine and then force push (to the feature branch).


git push --force-with-lease

Enter fullscreen mode Exit fullscreen mode

Done! Oh,,, then you remember - you want to improve your feature commit by adding a detailed description. So you rebase again (the now cleaned up, single commit), and add a description body.

Image description

And, as you have rewritten your feature branch history again, you will also need to force push again.

git push --force-with-lease
Enter fullscreen mode Exit fullscreen mode

And cheers - open a Pull Request after verifying everything was fine.

F.A.Q

When should I not rebase?

  • Never rebase and force push to a public branch without telling everyone first. In other words, only force push to your feature branch. That is a golden rule, because if you do push to common branches, you might overwrite others commits, mess up their forks and, in general create confusion for other developers. However as long as you are in your feature branch, do as you please. There are advanced cases where you want to, or even need to force push to main/develop etc, but that is out scope for this article.

Is rebasing widely used?

  • When submitting to Open Source projects it is not uncommon to get comments in the style of "looks fine, but please rebase your feature branch into one feature",

What is the --force-with-lease good for?

  • If you force push, make a habit of using --force-with-lease option instead of --force, for a bit of extra safety - it will stop you from overwriting other peoples eventual added changes and stop the push.

Should I always add a Git description?

  • You be the judge, but for big or hard to understand features, please take the care to add a longer description (body) besides the header message.

Should I sign and signoff commits?

How can I enforce this in GitLab?

  • To enforce a linear history in GitLab UI:

Image description

How can I enforce this in GitHub?

  • To enforce a linear history in GitHub UI:

Image description

But, if you also sign your commits - Sadly Rebase with UI is broken in GitHub, and you will lose your signature. There is a simple CLI workaround, read more under the F.A.Q in this article for a workaround hint.

Can I reuse this information?
All content in this article is licensed under the CC0 if not stated otherwise. Which means, use as you please, no need for any attribution. Please add suggestions and corrections if I mixed up something, or that you feel something important is missing.

Top comments (0)