Efficient git[hub] tooling
This is a follow-up to my previous blog post: “My github pull request workflow”.
I would like to dedicate a complete post on how you can be efficient when using git[hub].
It’s certain that you may have your own tips and tricks up your sleeve. Please share them in the comments.
Let’s begin!
Prereqs
First things first. All of my aliases, scripts and shell functions expect a certain configuration:
-
origin
is a remote which points to my fork (or a repository directly owned by me). -
upstream
remote points to upstream repository. -
git
fetches pull requests of my copies locally. For github these live in e.g.refs/pull/*/head
. You can easily tell git to fetch them locally for you as well:[remote "origin"] fetch = +refs/pull/*/head:refs/remotes/origin/pr/* [remote "upstream"] fetch = +refs/pull/*/head:refs/remotes/upstream/pr/*
Just add that line to
.git/config
or even directly to~/.gitconfig
. Therefore pull requests will be available locally on ref<remote>/pr/<number>
. -
I have a bunch of scripts which interact with github API. I have my token stored in
~/.github.token
. -
And finally, some scripts use two libraries:
python3-PyGithub
andpython3-tabulate
.
Contributing to a project
Imagine a simple scenario: you want to contribute to some project. What do we need to do in order to make that happen? (aside from the code itself)
We need to fork first
I have a nice shell function and a script for that. All I really do is:
$ gh-f fedora-modularity/conu
What would happen?
- The script would fork https://github.com/fedora-modularity/conu into my account.
- Clone it locally to a simple tree-like structure:
./fedora-modularity/conu
. - Configure remotes: origin and upstream.
- Set up git to fetch pull requests locally.
The shell function is trivial: it executes the script and then cd
s to the local clone.
What I especially love is that the script is idempotent. I can run it on repositories I already forked/cloned and it sets the repository to the expected state.
Coding
Now’s the fun part, writing the code itself! But let’s create feature branch first:
gb feature-branch
, is an alias forgit checkout -b
<code>
- commit with
gc
(an alias forgit commit --verbose
— with verbose flag, you’ll see the code you are about to commit in the editor) <code-some-more>
- commit some more (e.g. with
gca
—git commit --amend --verbose
) - or with
gri HEAD~3
(git rebase --interactive
), when you need to change multiple local commits
Create a PR
Code is done, we can propose it to upstream:
gp
stands forgit push -u
— with-u
, we would automatically track our local branch with the newly created remote one in our fork.gh-pr
is a neat script which would create a pull request:
- It opens an editor where you can edit the PR title and a description. The first line is the title, rest is the body.
- The script helps you by showing you all of your commits in your local branch.
- Once you’re done, just
:wq
and you can briefly see the metadata of the request: this is just a sanity check before creating it. - Last line of the script output is a link to the PR itself.
Changes after review
Your code is a hit! Upstream loves it, but asks you to do some changes.
- Let’s open a new shell window in our terminal.
- Navigate with autojump to our local clone:
j conu
. - Rebase the PR:
gpum
—git pull --rebase upstream/master
. - Do the changes and
gc
,gca
orgri
as needed. - And finally
gpf
:git push --force
.
PR was merged, good job!
Testing PR locally
Let’s assume role of an upstream developer now. Someone sent a PR and you want to review it. Ideally, our session would be as short as possible, right? Let’s give it a shot:
- Let’s open a new shell window in our terminal.
- Navigate with autojump to our local clone:
j conu
. - List proposed pull requests:
$ gh-list-prs (master)
╒══════╤════════════════════════════════════════════════════════════════════════╤═══════════════╕
│ #178 │ WIP: check-packaging added to Makefile to verify all packaging methods │ @enriquetaso │
├──────┼────────────────────────────────────────────────────────────────────────┼───────────────┤
│ #179 │ backend: fake - null container backend │ @jscotka │
├──────┼────────────────────────────────────────────────────────────────────────┼───────────────┤
│ #187 │ WIP WIP nspawn madness WIP WIP │ @TomasTomecek │
├──────┼────────────────────────────────────────────────────────────────────────┼───────────────┤
│ #182 │ First release candidate for 0.3.0 │ @TomasTomecek │
╘══════╧════════════════════════════════════════════════════════════════════════╧═══════════════╛
- You can see pretty-git-prompt on the right side of my prompt.
- By default, the script lists PRs for the repo you’re in, but can also accept an argument.
- Let’s check out the latest PR:
$ checkout-pr (master)
Fetching origin
Fetching upstream
Switched to branch 'pr/189'
Your branch is up to date with 'upstream/pr/189'.
HEAD is now at a2a390d No need to 'RUN mkdir' before COPY
$ make test (pr/189│upstream↓1)
Running the script resulted into being on branch pr/189
which is one commit ahead of upstream/master.
The script should be idempotent. Even if you check out a PR locally, running the script again in future will give you the latest changes.
Conclusion
That’s it! This is how I github.
I haven’t mentioned two really good tools, which I like to use:
- tig — terminal interface (ncurses) for git
- vim-figutive — git interface done as a vim plugin
All of my scripts are MIT licensed so you can do whatever you want with them. I
suggest copying them 1:1 and adding to your dotfiles or just on $PATH
.
It would be much better to compile all of them into a single tool. I’m just
lazy to do it right now as it’s much easier to edit a script which is already
on your $PATH
rather than develop a dedicated upstream project.
Happy hacking!