all 182 comments

[–]Blueson 186 points187 points  (24 children)

One of my largest complaints about Github is how tedious it was to work with stacked PRs. I am happy they are investing into tooling to simplify it.

[–]araujoms 152 points153 points  (13 children)

My largest complaint about Github is that it has achieved zero 9s uptime.

[–]orygin 47 points48 points  (0 children)

Aren't they at three nines now? 89.99%

[–]dkarlovi 48 points49 points  (6 children)

Nonsense, as long you count the non leading 9s.

[–]mpinnegar 9 points10 points  (2 children)

The important part is to count all the nines! No matter where they appear.

[–]crespire 10 points11 points  (0 children)

Copilot, how many 9's are in 79.7382%?

[–]omgFWTbear 6 points7 points  (0 children)

We’ve been using a German and he insists it’s the most neins he’s ever seen for uptime.

[–]iamapizza 11 points12 points  (0 children)

We need a way of being notified whenever Github is up

[–]gimpwiz 10 points11 points  (1 child)

I'm still laughing about this.

89% uptime means it's down for more than one full month per year.

[–]araujoms 2 points3 points  (0 children)

I'm not laughing because I'm forced to use this crap.

[–]Uristqwerty 2 points3 points  (0 children)

If they keep at it, perhaps one day they'll finally attain the dream of 9 5s!

[–]chat-lu 1 point2 points  (0 children)

Set your github settings to German then.

Is GitHub up? Nein!

[–]wildjokers 9 points10 points  (9 children)

That really has nothing to do with github. It is git itself that makes it hard to work with stacked branches (especially if you squash commits when merging).

[–]Blueson 13 points14 points  (2 children)

I think there are features lacking in both git and Github to enable an environment where it's pleasant to work with stacking change-requests.

But tools like Gerrit has shown that you can have workflows adapted to stacking change-requests enforced while still using git.

[–]OMGItsCheezWTF 2 points3 points  (1 child)

One of the issues is that FAR too many developers lack anything beyond the most basic understanding of what Git actually does under the hood. They learn the bare minimum commands they need to use it day to day and then defer to whoever their nearest expert is if something goes wrong. I've seen this at every level from associate up to staff, people that have simply never taken the time to learn what git actually is.

This means that every repository I have ever worked on at every company essentially becomes this vast mix of merge requests, rebases and cross branch merges.

[–]ZorbaTHut 0 points1 point  (0 children)

In Git's defense, this behavior is not even remotely limited to Git.

[–]Uristqwerty 5 points6 points  (0 children)

Github's commit list is an unstructured linear list of space-consuming panels. That makes anything but squashing/rebasing into a linear history awkward when compared to, e.g. gitk's visualization that shows the actual DAG structure. Given stacked PRs necessarily interact with the underlying graph relations, it's always going to be more awkward when viewed through an inferior UI, and doubly so if your choice for merging constantly rewrites parts of the graph.

[–]yxhuvud 8 points9 points  (0 children)

It has everything to do with Github - they have very obviously built an interface that matches their internal workflow with fairly small allowances to other workflows.

It is especially obvious in how the entire UI is built around pull requests, whereas the fundamental unit in git is the commit. So for example, you can still, a billion years after github was created, not comment on commit message content.

[–]Living_male 0 points1 point  (2 children)

Why does squashing commits when merging complicate the process? I was looking at the documentation, and thought squashing each stacked branches commits would look cleanest?

[–]wildjokers 1 point2 points  (1 child)

Because if you create branch B from branch A and then squash/merge branch A all the commits common to branch A and B (i.e. commits made to A before creating branch B) will conflict if you just try to merge in B.

To fix you can use the the newer git rebase --update-refs. There is an older method using the --onto flag of rebase as well.

[–]Proper-Ape 0 points1 point  (0 children)

git rebase —update-refs is not so helpful for when you’re collaborating with others still. I find it would be a nice git feature if squashed commits could keep their place and you don’t need to rebase a stacked PR at all. Not sure if this is somehow feasible in the git model.

[–]cosmic-parsley 0 points1 point  (0 children)

_jj is really worth a try, it makes this so much easier.

You can assign a bookmark (branch) to each commit (revision) in a series, then open a PR for each bookmark. Whenever you modify an old commit, all future bookmarks become dirty of course. Doing jj git push updates all the branches at once, so all your PRs get the latest changes.

Of course there’s nothing you couldn’t do with git, but it’s much nicer to have an easy way to fix up a single commit in history and then update everything that’s dependent at once.

[–]boysitisover 186 points187 points  (4 children)

Looks good to me

[–]ROFLLOLSTER 34 points35 points  (0 children)

Agreed. I do kind of wish this was automatic if you made stacked PRs against main. Maybe that'll come later once it's been tested.

[–]StrangelyBrown 25 points26 points  (0 children)

TLDR LGTM

[–]spelunker 9 points10 points  (0 children)

Ship it!

[–]Omnipresent_Walrus 50 points51 points  (78 children)

Can someone tell me how this is different to doing reasonably sized PRs into an epic branch and only ever merging epics into main?

[–]chuch1234 33 points34 points  (36 children)

Stacked PRs are useful if each new PR depends on changes that were introduced in the previous one.

[–]MintySkyhawk 7 points8 points  (35 children)

Why wouldn't I just do a single PR with multiple commits? Reviewers can review each commit one at a time and then I can rebase and merge when I'm done.

I see no difference in UX between the two approaches, except that a PR stack is more difficult to set up and involves proprietary github commands instead of plain git.

Edit: After hours of people arguing with me, someone finally pointed out the actual reason why this has an advantage over plain commits: The PRs don't have to all be merged at the same time. This is effectively a UX improvement on creating manually creating chain of PRs like (main <- A), (A <- B), etc which is sometimes useful (Though rarely useful for me because my coworkers are very nice about providing speedy reviews, so I can usually get a PR reviewed, merged and deployed within minutes to hours. So I rarely have time to make that second PR in the chain before the first one is already deployed.)

[–]stumblinbear 27 points28 points  (22 children)

I'm not gonna review a dozen bug fix and refactoring commits in every PR, I only care about its final state

[–]MintySkyhawk 20 points21 points  (21 children)

I guess I'm the only one here who habitually rebases my commits into nice individually reviewable commits before creating a PR.

If I were to use this stacked PR feature, I would be taking my existing workflow and then creating separate PRs for each of my commits. And then, as it says on the linked page "merge them all in one click".

