Pull Request Flow with usethis

R Markdown Source

A flow chart for the pull request functions in the usethis R package.

A usethis pull request helper function flow diagram. Download the image or read a text version of the flow chart. (CC BY)

Introduction

The usethis package is full of incredibly helpful functions that make life as an R developer easier. A lot of the package’s functions target R package maintainers, but there’s a small cluster of functions that are life changing for anyone who uses git or collaborates with others via GitHub1.

These pull request helpers all start with the pr_ prefix, but I initially found them a little confusing to use since they’re each designed to be called in a specific context — for example, when you have local work that isn’t associated with a PR or when a PR exists but you don’t have the work locally on your computer.

Once I wrapped my head around the functions, though, using them has utterly transformed my day-to-day workflow. I can move in and out of collaborative work seamlessly. The flow chart above is my personal mental model of this cluster of functions and I hope it helps you make sense of them, too.

There are other great resources available in the usethis documentation. Beyond the function documentation, there’s also the great Pull request helpers article. That article walks through a pull request from the perspective of a contributor and a package maintainer.

In this blog post, however, you get to play both roles and choose your own adventure!

Choose your own pull request adventure

Welcome to this choose-your-own-adventure blog post. You don’t need to read this one from top to bottom. Instead, jumping around is encouraged!

Look for the Decision time: icon. It tells you that it’s time for you make a choice. But don’t worry, there’s no wrong choice: if you take a wrong turn you can always use your browser’s back button to retrace your steps.

Decision time: Ready to get started?

Prep work

Let’s get started

You open your project in RStudio (or the editor of your choice). You make a note to yourself to come up with a better name for your package — for now you’re calling it acme because it’s mostly a collection of random functions you’ve written.

Anyway, you’re thankful that you closed all of your open files the last time you were working, but now the empty console beckons. You’re ready to do some work.

Where do you begin? With usethis of course.

library(usethis)

Decision time: That was easy! Lets do some work now.

What’s next?

Start new work

Time to start something new. You scan your project board, issues list, JIRA tickets, slack messages, emails, and post-it notes and finally settle on something to work on. Your task: add an example to the documentation for a function you’ve been working on.

You want to start your work in a new branch, so you call pr_init(), giving it the name of your new branch.

pr_init("add-example")
✓ Setting active project to '/Users/garrick/work/acme'
ℹ Pulling changes from 'upstream/main'
✓ Creating and switching to local branch 'add-example'
● Use `pr_push()` to create PR.

The console output reminds you that usethis updated your local repo and created and switched to a new branch for you.

Decision time: You’re ready to do some work.

Pick up something you were working on

You’re ready to get back to that thing you were working on. Which was what again? You should probably have another sip of your coffee. You were doing some work in a branch in your local of the project…

Decision time: Do you remember the name of the branch?

Get to a local branch

git branches, you’ve made a few. But which one you do you want to work in?

Decision time: Do you remember the name of the branch?

Choose a branch and resume working

Don’t worry, usethis has your back. Just run pr_resume() without any arguments to get a list of local branches available to you. pr_resume() helpfully sorts the branches by recency of work and tells you if any of the branches are related to pull requests.

pr_resume()
ℹ No branch specified ... looking up local branches and associated PRs
Which branch do you want to checkout? (0 to exit)

1:         add-example --> #11 ('@gadenbuie'): Add an example
2: feature-exploration

Selection: 1
✓ Switching to branch 'add-example'
✓ Pulling from 'origin/add-example'
• Use `pr_push()` to create or update PR.

We picked option 1 to keep working in our add-example branch. Notice that, since add-example is related to PR #11, pr_resume() also did you the extra favor of making sure that your local copy is up-to-date.

Decision time: Rock and roll, you’re ready to get back to work.

Get back to the branch

Look at you, smarty pants. You remember your git branch names! I’m proud of you.

Good news, pr_resume() can switch to the "add-example" branch for you and it also makes sure that your local copy is up-to-date!

pr_resume("add-example")
✓ Switching to branch 'add-example'
✓ Pulling from 'origin/add-example'
• Use `pr_push()` to create or update PR.

Decision time: Sweat deal! Okay, you’re ready to keep on working.

Review or collaborate with someone else’s work

