Improve DX when shipping multiple branches in stack
Let's say I've got the following stack:
main
feat/part-1
feat/part-2
feat/part-3
A common scenario we face is shipping all of these branches once they're approved. To do this, we git-town ship feat/part-1, and then run git-town sync --all to sync the stack and prepare it for another git-town ship.
However, what ends up happening is that we have to resolve merge conflicts every time we sync the stack after we ship. This is because Git Town (or your code host) will squash the commits in a proposal before merging the branch into the main branch. This also happens regardless of whether there are any conflicts between all of the feature branches.
If the stack is 3/4+ branches deep, this ends up being a pain to perform, and can sometimes lead to errors when resolving merge conflicts.
I wonder if it is possible to improve this process? What if we always rebased main/perennial branches onto feature branches, regardless of the sync-feature-strategy setting? Or perhaps we pre-squash commits before shipping? That way the commit hashes are the same once merged onto the main branch.
Ah one small detail I forgot to mention - my team currently uses sync-feature-strategy = merge. With v13 the rebase strategy is smarter about how it syncs from the main branch, so perhaps that could be a solution?
I agree that there are opportunities to develop the DX around shipping stacks more. The problem of fake merge conflicts with the "merge" sync-feature-strategy are known and documented by GitHub. They happen only with stacked changes because only there are branches that one keeps changing after parts of their commits were shipped.
This is essentially a Git problem. How does one sync and ship stacked Git branches in the best way? How would you do it manually without Git Town? If there is a solid process for it, Git Town should just automate that process.
The ideal solution from my perspective would be to compress each branch down to 1 commit and then merge branches via fast-forward-merge. Unfortunately, fast-forward merges are only available on GitLab but not on GitHub.
According to GitHub docs, the "rebase and merge" method still creates a new commit with a different SHA. So there is still potential for conflicting changes, but maybe Git is smart enough to resolve them. I think this direction is worth exploring.
Some random other thoughts. Have you tried using GitHub's merge queue? I don't know how well it can handle stacked changes though.
If no other branches were shipped in the meantime, you could run git checkout --ours to auto-resolve all merge conflicts and keep only the changes on the feature branches. Such an auto-resolve requires re-reviewing the feature branches, though.
In your example, would you ship all three branches (feat/part-1, feat/part-2, and feat/part-3) at the same time?
Fast forward merging may actually be possible according to this Stack Overflow post! Perhaps this is worth exploring once compress has landed?
In the meantime, I think the workaround will be to use the rebase strategy in conjunction with git checkout --ours to auto-resolve merge conflicts. Thanks for the tip!
Yes, one can do the fast-forward merge manually locally and then push the new commit. That requires allowing pushing commits to the remote main branch, though. Which comes with severe risks like having a broken main branch at any time, or people pushing unreviewed changes to main. I wouldn't be comfortable with that. Most teams therefore require that all changes to the main branch must go through a pull request and only get merged if CI is green.
Re-reading the Stack Overflow post, several people claim that pushing the main commit is somehow possible if the commit matches one in an existing green pull request. If this works that would be good news since Git Town's ship command could then be used for this!
According to my understanding, in this situation also the --onto might be helpful as mentioned in #3298.
Re-reading the Stack Overflow post, several people claim that pushing the main commit is somehow possible if the commit matches one in an existing green pull request. If this works that would be good news since Git Town's ship command could then be used for this!
Just keep in mind, that there are setups, where the main branch is protected and those does not allow any push from local. Only changes through PR (after all check have been passed) are then allowed.
git town compress and git town compress --stack are now available in v14. I'm curious if this helps with shipping stacks!
Another thing that could help here is that git ship syncs all descendents in the stack right before shipping (https://github.com/git-town/git-town/issues/764). This allows using git checkout --ours to resolve merge conflicts with minimal risk of losing changes.
Some random other thoughts. Have you tried using GitHub's merge queue? I don't know how well it can handle stacked changes though.
Over the past month my team has been working with Git Town + GitHub merge queues in a monorepo setup. We ultimately determined that merge queues did not improve the DX when shipping multiple branches in a stack. In fact, they made it worse: when shipping multiple branches in a stack, we end up waiting twice as long as we normally would for CI/CD to pass & for branches to merge into main. Let me explain why.
Any PR merged by a merge queue will always have CI/CD checks executed twice: once to pass the PR's CI/CD checks, and another to pass the merge queue's CI/CD checks. This is necessary because the merge queue allows out-of-date but non-conflicting PRs to be added to the queue. This makes sense, there's no guarantee that a PR is safe even if it's not in conflict, so the only way to guarantee this is to run CI/CD again. However, in the scenario where a PR is up-to-date and non-conflicting, CI/CD checks are still executed - there currently no way to bypass this.
I think we should explore alternative options here. I think the biggest hurdle for this issue would be minimising CI/CD. I also think the git checkout --ours is worth exploring - my gut tells me it could help reduce CI/CD churn.
Thanks for this experience report! It sounds like the biggest problem with GitHub's merge queue is double-testing of each pull request. Apart from that performance problem, does shipping a branch stack via the GitHub merge queue work? For example, if you give it the stack from your message that started this ticket (feat/part-1-feat/part-3), does it ship all three branches (after a long time double-testing everything) all by itself, without you having to do anything?
Apart from that performance problem, does shipping a branch stack via the GitHub merge queue work?
Unfortunately not 😕. We were unable to merge or queue up merges in stacked branches as GitHub prevents merging when the parent branch is currently being processed by the merge queue. Because of this I don't see merge queues benefitting Git Town for this particular issue.
@tranhl now that git town compress is available, have you had a chance to try out the idea of a fast-forward merge that you described in https://github.com/git-town/git-town/issues/3236#issuecomment-2024175255? If that works reliably, and you end up doing it all the time manually, we could automate it using Git Town.
@tranhl now that
git town compressis available, have you had a chance to try out the idea of a fast-forward merge that you described in #3236 (comment)? If that works reliably, and you end up doing it all the time manually, we could automate it using Git Town.
@kevgo Sorry for taking a while to get back, things have been pretty busy!
I managed to set aside some time to test fast-forward merging, and can confirm that fast forward merging works! These were the commands that I used:
git checkout main
git merge --ff-only <feature_branch>
git push
As the Stack Overflow answer described, GitHub will also enforce branch protection rules when performing this push locally as well. For example, I added a branch protection rule using an action that didn't exist (meaning required checks would always be pending and never pass), and this is what I got when running git push:
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
remote: error: GH013: Repository rule violations found for refs/heads/main.
remote: Review all repository rules at http://github.com/<redacted>/git-town-test/rules?ref=refs%2Fheads%2Fmain
remote:
remote: - Required status check "Never Passing Check" is expected.
remote:
To github.com:<redacted>/git-town-test.git
! [remote rejected] main -> main (push declined due to repository rule violations)
error: failed to push some refs to 'github.com:<redacted>/git-town-test.git'
This approach has potential. The downside is that we're integrating changes by directly pushing to the default branch as noted by @breml.
git-town compress does help overall with all of this, however it does not do well to compress merge commits created by squash merges or merge commits created by the merge sync strategy. It also doesn't squash commits by other authors. In practice, this makes the rebase sync strategy less productive overall compared to merge + squash merging PRs.
I managed to set aside some time to test fast-forward merging, and can confirm that fast forward merging works!
This is really awesome news! 🔥
The downside is that we're integrating changes by directly pushing to the default branch as noted by @breml.
Since GitHub enforces CI checks and only allows commits with the same SHA that passed CI, this method should be just as safe as shipping via the GitHub UI. It might not feel that way because people are merging manually on their own machines, but it’s essentially the same process. I’m really excited to make this way of shipping available through git town ship. This will also give some much-needed purpose to this currently underused Git Town command.
git-town compress does help overall with all of this
I just shipped in v15.2 a new sync-strategy called compress. When you enabled it for your feature branches, it automatically compresses all branches that it syncs!
However it does not do well to compress merge commits created by squash merges or merge commits created by the merge sync strategy. It also doesn't squash commits by other authors. In practice, this makes the rebase sync strategy less productive overall compared to merge + squash merging PRs.
I think you talk about the rebase sync strategy here. I agree it has a lot of problems and I keep being surprised how popular rebasing is despite all these disadvantages. I expect that for your use case you need the compress sync strategy.
I can confirm that this works. GitHub even deletes the remote branch when doing this!
This is now released in Git Town 16. Please try it out and let us know how it goes!