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:
- fetch any new commits on the
develop
branch in the 'clean'develop/
copy (in case the remote diverged) - copy the
develop/
repo with a new name underneathwip/
- change its branch to its new directory name, prefixing with your username to keep organised
- e.g.
git checkout -b "lmmx/my-new-feature"
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).
- Push branch
- Squash merge the PR with
gh
and delete the feature branch cd
to repo's top level, thencd ..
to the repo's parent directory- Delete repo
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:
- commit some edits
- open a PR
- commit a 2nd round of edits
- run
gh pr merge --delete-branch --squash
...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.