Ask HN: What Git branching strategy do you use in 2023, and why?

8 points by rufugee ↗ HN
I'm working with a new small team (less than 8) and converting them to Git. It's been awhile since I started with a green field when it comes to branching strategy. What do you follow in 2023, and why?

16 comments

[ 3.7 ms ] story [ 50.1 ms ] thread
Still on Gitflow.

It's rather simple for a new employee to understand, meshes in nicely with CI/CD systems and the release processes of the places I worked at.

I think git flow has a lot of merit despite being classified as outdated by many.

Many modern workflows literally have has you prematurely deploying experimental code into production. Hidden behind feature flags. Release branches may not be needed when you do that, but that's a transfer of complexity, not an elimination.

Also the alternatives usually involve a lot of cherry picking. That's nice, but cherry picking requires good understanding of the code to properly do. Complexity moved away from one area into another again.

I work in a feature branch, the tech lead does a code review (for real!) then merges to develop, the tester tests the develop branch, if it passes that gets merged to the master branch, the prod system is updated, the tester tests it again.
My personal recommendation:

Development happens on short lived branches, gets merged to master via pull request after code review if needed, but at least some automated testing should be done.

NEVER DO MERGE COMMITS: Always rebase + fast forward merging only.

Branches that never reached production and likely never will should be discarded after a while.

Branches that reached production e.g. hotfix branches should be converted to tags after a while.

I am the opposite. When it comes to bring short lived branches into the main branch, we only allow MERGE COMMITS, we don't ever do fast forward commits.

When looking at history on our main branch, we ALWAYS use `git log --first-parent` which ONLY shows merge commits. Then it shows you exactly what was important (the merge) and in the correct order.

I really wish this was the default view of git log, or really, if git log acted like bzr log that would be even better. It would stop all these arguments about how to have linear history.

Merge commits are annoying IMO because the revert command isn’t always clean or obvious, and you see a lot of noise . But your recommendation of `—first-parent` might alleviate the second concern.

Personally I’m a `merge —squash` guy but to each their own

Do you mean "fast-forward" merges that don't have a merge commit?

An actual "merge commit" should make it easier to revert/reset. I always use merge --no-ff to ensure a merge commit is created. With a merge commit it's clear where the feature branch starts/ends.

I agree, merges are great, since you have a single commit you can pull out.

To back out a merge, you would just do another merge with a reverse diff: `git merge merge-commit..merge-commit^`

Also, it makes things like backporting much easier, since you can just pull out the merge commit and it brings in any "subcommits", rather than individually cherry-picking each commit, like you'd have to do with a fast-forward.

It also makes life so much easier on other developers that are just working in feature branches. No need to rebase. No need to squash. No need to clean up commits. We allow merging master back into your feature branch rather than rebasing, since in the end it doesn't matter (and sometimes, everything you did is interesting).

Agree as well. In the team we all do:

- git checkout -b "new-branch"

- git commit

- git merge --no-ff master (to get the latest from master into our branches)

- git push

That's all you need. No rebase or force pushes. Easy.

Exactly. And if the default view of `git log` wasn't so terrible (if it acted like git log --first-parent), then I think a lot more people would do that too.

But I understand why people who just use `git log` complain with merge commits. It is frustratingly hard to see what actually happened, because subcommits from that merge branch show up in the past in your linear timeline and aren't grouped together. It clutters your log and makes it really hard to tell what is going on and what commits are related to each other.