Version control software like git and collaborative coding platforms like GitHub are plenty of fun when you’re working on your own. But they really start to shine when you use them to collaborate with others.

The same is true for the usethis PR helper functions. Setting up your local repo to pull down the changes from a collaborator – changes that probably live in a branch in their copy of the repo – can be a frustrating experience full of online searching to remember the specific incantations required to get your collaborators onto your computer and open in front of you.

But have no fear, pr_fetch() does all of this for you, in just one command in your R console.

Decision time: Do you know the pull request number?

Open a remote branch, locally

The pull request is open on GitHub and you can see the changes there, but you just can’t interact with the code in the same way through the browser. You have to bring that code into your local IDE where you can hold it in your hand and poke it with debuggers.

Decision time: Do you know the pull request number?

Choose a PR and bring the changes onto your computer

Your friend, who affectionately calls themselves @wileycoyote on GitHub, helpfully contributed a new function to your bag of tricks package, acme. They sent you a message on Slack letting you know they submitted a PR, but they didn’t mention the PR number.

No big deal — you can call pr_fetch() from your local repository without any arguments and pr_fetch() will look up any open pull requests and give you the option to pick the one you want.

pr_fetch()
ℹ No PR specified ... looking up open PRs
Which PR are you interested in? (0 to exit) 

1: 'gadenbuie/acme/#11' (@gadenbuie): 'Add an example'
2: 'gadenbuie/acme/#14' (@wileycoyote): 'Model tuning features'

Selection: 2
✓ Checking out PR 'gadenbuie/acme/#14' (@wileycoyote): 'Model tuning features'
✓ Adding remote 'wileycoyote' as 'git@github.com:wileycoyote/acme.git'
✓ Creating and switching to local branch 'wileycoyote-toone-model'
✓ Setting 'wileycoyote/toone-model' as remote tracking branch

You find your friend’s PR in the list and choose selection 2. Next time you can perform the same steps by providing the PR number — pr_fetch(14) — or you can use the menu again. Who has time or brain space to memorize pull request numbers?

Now that you have the code from the PR available to you locally, you’re free to poke around to try out the code and review it.

Decision time: Okay, you’ve had a chance to look at the code, what do you want to do next?

Open a PR locally

You’ve been working with your friend, @wileycoyote, on a new set of model tuning functions. They’ve helpfully started working on a PR, but since you’ve been reviewing the code carefully with them over the past few days, you actually remember the pull request number. Lucky number 14.

pr_fetch(14)
✓ Checking out PR 'gadenbuie/acme/#14' (@wileycoyote): 'Model tuning features'
✓ Switching to branch 'wileycoyote-toone-model'

You already have a copy of the PR branch in your local project, so pr_fetch() simply switched you into the wileycoyote-toone-model branch that it created when you first fetched the PR.

Because you might have been doing some work here before, pr_fetch() does not try to update your local branch. But you haven’t done any work yet other than looking at the code, so you follow up with pr_pull() to pull the latest changes into your project.

pr_pull()
✓ Pulling from 'wileycoyote/toone-model'

Decision time: Now that you have the latest PR code, what do you want to do next?

Open your PR on GitHub

You can jump straight to the GitHub pull request page for the branch you’re in with pr_view()!

pr_view()
ℹ Current branch ('add-example') is connected to PR #11
✓ Opening URL 'https://github.com/gadenbuie/acme/pull/11'

Decision time: When you’re done with the PR on GitHub, come back here to decide where you’re headed next.

Open their PR on GitHub

You can jump straight to the GitHub pull request page for the branch you’re in with pr_view()!

pr_view()
ℹ Current branch ('wileycoyote-toone-models') is connected to PR #14
✓ Opening URL 'https://github.com/gadenbuie/acme/pull/14'

Decision time: Welcome back to your project, I hope everything went well over on GitHub. Where to next?

Update your local copy

Update your local copy with a reviewer’s changes

Maybe a reviewer suggested changes and you merged them from the PR page on GitHub. Maybe a GitHub actions workflow automatically re-styled your code. Or maybe you did a little side-project updating on your work computer.

However the changes happened, the code in the PR branch has changed, and now you want to pr_pull() those changes into your local copy.

pr_pull()
✓ Pulling from 'origin/add-example'

