It seems like a very contrived example to me. We have been running rebase/fast-forward only for close to 10 years now, and I have never experienced anything that unfortunate.
It seems like a very contrived example to me. We have been running rebase/fast-forward only for close to 10 years now, and I have never experienced anything that unfortunate.
I run in to this quite frequently, even on projects where I'm the only one working on it (I tend to have a lot of things going on in parallel). Once branches diverge and commits accumulate it can become a right pain. Usually my solution is to merge master into the branch just to keep up to date and then just undo everything, make one new commit in the master, and rebase that. But in some more difficult cases it was "just merge and fuck it because life's too short". I've also just manually "copy/paste merged" things to a new branch, because that seemed quicker than dealing with all the merges/conflicts.
Maybe there are better ways of doing this, and arguably I shouldn't have all these long-lived branches in the first place (but it works well for me, so...), but it's not that much of a contrived edge case.
This is the problem here. If you have multiple long-lived branches, there's no technical solution to preventing rot -- you must actively keep them in sync.
Regularly merging in main is the opposite of the proper solution. Constantly rebasing on top of main is the proper solution.
- Pick a shared branch to work on.
- Work.
- If you complete within a day, push to shared branch.
- If you need to hold onto it longer, make a new branch, switch to it.
- (Possibly recurse.)
- Complete work, rebase new branch on shared branch, push.
And of course feel free to replace branch with remote/branch. It is distributed, after all, nothing special about any particular server.
A worked example, to make it more concrete:
- Pick the shared branch main.
- Work on a feature for more than a day, so:
- Create a feature branch feature/e2ee, switch to it.
- Recurse, since you'll be doing database updates and I'm adding the UI.
- Pick the shared branch feature/e2ee.
- I create a branch git.sr.ht/~couch/new-twitter/feature/e2ee
- You work and push to feature/e2ee.
- I complete my work, rebase the branch, and push to feature/e2ee.
- We are satisfied that we've completed the feature, rebase and push to main.
> If you have multiple long-lived branches, there's no technical
> solution to preventing rot -- you must actively keep them in sync.
Rebasing isn't an alternative to this, it's just a different way of
manually keeping in sync. > Regularly merging in main is the opposite of the proper solution.
> Constantly rebasing on top of main is the proper solution.
Why? You've given no justification for your preference.I never said it was, I said it was the right way to keep them in sync.
> Why? You've given no justification for your preference.
I don't need to, the GGGGP said it perfectly: https://news.ycombinator.com/item?id=33705026
If done correctly, that's true, but it's beside the point. The reason to prefer one over the other is the failure mode.
> A rebase is more error prone though.
On what metric? In my experience, a merge is far, far more likely to silently introduce a production bug. I've never seen a rebase fail that way.
1) The merge auto-applies cleanly, but the merged code is wrong. This is pretty niche, usually, but happens in certain edit patterns. I've never seen this produce a syntactically-valid, semantically-invalid construct (but I suppose it's possible) so generally these are caught by the compiler.
2) The merge does not auto-apply, so you get into manual resolution. This is where things get hairy.
The merge commit really ought not have any changes of its own, but lots of people consider minor conflict resolution legal. So you end up with a bunch of code changes that logically belong to another commit, and are grouped together for purposes of expediency.
Rebase applies your changes to another branch as though they had been made there originally. If a conflict comes up, you already have all the context needed for how to resolve it, because you just wrote that code. The fix goes where it belongs.
All I can tell you is that I've been bit by merge-induced production bugs enough times that I now work to avoid that particular failure mode.
I'm not sure where this rule comes from. For code review, I for one normally review all of the changes that are going into master, and only look commit-by-commit if it becomes overwhelming - so, unless this is a huge merge (which should generally be avoided anyway), I wouldn't really see how this is a problem.
The only real problem I have with merging into your local branch to keep it in sync with master is the way it pollutes history when it is finally merged back into master. This is enough of a problem that I and my team always rebase unless we end up in one of these rare cases that I was highlighting.
Well, merge actually works much smoother and rebase gives a lot more grief, so the problem is with rebase.
> Regularly merging in main is the opposite of the proper solution. Constantly rebasing on top of main is the proper solution.
The "proper" solution is the one that allows me to get stuff done. The only thing that matters is how the main branch ends up looking in the end, and what I do before that isn't really all that important.
Another problem with rebase is when multiple people are working on the branch; it requires careful coordination if you don't want to lose work. Overall, just merge in main is usually the best strategy here.
> The "proper" solution is the one that allows me to get stuff done.
Yeah, but the stuff that needs to get done doesn't end with your commit, it starts there. Merge commits are prone to introduce unexpected and uncaught bugs: rebases just don't.
How so? If I make an error with a rebase then I risk losing my changes. You can fetch it from the local reflog, but that's not so easy. With a merge I have a merge commit which records what was merged.
So probably? But I want to avoid https://i.redd.it/jdqjhi8qv3x71.jpg
Given that this scenario is common for you but sounds contrived to others, I would argue that this doesn't work well for you. It's just familiar enough that you're willing to deal with some pain.
Short-lived feature branches sidestep this hell. Longer-lived projects can almost always be partitioned into a series of shorter mergeable steps. You may need support/buy-in from your manager, I hope you get it.
I know some people think this is crazy, but it works well for me and I'm fairly productive like this, usually producing fairly good code (although I'm not an unbiased source for that claim).
In the end I don't want to radically change my workflow to git or other tooling; I want the tooling to adjust to the workflow that works well for me.
If every error in your system needs a separate entry in the error enum, or every change needs an entry in the changelog - loads of changes will try to modify the last line of the file.
If order actually matters then yeah, git can't magically know where each new line should go.
I'm not saying these situations are impossible. But you can work towards reducing when they arise. If everyone needs to change the same file, then it sounds like something should be refactored (it's probably a quite big file as well?).
If every error needs to go to the same error enum, that sounds like an error enum that might benefit from being split up.
And if every change needs to write to a common changelog file, I would personally find a new way to produce that changelog.
If it's that big a painpoint, then I would look into different ways to get around it.
Rebase is still by far the most common case in our repo, as yes, these cases appear very rarely. But when they do happen, it's often worth it to do a merge and mess up a history a little bit (or squash, which messes with history in another way) rather than resolving conflicts over and over.
Someone else was also suggesting rerere for this use case, but I've never used it myself and I don't know how well it actually handles these types of use cases.
But we try to reduce the chance this happens quite a bit, by avoiding letting files grow too big, for example.
Other things we do, is use codeformatting with rules that reduce the chance of merge conflicts. For instance, instead of having imports like:
import SomePackage.{A, B, C}
we format it to: import SomePackage.A
import SomePackage.B
import SomePackage.C
That alone helps a lot. Other formatting rules that avoid dense lines, and instead splits over multiple lines also have a huge impact on merge-conflicts.Sadly, my current approach is to just reset my development branch to the merge base and make one huge commit, and then rebase.
When that happens, we look into if it's possible to do more frequent merges (fast-forward rebases through Gerrit, to be specific) of our smaller commits to master, so we don't accumulate too much in isolation.
I find it helps reducing bugs as well, if two or more members are doing active work in the same area in that way, it's not good to be working in complete isolation as it just opens up for bugs because of incompatibility with the work going on in parallel.