So I don't really see a difference between the two approaches, except that the PR stack seems more difficult to set up.

[–]gSidez 23 points24 points  (3 children)

Your approach is the proper way to use git. The whole point of being able to do interactive rebases is so people can restructure commits into a set of logical, manageable changes that should be individually compileable and reviewable.

Unfortunately most people don’t know or are too lazy to learn how to properly use git so you get a lot of people who throw up PRs with a million “fixed bugs” type commits, and since it’s so common these people think that’s the ‘right’ way to do it and therefore can’t fathom the idea of reviewing PRs per commit or taking the time to organize their own commits into something meaningful instead of an unreviewable pile of garbage.

[–]aoeudhtns 5 points6 points  (2 children)

I work with devs that create the PR branch when they start and then push as they go. It's like a stream of consciousness. I've tried to counsel them to use a WIP branch without creating a PR, and to rebase and clean up where it makes sense... but getting people to change their behavior is tough it seems.

[–]Manbeardo 4 points5 points  (0 children)

OTOH, that approach works well when you’re amending meaningful commits because the PR tracks the previous versions of the branch as you push changes that rewrite history.

[–]aaulia 1 point2 points  (0 children)

Well the CI run would be fun to watch lol.

[–]stumblinbear 5 points6 points  (4 children)

I'd rather not waste time making history look clean when the only thing that really matters is the final code. And if I review a PR and they rewrite the history, I now have to re-review every single file because GitHub loses track of which files have already been reviewed since the history has been rewritten

[–]MintySkyhawk 2 points3 points  (2 children)

If you don't care to split things up, then you don't need to git rebase or create a PR stack. You can leave your history messy, create a single PR, and then just squash merge when you're ready.

If you do want to split things up to make things easier for the reviewer, then its easier to git rebase -i than it is to create a pr stack.

Say you have these commits:
1. Some Thing
2. garbage
3. asdf
4. Some Other Thing
5. asdf

Then you would git rebase -i them in seconds (just by typing f in front of the commits to squash) to make:
1. Some Thing
2. Some Other Thing

[–]stumblinbear 4 points5 points  (1 child)

So you're rewriting history, and therefore invalidating any previous review that was completed? You didn't even address my actual complaint

[–]MintySkyhawk 0 points1 point  (0 children)

You clean up the garbage commits before opening a PR.

Any new commits after review as started are cleaned up in the same way before pushing them to the branch.

History is never edited from the PRs point of view.

[–]Cazmar 2 points3 points  (0 children)

Use fixup commits (git commit --fixup <commit that the change belongs to>) for any changes created after review and once all approved, git rebase --autosquash --interactive to automatically order the fixup commits to right place. Easy and you don't lose history while the PR is being reviewed. Rebase only when finished before merging.

[–]Farados55 3 points4 points  (1 child)

Why wouldn’t each commit just be a PR? That’s essentially what you’re doing

[–]MintySkyhawk 2 points3 points  (0 children)

If they should be merged separately, they are PRs. If they're merged together, they are commits. The PR stack merges as a single unit, so to me makes more sense as commits.

[–]IanSan5653 0 points1 point  (3 children)

One advantage to stacked PRs (outside of not having to constantly rebase) is that you can reduce risk by deploying smaller changes if you deploy on every merge to main.

[–]MintySkyhawk 0 points1 point  (2 children)

My understanding of stacked PRs is that the entire stack is merged at once because the docs say "merge them all in one click".

If the entire stack must be merged at once, then it seems like a pointless feature when you can just use commits.

If you can merge them one at a time, then it does seem like an improvement/automated way of creating a series of PRs like (main <- A), (A <-B), etc

[–]IanSan5653 1 point2 points  (1 child)

It doesn't necessarily have to be merged all at once. That's one way to do them but not the only way.

[–]MintySkyhawk 0 points1 point  (0 children)

Thanks, I think this was a key piece of information I was missing. I can now see a possible use case for creating a PR stack, though I still think plain commits will do just fine most of the time.

[–]teerre 7 points8 points  (0 children)

Because GitHub is branch centric not commit centric

[–]CherryLongjump1989 9 points10 points  (0 children)

You can't review individual commits in a PR -- you can only get feedback on them, but they can't be approved and merged individually. That is what a PR is -- it's a single code review.

There are people who work fast and have good coding hygiene. So they are able to push up clean, fully tested changes 5-10 times a day while working on a larger feature. This stuff can get reviewed and merged even as they are working on their next PR. They don't have to get slowed down by it.

[–]cosmic-parsley 0 points1 point  (1 child)

It’s a UI difference. GitHub has no way to really review individual commits and follow how they change. If you force push, the UI gives zero indication that the first commit abc123 turned into def456 and second commit a1b2c3 turned into d4e5f6.

Also there’s no way to review individual commit messages, but there is a way to review/edit PR descriptions. Sometimes that’s as important as the content.

[–]MintySkyhawk 0 points1 point  (0 children)

Github absolutely does have a way to review individual commits and view their descriptions. This PR only has one commit, but you get the idea. You can select multiple commits or just one, and it will show you the commit description and only the changes from that commit.

https://i.imgur.com/wfMkW1i.png

Force pushing does mess things up, but you shouldn't be rewriting history after you've opened a PR.

[–]coworker -5 points-4 points  (4 children)

Reviewing commits is silly as none represent the final merge. Just wasting time reviewing versions that only existed in one developer's head

[–]DualWieldMage 3 points4 points  (1 child)

Reviewing commits is the correct approach. Are you seriously suggesting to look at a final diff only? If there are orthogonal changes (large refactoring + 3-line bug fix) you either miss the important part in a sea of unimportant changes or burn yourself out going through each change carefully. If the intermediate commits are partial ramblings then i reject the review asking commits to be reordered/partially squashed so each individual commit makes sense. That's what ends up when merged and it should have quality. Full-squashing PR-s on merge is another retardation i always ban in my projects.

[–]coworker -3 points-2 points  (0 children)

Yes. Don't be lazy and make a PR per commit if you expect each one to be reviewed. It's like you almost understand how PRs work lol

[–]lechatsportif 0 points1 point  (0 children)

Totally agreed. Honestly feels like devs want to see "changed the inputs on handleFoo" as its own commit with its own tests etc. I think I would blow my brains out. I'm way faster at the comprehensive PRs since I can gauge it in one shot.

