I do agree that resolving conflicts in merges is risky though. It can make sense when merging just one way between permanent branch (e.g. a 1.x branch into a 2.x), but as soon as cross merges become a possibility it’s probably a mistake.
How do you do otherwise, though? Or is your workflow a combination of rebases and merges? Continual rebasing of the feature branch onto `main` and then a final merge commit when it's ready to go?
You get the equivalent of “mergeless” history (just restrict your git log to merge commits) but can dig into the individual feature histories easily.
Yes. You don't usually need "continual" rebasing, most commonly just once just before merging.
In fact a good merge tool can do it for you (and refuse to merge if there are conflicts when rebasing).
I like cherry-pick, but I barely use it (e.g., I need to cherry-pick one commit from branch X into my branch). I don't like rebase much because it requires force-push.
If you do have multiple devs working on the same branch, use `git pull --rebase` to stay in sync with each other, don't use merges and leave lots of merge commits. If you need to resolve conflicts with upstream, make sure other people have stopped working on the branch, rebase it, then merge.
Even though I much prefer a linear history, losing 1h or more to the tedious work of re-resolving the same conflict over and over is not worth it, in my opinion.
Even if we were to do that, rebasing master is likely to lead to the same issue.
My preferred solution is rebase featureB onto master for the 99% or 99.9% of use cases where this is smooth, and in the rare case that you have too many conflicts to resolve, merge master into featureB (and/or squash featureB then rebase onto master, depending on use case).
Can't say I recommend this approach.
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.
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.
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.
Also, more fine-grain commits can help you trace down a bug, perhaps with the help of git bisect. Once you've tracked down the commit that introduced the bug, things will be easier if that commit is small.
Fortunately you can just merge from master, bringing your code back in sync with master without touching master itself. I see Beltalowda has mentioned this.
For what it's worth, I had heard of git rerere before, and have looked at the man page, but haven't understood how it's supposed to work, and haven't had time to play with it to see how well it actually works in practice. `git merge` or `git squash` and accepting a little bit of a mess in history seems much easier than spending time to learn another git tool for some use case, but I fully admit I may be missing out.
For big rebase, this can add up to a lot, which I just paid the price last week.
I might actually try this in Pijul because I too encounter this semi-regularly (it's not a freak occurrence at all) and my solution is basically to give up and squash my branch before rebasing.
I wish it works like merge, or exist a way to merge, resolve conflict, rebase?
The aesthetic reason is that it tells a more coherent story. The codebase is a single entity, with a linear history. If I asked you "how old were you last year", and you asked "which me are you asking about?", I'd be confused. Similarly, if I want the answer to the question "what was the codebase like at this point in time // immediately prior to some point?", you shouldn't need to ask clarifying questions. `HEAD^` should only ever point to a single commit.
The practical reason is that it discourages a bad-practice - long-lived branches. The only vaguely compelling reason I have heard for merge commits is that they preserve the history of the change, so that when you look at a change you can see how it was developed. But that's only the case if you're developing it (in isolation) for a long-enough time that `main` will get ahead of you. You should be pushing every time you have a not-incorrect change that moves you closer towards the goal, not waiting until you have a complete feature! If you make it difficult to do the wrong thing _while also_ making it easy to do the right thing (too many zealots forget the second part!), you will incentivize better behaviour.
(Disclaimer - I've been lucky enough to work in environments where feature flagging, CI/CD, etc. were robust enough that this was a practical approach. I recognize this might not be the case in other situations)
And yeah, I'm kinda intentionally invoking Cunningham's Law here, hoping that Merge-aficionados can tell me what I'm missing!
I would assume that such a question would talk only about the main branch. However, I will point out that "what was the state of feature X" is only answerable with a non-linear story.
> The practical reason is that it discourages a bad-practice - long-lived branches.
Wait, long-lived branches are bad? Merging in partially done features is good? That seem insane.
First, if the feature is small enough to knock out in an hour, that's great. But sometimes it can take a couple of days. I should hope you have enough activity that the main branch will move in that time.
But committing partial features is crazy. Sometimes you realize the way you are implementing it (or the whole feature) is a bad idea and all the work should be orphaned. Other times, a feature requires changing something (e.g. an API) where a partial change cannot really work - and sometimes where you need to have a meeting before you do it. Consider the feature to be "update dependency X", which means you now have some number of bugs to track down due to the new verison.
Heck, sometimes a feature might need to be mothballed. Sometimes you have to wait for an external dependency to be fixed. And you can chuck your work, commit something broken, mothball it and come back when the external dependency is fixed or switch your dependency.
...uhhh, yes? I've never heard anything to the contrary. Can you explain why you think the opposite?
For long-lived branches: The longer a branch exists separately and diverges from main, the more pain you'll create when you try to merge it back in - both because of changes that someone else has made in the meantime (and so, conflicts you'll (possibly) have to resolve), and because you are introducing changes that someone else will have to resolve. The pain of resolving conflicts scales super-linearly - it's much better to resolve lots of small conflicts (ideally, so small that they can be algorithmically resolves) than to resolve one large one. Plus all the arguments from the point below...
For checking-in early and often: flip it around - what is _better_ about having the change only on your local repo, as opposed to pushed into the main codebase? If the code's checked in (but not operational - hidden behind a feature flag), then:
* your coworkers can _see_ that it exists and will not accidentally introduce incompatible changes, and will not start working on conflicting or overlapping work (yes, your work-planning system should also account for that - but extra safety never hurts!) * if you have introduced a broken dependency, or a performance black-hole (which might only be possible if you're running your code in "shadow mode", executing but not affecting the output until it's fully ready - which, again, is only possible if you check in early-and-often!), you can discover that breakage _early_ and start work on finding an alternative (or, if necessary, abandon the whole project if it's intractable) earlier than otherwise
In fact, to take your example - "sometimes you realize the way you are implementing it (or the whole feature) is a bad idea and all the work should be orphaned" - yep! This happens! This is not a counter-example to my claim! Orphaning an inactive "feature" that has been pushed to (but not fully activated in) production has no more impact than abandoning a local branch. Even orphaning a feature that has been partially activated is still fine, so long as it didn't result in irreversible long-term state-updates to application entities (e.g. if it added a "fooFeatureStatus" to all the users in your database, rolling it back will be tricky. But not impossible!). So there are very few (or no) downsides, and all the advantages I described above.
I do agree that API changes are the one exception to this rule - you should have those reasonably nailed down and certain before you make changes, since those affect your clients. But any purely-internal change which can be put behind a feature flag, on an inactive code path, in shadow mode, in canary/onebox testing, or any other kind of "safe to deploy in prod, but not _really_ affecting all of prod" - do it!
Not saying trunk-based is wrong, but to say abandoning a feature is as cheap as in branch-based development fails to account for everything.
I mistyped at one point by saying to avoid a partial-feature commit when I meant partial-feature merge onto the main branch. Yes, commit to the feature branch often. Hopefully clarifying that resolves most of the issues that you raised as advantages.
Meanwhile, managing partially built features by feature flags seems worse. It has orphaned code migrate into the main codebase and stay there. You brought up a broken dependency. What happens if a dependency is broken and not likely to get fixed for a month? Just leave that code in the main codebase orphaned for a month? Further, having multiple partial feature commits complicates bisecting or simple reading a feature's history.
I concede feature flags for deployment has some advantages, especially for feature specific elevation through testing.
Then we'll have to agree to disagree, as this is pretty fundamental to my argument - everything else ("Your coworkers get to see what you're working on and will notice clashes of intention earlier", "You can run incomplete features in shadow-mode to ensure they don't affect performance in production", etc.) is just sugar.
I really appreciate your well-reasoned and civil discussion!
Suppose "masterX+1" is called latest
Suppose "masterX" is the SHA of your mergebase with master (on top of which you have 10 commits)
`git rebase --onto latest masterX`
Rebase is being annoying here mostly because it's doing exactly what you want it to do: warn you about merge conflicts for every commit in the chain that might have any.
Either that, or you lobbed 10 different issues into the same branch, which is a whole different barrel of "no one benefits from this, you're just making it harder to generate a changelog, can you please not" fish.
Merge is perfectly fine and it is the only way to synchronize repositories without changing the history, which is very important for a decentralized system. It certainly has the potential to make a mess if used improperly, but so do rebase, cherry-pick, and basically every other command.
> If you use merge to sync two branches continously, you completely lose track of what changes were done on the branch and which where done on the mainline.
If you do things correctly, that is by making sure that when you merge changes from a feature branch into the mainline, the mainline is always the first parent, you shouldn't have any problem. Git is designed this way, so normally, you have to go out of your way to mess things up. If did it like that and you don't want to see the other branch commits, git-log has the --first-parent option.
If you always rebase the branch, the commits can be applied directly.
(starting on main)
git checkout -b feature
do work
git commit -a
git checkout main
git pull
git checkout feature
git rebase main
publish code review, get approval
git checkout main
git merge feature
You still use merge at the end, even though it's not actually doing anything that'll result in a conflict.
The last command is:
cp .git/refs/heads/feature .git/refs/heads/main
No merge needed. git reset --hard main featureI'd also use "merge --no-ff" to force an empty commit that visualizes where a feature begins and ends.
I can recommend git diff @{1} post rebase I alias it to d-
I've never seen a rebase gone awry introduce production bugs, but I've known multiple gnarly bugs caused by errant merges. YMMV.
In a merge, you solve conflicts once. Whereas in a rebase, those conflicts will turn into incremental conflicts.
If the branch history is "tidy", with discrete, purposeful commits, this can be easier. Especially if incrementally rebasing.
The main difference is one rewrites history and the other does not. A rebase is by nature destructive and as such can introduce subtle changes in the process, especially if commits are reordered / modified in the process
The main difference is that a merge sticks around in your repo forever, a commit that people assume has no real code changes in it but actually sometimes it does. A rebase is done once, and then your git history doesn't have to deal with it ever again.
Yes, you raise a fair point that if you've dug yourself into a deep pit already with long-lived branches and overlapping work, it might be slightly easier to extract yourself from the pit with a merge. But then you're leaving that fetid pit in your repository forever.
Don't get me wrong, I _often_ rebase, about a dozen times a day and it's been a core part of my workflow for 2 years. In that time I have learnt a lot, silently lost changes and ended up in a few mishaps.
I am in no way against the idea of rebasing, I frequently do. And personally, I often rebase && merge --no-ff. But, IMHO it's far too easy to mess up too that I'd adopt it as a dogma.
I also question the notion that VC history is best thought of in linear terms. I'd argue it's fundamentally flawed to force a DAG into a more linear structure.
In my experience, the desire to do this is to construct a DAG that's pretty in log viewer XYZ, rather than anything else. I consider this highly overrated. Just look at the DAG of the git project. Yes, it's intense, but the primary purpose of the history DAG isn't to immediately present a simple linear history.
Rather, it's to preserve a common, shared, decentralized history where it's easy to go back to a precise moment and see what was done to what and why. A dogmatic always rebase history is in my experience often a relatively pointless pursuit of constructing a git log --graph that's "simple" by default. Ie, rather than solving for the problem of "How to overview a DAG", the solution is to reformulate the DAG in a linear way which often conforms better to how we humans like to overview information.
Again, this is a personal viewpoint and I don't mean to pass judgement but I often find that such approaches, which one might liken to treating symptoms instead of curing the cause, is better solved the other way around. It's a real joy to delve into the git projects history, despite the fact that it's _littered_ with merge commits.
Basically, I think the quest for a "simpler" looking history DAG is somewhat overrated and not something I'd personally recommend pursuing.
It's a neat trick to explode your alias namespace, since you'll never see a tool published named `ls-` So you have reserved a huge "address block" for your personal aliases :)
https://github.com/CervEdin/gut/blob/config/.gitconfig
NB, that some are personal custom scripts, like git-branch-status, which I also publish in the same public repository.
It's very much opinionated and geared to my use but feel free to use it, submit feedback and/or PR
I see d- there, so the one use of dash is used sparingly. There's a lot of git functionality I'm leaving on the table, looks like. Maybe I'll collect them as needs arise, and at least try to learn a new one a week.
I'm proud to have carefully incorporated commits from a separate repo to another as a series of patchsets instead of just copy-pasting the lot. Authorship matters, and it let me fix some issues coming in.
Currently, I'm using a shell script to help with a git conflict resolution flow that does something like
read -p 'Conflict. Resolve and press [Enter]'
And fix in a separate tmux window (git add, git cherry-pick --continue).TUIs and autocomplete are nice, but there's opportunity for a deeper understanding writing one's own tools. So I'm hoping to combine ZZ and `read -p` (or similar) to coax nvi (Keith Bostic) to something for Java stuff. Or at least build some primitives around that.
Encountering the equivalent of "flash of unstyled content" when switching from text editor to a--for example--Java method chooser feels like the philosophical difference between "Let's SPA" versus "Click flashes between pages is fine."
The flow would be something like
1. In nvi, keystroke equivalent of
Ctrl-Space brings up an Intellisense
tool.
2. The tool loads up a list of
autocomplete methods as well as its
own hotkeys.
3. Pressing up and down manipulates a
temporary text file that just prepends
">" next to the line, for example.
4. And Enter somehow brings back nvi with
the method added, right after the
period (with our partial typing
replaced).
All this to say, dotfiles and git config are no big deal, but in CLI it's an escape hatch to molding a custom environment.If you do things the way you're suggesting, you'll make it really hard to tell what commits were made on your branch. Git clients tend to assume the first parent is the branch you care about.
Both tools are pure vandalism compared to merge. Among the two, cherry-picking is preferable in this case because you're "only" destroying your own history, so in the end, it's your funeral.
> Developer end up fixing additional issues in the merge commit instead of actual commits.
A merge commit IS an actual commit, in every sense of the word. The notion it somehow isn't, is what you need to get rid of.
You have been repaid the time investment spent learning rebase commands, after once being able to avoid a really bad merge conflict.
This way, you know which set of commits was in the branch by looking at the parent commits of the merge commit, but the merge commit itself did not involve any automated conflict resolution.
I really do want the natural semantics of merge --ff-only --no-ff.
git rebase -r target source && git checkout target && git merge --no-ff target
since what your asking for is not a merge, you'd have glue it together by yourself. Perhaps, add it as a script
Rebasing takes longer and is actually more prone to error because of the clunky interface. There is absolutely nothing wrong with squashing commits in a feature branch and merging that into master/main. In fact, it's generally better for the health of the repo and the mental health of developers.
As long as the merge commit is being reviewed with the rest of the PR, that's fine, right? (We use rebase while working on feature branches, and then squash & merge for completed PRs, which seems to be the best of both worlds)
I don't get it. If you rebase, you get 20 chances to do the same.
Rebasing is the process of redeveloping your feature based on the current master. This is smaller, easier steps to review later.
It is a pitty that we can't have tooling to create "hidden" merge commits to allow to connect rebased branches, this would retain the history better and allow pulling more easily.
Also, a way to "rebase" that works the same as cherry picking commits on top of the target. As far as I can see, the regular rebase works it's way up the target branch, so that I end up resolving conflicts in code that eventually changed in the target.
* If you screw up a merge, you undo the merge commit. Now your branch is exactly as it were. May not happen with a rebase.
* If you push some code to the remote, and later find out it was outdated, you can merge it with main and push again: no need to force, github can distinguish what's already been reviewed and what hasn't. With rebase, you may need to push -- force, and if someone already reviewed the code they're going to be shit out of luck, as github will lose the capability to review "changes since last review", as the reference it has may have been lost.
I also merge these features using squash commits, which provides a very linear history. This also saves some effort (you don't need to be rebase the commits in the feature branch, which can be a pain in the ass for unorganized people and git newbies, and you are pushed towards making smaller, granular PRs that make sense for the repo history).
If there are no conflicts, you might as well rebase or cherry-pick. If there is any kind of conflict, you are making code changes in the merge commit itself to resolve it. Developer end up fixing additional issues in the merge commit instead of actual commits.
If you use merge to sync two branches continously, you completely lose track of what changes were done on the branch and which where done on the mainline.