Decision time: That was easy! What’s next?

Update your local copy with someone else’s changes

Changes were made and hopefully the code has been improved, but you’d like to run through the changes yourself to make sure it works as expected.

Use pr_pull() to get the latest changes from the PR branch into your local copy.

pr_pull()
✓ Pulling from 'wileycoyote/toone-model'

Decision time: That was easy! What’s next?

Bring your work up to date with the main branch

While you were working on adding a new example to the documentation, your friend @wileycoyote went and submitted a PR that changes the function you’re working on.

You found out when you looked at your pull request on GitHub and it said

Caution: This branch has conflicts that must be resolved.

Usually two people can work in the same project on two different areas without running into each other, but when both you and someone else want to change the same lines of code, you run into this issue.

Helpfully, pr_merge_main() can get the latest changes from the main branch of your project into your current pull request!

pr_merge_main()
✓ Pulling changes from 'origin/main' (default branch of source repo)
There are 1 conflicted files:
* R/atom_arranger.R

Are you ready to sort this out?
If so, we will open the conflicted files for you to edit.

1: Yes, I'm ready to resolve the merge conflicts.
2: No, I want to abort this merge.

Selection: 1
Please fix each conflict, save, stage, and commit.
To back out of this merge, run `gert::git_merge_abort()` (in R) or `git merge --abort` (in the shell).

If you need to fix any merge conflicts, pr_merge_main() will alert you that there are conflicts that need to be addressed. It also opens the files for you if you want it to, or you can choose to abort the merge and find another way to resolve the conflicts.

You want to resolve the conflicts, so you picked selection 1 to open the conflicted file where you start looking for blocks in the source code that look like this:

<<<<<<< add-example
Code in the first section appears in
our *current* version in the `add-example` branch
=======
Code in the second section appears in
the *incoming* version, i.e. the `main` branch
>>>>>>> main

To resolve the conflict, you

  1. Edit from <<<<<<< to >>>>>>> choosing one or the other or a blend of the two versions

  2. Save the file and stage it with gert::git_add()

  3. Repeat for other files with merge conflicts

  4. Commit the updated files with gert::git_commit() or git commit in the command line

Decision time: Great, your branch is up to date with the main branch!

Bring their work up to date with the main branch

While you and @wileycoyote have been working together on this new feature, you’ve been doing some work in other areas of the package. Since that other work has been added to the main branch, you might want to make sure that everything is up to date in @wileycoyote’s toone-model branch.

To do this, run pr_merge_main(). It makes sure your default branch is up to date and then merges any changes into the current branch.

pr_merge_main()
✓ Pulling changes from 'origin/main' (default branch of source repo)
Merged origin/main into wileycoyote-toone-model

If there aren’t any merge conflicts, you’ll get a nice, quick confirmation that the merge went well.

Decision time: Great, @wileycote’s branch is up to date with the main branch!

Get to the right place

Let’s make sure we’re in the right branch in your local project.

Decision time: Which branch do you want to work with right now?

Work

Do some work

You take a sip of your coffee (or the beverage of your choice), turn up your favorite music to write code to playlist on Spotify, and line up 25 minutes in your Pomodoro app.

You’re ready to get something done! Go write some code. ⏲️ 🧑‍💻 🎧

Decision time: Sweet! Now that you feel good about your work, you’re ready to take a snapshot of your updates.

Keep doing some work

Taking breaks and resting is important!

Last night I got caught up in a nested #RStats list problem and couldn’t solve it.

Wrote the code this morning first shot.

Take breaks! You need them!

— Nicholas Tierney (@nj_tierney) April 16,2019

Sometimes the best thing you can do to solve a problem is give it some time. Go for a walk, read something engaging, talk to a friend, take a shower.

And now you’re back with fresh eyes, more energy, and a full cup of coffee. This time, things go better! You fall easily into a flow and start writing some code. As you go, you periodically pause to stage files with gert::git_add() or to commit batches of changes with gert::git_commit().

After a bit you surprise yourself when you realize that the thing you couldn’t figure out when you were tired yesterday you’ve knocked out in an hour.

Decision time: Happy with your progress, you’re ready update the PR with your latest changes.

Collaborate with someone else