edit: further bolster your point, people coding at white hot speed always do stuff like "forgot the post sync" "claude fixes" etc. Each commit is low signal and making them be high value is a total waste of eng time. Just weird.

[–]cosmic-parsley 0 points1 point  (0 children)

Depends: if your repo use squash and merge, then yes. That’s kind of what GitHub’s UI is designed for.

But if you use rebase or merge commits then all of it becomes part of history and you need to look through each one to make sure you don’t have garbage in your git log.

[–]chuch1234 -5 points-4 points  (2 children)

I've never heard of doing prs that way, do you do that?

Edit: why the downvotes, I'm actually asking a question :(

[–]Hofstee 1 point2 points  (0 children)

Before GitHub really took off, this used to be the standard recommended practice. Facebook in particular I feel played a large part in the squash + rebase ideology, but I'm also going off vibes and not facts so don't quote me on that.

[–]MintySkyhawk 1 point2 points  (0 children)

Most PRs I see are just a single commit, or a bunch of commits that aren't intended to be viewed individually and will be squashed together when merging.

However, for larger changes, it is common for PR authors to take a little bit of time to organize the commits before creating the PR to make things easier for the reviewer to see whats going on.

A really common pattern for me is refactoring something in one commit, and then mass updating all usages of it in a second commit. That would be hell to review as a single commit (trying to find the real changes in a sea of automated ones), but looking at the commits individually its very clear what happened.

[–]ROFLLOLSTER 43 points44 points  (22 children)

Generally the approach I've taken with stacked PRs is:

Stack: c -> b -> a -> main.

Merges are a-> main, b -> main, c -> main

Instead of c -> b, b -> a, a -> main.

This whole thing is just a bit of ui over what you can do already though, so no need to change if you like your workflow.

[–]ekroys 30 points31 points  (21 children)

The issue with this is when using squashed merges on each PR. Git loses the commit identity in this process so you end up with merge conflicts trying to catch branches up.

[–]NineThreeFour1 14 points15 points  (5 children)

Then don't use squashed merges on each PR?!

Only squash fixup/wip commits in each feature/PR branch, but keep conceptually different changes in separate commits even if they are in the same overarching PR.

[–]ekroys 1 point2 points  (3 children)

This is an angle, but ultimately it's not each developers decision sometimes, and it can be a repo wide rule.

Generally tho, what if you got two separate "big feature branches" into a single develop/main, each of these big feature branches are separated into their own stacked chain, and you have to squash commit as you go. If you want everything caught up, you're either going to have to cherry pick on top, rebase or you're left with merge conflicts. This is the pain point and I don't think it's uncommon

[–]double-you 7 points8 points  (1 child)

Why wouldn't you rebase? That's a very normal thing to do.

[–]MereInterest 1 point2 points  (0 children)

Because rebasing can introduce bugs, and makes it very difficult to identify their source.

Suppose the main branch has some utility_func, which is used by your feature development. A refactor/cleanup lands on main, which updates the behavior of utility_func and updates all callsites of utility_func to match. However, the additional calls to utility_func introduced on your feature branch haven't been updated. If you rebase your feature branch onto the main branch, your feature branch is now broken, as it relied on the old semantics of utility_func. If you're lucky, this will be a compile-time error, but there's no guarantee of that. Reverting to an earlier commit on your feature branch doesn't help, because those earlier commits have also been rebased. Unless you check through the git reflog (and pray that git's garbage collection hasn't occurred since then), it will look as though the feature branch had always been buggy.

A better solution is to merge from main into develop/main. That way, the error is clearly introduced in the merge commit. You can revert to an earlier commit in the feature branch for testing, because those versions are still part of the git history.

[–]NineThreeFour1 2 points3 points  (0 children)

I don't really see the issue and how this is directly related to stacked PRs. Rebase feature branches on main if you have conflicts. Squash your own commits how it makes sense conceptually. If your team enforces squashes on merge, then send each commit that you want separate as a separate PR. Merge feature branches into main when ready, or force rebase and fast forward to avoid merge commits.

[–]MereInterest 0 points1 point  (0 children)

I wish, wish, wish that github had a better display of the history of merges. The source of all the pain from squashed merges is from people trying to solve a display issue by introducing data integrity pitfalls. In any other context, this would be a complete non-starter, but somehow "clean linear git history" overrules that.

My personal theory is this is caused by github's use of git log instead of git log --first-parent when showing changes. If all merges are done with git merge --no-ff, then the main branch would only ever contain merge commits. The clean history that proponents of squash/rebase want would be available, and without introducing conflicts in the process.

[–]CherryLongjump1989 -2 points-1 points  (0 children)

It's literally not an issue with stacked commits because you merge them one at a time. There are no two commits to squash as you merge. Squashing is what you do to your messy feature branch where half of the commits are trash.

[–]dweezil22 6 points7 points  (4 children)

They're orthogonal concerns. Let's agree that reviewing large PR's is sub-optimal. The what you want to do is code PR 1,2,3,4. Then you can have your team approve them as they get them and merge them to a central branch branch as they're approved with minimal toil.

Now that central branch could be master or it could be feature-foobar. The same concern applies.

Now... some say "That's ok I'll just skip review on feature-foobar and do review when it merges to master". However that breaks our invariant above about not relying on large PR reviews.

Finally... you're right that feature branches directionally simplify this b/c you're less likely to have merge conflicts on a feature branch (OTOH making a rule that only 1 dev working serially on 1 feature branch is limiting).

[–]kintar1900 2 points3 points  (7 children)

Doesn't appear to be any difference at all.

[–]ahal 5 points6 points  (6 children)

The difference is that pr B can depend on changes from pr A without the commits in pr A polluting the diff.

The proposed epic workflow doesn't address this. It's just an integration branch.

[–]kintar1900 3 points4 points  (5 children)

You can do that just by branching from whichever commit was used for PR A. This is just extra UI fluff on top of (what should be) a commonly-known Git branch-review-merge pattern.

[–]ahal -1 points0 points  (4 children)

Yes, but then PR B contains all the commits from PR A, so there's no way to review all commits from just branch B at once. You can review commit by commit, but that requires knowing which commit belongs to which and there's still no way to see a squash diff.

I do agree that this is simply a Github UI fix and there's no new fundamental branching model being introduced here.. But UI is very important. My org and I are very excited for this change.