bazaar's (now called breezy) log (which we used before moving to git) was really nice. By default, it only showed first parents (direct commits or merges). And if you wanted to see more than that, it grouped merges together. This is what happens when you call `bzr log --include-merged` to show not just the top level merges/commits, but also the sub-commits from each merge:

  ------------------------------------------------------------
  revno: 33 [merge]
  committer: John Doe <john.doe@example.com>
  branch nick: master
  timestamp: Tue 2013-09-03 11:39:37 -0500
  message:
    Fixed bug #413 - Add in a blend option
      ------------------------------------------------------------
      revno: 32.1.2
      committer: John Doe <john.doe@example.com>
      branch nick: fix-413
      timestamp: Tue 2013-09-03 10:07:03 -0500
      message:
        Merged the Role changes from fix-412
      ------------------------------------------------------------
      revno: 32.1.1
      committer: John Doe <john.doe@example.com>
      branch nick: fix-413
      timestamp: Tue 2013-09-01 10:04:28 -0500
      message:
        Some initial work on a blend mode
  ------------------------------------------------------------
  revno: 32 [merge]
  committer: John Doe <john.doe@example.com>
  branch nick: master
  timestamp: Tue 2013-09-03 09:17:27 -0500
  message:
    Fixed bug #408 - Make the interface prettier
      ------------------------------------------------------------
      revno: 29.2.3 [merge]
      committer: John Doe <john.doe@example.com>
      branch nick: fix-408
      timestamp: Wed 2013-09-02 15:31:32 -0500
      message:
        mft
      ------------------------------------------------------------
      revno: 29.2.2
      committer: John Doe <john.doe@example.com>
      branch nick: fix-408
      timestamp: Wed 2013-08-24 12:13:17 -0500
      message:
        Fixed multiline support
      ------------------------------------------------------------
      revno: 29.2.1
      committer: John Doe <john.doe@example.com>
      branch nick: fix-408
      timestamp: Wed 2013-08-22 11:58:48 -0500
      message:
        Added directories for other layouts. Changed out the logo, and force a size during the signon
  ------------------------------------------------------------
  revno: 31 [merge]
  committer: John Doe <john.doe@example.com>
  branch nick: master
  timestamp: Wed 2013-08-28 15:13:24 -0500
  message:
    Fixed bug #404 - Pressing back in the preferences exits the application
      ------------------------------------------------------------
      revno: 30.1.1
      committer: John Doe <john.doe@example.com>
      branch nick: fix-404
      timestamp: Wed 2013-08-28 15:11:24 -0500
      message:
        Got rid of the no history junk. It looks like the problem can be solved by calling finish
The top level merges are in order by commit/date, while the sub-commits in each branch and in order with respect to their parent, not the entire repo!

This even resolves stuff, like in revno 29.2.3 where the master branch is merged into the feature branch (fix-408) to get fix-408 up to date.

That works if you have one commit. Otherwise it's more like git commit, git merge --no-ff master, git commit, git merge --no-ff master, git commit, git merge --no-ff master… which shows as bunch of really ugly and unnecessary merge commits from master to feature branch. In contrast, I can do 50 times git rebase and you wouldn't know.
Those merges from master into a feature branch are useful though. If there is a conflict that you have to resolve in your feature branch, then there is a clear place where you did it, and that can be double checked. If you are rebasing, then when I review your feature branch, I have no idea if you had conflicts, and if so, how many and how you resolved them.

git should be used like an audit log, where you can trace the thought process and actions of your developers. Erasing that by rebasing/squashing for a "clean" log isn't worth it in my opinion.

I'd much rather see 40 commits across two weeks on your branch than one "clean" commit with everything. With the 40 commits, I can trace back through your work, and understand your challenges (sometimes seeing 5 commits in a row with 'trying to get X to work' is really useful).

Now, do I want all that detail once your branch is in master. Absolutely Not! I just want a single merge commit that brings in your whole branch. Unfortunately, `git log` shows everything, which is why I advocate for `git log --first-parent` being the default.

If git log's default behavior wasn't so terrible, I don't think this argument would even come up so often.

`merge --squash` generates a nice linear history in the main branch and other developers don't need to worry about rebasing their feature branches or keeping them super clean. That's great.

However, `merge --squash` loses its association with that feature branch, which is annoying. You also don't get to see any of the history of what happened in that branch, which can sometimes be really useful if there ends up being a bug that you want to bisect. You don't have any of the original developers individual steps, since it all comes in as a big, unassociated chunk.

Short lived branches. If tests are green and it passes code review, squash merge, bump the release, and deploy. Never force push anything ever. Rewriting history is bad.

It is all about reducing feedback cycles.

Github flow [1] for me - which is basically what others have mentioned about branching feature branches directly from main and merging as soon as the pull request is approved.

[1]: https://githubflow.github.io/