This is exciting! Your friend — which feels like a totally natural way to describe @wileycoyote, a person you know primarily from Twitter — had a pretty cool idea and you’re feeling the buzz of inspiration. Even though they started the pull request, you’re about to riff on the ideas they started laying down.

You pick out your favorite RStudio theme and turn up your favorite dance-slash-coding-slash-singing-out-loud music.

As you work, you pause occasionally to stage files with gert::git_add() and to commit them with gert::git_commit(). Since you’re working with someone else, you remember to write good commit messages, but since you’re having fun you also use gitmoji to give each commit a good emoji.

It’s a lot later than you expect when you start wrapping up. That’s okay, @wileycoyote is going to be stoked to see what you’ve been working on!

Decision time: Go head and update the PR with your latest changes.

Take a snapshot of your work

If you like it then you shoulda made a git commit.

There are two parts to taking a snapshot of your code. And really, it’s less like taking a picture and more like sending yourself an email with a copy of your files.

First, you’ll pick the files that you want to add to the snapshot (or commit). You can do this in the R console using git_add() from the gert package. Give the function a vector with the paths to the files you changed.

gert::git_add(c("R/atom_arranger.R", "man/atom_arranger.Rd"))

In git-speak, git_add() adds the changes in the listed files to a staging area. Running this command is like dragging a file into the email you’re writing. We haven’t officially sent that email yet, but we have a copy ready to go. By the way, you can still make more changes to the file knowing that there’s a temporary copy in that email draft in case anything goes wrong.

Then, once all the files you want to commit have been added, you commit the changes. In our email metaphor, committing is a lot like pressing send on the email.

And just like an email, the commit includes a message where you can describe the updates you made to the files in the email (commit).

For this step you can use git_commit() (also from the gert package) which takes the commit message as a parameter. Think of this message like the subject of an email to your future self.

gert::git_commit("Add an example to ?atom_arranger()")

P.S. You can do this adding-and-committing dance in the Git pane in RStudio, too.

Decision time: Are you ready to share your work?

Fix something

Oh no, you need to fix something! I hope it’s just a typo and not a big scary error! But first…

Decision time: This thing that needs to be fixed — what branch is it in?

Fix something

Oh no, you need to fix something! I hope it’s just a typo and not a big scary error! But first…

Decision time: This thing that needs to be fixed — what branch is it in?

Fix something

Oh no, you need to fix something! I hope it’s just a typo and not a big scary error! But first…

Decision time: This thing that needs to be fixed — what branch is it in?

Share your work

Fantastic! Your work is awesome and everyone is going to be happy to try it out.

Decision time: Let’s get your work off of your computer and out into the world.

Create a new pull request

Fantastic! Your work is awesome and everyone is going to be happy to try it out.

Let’s get your work off of your computer and out into the world. To create a new pull request, you call pr_push().

pr_push()
✔ Pushing local 'add-example' branch to 'origin:add-example'
✔ Setting upstream tracking branch for 'add-example' to 'origin/add-example'
✔ Create PR at link given below
✔ Opening URL 'https://github.com/gadenbuie/acme/compare/add-example'

pr_push() sends the local changes in your new branch to GitHub, and opens a browser window where you can review your changes once more. If everything looks good, go ahead click the Create Pull Request button.

Decision time: Good job! How are you feeling now?

Update your existing pull request

You just made and committed some changes to an existing PR you created, and now you’d like to update that PR on GitHub. pr_push() also does this for you! Helpfully, before it pushes it checks to make sure your local branch is up-to-date.

pr_push()
✔ Checking that local branch 'add-example' has the changes in 'origin/add-example'
✔ Pushing local 'add-example' branch to 'origin:add-example'
✔ View PR at 'https://github.com/gadenbuie/acme/pull/10' or call `pr_view()`

Decision time: Phenomenal! What do you want to do next?

Update someone else’s pull request

At this point, you have changes in your local copy of your PR branch that you need to push out to the source branch for the PR, which happens to be in @wileycoyote’s fork of your repo.

pr_push() is very smart and knows how to send the changes to the correct repository, pushing to the toone-model in the wileycoyote/acme.