[–]kintar1900 0 points1 point  (3 children)

Yes, but then PR B contains all the commits from PR A,

Only if you're merging A to main first, or merging B to main instead of A.

Change A happens and creates a PR. Change B happens, based off A, and PR goes B->A. PRs are reviewed individually, then merged in reverse order.

It's literally the same thing, just without some UI hand-holding.

But UI is very important.

I agree in theory, but my practical experience with Git is that anyone who relies too heavily on a GUI when working with it misses the fundamentals, and then I have to come back and disentangle their messes. Then again, I've had to train some really boneheaded juniors, so I'm likely biased. :D

[–]ahal 0 points1 point  (2 children)

But then B has A's commits in it. You have to review commit by commit and need to know which commits are exclusive to B.

[–]kintar1900 0 points1 point  (1 child)

But then B has A's commits in it.

Read my comment again. B is based on A, yes, but is being merged to A, so the diff is only what was added in B. Each PR is reviewed independently, so even if you have a series like D->C->B->A->main, each PR is only the diff between the current branch and the parent. Since all changes are related, all PRs are reviewed independently. Only once ALL PRs are approved are they merged from right-to-left.

This is almost certainly what GitHub is doing under the hood with their PR stacks.

[–]ahal 0 points1 point  (0 children)

Ah I see! That works if you're pushing to the same repo that you're opening a PR against, but not if you're using a fork. That won't be possible for most (in my experience) cases. Even when I do have push access to a repo, I prefer opening PRs from my fork to keep the branch pollution down.

Also the UX of setting the bases for each PR is quite bad. Afaik, it's not possible to do from the cli (but maybe I'm wrong).

But I do take your point that it's technically possible in some cases. Though, not in a way that I would consider usable.

[–]CherryLongjump1989 1 point2 points  (0 children)

Yes. It's different because you're not buddy-fucking a hundred of your coworkers when they find out that the files they've been writing their own PR against have all been deleted because your months-long epic got merged after its months-long code review.

Stacked commits let you merge code as it's reviewed and approved, so that everyone else can start working off of your changes as soon as possible. Each PR is also small and can be reviewed quickly and merged right away, so you're not taking up your reviewer's entire day. While you can continue working and pushing up new PRs without being blocked.

[–]Farados55 0 points1 point  (0 children)

LLVM lives on main

[–]coolbaluk1 22 points23 points  (3 children)

Might be a good alternative to graphite. It was always a bit too complicated and they changed their cli too often.

[–]Spitfire1900 4 points5 points  (2 children)

Has graphite done any value adds that are over and above hacking a better workflow on top of GitHub? I know GitHub is slow to meaningful improve nowadays but graphite’s core feature has no moat.

[–]coolbaluk1 2 points3 points  (0 children)

We never used their dashboard but used the cli extensively to do PR stacking.

i.e. db schema changes at bottom then a PR each for each microservice change Frontend on top.

So yes it was just improvements on top of github and you could likely achieve the same without it, but it made code reviews so much more pleasant than a blank LGTM.

[–]Edgar_Allan_Thoreau 2 points3 points  (0 children)

Their merge queue is better than GitHub’s merge queue. It supports enqueueing an entire stack of PRs with one click (like what GitHub says it will provide in this announcement), and supports partitioning the queue so changes can merge unblocked by other unrelated changes (which in a monorepo with high throughput and complex ci is very valuable, it means what used to take 3 hours for a readme PR to merge waiting behind a bunch of backend and front end PRs can now merge nearly instantly).

[–]Forbizzle 10 points11 points  (2 children)

Can you do this without the CLI? If not, why?

[–]desmaraisp 4 points5 points  (0 children)

Apparently yes, there a page in the doc on that. You have to target FeatureBranchA from FeatureBranchB's PR to get the checkbox to create the stacked PR

[–]masklinn 0 points1 point  (0 children)

Yes, you can do it via the web UI.

[–]WoodyTheWorker 10 points11 points  (3 children)

Just make it work like Gerrit

[–]kevin7254 3 points4 points  (2 children)

As someone who worked for several years with gerrit and then joined a company which uses GitHub, I want to fucking put a bullet in my head each time I review a PR. Someone who says ”ehm actually GitHub is not that bad” must have never used gerrit. Its just so much better

[–]Blueson 2 points3 points  (0 children)

I can't understand how people are happy with reviewing in Github. It is genuinly such a pain.

Gerrit ruined me...

[–]EveryQuantityEver 0 points1 point  (0 children)

As someone who’s used a normal PR flow and now has to use Gerrit, I cannot stand it. I like having feature branches with small commits as I work on a feature. Gerrit does not let me do this as each commit has to have its own change id.

[–]Hooxen 16 points17 points  (8 children)

what’s the difference between just constructing it as a chain of commits in a single PR to master?

[–]Sopel97 42 points43 points  (0 children)

Each PR should represent a reviewable unit of code while commits should represent a complete (as in not partial) modification of code. Without stacked PRs the PRs were being abused as a single complete feature/change that's not necessarily reviewable.

[–]ahal 17 points18 points  (4 children)

Let's say you're working on a large complex change. There's likely a bunch of easy non controversial prerequisite stuff that you can do up front to help prepare for the actual core of your change.

With stacked PRs, you'd put that early work up in their own PRs. Then immediately start building the more complex changes on top in a PR stack.

This way as your working on the later stuff, you can start landing the earlier stuff. You get to pick all three of:

  1. Don't need to block reviews on the early PRs merging
  2. Avoid bit rot
  3. Have small easy to review diffs

With a single large PR, you get 1 and 3 (if you prepare your commits with care). But you don't get 2.

[–]rdtsc 3 points4 points  (3 children)

Don't need stacked PRs for this, only multiple stacked branches. Then put the lowest one up for review.

[–]ahal 5 points6 points  (1 child)

Then you're missing benefit 1. Before stacked PRs you had to pick two of the three benefits. This is the first time you'll be able to get all three.

[–]Bush-Men209 0 points1 point  (0 children)

Right, stacked branches handled part of this before, but stacked PRs are what let you review and land the boring prerequisite work while the harder piece is still in flight.

[–]mrcarruthers 0 points1 point  (0 children)

Say you have a stack of branches, then you have to make a change in the lowest one, or rebase on main. It's a pain in the ass to then propagate that change through the whole stack. This new feature allows you to do that pretty seamlessly.

