|
| 1 | +--- |
| 2 | +description: "This is a general explainer for pull request reviews where I think unrelated changes should be split into a separate PR." |
| 3 | +image: |
| 4 | + alt: Famous portrait of Samuel Johnson squinting up close at a newspaper |
| 5 | + src: "~/assets/blog/samuel-johnson-reading.webp" |
| 6 | +pubDate: 2025-01-13 |
| 7 | +tags: [ |
| 8 | + "pull requests", |
| 9 | + "reviews", |
| 10 | +] |
| 11 | +title: "Split Out Unrelated Changes" |
| 12 | +--- |
| 13 | + |
| 14 | +import samuelJohnsonDuo from "~/assets/blog/samuel-johnson-duo.webp"; |
| 15 | +import LabeledImage from "~/components/blog/mdx/LabeledImage.astro"; |
| 16 | + |
| 17 | +Hi! |
| 18 | +You might have been linked this blog post in a pull request that has a lot of changes in it. |
| 19 | +There might be a request linking to this blog post, asking that you split out some or all of those unrelated changes from it. |
| 20 | + |
| 21 | +This blog post is an explainer for why to split out unrelated changes from reviews. |
| 22 | + |
| 23 | +<LabeledImage |
| 24 | + alt="Two famous portraits of Samuel Johnson, side-by-side. The left one is him squinting up close at a newspaper. The right one is him looking at the viewer, confused." |
| 25 | + description="Famous writer Samuel Johnson reading a 100-file pull request tackling a half dozen issues at once." |
| 26 | + original={{ |
| 27 | + href: "https://knowyourmeme.com/memes/samuel-johnson-reading", |
| 28 | + text: "original", |
| 29 | + }} |
| 30 | + src={samuelJohnsonDuo} |
| 31 | +/> |
| 32 | + |
| 33 | +## Split Out Unrelated Changes |
| 34 | + |
| 35 | +For the most part, the larger a pull request, the longer it will take to review and get merged. |
| 36 | +Larger pull requests generally come with more cost and more risk than smaller PRs. |
| 37 | +They also clutter the Git history for files, making it harder to search back over time to see why logic changed when. |
| 38 | + |
| 39 | +> 👉 See [Why Open Source Pull Requests Can Take A While](https://www.joshuakgoldberg.com/blog/open-source-pull-request-timing) for why open source pull requests in particular can take a long time. |
| 40 | +
|
| 41 | +Anything we can do to reduce that cost is helpful. |
| 42 | +My favorite strategies in particular are: |
| 43 | + |
| 44 | +- Instead of one pull request that resolves multiple issues at once, it can often be better to send one pull request for each one of those issues. |
| 45 | +- Separating changes that impact code logic out from changes that only impact layout, formatting, or style. |
| 46 | + |
| 47 | +## One Pull Request Per Task |
| 48 | + |
| 49 | +It's generally easier to review a pull request that resolves one task (e.g. GitHub issue) rather than a pull request that resolves multiple tasks. |
| 50 | +Each area of change you add to a pull request increases the size and complexity of the pull request, making longer and more convoluted to review. |
| 51 | +It becomes harder to keep track of which changes apply to which task as a pull request grows in size. |
| 52 | + |
| 53 | +As a general ideology, I try to make each of my pull requests address a single GitHub issue. |
| 54 | +There should be one granular `fixes #<number>` in the pull request description per GitHub's docs on [linking a pull request to an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue). |
| 55 | + |
| 56 | +### Working with Automation |
| 57 | + |
| 58 | +Furthermore, many repositories -including most of mine- automatically generate changelogs and new releases. |
| 59 | +Granular pull requests can become individual changelog entries and releases. |
| 60 | +Catch-all pull requests don't fit well into those systems. |
| 61 | + |
| 62 | +As a general rule, I try to make sure all my pull requests can be described in a single [conventional commits message](https://www.conventionalcommits.org/en/v1.0.0). |
| 63 | +If you can't describe your pull request with a title like a `feat: add abc option` or `fix: avoid crash when xyz` then it might be a sign of an overloaded set of changes. |
| 64 | + |
| 65 | +## Separate Logical and Stylistic Changes |
| 66 | + |
| 67 | +Stylistic changes generally don't require very much review time per line of code changed. |
| 68 | +Teams sometimes take a long time to discuss their stylistic preferences, but once those discussions are resolved, changing code to adhere to new styles is often straightforward. |
| 69 | + |
| 70 | +Logical changes to code, on the other hand, generally take much longer to review. |
| 71 | +Which is good: logical changes come with a much higher risk of bugs. |
| 72 | +Every changed line of code in a pull request that changes logic should be scrutinized to make sure it doesn't break something. |
| 73 | + |
| 74 | +Introducing stylistic changes in a logical pull request makes that those stylistically changed lines appear to need scrutiny. |
| 75 | +Reviewers will have to remember that those lines aren't logically changed in reviews. |
| 76 | +That's a tax that slows down review of the pull request. |
| 77 | + |
| 78 | +Consider trying to understand this large set of changes to a `repeatMessage` function: |
| 79 | + |
| 80 | +```diff |
| 81 | +-export function repeatMessage( |
| 82 | +- times: number, |
| 83 | +- ...messages: string[] |
| 84 | +-) { |
| 85 | +- for (let i = 0; i < times; i += 1) |
| 86 | +- for (const message of messages) |
| 87 | ++export function repeatMessage(times: number, messages: string[]) { |
| 88 | ++ for (let i = 0; i < times; i += 1) { |
| 89 | ++ for (const message of messages) { |
| 90 | + console.log(message); |
| 91 | ++ } |
| 92 | ++ } |
| 93 | + } |
| 94 | +``` |
| 95 | + |
| 96 | +Now consider this version of the same change but with only logical differences: |
| 97 | + |
| 98 | +```diff |
| 99 | +-export function repeatMessage(times: number, ...messages: string[]) { |
| 100 | ++export function repeatMessage(times: number, messages: string[]) { |
| 101 | + for (let i = 0; i < times; i += 1) { |
| 102 | + for (const message of messages) { |
| 103 | + console.log(message); |
| 104 | +``` |
| 105 | + |
| 106 | +Much easier and faster to read through, right? |
| 107 | +All that was being changed was removing the `...` -- but the first diff obfuscated that straightforward intent with misleading stylistic changes. |
| 108 | + |
| 109 | +### Automate Stylistic Preferences (If Possible) |
| 110 | + |
| 111 | +Co-opting logical pull requests to enforce stylistic preferences is not an efficient way to roll out those preferences. |
| 112 | +Stylistic preferences lend themselves well to automation. |
| 113 | +Trying to touch them up one-by-one is sisyphean: a never-ending expenditure of effort that yields little to no real results. |
| 114 | + |
| 115 | +Formatters and linters are pretty good at **automating** most stylistic conventions. |
| 116 | +If you want to enforce a stylistic preference, do so with as much automation as possible. |
| 117 | + |
| 118 | +Most of the `repeatMessage` formatting from earlier could be enforced with any common formatter such as [Prettier](https://prettier.io) and linter such as [ESLint](https://eslint.org). |
| 119 | +I personally include [typescript-eslint's `stylisticTypeChecked` config](https://typescript-eslint.io/users/configs#stylistic-type-checked) along with Prettier plugins such as [prettier-plugin-curly](https://github.com/JoshuaKGoldberg/prettier-plugin-curly) to enforce a large set of consistency preferences in my projects. |
| 120 | + |
| 121 | +## Everything in Moderation |
| 122 | + |
| 123 | +These are't absolute rules. |
| 124 | +Sometimes the convenience of including a small unrelated change in a pull request is worth more than the benefits of splitting up changes. |
| 125 | + |
| 126 | +Filing an issue and reviewing a pull request both take time. |
| 127 | +If that time is greater than the cost of adding a small unrelated change to a pull request, then it may be worth it to just stick with the one pull request. |
| 128 | + |
| 129 | +If I'm already refactoring an area of code, and touching up some stylistic preferences wouldn't bloat the pull request too much, I might throw them in. |
| 130 | +Especially if those changes aren't something that need to be applied elsewhere in the repository. |
| 131 | + |
| 132 | +When including an unrelated change in a pull request, I recommend explicitly adding a comment explaining why it's there. |
| 133 | +That might convince a reviewer that it is indeed worth it to keep the changes together. |
| 134 | +At the very least it will explain your thought process and make the choice seem intentional rather than mistaken. |
| 135 | + |
| 136 | +## Further Reading |
| 137 | + |
| 138 | +If you want to read more about why granular pull requests are often preferable, I recommend: |
| 139 | + |
| 140 | +- [Google Testing Blog: In Praise of Small Pull Requests](https://testing.googleblog.com/2024/07/in-praise-of-small-pull-requests.html) |
| 141 | +- [Are small pull requests better than one large all-encompassing features, and how can I convince my team of that?](https://softwareengineering.stackexchange.com/questions/425665/are-small-pull-requests-better-than-one-large-all-encompasing-features-and-how) |
0 commit comments