pr_push()
✔ Checking that local branch 'wileycoyote-toone-model' has the changes in 'wileycoyote/toone-model'
✔ Pushing local 'wileycoyote-toone-model' branch to 'wilecoyote:toone-model'
✔ View PR at 'https://github.com/gadenbuie/acme/pull/14' or call `pr_view()`

Decision time: Phenomenal! What do you want to do next?

Wrap Up

Pause your work

You’re done but you have a feeling you’ll be back here again soon. What you need is a little pause.

Call pr_pause() to switch from the current branch back to the default branch, and to make sure you’ve got the latest changes in the default branch. Don’t worry, the work will be waiting patiently for you in a local branch when you’re ready to come back to it.

pr_pause()
✓ Switching back to default branch ('main')
✓ Pulling changes from 'origin/main'

Decision time: Go back to Start to decide to decide what’s next in your adventure.

Finish up your work

Hooray! A PR was merged and the new code is now in the default branch. That’s one less branch you need to keep around on your computer.

Finish your work

Congrats! Your pull request was merged! That’s awesome. Now it’s time to wrap things up by calling pr_finish().

pr_finish()
✓ Checking that remote branch 'origin/add-example' has the changes in 'add-example'
✓ Switching back to default branch ('main')
ℹ Pulling changes from 'origin/main'
✓ Deleting local 'add-example' branch

Decision time: Go back to Start to decide what’s next in your adventure.

Finish your work

Well done, your collaboration with @wileycoyote is complete and their PR is merged! That’s awesome. Now it’s time to wrap things up by calling pr_finish().

pr_finish()
✔ Checking that remote branch 'wileycoyote/toone-model' has the changes in 'local/wileycoyote-toone-model'
✔ Switching back to 'main' branch
✔ Pulling changes from GitHub source repo 'origin/main'
✔ Deleting local 'wileycoyote-toone-model' branch
✔ Removing remote 'wileycoyote'

Notice that pr_finish() does quite a lot! It makes sure that you don’t accidentally leave behind some changes in your local branch that might have forgotten to push. Then it switches back to main and makes sure that’s up to date, too. Finally we forget all about @wileycote’s branch and forked repo.

Decision time: That’s all folks! Head back to the beginning to choose your next adventure.

Clean up now that the PR has been merged

You don’t have to be in your local copy of the PR branch if you know the PR number. Give the number to pr_finish() and it will do the rest for you: it moves back to the default branch, updates your local copy, and deletes the old PR branch.

pr_finish(11)
✓ Checking that remote branch 'origin/add-example' has the changes in 'add-example'
✓ Switching back to default branch ('main')
ℹ Pulling changes from 'origin/main'
✓ Deleting local 'add-example' branch

Decision time: Go back to Start to decide what’s next in your adventure.

Forget about your work

Well that was fun. But now you’re done with this branch and you call pr_forget() to delete it from your local copy.

pr_forget()
Local branch 'add-example' has no associated remote branch.
If we delete 'add-example', any work that exists only on this branch work may be hard for you to recover.
Proceed anyway?

1: No way
2: Not now
3: Yes

Selection: 3
✓ Switching back to default branch ('main')
✓ Pulling changes from 'origin/main'
✓ Deleting local 'add-example' branch

Notice that pr_forget() warns you about any work you might lose and gives you a chance to rethink it.

Decision time: Go back to Start to decide what’s next in your adventure.

Forget about their work

Your work here is done and you’d like to move on with your life. pr_forget() lets you put this branch behind you. And while it’s in there, it also cleans up your repo, deleting the local copy of the PR branch and removing the PR author’s repo fork from the list of remote repositories your git project could sync with.

pr_forget()
✓ Switching back to default branch ('main')
✓ Pulling changes from 'origin/main'
✓ Deleting local 'wileycoyote-toone-models' branch
✓ Removing remote 'wileycoyote'

That was smooth! Notice that pr_forget() moved you into the default branch of your repo, main, and it even made sure that branch is up to date.

pr_forget() is safe and cautious. If your local wileycoyote-toone-models branch had had any changes in it that you might have lost, pr_forget() would have warned you. You also know that you can get back to the PR branch any time using pr_fetch().

Decision time: Worry and care-free, you head back to the beginning to decide what’s next in your adventure.


  1. I primarily use GitHub, but I think these functions will generally work for other code-hosting platforms as well, like GitLab or others.↩︎