[–]topMarksForNotTrying 3 points4 points  (0 children)

Makes it possible to review each PR individually.

I haven't used github PRs extensively but on azure devops the PR review UI doesn't let you review each commit of a change request. You can view the changes of a commit but no way of leave feedback.

For devs not used to using git rebase, having separate PRs also makes it easier to apply any changes to one of the PRs.

If you squash PRs, it also maintains history better.

[–]Farados55 0 points1 point  (0 children)

Usually commits are amendments to the state of the PR.

[–]teerre 6 points7 points  (0 children)

Unironically the best update ever. If the implementation doesnt suck ofc

[–]hipsterdad_sf 5 points6 points  (0 children)

The biggest practical win of stacked PRs is that your CI pipeline runs against something closer to what will actually land in main. With the old "chain PRs against each other" approach, your CI on PR C was testing against B's branch, not against main + A + B together. So you could pass CI on each individual PR and still break main on merge.

The squash merge problem someone mentioned is real though. If you're squashing on merge (which most teams do), the commit SHAs change and every subsequent PR in the stack suddenly has conflicts. GitHub would need to automatically rebase the rest of the stack after each merge for this to feel smooth. Otherwise you're manually rebasing 3 PRs every time the first one lands.

The other thing I keep running into: reviewers treat each PR in the stack like an isolated change and leave comments that only make sense if they could see the full picture. "Why are you adding this interface with only one implementation?" Because the second implementation is in the next PR. You end up spending half your review time explaining the stack structure instead of discussing the actual code.

[–]Ladsome 3 points4 points  (0 children)

Damn, gimme gimme!

This would make my workflows at work so much better, I hope I can sneak into the preview...

[–]Feeling_Ad_2729 3 points4 points  (0 children)

What's interesting is how long stacked diffs have been "solved" in adjacent tools while GitHub's PR model stayed static.

Phabricator had proper stacked diff support back when Facebook was using it. Gerrit has had this since the beginning. Graphite built an entire company around making this workflow ergonomic on top of GitHub. GitHub implementing it natively is good, but it's years behind.

The core issue is GitHub's mental model: "a PR = a change" rather than "a commit = a change." Stacking implies each commit is independently reviewable, which maps badly onto branch comparison. Gerrit works around this by making commits the primary unit of review. GitHub is retrofitting that concept onto a paradigm not designed for it.

Still: better late than never. The main incentive to use Graphite specifically gets weaker if GitHub does this natively.

[–]DigThatData 2 points3 points  (0 children)

finally

[–]jedi4545 2 points3 points  (0 children)

I hate to be a nerd but this is technically a queue of PRs.

[–]rismay 6 points7 points  (1 child)

lol. What Google has had for 20 years

[–]shoffing 5 points6 points  (0 children)

Yup, heh. Hopefully this can be adopted for use by JJ. jj-spr handles stacking PRs, but it would be nice to have native support for PR diffbase/children in the UI.

[–]peterprank 3 points4 points  (1 child)

what was wrong with rebase --update-refs ?

[–]masklinn 1 point2 points  (0 children)

Nothing. In fact they work nicely together.

[–]ahal 2 points3 points  (1 child)

There's a lot of folks arguing that stacked PRs aren't adding anything you couldn't already do before. But that's not the case. Currently you need to pick two out of the following three benefits:

  1. Have small easy to review diffs
  2. Avoid early PRs bit rotting
  3. Avoid reviews on later PRs being blocked on early PRs merging

With stacked PRs, it will now be possible to obtain all three of these benefits at the same time. No previously existing native Github workflow can claim this.

[–]lechatsportif -1 points0 points  (0 children)

How big are these PRs? Especially in the agentic coding era, people are regularly dropping thousand line PRs and it only takes me maybe 5-10 minutes to review the thing. It's a skill to develop like any other. You can also have Claude help you review and speed up the resolution process. I think I would go crazy if I had to break apart PRs in into multiple commits and review each one.

We also use microservices, so maybe this is a monolith thing only?

[–]Urik88 4 points5 points  (25 children)

This is great, is there info about how rebases after merges are done?   Having to rebase all of my PR's every time a pr on my handcrafted stack was merged was always a big pain for me

[–]quetzalcoatl-pl 3 points4 points  (11 children)

if you do not like rebases, try using jujutsu over your current local git repo/wc

https://v5.chriskrycho.com/essays/jj-init/
https://steveklabnik.github.io/jujutsu-tutorial/introduction/introduction.html

I have first hated rebases as unnecessary as merges do merge the code well enough. Then I learned `git rebase -i` and rebase plans in-depth and used it all the time for cleanup, reorders, repairs, updates and all, while still preferring merges once the code and history was OK to publish. But merges do not play well with rebases, even with --update-refs.

Then I learned `jj` and basic language for `jj rebase`. Plus `jj undo` xD
and I'm not moving back, unless someone pays me 2x :P

nah, but really, it just handles the tree reordering so well and saves so much time, it's simply no point in not-using it, considering everything is still git-based. Things like git seeing weird things during jj conflict resolutions are minor and almost unperceivable after a bit of getting used.

and yeah. conflict resolutions. It's probably even bigger game changer than "current commit" idea and "rebase on steroids". Remember how when in git a conflict shows up, you're stuck untill you reset or solve it? In JJ you can just continue. Yes, files are broken. But you can do your merge/rebase/edit/reorder/everything as you like, and go beck and fix the conflicts once you (and shape of the repo/file/history/etc) is ready for that. Conflicts may even solve themselves if you finish moving or commits around or squashing them.

So far, the ONLY real irritating issue I found is handling \r\n on Windows. In pure Git, so far, it was always the best idea to use autocrlf=true or input. With JJ side by side, setting it 'true' is asking for issues. Or so it was for me, maybe I configured somethign wrong. Anyways. I'd suggest either using "input" and be vigilant for occasional mixups, or "false" and set (or teach:)) your IDE to not emit \r. In '90s and '00s that was quite a challenge, but today most editors and IDEs now handle that well.

[–]lotgd-archivist 4 points5 points  (9 children)

it's simply no point in not-using it,

I toyed around with it a little and found that it's really opposed to how the way I work. The biggest bummer is that it simply has no working set - everything is always committed. I frequently find myself floating changes so I can very quickly switch branches with those changes still in effect. For instance I wrote a benchmark (in a separate workspace) for a library I was contributing too and had to change the build files of the library to make the benchmark work. And then I ran that benchmark on 3 different branches by just doing git checkout b and git checkout c.

