git rebase: what can go wrong?

git rebase: what can go wrong?

Julia Evans
would love to hear about any things that can go wrong with rebase that I missed in this blog post https://jvns.ca/blog/2023/11/06/rebasing-what-can-go-wrong-/
git rebase: what can go wrong?

git rebase: what can go wrong?

Julia Evans
@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

@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

@jamiemccarthy @b0rk I've started using "git absorb" for PRs. One absorb per nitpick, push, resolve, then next nitpick, and when approved either rebase or squash.

Then again, I REALLY hate octopus merges.

@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 and the obvious gotcha. Rebasing on the origin vs the remote branch haha

@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

reveal-md

@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 oh, good one!

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 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.
@dolmen thanks! i guess everyone has different ways to do the same thing (it's the same as HEAD~8)

@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.

Thanks for the article.😀

@b0rk I'm a big fan of knuckle-rolling a new branch whenever something's up.

@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 I'm reading this carefully because I like rebase, so I'm curious too as to why some people don't like it.

Plus I have a team that is new to git and I want to adopt "beginners mind" when we transition them from svn (don't ask) to git.

@b0rk Not another thing that can go wrong, but an alternative solution to using `reset --soft` for splitting commits: You can `edit` on the commit before the one you want to split, then use `git checkout <commit-sha> <file>` to grab files out and commit them before, then when you continue the rebase, git will remove them from the later commit when it realizes they've already been applied.
@b0rk in my experience, everything 😂
@b0rk aaaaand shared with my team :D thank you for putting so much good content out there, Julia
@b0rk thanks for this! Your solution for ‘undoing a rebase’ is going to save me a bunch of time (whenever I do a knarly rebase I’ve been creating a “backup” brach from my starting point and then using that as a “target” for git reset if something goes wrong 😅)
@petert ooh I think I'll mention the "backup branch" workaround too, that's really smart
@b0rk @petert What happens if the GC runs before you get a chance to undo the rebase? Will be old commit even be there? According to docs, many commands automatically trigger the GC depending upon the repo size. Backup branch may be a safer bet here. (I use the same concept to preserve development history).
@b0rk @petert I use backup branches all the time. After a rebase with merge conflicts, especially when rewriting history, I’ll typically git diff between the rebased branch and the backup just to check I didn’t do anything stupid like drop or duplicate a line.
@b0rk This is why I avoid erasing commits with rebase, and instead use: git merge --edit --squash <source_branch>

@b0rk the first thing I looked for when I got the idea of what this post was about was if you knew a solution to loss of gpg-signatures. It’s very much still something some people (at least me) care about a lot.

Another thing to add to the list is rebase merges can mess up the SHA of commits, making it impossible to add a .git-blame-ignore-revs in a PR containing the thing you wanna ignore.

@cafkafk ooh what is .git-blame-ignore-revs ? I've never heard of it
@b0rk It's where you put those super large trivial refactors that you don't want to clutter blame, e.g. when you do a treewide s/something/else/g
@b0rk love this!!! Thank you so much!!!!
@b0rk Wrt the “force pushing makes code reviews harder”, GitHub has had links that diff between the old branch and the new one for a while (initially hidden behind the "force pushed" in the "User1 force pushed commits…" line, now there's a dedicated and prominent button on that line).
So at least it's easy to see what changed between the two PR "states"; it's much harder to see what changed in each commit though; but nothing can give it to you besides commit-oriented review tools, such as Gerrit

@b0rk great post! I particularly liked "stopping a rebase wrong", because I've done that a few times but never quite gave it its own brain-category for "yes that is a specific mistake I keep making".

"complex rebases are hard": yes, when I'm polishing a personal branch to push, I often take it a lot further than 2 rebases. More like Θ(N) rebases to sort out N commits!

(That _feels_ wasteful because it's quadratic time overall! But git's working time is dominated by my thinking time.)

@b0rk Good tips! I am a firm believer that if you commit early and commit often it's hard to actually lose anything with git (barring hardware failure), but I also heavily rely on rebase to tidy all that up before pushing and I remember it being pretty daunting at times when I was just getting started.
@b0rk to squash commits, I generally prefer doing `git rebase main --keep-base` instead of counting how many commits I need to go back with `^`. It is a lot easier.

@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.

@pmdj thank you! will add that as a reason
@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 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 We use git submodules extensively.

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 heads up, the `what could a merge-only workflow look like?` link in the list is broken
@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).
@tikitu @b0rk I'm a rebase person myself, but my team uses squash + merge for the sole purpose of making sure ci ran on every merged patch. Doing multi-commit rebase merges (in github parlance) means ci steps only run for the last commit in the series, so the preceding commits might be broken, and nobody would notice until they tried git-bisect.
@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 I wrote https://git.sr.ht/~nhaehnle/diff-modulo-base specifically to help with the "force pushing makes code reviews harder" problem
~nhaehnle/diff-modulo-base - sourcehut git

@b0rk I love this. Thanks for writing it!
@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.

@pcreux @b0rk I came here to say the same thing. I commit _a lot_ and don't ever bother thinking about my commit history until I'm getting ready to submit a PR.

If I somehow ended up with a PR that's large enough that it really should be split into multiple commits, I'll create a new branch off main, do a `git merge --squash`, only add the files/lines that should be part of that commit, and discard the rest.

@b0rk Also, you mentioned having a bunch of "wip” and "typo" commits, which I do do *sometimes*, but most of my commit messages are notes to myself. “Got most of the tests working, except for that one DB call”, “Everything runs, but need to format the new file”.

Super useful when I'm working on something a few hours at a time over the course of a week, it helps me remember what I was thinking.

@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 just a message to thank you for your work to explain complex computer things :)

@b0rk I’m a little late to this, but...

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?

(those are all genuine questions!)

@b0rk @njr I often amend commits, and it doesn't feel like rewriting history to me. It feels like telling a cohesive story, one chapter at a time. I often treat the commit log as a document; while writing I use the 'track changes' functionality, before shipping I git rebase --autosquash --interactive to do a final editing round. For project with dependable and fast test suites, adding --exec "make test" to the rebase command could be an improvement to that process. 🤔
@njr @b0rk I like to use git-fixup.sh to quickly write amend-commits, like git fixup HEAD^
@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