Post

A practical approach to keeping a clean Git history

Preamble

Here we’ll talk about Git.

I’d like to start by saying that there is no single universal solution.

The approach presented here may not perfectly match your needs. But it can very likely serve as a starting point — or as ideas you can pick from — to build a workflow adapted to your context 🙂.

Vision

If we consider that the best practices come from those who practice, we should listen to the developer community and learn from past experience.

That said, this is not about blindly following what is written somewhere, but about finding what works for us and for the team we work in.

Managing long‑lived main branches

The well‑known main branch — previously called master. Often accompanied by a reassuring and long‑standing dev branch.

The key idea is that these two branches serve different purposes and therefore imply different behaviors.

Otherwise, it’s simpler to start with a single main branch and introduce another one later if needed.

Indeed, the more long‑lived branches you have, the higher the risk of conflicts and forgotten updates.

These are also typically the branches where CI/CD pipelines are attached.

…and short‑lived secondary branches

These branches are usually linked to a new feature, a bug fix, a refactoring task, or a documentation update.

They are often induced by a User Story or a Product Backlog Item (or one of their subtasks), depending on the project management framework in use.

Such branches should live as briefly as possible to avoid conflicts with changes already merged into main branches — which in our example are dev and main.

Changes are then merged via a PR/MR.

Choosing between squash / rebase / merge when validating PRs

During validation, favoring rebase can reduce long‑term conflicts compared to always using squash or merge.

This preserves the history of the destination branch.

A squash remains useful, especially when a developer works with many small commits. For example, when practicing TDD, squashing all commits into one may improve readability of the main branch history.

Finally, a merge commit creates a new commit that aggregates all previous ones. In terms of readability, it clearly shows that the change originated from a past work branch.

Using prefixes in commit messages

By following conventions such as https://www.conventionalcommits.org/en/v1.0.0/ — or an adapted version that best fits your project context.

For example, if I add a new feature, I might push a commit with a message like:

1
2
3
feat: file name must be less than 50 chars

to follow the XY principle

It’s generally recommended to keep the main line under 50 characters and add more details below if needed.

For instance, when fixing bugs or performing refactors, I often include references to articles, RFCs, or API documentation to justify the change.

Small commits or large commits?

A large commit may involve dozens or hundreds of changes. This can make sense at the end of work, when everything is ready for a PR/MR.

The rule is that there is no strict rule 🙂. The most important thing is to commit regularly.

This helps test behavior and quickly detect failing tests (assuming tests cover the modified code…).

Conclusion

I’ve talked a lot about Pull Requests / Merge Requests because this workflow is very common today.

Of course, it is possible to work differently — for example with pair programming or mob programming — and push directly to main branches.

In very small or highly experienced teams, and depending on context, this may be perfectly fine as long as it works.

From my own experience, whether in pair or mob programming, going through a PR/MR still brings value. It allows CI checks to run and also gives the opportunity for a fresh review the next day.

References


Have I already mentioned my favorite universal rule, inspired by the scouts? Probably not enough 🙂.

Try to leave this world a little better than you found it. (Baden‑Powell)

This post is licensed under CC BY 4.0 by the author.