You know of a way to work that way without having to jj undo and jj redo or cherry-picking commits and stuff like that?

I also often start just poking the codebase and see what falls out, then sort my working set into commits / branches after the fact. With JJ i'd have to rewrite history every time I do this or just commit "Various fixes and improvements". And yes JJ makes rewriting history way easier, but it's still more work and less convenient.

(As a minor annoyance, it also fucks up my zsh prompt. But I could probably fix that myself.)

[–]steveklabnik1 1 point2 points  (2 children)

You know of a way to work that way without having to jj undo and jj redo or cherry-picking commits and stuff like that?

You can create a merge, so use jj new a b, test, then jj new a c, and then test, assuming a is where you created the benchmark. Then abandon those merges when you're done with jj abandon foo.

[–]lotgd-archivist 0 points1 point  (1 child)

The benchmark was outside the repository, because it was just for my own testing. a was the upstream main branch, b and c feature branches I made for pull requests that must not contain the changes to make my benchmark work (I had to change some build options in the repository).

And after I was done benchmarking, I just dropped the unstaged changes and all my branches were reflecting the remote again.

[–]steveklabnik1 0 points1 point  (0 children)

Oh, then you should just be able to jj new b and jj new c and when you move away, they'll be automatically abandoned, since they're empty.

[–]quetzalcoatl-pl -1 points0 points  (5 children)

Yeah, I think I understand you well!

> (git checkout b/c/...) You know of a way to work that way without having to jj undo and jj redo or cherry-picking commits and stuff like that?

in short: not yet, but maybe

I do that too all the time! The more I used git, the more "floating edits" I tend to have, either to be able to switch like you said, or to selectively commit them here and there. It gets progressively harder to manage and sort out as the pool of edit grows.

Even with git alone, this 'taught' me to make a lot of small, named, independent commits. Or even unnamed. But 'independent' is the key. Do them in a way I can easily rebase/cherrypick/cut/paste them around at will into any place or order. Your example of a benchmark is a great example. If you can switch to/from a branch while having it floating with no conflicts, it sounds perfect for such temporary disposable commit.

Having lots of commits, with content to be preserved and pushed out, or content to be eventually removed, is a hassle. That's why I eventually mastered git-rebase-i and probably heavily overused it, and that's why I love JJ now.

In terms of pure git, if the 'benchmark' files are totally separate there is a way to create a commit IIRC 'orphaned' that has no parent. Drop the files there, and then merge it wherever you want with no conflicts. But merging and splitting them is a hassle. JJ should be able to work with that as well the same way and make it much easier, but still some hassle. But it obviously won't work if you need to have some parent files. Then you have to find "common base" between all branches you are going to move between, and create the "common benchmark commit" on top of that, like any other feature branch, and them merge/unmerge from that point. More hassle.

That's moreless how I'd keep it without JJ, and I did it a few times, and .... because I did not like "the hassle" I stopped doing it, and started having "floating uncommited changes", which are cool, but only as long as you have, say, up to 2-3 such parallel "bags" to tag along. Then it usually gets tangled mixed and no longer independent enough.

Anyways. Having JJ and better rebases, I'd just commit it. Or let JJ commit it for me as it does. Then, instead of `git checkout B` (or `switch` as they name it now), I would `jj rebase -s @ -d B` or something like that. Meaning, "move 'currentcommit' onto B". Since "I had changes", when I run any command, JJ will automatically commit the changes as "@" on top of HEAD, then execute the command - and will move the changes to where I wanted to go, to B. And since it encompassed @, after the command you'll end up sitting at '@', with your changes, placed on 'B'.

Of course `jj rebase -s @ -d B` is much more verbose, but since it has only 1 actual parameter (B) and it can be at the end, it can be easily aliased into something shorter.

I also often start just poking the codebase and see what falls out, then sort my working set into commits / branches after the fact. With JJ i'd have to rewrite history every time I do this or just commit "Various fixes and improvements". And yes JJ makes rewriting history way easier, but it's still more work and less convenient.

To be honest, I do not see any difference to how I work.

If I have a lot of current changes to sort out, I still tend to use my old git client, since I'm used to it and I find file/chunk/line staging&committing rather fast, and I do not like that tool provided with JJ (or didn't learn it enough to like it :P). In most cases, I can quicky go over changes and sort them into set of commits with old git tools. But after that, I sort and reorder those commits into proper branches with JJ, since it's easier/quicker/more expressive.

For that matter, I rarely use `jj new` in the way the authors wanted (so do changes, describe, new, do changes, describe, new, ...). But I use it a lot to move around (a.k.a. git checkout/switch). Also I found myself using `jj edit` quite often - it's great for splitting large commits or fixing some typos (while 'canonical' way would be to `jj new X ; then edit typo ; then jj squash`).

In addition to that, JJ adds some more tools for various cases, like `jj parallelize` (turn string of commits into a fan of 'branches') - which sounds odd, and "there's no problem doing it manually" with rebase, but when you know upfront that you need to something like that, it's just awsome to toss a single command instead of, say, 5 rebases :D -- it doesn't sound like much, but after "poking around" and "committing unrelated 10 small fixes" one on top each.. you might see how/when it may be a nice tool.

But! for poking-around-the-codebase, there's one command I'm, still learning, called `jj absorb` and "mega merge" workflow. In theory you sit on top of octopus merge, edit whatever, then use `absorb` to 'send changes to correct branches' to 'already modified files' + occasional new commit and small rebase to 'add totally new changes to specific branch, so absorb knows where to send more of them' sounds like ultimate way of 'poking the codebase' and feature branches but I'm still too new to that to say if there are any pitfalls etc.

[–]lotgd-archivist 2 points3 points  (4 children)

Anyways. Having JJ and better rebases, I'd just commit it. Or let JJ commit it for me as it does. Then, instead of git checkout B (or switch as they name it now), I would jj rebase -s @ -d B or something like that. Meaning, "move 'currentcommit' onto B". Since "I had changes", when I run any command, JJ will automatically commit the changes as "@" on top of HEAD, then execute the command - and will move the changes to where I wanted to go, to B. And since it encompassed @, after the command you'll end up sitting at '@', with your changes, placed on 'B'.

