@b0rk Re rerere, shouldn’t it be working all the time anyway? I was under the impression that if I have to manually edit the same fix for the same textual conflict more than once, rerere isn’t working correctly for some reason. (Or, I’m mistaken and it’s not really the “same” textual conflict I’m fixing)
@jamiemccarthy are you saying that rerere works automatically and you don't have to explicitly enable it? (maybe the deal is that you need to configure rerere.enabled to true and then it'll automatically do its thing?)
@b0rk Right, good point. I’d forgotten it has to be enabled because enabling it in my global git config is one of the first things I do on a new machine. Once it’s enabled it works automatically
Resolving a conflict in someone else's commit in a git rebase can sometimes take over authorship, and you might not even notice. I think it's related to using git commit instead of git rebase --continue. I like the former to give me a chance to have another look at the commit before continuing.
@b0rk You point out some great examples of how rebase/force-push can confound peer review. That’s my main gripe with it.
My workaround is, after I’ve asked for reviews on a PR, if I have further nontrivial fixes, I ‘git checkout -b mybranch-wip’ and hack on that for a while. I can push it, see CI results, and ask for informal feedback. Once I’m done I can squash it back onto mybranch, reset —hard mybranch to it, and push to cleanly update my PR
git rebase -i
... Do stuff
git add .
git commit --amend
git rebase --continue
.... Oh $&#@ I merged two commits together instead of keeping them separate
@b0rk@nil yes me too! I edit commits while rebasing way more often than I resolve conflicts, and the first one involves a git commit —amend before the git rebase —continue, but if I do it on a conflict resolution I merge two commits together 😭 which I usually do late at night when I am trying to get a PR out for review and don’t have the brain power to split the commits apart again
@b0rk oh, i feel very seen having messed up all these things while learning how to rebase hahaha.
One thing i didnt see is rebasing when you or someone else has pushed a merge commit/figuring out how to drop a merge commit since that commit doesn’t appear in the rebase commit log view
@b0rk Taking commits from other people with you when rebasing on a branch that corresponds to another major version (is that rebase type 3? or maybe just a special case of type 2?)
Solution: either use --onto or do an interactive rebase and make sure to take only your commits. You can customize the format to make them easier to spot: https://greg0ire.fr/git-gud/#/21
@b0rk Not something you missed, but maybe useful as addition to the force push section:
We sometimes intentionally force push to shared feature branches, to rebase it against the main branch. This breaks all derived branches, but they can be repaired using git rebase --onto <feature-branch> <last-hash-of-local-feature-branch> on a branch created from the feature branch.
Sorry if you already knew, but I saw no mention of --onto in the blog
@b0rk The link for "accidentally run git commit instead of git rebase –continue" is wrong, #accidentally-running-git-commit-instead-of-git-rebase-continue in the ToC vs accidentally-run-git-commit-instead-of-git-rebase-continue in the source ("running" vs. "run")
@b0rk rebase is a great tool to have, but I’m increasingly convinced that building your primary workflow around it (vs exceptional cases) is a mistake. Merge is what everyone should be using. Squash merge if you must…
@b0rk Personally I don't like squash and merge because it interferes with me finding the source of bugs via bisection. But, as long as the final squash isn't too big, it's fine.
The current branch I'm working on is 6k lines (so far), and going to be hard enough to review as separate commits. Impossible as a single squashed commit.
While the feature could technically be broken down further, no smaller chunk could be independently used in production (for us+now master = production).
@BoydStephenSmithJr yeah that makes sense! I think I'm trying to say less "everyone should squash their changes" (that's ridiculous) and more wondering if there's a better recommendation than "you should rebase" for people who are new to git or uncomfortable with it.
@b0rk I certainly understand that. In the bad old days, you'd resolve the merge conflicts and generate a new patch (set) on top of the CVS HEAD / Subversion trunk.so I think either a squash merge or a rebase are improvements over that.
I wonder if rebasing in a separate work tree would reduce the risks.
A non-squash merge is also a possibility. But, some people don't like a non-linear history. I prefer it, when there are no backwards merges (from pull, e.g.).
@cccpresser@b0rk I often use rebase in the process of cleaning my history (this branch is about a dozen commits, but I've committed on it hundreds of times and split/merged then into logical groupings), but a clean history is not something that's easy to motivate and the tools for cleaning a history have a lot of "sharp edges".
So, I don't see "clean your history" as a viable solution to avoiding rebase risks...
@b0rk I do clean up with interactive rebasing. Not sure if there is another way to do a clean-up.
That cleanup is done in my local branch, before rebasing onto the remote/main/whatever target.
(it feels like I misunderstood the original question. Need to read it again.)
@b0rk As a maintainer, I'm often using rebase to update contributor branches submitted as PRs when they go out-of-date and have conflicts. This has to be done so that CI can run, which is required so we can merge.
Generally, it's not terrible... Unless the branch has been lingering for a long time. And when that happens, it's often easier to recreate the branch by cherry-picking commits on to a clean checkout of the target branch.
@b0rk@chandlerc I was introduced to rebase as it being about "commit hygiene".
Whenever I want to understand why a piece of code is the way it is, and how it got there, I hate stumbling across commits like "now trying X instead" and "merging that branch into this branch". I do not want to see how you stumbled in the dark, I want to see what you did once you found the light switch!
Rebase provides a way to turn the mess left after experimenting into a neat transcript of the effective changes.
@chandlerc@sbi@b0rk I often have unrelated refactorings that I want to keep separate. And at least while I'm working and discovering the changes I need, I don't want to keep separate branches. So I need more fine-grained squashes, i.e. interactive rebase.
@foonathan@chandlerc@b0rk Yeah, I usually rebase so all the small bugfixes, refactorings, and cosmetic changes which are not relly relevant to the semantic changes made in a branch are in a single commit, preferably the first commit of this branch.
@b0rk I dream of a day when GitHub offers a way to combine the two. I often have 5 commits that I want to be two. As it is today, I usually need to locally rebase with a mix of some squashes and some edits.
@b0rk I sometimes feel like rebase is really the workflow that people want to use even though git (and GitHub) do not make it easy. But people make it work anyway!
@b0rk I tend to commit a lot because I rely on the CI system to build and deploy my work in a test environment. I end up with stupid commit messages like "Fix it again!" "Arg, this time for sure!" and such. Not only do I rebase to because I don't want to look like an idiot when my co-workers review my PR, but also because I want to reduce any friction at review time.
After I'm done working, I organize my commits into logical chunks, like I knew what I was doing all along and the commits tell the story of what the PR is all about. In the PR I encourage folks to review the changes in commit order. It really helps on big PRs!
@b0rk For me, there are cases where it's really useful, but it's SUPER dangerous and when I was first learning to use it I destroyed so many clones with it.
(And when I did, someone was always waiting to say "oh, that's easy to fix, just edit your reflog to rejortle the blarfo tree" or something. I just did a fresh clone instead.)
The best advice I can give for rebasing is never casually recommend to someone that they rebase!
@b0rk this is exactly what I do. MOST people’s branches aren’t perfect orders and you change things based on review feedback out of order; trying to replay those changes correctly is just not worth it to me
@b0rk The colleague who introduced me to git told me that rebase is about "commit hygiene".
Whenever I blame some file to understand why a piece of code is the way it is, and how it got there, I hate stumbling across commits like "now trying X instead" and "merging main into this branch". I do not want to see how you stumbled in the dark, I want to see what you found once you managed to turn on the light!
Rebase provides a way to turn the mess into self-contained commits and grokable changes.
rebase -i HEAD~4
That one doesn’t always properly count to 4 or whatever, and it isn’t necessarily because of merges. I haven’t figured out the reason yet.
rebase and tags
Tags become kind of orphaned, their history lost (easy enough to fix, just checkout the tag, create a branch from there and push, but that’s annoying)
rebase and collaboration
If anyone else is working on a branch that you keep rebasing, you’re going to have conflicts of a different kind 😈
@b0rk you're asking about folks who use "squash & merge" their opinion on this:
I'm a fervent defender of squash & merge workflow. Like you said, this workflow also gives clean and linear history but it also has a few additional perks:
it's much easier because way less pitfalls than rebase (=more accessible)
not doing rebase on a PR on Github allows the reviewers to see the progress on a PR clearly and easily
I'm good with rebase but I don't do it because imo it's a waste of time
@b0rk every time you fix something according to a comment on your own PR you can refer to it by replying "Done in XXXXX" (you make a dedicated commit for it)
It's a lot easier for reviewers to understand where you came from and how you went through the changes of your PR
Team working with "squash & merge" is way better than rebase and I, for one, value the work of my reviewers which I find very important
@b0rk now if you want to go even deeper on how good not using rebase is, here is a hard one for you:
In rebase people will complain about fixing the same conflicts all the time (or they are using git rerere). This is because you solve conflict by putting your commits on top of the main branch one by one
In squash&merge you do the opposite, to update your branch you merge the entire main branch into your own branch, so you solve all the conflicts only once. But there is more to know
@b0rk On GitHub you will see only one commit for that merge. The history on your PR won't be polluted so it's great for reviewers. And you never have to come back to your older commits, you keep building on top
Most of the time (90%) resolving all the conflicts at once is preferable. But in some cases it's harder because you lack context to resolve your conflicts. Now comes git try-merge: it merges the main branch up to the point of the first conflict so you can solve conflict separately
@b0rk It would be helpful to add a note to that post to explain the HEAD^^^^^^^^^ syntax. While I have 15 years of Git experience I have never seen it.
@b0rk It sometimes speeds things up for me if I know that some changes are already on my branch, I'll delete them first on my branch and the rebase goes better. I'm a huge fan of rebase and use it all the time to make my changesets cleaner.
@b0rk For "never force push to a shared branch" what is a shared branch? When I first encountered this I interpreted it as anything that anyone else could even have seen, but that's maybe too strict?
I started a new role recently and they are in the habit of having branches up for PR which are mostly one person but then someone else comes in during PR and rebases to clean up the history but also adds things that they thought would improve the PR, but then it turns out some other change is needed because a change in another repo is now causing CI to fail... This is my first encounter with a rebase heavy workflow.
@b0rk Unsure if this is best practice or a horrible hack, but if I'm going to do something complicated with a branch I always create a backup first. Branches are cheap, so if you do "git branch backup" before your risky rebase, you can always get back to where you started. All without playing with the reflog.
there's a git reflog for every branch, not just HEAD. and while git reflog without a branch defaults to HEAD, @{1} without a branch defaults to your current branch. so @{1} is always the state right before your rebase, no need to consult git reflog
for reviewing rebased branches, git range-diff is awesome. unfortunately none of the forges have it integrated into their web UI, so you need to fetch both old&new commits and compare locally 😥
@b0rk and yes I have a git alias for both because I screw up my rebases this often ...
I also have an alias for git diff @{1} as a very quick check that I haven't lost any change or added something in a rebase that only changed the commits, not the baseline
@b0rk A great summary! I'll share widely. A couple of notes:
HEAD^^^^^^^^ [was already addressed in earlier comments]
git rerere is great when you expect to have to refix conflicts, but it will also record and reapply incorrect fixes automatically. git rerere forget or clear help, but I still prefer to enable it explicitely when needed, and leave it off the rest of the time
@b0rk what an excellent post, it contains almost everything I'd have wanted to write about rebasing. The one thing I miss is how others can deal with a --force(-with-lease) pushed branch. Either git reset --hard origin/feat, or when they have pending commits themselves using git rebase --onto origin/feat HEAD~2, where the 2 is the number of commits someone else has pending locally. Hopefully that's clear as I realize it's quite succinct. Anyway, thanks for the great article!
@cryptix found a concrete example of why/where got beats git (apparently):
> splitting commits in an interactive rebase is hard
this confused me at first because "rebase" sounds like just replaying commits to me, not like splitting or otherwise rearranging them. after all, the latter is what 'got histedit' is for!
@b0rk also, now I'm very curious about a similar version with "git merge: what can go wrong?". As I tend to avoid using "git merge" I guess I could learn a lot from that other side.
And I'm also very curious about that side as #Mercurial seems to emphasize/optimize a lot more on merged history compared to #Git? (at least that's what I had heard)
@b0rk urgh... the "Stopping a rebase wrong" is something I can relate to and did so a few times in the past. It's particularly annoying bc. the "git reset --hard" looks correct initially. Only when trying to start a new "git rebase" I get errors. And then I often reflexively typed "git rebase --abort". And then all my changes between the "git reset --hard" and "git rebase --abort" are "lost" and I have to resort to "git reflog" to dig them up again.
@b0rk to reduce issues with "force pushing makes code reviews harder" I add a small changelog, with one short sentence for each change. And add that as an extra comment on Github/Gitlab. Or below the "---" in a commit for email based, single patch submissions, so that it won't be added to the git log history. Or in the cover letter.
And I really like that Github now also takes note and allows to show the diff with one click.
What I also found out about only recently: b4 diff -- <message-id>
@b0rkgit revise can help with a lot of those pain points like splitting commits and git absorb is pretty handy too for when you have a couple major curated commits in a branch and want to keep stuffing new little bits in them. Also worth mentioning that Git can now pretty robustly rebase while preserving merges.
@b0rk this is really great, thanks for this.
Q. “Weird interactions with merge commits” is this specifically about when you merge other branches into your feature branch, then attempt a rebase? What does “underlying project” refer to?
@b0rk There is third-party git-imerge tool that can help with rebasing a very long string of commits (and stop in the middle, and share partially rebased/merged state). Unfortunately, it looks like it is not actively developed, though I might be wrong.
My own feeling about rebase is not so much “what can go wrong” as a philosophical dislike of effectively re-writing history. A practical consequence of this is that when you look back at the commits, they aren’t the ones you did (or anyone did), which, for me, makes it harder to get my mind back to where it was when the changes were being made. And that’s a lot of what I want from history.
@njr i’ve heard this objection a lot of times and i’m really curious about it but struggle to relate to it — personally the history i’m rewriting is often 17 “wip” or “fix” commits which to me are totally worthless as a historical artifact. Are you saying that you already make pretty nice commits to begin with? do you ever amend commits? does amending a commit feel like rewriting history to you?
@njr i guess concretely when you say “i don’t like rewriting history” i’m really curious about what an example of the before/after you’re thinking about is
I’m a mathematician, and most mathematicians would say Gauss was the greatest ever mathematician. But he hid his working. He presented his results fully formed and perfect, and it was very hard for people to see how he had got to them.
In software, there are many wrong turns and dead ends (as in maths). I prefer (philosophically) that the records includes the wrong turns that happened because…well, because they happened, and can be illuminating.
@b0rk If the historical record aspect is important, it seems natural to want it to be accurate.
Of course, people can choose how often they commit, and I think your viewpoint (and that of many others) is that there are conceptually different kinds of commit—kind of checkpoints (perhaps “just” for backup, swapping machines, recovery etc.) and real commits where you finish something. But there’s not so much difference in my mind.
@b0rk 3. For me, at least, I remember specific changes and when I need to go back and fix something, it’s much harder for me to do so if that change has been … well, altered in some way. It’s harder for me to find, and harder for me to get my head back to where it was. So this part of my dislike of rewriting history is practical, not philosophical.
@b0rk I don’t think I’ve ever used “wip” or “fix” as commit messages, but I do use “.” fairly frequently and used “..” the other day (when I needed another one after “.” And didn’t want to repeat myself :->).
But those are almost always commits that are fixing a stupid mistake…forgetting to add a file, leaving a debug message in, breaking a test in some trivial way. Almost always, the previous non-“.” commit messages remains valid when I do this.
@b0rk The only time I ever really use rebase is when I’m developing teaching material. Sometimes I want to show a repository in a very specific sequence of states, and in those cases I’m completely fine with rebasing to create the right history because I’m not smart enough to create all the right intermediate states linearly. But that feels like a very different use of git/version control to me from the usual one.
@b0rk Didn't know about git rerere. Thanks for sharing! I suggest amending:
> use --force-with-lease when force pushing, to make sure that nobody else has pushed to the branch since you last fetch
to emphasize that fetching immediately before pushing this way defeats the protection of --force-with-lease. I've recommended this flag to less experienced developers eager to improve their Git skills, failing to make this gotcha crystal clear. On one or two occasions, it has led to lost work.
@b0rk Great post! We use Squash + Merge at work too. I started using it at Kickstarter 5 years ago and was very reluctant at first - mostly because I was taking pride in creating pull requests with a very clean Git history... I wouldn't go back though: I can now keep it dirty simple, move faster and choose to rebase or merge depending on the use case. It also makes main's history much cleaner: 1 PR == 1 commit.
@b0rk A sort of work-around for the git commit --amend/git rebase --continue confusion is to always use git add followed by git rebase --continue. Those steps do the right thing whether you're resolving a conflict or editing a commit.
@b0rk you asked about real-life users of the GitHub squash-and-merge strategy: that’s my team. Folks who are confident with rebase can use it to keep their PRs tidy, folks who prefer not to can merge main into their branches, and either way we squash to land the PR. We like squashing also because we find individual commits relevant for code review but whole-PR granularity more useful for long-term history (e.g. it’s easier to match to our ticketing system).
If we have a repo with multiple local (as yet, unmerged) commits that reference other local commits in a submodule, then rebasing the submodule branch requires also fixing up each commit in the parent, since those now point to invalid submodule commit hashes.
(I'm currently working on tooling to make this process easier)
@b0rk Re: code review: it's been a while since I used Gerrit but my recollection is that Gerrit's workflow is very rebase-intensive (and it will do trivial rebases for you automatically as and when other changes get merged).
@b0rk I also find the rebase path preferable. Another tip: especially in team environments, having branch protection rules turned on to prevent accidental force pushes after rebases helps a lot. (Github, Gitlab, have this, not sure about other servers) I tend to stay away from shared temporary branches outside of main, but enabling branch protection on those too when lots of people are working together can help avoid things.
@b0rk Nice write-up! I learned a few things, now to remember to use them next time. 😅
> I was curious about why people would run git push --force on a shared branch.
Collaborative feature branches are a thing, especially when you’re not shipping a web app but a client side native artefact. Rebasing those feature branches on the main branch is useful as much as it is for single-developer ones. But it needs to be coordinated; I usually rename the remote branch to a backup name.
Add comment