10 ms·
Stacked Diffs with git rebase —onto
- hahahacorn 10mo agoI consider myself a shmedium experienced dev who likes to learn their tools and read source. This seems like a house of cards whose juice isn’t worth the squeeze. But I would love to split up my PRs into smaller pieces. I just would hate to waste time with an incomprehensibly goofed git history because I forgot a command.
- the_gipsy 10mo agoI usually just `git rebase origin/main -i` after the base branch has been merged there, and this means I need to explicitly drop the merged commits, but I can inspect what's happening.
- Xophmeister 10mo agoYeah, I do this too: The `--onto` solution feels a bit too magical at times and an interactive rebase is pretty clear about what's happening.
- WorldMaker 10mo agoAdd `--update-refs` to your interactive rebase and it will give you an easy line to know how many commits to drop because it will add an `update-ref` line for the old branch. You can just easily delete everything up to and including that `update-ref` line and don't have to manually pull up a git log of the other branch to remember which commits already merged. (Plus, of course, if you have multiple branches stacked, `--update-refs` makes it easier to update all of them if you start from the outermost branch.)
- Xophmeister 10mo agoNice; thanks :) I usually squash-merge. Does that break this workflow?
- WorldMaker 10mo agoDepends on how fast you delete merged branches? I picked up `--update-refs` to not lose my mind working in squash merge repos. I much prefer merge commits. I often make good, well documented commits and I like having access to the original commits if I need them, so when in a squash merge repo I become a branch hoarder renaming merged branches with a `zoo/` prefix (to drop them low in sort order, among other reasons it is name `zoo/`). I will often keep experiment branches around and `--update-refs` helps me manage that because if I see commits that might update-ref a `zoo/` branch I know to drop them from the experiment branch. All of that discovery of already merged commits would be automatic/cheap in rebases with merge commits. I was very frustrated before discovering `--update-refs`. I'm still often frustrated with squash merging, but keeping a large `zoo/` and having `--update-refs` is extra work that almost replicates the experience of just using merge commits in the first place. I don't know why so many think squash merge workflows are "simpler".
- perspectivezoom 10mo agoI'm a heavy user of git-spice: https://abhinav.github.io/git-spice https://abhinav.github.io/git-spice (created by a former coworker) and can't really go back to a time without it. While still not nearly as good as Facebook's Phabricator, it's probably the best workflow for small, focused stacked PRs you can achieve in a Github / Gitlab based repository.
- jbjbjbjb 10mo agoI think ‘git rebase —-update-refs’ is the better way to go for this scenario
- enbugger 10mo agoIs there any good guide on how to solve the issue which OP solves?
- sirsuki 10mo agoYou don’t really need docs as --update-refs does what the OP does automatically instead of manually like the OP does.
- ptx 10mo agoHow? I tried recreating the scenario from the article (the section "First rebase –onto") and ran the first rebase with "--update-refs": $ git checkout feature-1 $ git rebase --update-refs main Successfully rebased and updated refs/heads/feature-1. Updated the following refs with --update-refs: refs/heads/feature-2-base But all it did was update feature-2-base. It still left feature-2 pointing to the old commits. So I guess it automates "git branch -f feature-2-base feature-1" (step 3), but it doesn't seem to automate "git rebase --onto feature-1 feature-2-base feature-2" (step 2). Presumably I'm doing something wrong?
- happytoexplain 10mo agoFirst, you don't need the extra "marker" commit. This flag obviates the entire workflow. Second, you run it on the outermost branch: feature 2. It updates all refs in the chain.
- mhw 10mo agoYeah, you need to rebase the tip of the feature branch stack. git will then update all the refs that point to ancestor commits that are moved. So in this case $ git rebase --update-refs main feature-2
- dimitrieh 10mo agoJujutsu comes in handy here for the same usecase: https://github.com/jj-vcs/jj https://github.com/jj-vcs/jj https://www.stavros.io/posts/switch-to-jujutsu-already-a-tutorial/ https://www.stavros.io/posts/switch-to-jujutsu-already-a-tut... Also found https://github.com/gitbutlerapp/gitbutler https://github.com/gitbutlerapp/gitbutler
- ndr 10mo agoThe main issue I kept having when trying to do this with just git is then managing all the branch names to be attached to the right moved commits, so that my stack could be reviewable on github's open PRs. Does jj help with that at all? I've experimented a bit with git-town.com (OSS) and now everyone at $DAYJOB uses graphite.com (SaaS) which does that part very well.
- baq 10mo agoIt’s one of the core features that rebases, including branch names (bookmarks in jj) work ‘correctly’. You can rebase whole dags, including merges, with multiple named heads with just one jj rebase -b.
- arccy 10mo agonote that bookmarks don't float, unlike git branches, so if your pattern is to produce a lot of commits, you'll want something to keep your jj bookmarks pointing to the top of your pile of commits. this is less of a problem if you're more into the 1 change == 1 commit workflow.
- lima 10mo agoThere's an experimental-advance-branches feature which helps with that!
- pimeys 10mo agoThere's a very common alias `jj tug` for this case: tug = ["bookmark", "move", "--from", "heads(::@- & bookmarks())", "--to", "@-"] It moves the nearest bookmark to the commit before the current one (which should be your working commit).
- politelemon 10mo agoThis marker branch step feels like a workaround to a missing capability. It's something I can easily see one forgetting especially if they haven't been doing stacked diff workflows regularly.
- sublinear 10mo agoI agree it seems error prone. I'm not sure if I'm misunderstanding something, but I use `git cherry-pick` when I know I need to move commits around that might have conflicts. The problem with rebase can be that the user doesn't fully understand all the options being applied and end up with a "bad" merge. I don't usually want to rewrite history. I just want the target branch with all my commits on top (I usually squash the feature branch into one commit anyway). I have yet to run into a situation where this isn't good enough. If the branch diverges so much and has so many commits that this simpler approach doesn't work, that might not be a git problem, but a project management one. It's still always nice to know git has tools to get me out of a jam.
- 1718627440 10mo agoRebase is just automated cherry-pick, so it ends being the same. The pick command in rebase is exactly that. > and end up with a "bad" merge. They end up with exactly the same merge when using cherry-pick directly? > I don't usually want to rewrite history. I just want the target branch with all my commits on top That's ... what rewriting history is?
- imron 10mo agoThe capability is there. Just use git rebase --update-refs ...
- url00 10mo agoWow you aren't wrong, the first blog post on Google talking about this is exactly what this complicated method does just built-in.
- 10mo ago
- nopurpose 10mo agoThat particular case can be solved much easier by rebasing outer-most branch with `--update-refs` flag.
- imron 10mo agoYep. I set this in .gitconfig
- happytoexplain 10mo agoI came into the comments specifically to ask if this flag existed. I feel bad that the author developed this whole flow just because they didn't know about this, but that's pretty common with git.
- fwip 10mo agoI'm pretty sure the author was Claude, so don't feel too bad for it.
- duskdozer 10mo agoI'm guilty lol. I wrote a helper to do rebase chains like this
- nopurpose 10mo agoupdate-refs works only in a narrow case when every branch starts form the tip of a previous. Your helper might still be useful if it properly "replants" whole tree keeping its structure.
- WorldMaker 10mo agoThough at that point it may be easier to rewrite your helper to manage rebase's interactive scripts.
- duskdozer 10mo agoNo, as far as I can tell, it's basically just doing update-refs. But in my defense, I just found out by looking for the option that for some reason my git manpages are from an old version before it was introduced
- schacon 10mo agoGitButler handles all of this pretty automatically, if you don't want to deal with the Git gymnastics needed here. https://blog.gitbutler.com/stacked-branches-with-gitbutler https://blog.gitbutler.com/stacked-branches-with-gitbutler
- motoboi 10mo agoI believe the author would love stg: https://stacked-git.github.io/guides/tutorial/#patches https://stacked-git.github.io/guides/tutorial/#patches
- nrhrjrjrjtntbt 10mo agoFun stuff, but I'll stick to trunk based dev, small PRs... thanks!
- taejavu 10mo agoStacking commits lets you do that without having to wait for each change to be reviewed/merged to the main branch before you iterate on top of those changes.
- nrhrjrjrjtntbt 10mo agoTrue. I find I rarely need it: standard rebase or merge do the trick. If they don't the review cycle or PR size may be too high. Super rare I need onto. So rare I look up how to do it when I do.
- flr03 10mo agoIt's such a complicated way to work though, you start another set of changes then you go back addressing comments then you go back updating the stacked branch and you might need to do that few times... Teams should focus on getting stuff merged in and not create massive PRs that live forever, life becomes so much easier.
- sockbot 10mo agoWe use graphite at work to automate this workflow. The whole point is avoid this toil.
- ragebol 10mo agoAh, I've been doing this for ages but apparently this practice has a name
- dspillett 10mo agoFor the example given, would merging branch 2 into branch 1 then branch 1 into main achieve the same effect? Perhaps not an option if you need to release the work in branch 1 before the work in branch 2 is ready/reviewed/etc.
- happytoexplain 10mo agoThe point of this technique is to keep them separate. See the other comments about `--update-refs`.
- happytoexplain 10mo agoEven if `--update-refs` didn't exist, my experience is that git can identify duplicate commits produced by rebase, and knows to skip them when rebasing the same commits to the same place again. Am I imagining that?
- 1718627440 10mo agoIt definitely fast-forwards unchanged commits.
- adrianN 10mo agoThat works until you had to fix conflicts during the rebase and the commits are no longer identical.
- swaits 10mo agoEvery time I see one of these nifty git tricks or workarounds I find myself wondering, “why not just use jj?” You get a nicer, significantly simpler interface. You don’t need any tricks. You don’t have to google how to work yourself out of a bad state, ever. And you get near-perfect git compatibility (ie you can use jj on a shared git repo, doing all the same things, and your teammates won’t know the difference). I’ve wondered if there is a psychological thing here: someone who spent time memorizing all the git nonsense may have some pride in that (which is earned, certainly), that introduces some mental friction in walking away???
- YmiYugy 10mo agoFor me the answer is lazygit. I rarely use the git cli. I don't want to learn the jj cli and the TUI wrappers for jj seem less polished.
- smcameron 10mo agoFor me, the answer is stgit. https://stacked-git.github.io/ https://stacked-git.github.io/
- max_k 10mo agoOh, there's another stgit user! ^5 Coming from darcs, I couldn't use git until stgit came along, and today, it's one of those few tools I can't imagine working without. Nothing else matches my way of code hacking. So often, I watch people making a big mess with git, and I always recommend stgit to them, so they can post proper and reviewable branches for merging. But in all these years, I could never convince anybody.
- deleted 10mo ago[deleted]
- Fang_ 10mo agoIs there any reason besides merge commits ending up in history to not do this with merges instead? ie merge main into feature-1, then feature-1 into feature-2. Sounds like using --update-refs would let you do all that in a single operation, but you still need to force-push and don't maintain an explicit merge/conflict resolution history, both of which could be considered sub-optimal for collaborative scenarios.
- happytoexplain 10mo agoThe use case is that they are not ready to merge yet.
- saint_yossarian 10mo agoThey meant merging the other way, i.e. merging the new changes from main into the stacked feature branches. This is functionally the same as rebasing, except that the new changes show up at the tip of the commit chain rather than the base. And because it doesn't rewrite history you don't need to force-push.
- bvdw 10mo agoI've settled on a workflow that reverses the situation. I simply commit all my work to the main branch and cherry pick commits into temporary feature branches only when submitting PRs. This way I only need to worry about maintaining a single consistent lineage of commits. I've been using this workflow for about a year now and find it to be much easier than juggling and rebasing feature branches. In case anyone's interested, I made a tool that automates this workflow. The worfklow and tool are described here: https://github.com/bjvanderweij/dflock/ https://github.com/bjvanderweij/dflock/
- motoboi 10mo agoHow many people on the team?
- bvdw 10mo agoI work in a small team, about five people.
- JimDabell 10mo agoYou might like Jujutsu – you commit without being on any branch and then later you can decide where and how you put things onto branches.
- bvdw 10mo agoInteresting, I'll be sure to check it out. It sounds pretty similar to the tool I built which lets you edit a "plan" in a text editor to assign commits to feature branches - the plan is saved so it can be amended continuously.
- mytailorisrich 10mo agoInstead of "stacked diffs", isn't the more "continuous integration" solution to split a big feature into small chunks that actually get merged? Having to rebase again and again is a symptom that a dev branch is living for too long.
- glenjamin 10mo agoI’m amazed that this comment is so low down Stacked diffs seems like a solution to managing high WIP - but the best solution to high WIP is always to lower WIP Absolutely everything gets easier when you lower your work in progress.
- happytoexplain 10mo agoThis seems idealistic. It's very normal to be working on a feature that depends on a not-yet-merged feature.
- glenjamin 10mo agoI invite you to look into feature flagging. It is entirely viable to never have more than 1 or 2 open pull requests on any particular code repository, and to use continuous delivery practices to keep deploying small changes to production 1 at a time. That's exactly how I've worked for the past decade or so.
- thunderfork 10mo ago[dead]
- SideburnsOfDoom 10mo ago> It's very normal to be working on a feature that depends on a not-yet-merged feature. Oh sure, many bad ideas and poor practises such as that one are quite "normal". It's not a recommendation.
- happytoexplain 10mo ago
- andrebianchessi 10mo agoStacked commits is awesome, but it sucks that with git you need all these workarounds, and that the servers (GitHub etc) are not designed with that workflow in mind. I left Google a few months back to work on another project. I missed the internal version control so much that I dropped that other project and am now focused http://twigg.vc http://twigg.vc It's very similar to what's used at Meta and Google. Atomic commits, linear history, auto rebase, code review compatible with stacked commits.
- davidfstr 10mo agoI’ve been meaning to write this article for a long time @flexdinesh . Thanks for taking the time to share this technique for managing stacked diffs using vanilla git rebase!
- IshKebab 10mo agoEh I just use `git rebase -i` and delete the commits I don't want. Much easier to think about. But the real problem with this workflow is that neither Github nor Gitlab support it at all. Not even Forgejo does. Which blows my mind because is such an obvious way to work. As far as I know only Tangled actually supports it, but unfortunately Tangled is tangled up in its own weird federation ideas. There's no way to privately host it at all.
- taejavu 10mo agoI’m not sure what you mean, I stack PRs all the time with GitHub - select the earlier branch as the merge target instead of the main branch.
- IshKebab 10mo agoThat doesn't work for cross-repo PRs which is by far the most common way of creating PRs on GitHub (at least for open source stuff).
- ptx 10mo agoWhat I would really like is a Git equivalent to Mercurial's "fold" operation. I usually make a bunch of commits as I work, just as checkpoints, which I then want to turn into a single final commit when it's done, which could be quite some time later, e.g. "started on thing", "broke it", "broke it more", "Add thing to improve foo of bar". Mercurial's "histedit" command offers two operations to combine the commits: "fold" and "roll", where "fold" brings the earlier commit into the later commit and "roll" does the reverse. The "fold" operation does exactly what I want in this case: It brings all the changes into the final commit from when I actually finished it, using the commit date of that commit. Git's "rebase -i" command offers "squash" and "fixup" and "fixup -c" and "fixup -C", but they all do the same thing as "roll", i.e. they keep the earliest commit's date, the date when I started working on the thing and not the date when I finished working on it. (So I then cut and paste the correct date and update the commit afterwards. This may not be the best way to do it.)
- WorldMaker 10mo agoThat is an interesting desire to use a later commit date rather than an earlier one. So many prefer that commit date of when an effort started. One way to accomplish that is to reorder the commmits in `rebase -i`: "pick" the last commit first and squash/fixup the rest after. That can produce some very weird merge conflicts, but it works well more often than you might think, too. At the very least, you can do your hand editing of the commits during the rebase instead of after by switching the earliest commit from "pick" to "edit" to have the full power to amend the commit before it moves on (with `git rebase --continue` when you are satisfied). (Versus "reword" if you just want to change the commit message only.) Also, instead of naming commits things like "broke it" and "broke it more" an option is to use `git commit --fixup={previous commit hash}`. That auto-generates a "fixup!" name based on the previous commit and auto-marks it as a fixup if you use the `--autosquash` flag to rebase.
- ptx 10mo agoI do use fixup commits as well, for fixing small issues discovered after I make the proper commit, and in that case it makes sense to use the date and message of the earlier commit. But in the case I'm describing, the earlier commits are essentially just temporary snapshots on the way to making a proper commit. Just a more explicit undo history, basically. I usually don't even bother naming them anything as meaningful as "broke it", actually. Maybe most people wouldn't even bother making these commits – or maybe they would if Git supported "fold".
- foobarian 10mo agoThis made good sense, though by the time I got to bits saying "that last step is critical. Without it, your next sync will break" and "Don't forget!" I was laughing out loud. I love git :-)
- azophy_2 10mo agoI feel that stacked PR in git should be common knowledge, however many of the documentations are scattered. I found stacked.dev but feels that its not exploring pure-git workflow that well. thats why I tried to collect all of those docs into a single website that's dedicated into that singular workflow. would love to get any feedback: https://stacked-pr.github.io https://stacked-pr.github.io (disclaimer: partly supported by AI, but most of the writing structure were made by hand)
- mytailorisrich 10mo agoIf you adopt a good continuois integration process you don't need stacked commits at all, which are more a symptom of a process issue, IMHO. It's fashionable at the moment so there is a bit of herd mentality going on. In 2 years they'll be a reinvention of the wheel again against it.
- dfawcus 10mo agoEven for 'normal' rebases of a multi-commit series (without named feature branches), I habitually use the --onto form. It is simply easier to conceptualise what is happening if one is explicit about the 3 references. As such, using it for the situation described in the piece then becomes a trivial matter, especially if one also habitually runs a graph viewer - I generally have one or more instances of gitk running all the time.
- buster 10mo agoI fail to see how it makes it much easier to review a lot of interdependent branches where branch C needs new logic from branch B which needs commits from branch A. Sounds like a nightmare to me. People should really try to work on trunk.
- tizzyapunkt 10mo agoI‘m using git machete since a few months to help with the stacking workflow. The nice thing is that it can use the GitHub CLI to manage the resulting PR stacks as well. https://github.com/VirtusLab/git-machete https://github.com/VirtusLab/git-machete
- tdiff 10mo agoHard part is keeping the feature branches orthogonal and maintaing all the -base branches. Every fixup commit would invalidate them.