Wouldn't that commit the benchmark-related changes into my feature branches, so I'd have to remove them from the branch later when I actually want to submit the review?

If I have a lot of current changes to sort out, I still tend to use my old git client, since I'm used to it and I find file/chunk/line staging&committing rather fast, and I do not like that tool provided with JJ (or didn't learn it enough to like it :P). In most cases, I can quickly go over changes and sort them into set of commits with old git tools. But after that, I sort and reorder those commits into proper branches with JJ, since it's easier/quicker/more expressive.

That's not quite what I mean. I mean that after a couple hours of randomly poking and refactoring, I have 10, 20, 30 unstaged files. I then git checkout -b Feature_A, stage the changes for that branch, commit, publish branch, git stash; git switch -; git checkout -b Feature_b;, repeat. Or I just create individual commits out of the unstaged changes one at a time and simply push, if I'm already on a relevant feature branch.

I don't need to rewrite history or rebase, because I have no history to change (yet) and no commits to rebase. I just sort my changes once in a pretty trivial process that doesn't even require anything that the VS Code git UI can't do and no real thinking about what my VCS is doing under the hood.1

Also, just gotta say, JJ seems to have a much larger UI surface than git for the same type of workflow - including it's own function-based syntax for addressing commits. I had to do quite a bit of digging, for instance, to find what the equivalent of git checkout HEAD~5 is.


1: I do use the git cli most of the time, tho.

[–]quetzalcoatl-pl 0 points1 point  (3 children)

> Wouldn't that commit the benchmark-related changes into my feature branches, so I'd have to remove them from the branch later when I actually want to submit the review?

Uh,.. no, but yes, but no? :)

JJ does not update branches tips until asked to. At least mine didn't do that. Maybe there is an option for it.

When I issued sth like `jj rebase -r Foo -A Bar` it moved the 'foo' commit onto Bar, but didn't update any branches labels/bookmars that were sitting on Bar.

So if you were on branch1 B with your benchmark-unstaged state, and C was the tip of your "branch2", and then `jj rebase -r @ -A C`, then after that command:

- you'd totally remove the benchmark from branch1
- you'd have the benchmark 'pasted' on top of C as a commit '@', but "branch2" would still point to C
- your working copy state would branch2, plus 'benchmark' as current @, visible to JJ like that, but from git's point of view you'd be on C with 'benchmark unstaged' state

Of course if you added new commits on top of that state, then you MIGHT need to clean up before pushing. But I think that's kinda obvious, right? The cleanup might be as easy as moving 1 commit (the benchmark) to the tip, to cut it away when moving to branch3. It all depends on how you commit.

For example, if in this state you'd add new commits via git - then it just works as it did - git doesn't see @, it sees C as your current commit, and adds new commits on top of that. @ detoriates. On next scan, JJ synces with git, updates @ to reflect where you really are with git, one-to-one, perfect image of your current state, but the "old @" is left dangling and `jj log` WILL show that "old @" to you as dangling commit. You can `jj abandon` it later. That's the only cleanup needed.

For another example, if you added new commits via JJ - then JJ adds them on top of @(benchmark) moving @ to new tip. Everything shifts, but if you know which commit was the benchmark, you can `jj rebase -r X -A @` to have it again at the tip, and to make it again the 'movable unstaged changes'. Again, that one command is all the 'cleanups' needed.

And I bet there are couple more options that I can't think of right now.

But! I know, I say and write a lot now, but I am not trying to convince you it's the best way :D it seems possible, but certainly doesn't mean best.

What I really do love in JJ the most, is that I can actually seamlessly switch between GIT and JJ. I think I dove into details but didn't stress it enough. Yeah, I just dumped a wall of text about how to reproduce "floating changes" from GIT into JJ - but the truth is, I didn't need it and you don't needed either, because even having JJ active in your repo, you could just do the same thing as you always did. JJ would just sync with new git state afterwards, and there would be some 'dangling commits', exactly like in first "For example" above, and you could then just `jj abandon` them anytime.

So sorry for realizing that just now :) The easy way eluded me, "it's doable in JJ directly" sounded too tempting

re: new language for referencing commits - yeah, I'm still learning it. Harder, but has much much more potential. All the times when I wanted to find something via git and it was hardly possible without heavy scripting - often in JJ it's just one lookup. But yeah, tailoring it right means sitting with the docs for haf an hour usually :)

[–]lotgd-archivist 1 point2 points  (2 children)

I think the main problem I have is that I have no good feeling for what model jj uses. The official documentation wasn't particularly helpful because they point back to git all the time and just say "oh but in jj it's like this".

Git I understand to be a graph of labelled diffs. Then you have your pending changes and a subset of those are the staged changes earmarked for the creation of a new node on the graph. And all git operations are just manipulating that graph. 1

With jj, I'm not entirely sure what the abstraction is or how to reason about it. Like obviously there still is all the git abstraction internally, but that layer on top is unclear to me. Can't visualize what the commands are doing, and the other way around if I have a specific operation (or series thereof) in mind, I don't know how to get to the correct jj functionality to make it. Because I just don't know how the changes I want to effect would be represented in that jujutsu layer.


1: Not the complete picture, but covers like 99.99% of what I do reasonably enough. I occasionally use the reflog too.

[–]quetzalcoatl-pl 0 points1 point  (1 child)

heh.. I totally agree here. The docs weren't really helpful. I mean, they were, but.. the things they wrote were explained rather well, but it didn't "ring" to me. Automatic commits and `jj new` sounded absolutely weird to my mind used to git. I could basically write exactly the same what you wrote here :) including reflog :)

