How I got good at git with gh

Or how I learnt to stop worrying and love headless PR flow

I recently switched from manual PR flow (using git in the usual way then completing the PR in the browser) to a 'pure CLI' thanks to gh.

I initially bristled at gh when it came out: I suspected it might co-opt my use of git at the time, and be deskilling. I now see it as orthogonal, primarily using GitHub API endpoints not accessible to git.

That said, part of the greatest benefit does come from taking over (smoothing) what git previously handled. In reality, I found that there are automated ways of achieving efficient git workflows (sequences of git commands) available in gh, so using it became a no-brainer.

1. Previous Approach (Click, type, scroll down, click)

There's always variation, but the general outline of the typical git flow I had was:

clone → branch → edit → commit (→ pre-commit linter suite) → push
                                                              ↓
                           open PR submission form and write PR
                            ↓
              iteratively edit issue/PR in browser
             (switch between editing code + browser)
                            ↓
   juggle browser tabs (docs/LLM chats) with PR browser tab
    ↓
   find/reopen PR tab → comment "done" → click merge/squash button
                                                             ↓
   leave stale branch undeleted locally ← click delete branch button
     ↓
   switch branch back to default branch

This amounts to one iteration cycle.

This in itself was an improvement on skipping PRs/pre-commit flow, but there was a lot that could be improved further.

It's important to note here that the end result is: you're sat in a browser, you're mentally double checking that you've done all the cleanup tasks (did you definitely delete that branch?) and you are suddenly unmoored of any context. Your PR was closed, you close the browser tab, now what?

2. My improved approach (bashrc function fu)

A few months ago I came up with a better idea: I'd keep "clean" copies of repos I was iterating on actively:

my-repo/
├── clean
│   └── develop
└── wip
    └── my-new-feature

The develop and my-new-feature directories are both just repos, but on different branches.

To start working on a new feature branch therefore, all you have to do is:

I put all of this in a bashrc function, newwip so I could just type newwip my-new-feature and after a short pause I'd be on a command line inside a freshly set up repo.

The short pause here was a bit annoying, but it was still much smoother than before to set up a PR feature branch.

3. So close to getting it (using gh for cleanup)

An obvious problem with all these copies is still having to manually tidy them away.

I later wrote another bashrc function, gject, to make a repo 'self-destruct' (merge and delete it from the wip directory).

This was the first time I'd used gh for PR merging. I was immediately a fan of how you could delete the branch both locally and on the remote in one go!

This made it dawn on me: what if I didn't have to use GitHub browser-based PR flows at all...

4. Tight iteration loop with 'headless' PRs (using gh for PR creation and merging)

If running things in a browser from a command line program is 'headless' operation, then I would call the creation and merging of PRs from the command line 'headless' PR flow.

It was a bit daunting at first, but you simply don't need to click buttons manually, and it's so much faster:

gh pr create -f
gh pr merge --delete-branch --squash

The -f flag will fill out the PR body with your commit messages (I use Conventional Commits), and the title will be the dehyphenated branch name (e.g. lmmx/a-cool-feature -> "lmmx/a cool feature").

A valuable side effect already mentioned is that merging with --delete-branch will delete your local branch, and in doing so put you back onto the default branch.

Note that this will discard any commits not pushed to the remote!

Use with caution: if you created a PR, do not merge it if you didn't push all local commits.

If you:

...you will lose the 2nd round of edits, those commits weren't in the PR you merged!

This is intuitive if you're conscious that you're working with GitHub here, and its PR has nothing to do with your local repo's branch until you push.

Another side effect you may not expect is that if you have bots adding commits to your PRs, your remote branch can diverge.

For me, I haven't found this disruptive, I use the pre-commit.ci bot to run pre-commit on my commits in my Python projects. When I gh pr merge --delete-branch it'll notify me that "the remote has diverged!" but this doesn't stop the merge from going ahead.

Note that you can also gate your PR merge using the --auto flag, but I found that slowed down my flow. If CI tests passing is something you're frequently worried about in your own code, that might be something you want to use (e.g. high stakes open source projects with lots of breakable parts and lots of contributors whose PRs might break them). But not everything is a Herculean effort at SciPy scale, and in such cases we can be more nimble.