{"id":96,"date":"2025-03-30T16:21:00","date_gmt":"2025-03-30T16:21:00","guid":{"rendered":"https:\/\/blog.aviator.co\/?p=96"},"modified":"2025-09-23T12:58:56","modified_gmt":"2025-09-23T12:58:56","slug":"stacked-prs-code-changes-as-narrative","status":"publish","type":"post","link":"https:\/\/www.aviator.co\/blog\/stacked-prs-code-changes-as-narrative\/","title":{"rendered":"Stacked PRs: A Better Way to Review Code"},"content":{"rendered":"<figure class=\"wp-block-post-featured-image\"><img fetchpriority=\"high\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2022\/08\/stacked-pr-banner.png\" class=\"attachment-post-thumbnail size-post-thumbnail wp-post-image\" alt=\"stacked pr\" style=\"object-fit:cover;\" srcset=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2022\/08\/stacked-pr-banner.png 1024w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2022\/08\/stacked-pr-banner-300x169.png 300w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2022\/08\/stacked-pr-banner-768x432.png 768w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n<p class=\"wp-block-paragraph\">Pull request review time is not linear with the size of the change. A <strong>pull request that\u2019s twice as large takes more than two times as long to review<\/strong>, or, at least, to review thoroughly. This usually means big PRs either tend to languish and grow stale or simply get rubber-stamped.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Why? I\u2019m not entirely sure, but one of the biggest reasons is that with bigger PRs, it\u2019s harder to untangle the story that the pull request is telling. Consequently, reviewers have to spend longer <strong>trying to understand the narrative of the pull request:<\/strong> why each bit of code is changing the way it is.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Often, <strong>big PRs start out as little PRs<\/strong>. Developers just want to fix one little bug. But, as you likely already know, little bugs aren\u2019t always as little as they seem. What should have been a ten-line change turns into a three-hundred-line refactor. It\u2019s easy to see how the story gets lots: why is a function so far away from where the bug is or at least, where we think the bug is changing so much?<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><a href=\"https:\/\/twitter.com\/iamdevloper\/status\/397664295875805184\"><img decoding=\"async\" width=\"834\" height=\"500\" src=\"https:\/\/blog.aviator.co\/wp-content\/uploads\/2022\/08\/Screen-Shot-2022-08-25-at-10.35.52-AM.png\" alt=\"\" class=\"wp-image-97\" srcset=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2022\/08\/Screen-Shot-2022-08-25-at-10.35.52-AM.png 834w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2022\/08\/Screen-Shot-2022-08-25-at-10.35.52-AM-300x180.png 300w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2022\/08\/Screen-Shot-2022-08-25-at-10.35.52-AM-768x460.png 768w\" sizes=\"(max-width: 834px) 100vw, 834px\" \/><\/a><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Alternatively, big PRs are created in an attempt to keep hacking on a feature. In order to implement a button on the frontend, we have to write the frontend code. And the backend code. And the API layer glue. And the database schema migration. And now our PR for a single button is massive.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What Are Stacked PRs<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">On GitHub, stacked PRs work by\u2026 well\u2026 stacking your PRs. A stack is essentially a sequence of PRs: the first PR to add the database migration is created from <code>main<\/code>, the second is <em>stacked<\/em> on top of the first PR, the third is stacked on top of the second, and so on. These <strong>smaller, incremental changes form a clear narrative<\/strong> when viewed as stacked diffs. Each stacked PR is created with the assumption that its ancestors will be merged (though, as we\u2019ll see below, we can handle the case where those PRs need to change as well!).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Stacked pull requests<\/strong> work so well in this humble author\u2019s opinion because they <strong>allow developers to construct a narrative with their PRs<\/strong>. The <a href=\"https:\/\/www.aviator.co\/blog\/technical-debt-and-the-role-of-refactoring\/\" target=\"_blank\" rel=\"noopener\" title=\"refactoring\">refactoring<\/a> of the library function can be done in a separate PR from the PR that needs the new functionality introduced. The backend code can build upon the database schema definitions that were done in the previous PR.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And, critically, PR stacking mean developers can <strong>start working on their second, third, and fourth PRs while the first PR is being reviewed<\/strong> and iterated upon.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That\u2019s great from the reviewer&#8217;s perspective, but stacking PRs also allows the developer writing the code in the first place to move faster. They can now work on multiple parts of a feature without waiting for reviewers to approve previous PRs. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This couldn&#8217;t be more important in a world where devs are working remotely (and across who knows how many time zones).<strong> Being blocked and waiting for your coworkers is, at best, frustrating<\/strong>. Sometimes it&#8217;s genuinely heartbreaking. Either way, the emotional and practical costs make for a bad time.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How Stacked PRs Work <\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">We\u2019ve taken a \u201cgit native\u201d approach to creating stacked pull requests, making them work smoothly with GitHub and common PRs code workflows. Stacked branches are just normal Git branches that were branched from their stack parent.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If that sounds confusing, let\u2019s look at a quick (and realistic!) example. We want to add a like button to the website we\u2019re building, and it makes sense to do it in three phases:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Add backend service code (e.g., database schema changes and model logic)<\/li>\n\n\n\n<li>Add a REST API interface<\/li>\n\n\n\n<li>Implement the frontend<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Without GitHub stacked PRs, we\u2019d have to implement this in one big PR or wait for each PR to be approved and merged before we could start on the next piece of the feature. Instead, we can open three separate PRs and have them reviewed both independently and in parallel (possibly even by different people!).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The Stacked Branching Strategy<\/h3>\n\n\n\n<div class=\"wp-block-group is-layout-flow wp-block-group-is-layout-flow\">\n<p class=\"wp-block-paragraph\"> \ud83d\udc49 In Git, a branch is like a linked list of commits. The branch name (such as <code>main<\/code> or <code>like-button-rest-api<\/code>) is essentially just a pointer to a particular node in the linked list.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u2020 <em>Technically, a Git branch is closer to a <\/em>directed acyclic graph due to merge commits, but for this discussion,<em> we\u2019ll assume there are no merge commits on the stack.<\/em><\/p>\n<\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">Since we want to open three different PRs, we need to create three different branches. To do this, we\u2019ll simply create each new branch off of the previous branch.<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><img decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-1.jpg\" alt=\"\" class=\"wp-image-4391\" srcset=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-1.jpg 1024w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-1-300x169.jpg 300w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-1-768x432.jpg 768w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Updating and Committing to Stacked Branches<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">One of the primary reasons to use <a href=\"https:\/\/www.aviator.co\/stacked-prs\" target=\"_blank\" rel=\"noopener\" title=\"Aviator stacked PRs\">Aviator&#8217;s stacked PRs<\/a> is to <a href=\"https:\/\/www.aviator.co\/blog\/code-reviews-at-scale\/\" target=\"_blank\" rel=\"noopener\" title=\"enable easier code review\">enable easier code review<\/a> which means that it\u2019s actually pretty likely that you\u2019ll have to modify branches that have other branches stacked on top of them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Modifying a stacked branch is just like modifying any other branch: perform your modifications and create a new commit!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">However, if you commit to a branch that has children, you end up in a scenario where the parent and child branches have diverged!<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-2.jpg\" alt=\"\" class=\"wp-image-4392\" srcset=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-2.jpg 1024w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-2-300x169.jpg 300w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-2-768x432.jpg 768w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">In the diagram, <code>like-button-frontend<\/code> doesn\u2019t contain the commit <code>2B<\/code> which we\u2019ve added to <code>like-button-rest-api<\/code> which means that all of the <a href=\"https:\/\/www.aviator.co\/blog\/managing-continuous-delivery-with-trunk-based-development\/\" target=\"_blank\" rel=\"noopener\" title=\"Continuous integration\">Continuous integration<\/a> and checks we\u2019re running for <code>like-button-frontend<\/code> are now out of date!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To fix this, we can rebase <code>like-button-frontend<\/code> on top of <code>like-button-rest-api<\/code>. This effectively \u201creplays\u201d commits <code>3A<\/code> and <code>3B<\/code> on top of <code>2B<\/code>:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-3.jpg\" alt=\"\" class=\"wp-image-4393\" srcset=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-3.jpg 1024w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-3-300x169.jpg 300w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-3-768x432.jpg 768w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<div class=\"wp-block-group is-layout-flow wp-block-group-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\ud83d\udc49 Here, we\u2019ve notated the rebased commits as <code>3A'<\/code> and <code>3B'<\/code> to illustrate the fact that, according to Git, these are actually new commits that are different from the old <code>3A<\/code> and <code>3B<\/code>. This is because Git considers the parents of a commit as part of the identity (i.e., hash) of that commit. Since <code>3A'<\/code> has parent <code>2B<\/code> and <code>3A<\/code> has parent <code>2A<\/code>, they are different commits. This is why rebasing requires a force push: Git thinks we\u2019re losing <code>3A<\/code> and <code>3B<\/code> and wants to make sure we mean to erase those commits from the branch.<\/p>\n<\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Stacked Branches Become Stacked PRs<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Since GitHub doesn\u2019t have native support for PR stacking, we have to be clever when it comes time to open PRs. If we opened a PR from <code>like-button-frontend<\/code> into <code>main<\/code>, GitHub would show the diff from all three branches, which defeats the whole point of using Stacked PRs!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead, we open PRs in a linked-list-like fashion:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-4-2.jpg\" alt=\"\" class=\"wp-image-4400\" srcset=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-4-2.jpg 1024w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-4-2-300x169.jpg 300w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-4-2-768x432.jpg 768w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">This turns out to work out really well! The diff for <code>like-button-frontend<\/code> only contains the changes from commits <code>3A<\/code> and <code>3B<\/code> as intended!<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Merge Time! (Oh, Wait Turns Out It\u2019s Hard \ud83e\udd27)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Merging is a very complicated subject, and to talk about it, we need to take a quick digression into the \u201csquash merge\u201d strategy.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A <strong>\u201csquash merge\u201d<\/strong> isn\u2019t technically a merge in the Git sense. Instead, a squash merge generates a new commit that contains the diff from the commits on a branch and adds that commit to <code>main<\/code>. GitHub (and many other Git hosts) will consider the branch \u201cmerged\u201d (and close the associated pull request), but Git doesn\u2019t actually consider the commits from the original branch as merged into main since the squash-merge-commit isn\u2019t technically related to any of the original commits.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<div class=\"wp-block-group is-layout-flow wp-block-group-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\ud83d\udc49 An actual <strong>Git \u201cmerge commit\u201d<\/strong> is a commit that has two (or more!) parents: the mainline branch (i.e., the branch that is being merged into) and the branch(es) that are being merged into the mainline. Git then considers the history of the mainline branch to contain the commits that were merged in. Many codebases use squash commits instead of merge commits because the history of individual feature branches is often messy and uninteresting.<\/p>\n<\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">Ultimately, this means we can\u2019t merge each PR in order (at least while using squash commits). Let\u2019s consider the branch diagram after we merge the first branch in the stack:<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-5.jpg\" alt=\"\" class=\"wp-image-4395\" srcset=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-5.jpg 1024w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-5-300x169.jpg 300w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-5-768x432.jpg 768w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Here, we\u2019ve added the commit <code>1S<\/code> to <code>main<\/code> which represents the \u201csquash merge\u201d of the <code>like-button-backend<\/code> branch. But, critically, Git doesn\u2019t actually consider the <code>like-button-backend<\/code> branch merged, even though GitHub does.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Now when we want to merge the second branch, <code>like-button-rest-api<\/code>, into <code>main<\/code>, Git tries to calculate all of the commits that it needs to merge into <code>main<\/code>. Since <code>1A<\/code> and <code>1B<\/code> are part of the history of <code>like-button-rest-api<\/code>, and not <code>main<\/code>, ultimately the squash merge will consist of the diff of <code>1A<\/code>, <code>1B<\/code>, <code>2A<\/code>, and <code>2B<\/code>. Since <code>1A<\/code> and <code>1B<\/code> have already been applied, this almost always results in a merge conflict.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Merge Time, for Real!<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">There are a few ways to get around the merge issues presented above.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One option is to only merge the top branch of the stack (<code>like-button-frontend<\/code> in the example above). This is what <a href=\"https:\/\/www.aviator.co\/merge-queue\" target=\"_blank\" rel=\"noopener\" title=\"\">Aviator MergeQueue<\/a> does (unless using our <a href=\"https:\/\/docs.aviator.co\/mergequeue\/concepts\/fast-forwarding\" target=\"_blank\" rel=\"noopener\" title=\"\"><em>fast-forward<\/em> mode<\/a> see below for details!).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Another option is to rebase each stacked PR after its parent is merged. This essentially means that we replay (<code>git cherry-pick<\/code>) the commits from a stacked branch on top of the squash commit generated by merging its parent branch:<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-6.jpg\" alt=\"\" class=\"wp-image-4396\" srcset=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-6.jpg 1024w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-6-300x169.jpg 300w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-6-768x432.jpg 768w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">This can cause some issues in high-throughput repositories where Aviator\u2019s <a href=\"https:\/\/docs.aviator.co\/mergequeue\/configuration-file#using-parallel-mode\" target=\"_blank\" rel=\"noopener\" title=\"\">parallel merge queue mode<\/a> is enabled. Since we can\u2019t start rebasing the next PR until the previous one is merged, the merge process becomes serialized on running CI for each branch in the stack (since CI is re-triggered after rebasing a branch).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Bonus: Aviator\u2019s Fast-forward Mode<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/docs.aviator.co\/mergequeue\/concepts\/fast-forwarding\" target=\"_blank\" rel=\"noopener\" title=\"\">Aviator\u2019s fast-forward mode<\/a> is a subset of parallel mode that works by only fast-forwarding your mainline branch to commits with known-good CI states.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<div class=\"wp-block-group is-layout-flow wp-block-group-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\ud83d\udc49 We talked about how Git branches are just pointers to commits above. In fast-forward mode, we simply move this pointer forward to a commit that we\u2019ve already validated (instead of merging a PR, which generates a new commit that hasn\u2019t already been validated).<\/p>\n<\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">This means we run the validation in a new branch where we\u2019re able to squash each branch into a single commit:<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-7.jpg\" alt=\"\" class=\"wp-image-4397\" srcset=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-7.jpg 1024w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-7-300x169.jpg 300w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-7-768x432.jpg 768w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Once the checks pass for the validation branch, we fast-forward main to 3S. This means that the commit that ends up in main is the exact same (i.e., same commit hash) as the one we validated:<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"314\" src=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-8.jpg\" alt=\"\" class=\"wp-image-4398\" srcset=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-8.jpg 1024w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-8-300x92.jpg 300w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2025\/03\/stacked-banner-8-768x236.jpg 768w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Aviator will automatically mark each of the individual PRs we created early as closed.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Best Practices for Working with Stacked PRs<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Stacked PRs work best when you keep a few simple habits in mind:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Keep each PR focused.<\/strong> Try to make each pull request do one thing well, whether it\u2019s adding a new model, updating an API route, or changing some frontend behavior. This makes it easier for reviewers to understand and for you to debug if something breaks.<\/li>\n\n\n\n<li><strong>Name your branches clearly.<\/strong> A good branch name helps you (and others) keep track of what each PR is about. Something like <code>feature-user-auth-api<\/code> or <code>refactor-payment-handler<\/code> goes a long way when you&#8217;re juggling multiple branches.<\/li>\n\n\n\n<li><strong>Rebase often, push with care.<\/strong> If you\u2019re updating a parent PR, remember to rebase the children on top of it. Yes, that means force-pushing but it also keeps your stack clean and your CI happy.<\/li>\n\n\n\n<li><strong>Add context to your PR descriptions.<\/strong> Even though your PRs are smaller, reviewers still need to understand <em>why<\/em> a change is being made. A short sentence or two explaining the purpose of the PR and how it fits in the larger picture helps a ton.<\/li>\n\n\n\n<li><strong>Use draft PRs to show progress.<\/strong> If you&#8217;re still working through parts of the stack, marking PRs as drafts makes it clear what&#8217;s ready for review and what\u2019s still cooking.<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2022\/08\/stacked-pr-best-practices.png\" alt=\"\" class=\"wp-image-4122\" srcset=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2022\/08\/stacked-pr-best-practices.png 1024w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2022\/08\/stacked-pr-best-practices-300x169.png 300w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2022\/08\/stacked-pr-best-practices-768x432.png 768w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">FAQs<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">What is Stacking in Git?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Stacking means creating a series of dependent branches, where each builds on top of the previous one. It helps break large changes into smaller, reviewable units.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How to Split PR into Two?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">To split a PR into two, create a new branch from the current PR, move some changes into this new branch (using <code>git reset<\/code>, <code>git add -p<\/code>, or <code>git cherry-pick<\/code>), commit and push both branches and open two separate PRs.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How to Make PR Smaller?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">To make PR smaller:<br>1. Tackle one logical change per PR.<br>2. Use stacked PRs.<br>3. Split feature into backend\/frontend\/API layers.<br>4. Avoid unnecessary refactors in the same PR.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Aviator MergeQueue<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><a href=\"https:\/\/www.aviator.co\/merge-queue\" target=\"_blank\" rel=\"noopener\" title=\"\">MergeQueue<\/a><\/strong>&nbsp;is an automated queue that manages the merging workflow for your GitHub repository to help protect important branches from broken builds. The Aviator bot uses GitHub Labels to identify Pull Requests (PRs) that are ready to be merged, validates CI checks, processes semantic conflicts, and merges the PRs automatically.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Stacked PRs work so well because they allow developers to construct a narrative with their PRs<\/p>\n","protected":false},"author":7,"featured_media":4121,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[78],"tags":[29,8,261],"class_list":["post-96","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-monorepo"],"blocksy_meta":{"styles_descriptor":{"styles":{"desktop":"","tablet":"","mobile":""},"google_fonts":[],"version":6}},"acf":[],"aioseo_notices":[],"jetpack_featured_media_url":"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2022\/08\/stacked-pr-banner.png","post_mailing_queue_ids":[],"_links":{"self":[{"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/posts\/96","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/users\/7"}],"replies":[{"embeddable":true,"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/comments?post=96"}],"version-history":[{"count":32,"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/posts\/96\/revisions"}],"predecessor-version":[{"id":4414,"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/posts\/96\/revisions\/4414"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/media\/4121"}],"wp:attachment":[{"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/media?parent=96"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/categories?post=96"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/tags?post=96"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}