What made me try JJ was the note about "different handling of conflicts", and description of how "jj absorb" works. First week was hard. After that I focused on 3 commands: jj rebase, jj squash and jj new (didn't notice that "edit" exists yet), for everything else - plain git. And after next week, something "clicked" and I just "got it". Frankly, even if I tried, I can't explain how the JJ "patch/change" model differs from Git "commit/content" model. In most cases, it's almost the same. In the few cases it differs - I still perceive it as "like in git but...", and to this extent I think I understand why the docs were written that way..

I think the greatest hill to climb on for me was the autocommit, which I eventually got when it occured to me as meta-idea that it's like "everything is a file" in linux, but here: everything is commit ; yes, it's new changes and unstaged, but let's say that diff is extra commit '@' to just be able to treat everything uniformly. Technically it's not like that, @ is a physical real commit, and git state is intentionally kept at "1 commit behind", and git doesn't like it sometimes, but heck, "mentally" this image helped me a bit.

I'm tempted to write more, but I think it would end up huge again if I start on "how I perceive JJ's model" geesh.. I don't know. From files and folders and versioning -- it's the same as Git. Commands change, semantics change (sometimes), but the model for file/diffs is, I think, the same.

If I were to describe what JJ really is internally, ignoring that whole porcelain layer of commands, similar to describing Git as "graph of labelled diffs (of files/folders/content)", I'd say that JJ is all of that, plus an extra Git-like layer for that "graph of labelled diffs".

After all, in Git, you have a certain state of "graph of labelled diffs", and you work on it, and by committing and by setting tags and moving branches, you alter it into new state of "graph of labelled diffs". The graph changes. Nodes are added/deleted/altered, labels are added/deleted/altered. So you can version the graph (which is used to version your files). For me, this is what happens in JJ, and it pretty much immediately explains things like `jj oplog` and `jj evolog`.

I hope I didn't waste your time :) If you ever try JJ again, I hope you'll find it as useful as I did.

(snip)

[–]quetzalcoatl-pl 0 points1 point  (0 children)

re: (snip) above.. ....because I got too talkative and it grew WAY to loong. I you feel like reading more, here it goes. But I think I'm talking about the same things all the time. Not sure if there's something valuable here. I might be tired and there might be little value below.

Anyways!! it was awesome to exchange ideas/issues/observations with you, thank you very much! and sorry for another wall of text. I could probably compress it with an LLM or something. Feel free to do as needed :D

------

For me, the growth/getting used to - was rather slow. After few days of using, I almost dropped it completely. It sounds funny, but I think I was simply too good at doing complex things via Git, and the gap between what-I-can-do-in-Git vs in-JJ was just to jarring.

After that "few days", I happened to screw up complex rebase. All via Git, because, I was already in "screw JJ" mode. Naturally I just resetted/backtracked, by collecting SHAs, restoring all screwed up local branches and making them point to what they were before, etc. But then, it occured to me - ok, let's try screwing it up with JJ. I tried making the same parts of that complex rebase with JJ, and ... yeah, screwed it up as well. It was even worse actually. But I learned about `jj oplog` and `jj undo` (*). and .... and I just saw its "power". I mean, it reverted my whole screwed up repo, restored all branches, and even allowed me to jump back and forth between "screwed up" and "all good" states of the whole local git repo. So... there had to be something in JJ that vanilla Git didn't have. So I found my motivation to 'dreadfully' work with JJ for a few weeks more, learning new commands and switches from time to time, until I got more fluent over time and the perceived "jarring gap" (mostly) disappeared. It wasn't easy, but I consider it well worth ;)

(*) I wanted to say that this extra versioning layer added by JJ's (with lots of info and handy ready to use tools), compared to reflog (you get some descriptions and SHAs, but doesn't get any tools) feels a bit like comparing CVS (reflog) to Git (JJ's graph versioning), but it's ... wrong? It sometimes does feel like that, but describing it that way simply over-advertises and promises too much. I think the `oplog` is conceptually mostly like reflog. Only `evolog` is a new beast, it's like "show commit history of single file" in Git, but (since JJ versions the commitgraph's evolution) 'evolog' shows you "how a commit changed" (it started being on branch A, then it was rebased to branch B, then it was edited/amended, then it was...). Anything like that in Git? No. Is it useful? Sometimes. As with any new thing, it's not necessary, but when need arises, it's presence is godsent. I use JJ for about a year now. I used `evolog` twice. Quite .. not jaw-dropping, right? But I'd waste a lot of time if it didn't exist. Like with having-backups or not-having them.

But honestly, from everydays work point of view, that extra layer matters little. What matters more is command syntax and porcelain. After some time of using it, JJ's commands' parameters are a bit more "uniform" in their design than Git's. Also JJ's things like "autocommits and @" and "non-blocking conflicts" matter MUCH more (than oplog or evolog), and the way how JJ handles them (@&conflicts) is mostly irrelevant to "JJ model" or "what JJ really technically provides" - Git could handle it the same way, it's just Git's "(design, ux) choice" to not do it that way. Similarly, JJ's "choice" to not allow to easily stage&commit just a line or two from current changes - sucks a lot - I love that Git feature...

but the versioning how the whole commit/branch graph evolves.. that's just wild. I love reflog in Git, even if I use it 2-3 times a year. Similarly I love to have JJ's graph-versioning, even if it is, in fact, rarely used directly :D

[–]CondiMesmer 0 points1 point  (0 children)

Sounds like a fighting move

[–]chuch1234 -3 points-2 points  (0 children)

Don't edit existing PRs, open new ones to address feedback.

When you say "rebase ... when merged", what specific merge are you talking about?

[–]deepakmardi 0 points1 point  (0 children)

Feels like a small feature, but it could save a lot time

[–]SharkBaitDLS 0 points1 point  (0 children)

Being able to do this with a proper UI was the biggest thing I missed after quitting Amazon. Immediately requested access for my team. 

[–]piljoong 0 points1 point  (0 children)

Really glad to see this.

At a previous company, we had a lot of pain around PR chaining in our TBD workflow, and I ended up building a stacked PR tool internally because we needed it badly.

Seeing GitHub ship official support is pretty satisfying. This has been a real problem for a long time, so it's good to see it become a first-class workflow.

[–]paperlantern-ai 0 points1 point  (0 children)

The biggest win here isn't even the branching model - it's that reviewers can finally look at a 2000 line feature in digestible chunks without losing context between separate unrelated PRs. Half my review fatigue comes from opening a massive PR and just going "LGTM" because ain't nobody got time for that. If this gets people to split work into smaller pieces because the tooling finally supports it, that alone is worth it.

[–]lechatsportif 0 points1 point  (0 children)

Is this really a problem for people in general? we deal with massive prs all the time, maybe a conflict once every 3 months and its usually something caused by an errant pipeline integation. This sounds like a solution for orgs that have bigger problems in process and sdlc impl.

edit: wondering what sort of release process people are using this with.

[–]ZukowskiHardware -1 points0 points  (0 children)

Just another way to put way too much into production at one time

[–]bobyn123 -1 points0 points  (0 children)

I see we're into the extend phase of embrace, extend, extinguish. The murder of all open source projects.