GitHub Protips: Tips, tricks, hacks, and secrets from Sarah Vessels

Image of Sarah Vessels

This post is a part of a series featuring protips from GitHubbers for improving productivity, efficiency, and more. Continue reading to learn about Sarah Vessels’s protips.


I spend most of my work day in code and pull requests. I like to build things and I like for y’all to use them, so I’m very motivated to get my changes out there! I want to share a process with you that’s worked well for me.

Sometimes my pull requests get too big to easily review because my branch includes too many changes relative to the base branch. Instead of forcing my team to slog through a thousand lines of code, while trying to keep everything in their head and spot any problems, I prefer to open smaller pull requests. Smaller pull requests tend to merge faster, because they’re easier to reason about, so reviewers can breeze through and approve with confidence, and because they have less chance of conflicting with the base branch.

However, my brain doesn’t think in terms of small pull requests! I get an idea, and I want to translate my idea to code before it slips away—get it all out in black-and-green in my editor. Being able to get my feature or bug fix written down in a single branch helps me confirm that it works as I expect and allows me to test the system as a whole. I can verify that the data model I’ve chosen is a good one based on how the UI and endpoints that use that data model come together. This everything-in-one-branch approach, unfortunately, is at odds with my desire to present my teammates with small, easily reviewable pull requests, especially as these branches tend to grow large.

If I try to split my code into separate branches from the beginning, I often struggle with knowing which code makes sense in which branch. I find myself spending too much time thinking about whether I’m putting a commit in the right branch rather than thinking about the feature I’m building. I feel like it hinders the creative process of coding, too, having to worry about neatly dividing code into coherent, easy-to-review branches rather than just going with the flow of where feature development takes me.

I reconcile the two approaches by doing them both, but sequentially: do my original work in a single branch, not worrying about how large it gets, and later open new branches to tease out smaller, atomic bits of work that can land separately. I can use git cherry-pick to pull bits of my large branch into a very focused smaller branch, then open a pull request from that smaller branch back to the default branch of my repository. As each smaller branch gets merged, when I update my original large branch with the latest code from the base, the diff gets smaller and smaller, because more of its changes are now already in the base branch. I can repeat this process as many times as necessary until the original large branch is no longer so large that it’s a pain to review.

Example

Say I want to implement categories in a blog. That might require: a database change to add a new table and field, logic to validate data at the database-adapter level, new endpoints to support reading/writing/deleting categories, user interface changes to allow you to hit those new endpoints, and finally JavaScript to improve the user experience. My approach to implementing such a feature would be to make a branch like add-categories and get everything out in that branch. I would add just enough tests to confirm my methods are doing what I expect them to do, and verify everything else manually by checking things out in the browser. The branch would be intended as a kind of spike, a test bed to verify that my implementation is the right one. I could open a pull request for this branch, not to solicit reviews and not to be merged yet, but to let my team try things out themselves, and to see the overall approach that I’m thinking about.

Once I’m happy with my add-categories branch, I’ll go back to the default branch of my repository and start a new branch. I try to think about the most low-level, early change that has to happen before any part of my feature branch can land. In this example, it’s the database changes. Without those, the endpoints and the UI and the JavaScript don’t matter!

I would open a branch like category-db and pull all the relevant work from add-categories into it. Ideally, while I had been working in add-categories, I would have made atomic commits such that I could run git cherry-pick commands to pull over just the database changes. That’s not always the case, though, and for those times, I tend to open my add-categories pull request on GitHub and go to the “Files changed” tab, where I can easily copy chunks of text and files to paste into new commits in category-db.

One nice side effect of pulling out changes into branches that are dedicated entirely to that change is that it leads to better test coverage. If I were trying to land my original add-categories branch, I would need to ensure it has good testing for the database validations, endpoints, user interface, and JavaScript functionality. I often end up with test code that is at least double the size of the code it’s testing, so tests really increase the size of a pull request! I don’t want that mental pressure of “is this test really worth adding, it’s going to make the diff so much bigger,” and that internal struggle goes away when I know the entire branch is very focused and doesn’t contain disparate ideas.

I treat my smaller branches as the ones that are going to be reviewed thoroughly by my team, so they get lots of polish. Thorough pull request description, test coverage, as well as consideration for performance and user experience are important. My original add-categories branch, in this example, would start as a draft pull request and stay that way until I finished landing all the smaller branches that I teased out of it. Once add-categories is small enough, because enough of its parts have landed in separate branches, I would round out its tests, update the pull request description to reflect the remaining contents of the branch, and finally drop its draft status so my team gets pinged for review.

Daisy-chaining pull requests

Sometimes it can be helpful to “daisy-chain” my pull requests. After creating my category-db branch, I like to see what changes remain in add-categories that aren’t already in category-db. Seeing what’s left in the original big branch will let me decide if it’s sufficiently small to submit it for code review, or if I need to pull out another small branch from it. To view the remaining changes it has, I would edit my add-categories pull request on GitHub to change its base branch from the default branch of my repository to category-db instead. Then I can check out the add-categories branch and git merge category-db before pushing the result up to GitHub. That updates the diff shown on my pull request such that all the code that’s now in category-db no longer shows in the add-categories pull request.

Maybe add-categories is still too large for me to be comfortable asking my team for review. The new data validations might be the next part I pull out to their own branch. While on category-db, I could create a new branch, say category-validations. I would repeat the process of before, cherry-picking relevant commits where possible, copying code wholesale from the pull request view of add-categories otherwise. Once category-validations has been pushed to GitHub, I can open a pull request with category-db as the base and category-validations as the head branch. Then I can once again update my add-categories pull request so that its base branch is now category-validations instead of category-db. This results in a daisy-chain effect where my branches are: maincategory-dbcategory-validationsadd-categories.

% git branch
add-categories // adds the whole categories feature
category-db // adds database table
* category-validations // adds validation logic + tests
master

Another benefit of chaining pull requests like this is that it unblocks me. Maybe category-db can’t be merged yet because the database change it describes hasn’t happened, so none of my other branches can be merged either. By opening many pull requests for separate small branches, I can still focus on getting each branch fully tested and reviewed by my team. I can get each set of changes ready to merge by making sure tests pass and they’ve been approved, then when category-db is ready to merge, I’ll be able to land several branches in short order since the branches depending on category-db have already gone through the review process.

Once one of my smaller branches gets merged, I can update the pull request(s) that are based on it to instead be based on the default branch of my repository. So when my category-db branch is merged, I update my category-validations pull request so its base branch is the primary. Eventually, all the smaller branches have merged and all I have left is a single pull request again, the original add-categories, based on the default branch. Only this time, add-categories is a reasonable size for me to ask for review from my team. By this point, add-categories has probably changed its purpose. Instead of adding the entire category feature, it may only be adding the finishing JavaScript touches to improve the user experience. I would update the pull request description to reflect this.

Summarizing my experience

By letting myself focus on expressing my ideas in code, and not focusing on the nitty-gritty details of how to neatly organize my branches, I’m able to develop a feature more quickly. By later splitting a large feature branch into many smaller branches, I help my team ensure we’re merging well-reviewed code, and I’m able to give individual attention to each piece to make sure it’s fully tested. Using git’s branches and GitHub’s pull requests, I’m able to marry the two techniques to hopefully improve the process of building out a feature for both myself and my team.

Do you have a GitHub Protip?

Do you have a tip, trick, or hack that makes your daily life easier on GitHub? Share it with us on social media with #GitHubProtips.