KISS

Keep It Simple Stupid

Checkout PR script

| comments

As I mentioned in the previous post about a git hook to insert ticket numbers into commit message, it integrates well with another script of mine that allows me to check out a PR easily.

The script

The script fetches the HEAD of the PR branch from Github and also uses the Github API to get the URL of the fork and the name of the branch. In my case it’s located at ~/bin/git-pr.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env bash

set -euo pipefail
IFS=$'\t\n'

NUMBER="${1}"
LOCAL_BRANCH="pr/${NUMBER}"
REPO_NAME="$( git remote -v | sed -nE '/^origin/ { s/.*:(.*\/.*)\.git.*/\1/p; q; }' )"
# note: assumes a git SSH url
GITHUB_HOST="$( git remote -v | sed -nE '/^origin/ { s/.*@([^:]+):.*/\1/p; q; }' )"

git fetch origin pull/"$NUMBER"/head
git checkout -B "$LOCAL_BRANCH" FETCH_HEAD
IFS=' ' read -r URL REF < <( curl -sSu "$GITHUB_AUTH" -H "Accept: application/vnd.github.v3+json" https://"$GITHUB_HOST"/api/v3/repos/"$REPO_NAME"/pulls/"$NUMBER" | jq -r '[.head.repo.ssh_url, .head.ref] | join(" ")' )
git config --local branch."$LOCAL_BRANCH".remote "$URL"
git config --local branch."$LOCAL_BRANCH".merge "$REF"

There are a few assumptions here:

  • the original repository, where the PRs are submitted to, is under the default name origin (check with git remote -v);
  • the URL scheme used for origin is SSH-based: git@github.example.org:MyOrg/project.git;
  • I’m using it with a Github Enterprise instance, and it may also work with the default github, but I haven’t tried that;
  • the script needs an access token with the repo access, stored in the GITHUB_AUTH environment variable: user:token.

I don’t really like how I’m passing two values of the GitHub’s API response from jq to the shell, but it’s worked fine so far and I couldn’t find a better way. Shell scripting is extremely convenient when I’m starting to automate something with a small script, but can quickly become unwieldy because the shell language is pretty odd and limited.

Fetching a PR

My global ~/.gitconfig has these aliases:

1
2
3
[alias]
    fpr = !sh -c 'git fetch origin pull/${1}/head' -
    cpr = !sh -c '$HOME/bin/git-pr.sh ${1}' -

The first alias, fpr, allows me to fetch the commits in the PR without checking out any branches and disturbing the working directory. That PR is now available at FETCH_HEAD (until another fetch):

1
2
3
4
5
6
7
8
9
10
$ g fpr 1123
remote: Enumerating objects: 93, done.
remote: Counting objects: 100% (93/93), done.
remote: Total 110 (delta 93), reused 93 (delta 93), pack-reused 17
Receiving objects: 100% (110/110), 25.27 KiB | 1.94 MiB/s, done.
Resolving deltas: 100% (98/98), completed with 78 local objects.
From github.example.org:MyOrg/project
 * branch                  refs/pull/1123/head -> FETCH_HEAD

$ g lb FETCH_HEAD

(Where lb could be an alias for log --graph --abbrev-commit --pretty=oneline --decorate=short).

This is described in the Stackoverflow thread “How can I check out a GitHub pull request with git?”.

Checking out a PR

The git cpr alias just runs the script at the top of the post with the given PR number.

A big feature for me is that it sets up the push settings correctly so that I can commit into this pr/123 branch and just git push it, and it will be pushed to the correct PR branch, something like git push git@github.example.org:alice/project.git HEAD:task/PROJ42/treat_warnings_as_errors. This is a big deal because I sometimes help other team members to resolve conflicts in their PRs with the updated base branch after something else is merged.

It looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
$ g cpr 12345
remote: Enumerating objects: 73, done.
remote: Counting objects: 100% (73/73), done.
remote: Compressing objects: 100% (15/15), done.
remote: Total 73 (delta 58), reused 70 (delta 58), pack-reused 0
Unpacking objects: 100% (73/73), 39.40 KiB | 209.00 KiB/s, done.
From github.example.org:MyOrg/project
 * branch                  refs/pull/12345/head -> FETCH_HEAD
Previous HEAD position was 98b4aa03 Merge pull request #12340
Switched to a new branch 'pr/12345'

# git fetch --all
$ g fa
Fetching origin
remote: Enumerating objects: 165, done.
remote: Counting objects: 100% (102/102), done.
remote: Compressing objects: 100% (21/21), done.
remote: Total 50 (delta 39), reused 35 (delta 29), pack-reused 0
Unpacking objects: 100% (50/50), 7.32 KiB | 98.00 KiB/s, done.
From github.example.org:MyOrg/project
   abcdef..012345          master      -> origin/master
Fetching my

$ g merge origin/master
Auto-merging Podfile.lock
CONFLICT (content): Merge conflict in Podfile.lock
Auto-merging Podfile
Recorded preimage for 'Podfile.lock'
Automatic merge failed; fix conflicts and then commit the result.

$ g ch --theirs -- Podfile.lock && g add Podfile.lock && podin && gs && gd && ga
Analyzing dependencies
Downloading dependencies
Generating Pods project
Pod installation complete! There are 6 dependencies from the Podfile and 7 total pods installed.
## pr/12345
M  AppDelegate.swift
M  Podfile
MM Podfile.lock
M  README.md
diff --git a/Podfile.lock b/Podfile.lock
index 30bc34..13aa96 100644
--- a/Podfile.lock
+++ b/Podfile.lock
@@ -442,6 +442,6 @@ SPEC CHECKSUMS:
   LibraryA: 7e16842b8386fda06b104ac50d73a3f1fcbcab7b
   LibraryB: eb6b7a8206790fe93992ea07aeea84774cd4a116

-PODFILE CHECKSUM: 8decc82ec18a57d7cb3a9c2ef09381e272a6b0e8
+PODFILE CHECKSUM: b43753e746f43e38722b97c4d9d24bebf2be6b7c

 COCOAPODS: 1.10.1
(1/1) Stage this hunk [y,n,q,a,d,e,?]? y

$ gc -m 'Merge latest `master` to resolve Podfile.lock conflicts'
Recorded resolution for 'Podfile.lock'.
[pr/12345 98983cb7] TA880: Merge latest `master` to resolve Podfile.lock conflicts

$ g push
Enumerating objects: 25, done.
Counting objects: 100% (25/25), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 935 bytes | 155.00 KiB/s, done.
Total 9 (delta 8), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (8/8), completed with 8 local objects.
To github.example.org:fork/project.git
   b3c81ac..98983cb7  pr/12345 -> task/TA880_disable_verbose_logging

Note how I’m git committing with a message and the resulting message automatically has the ticket number TA880. Then I simply git push and the merge goes to the correct branch in the correct fork!

g ch --theirs -- Podfile.lock && g add Podfile.lock && podin && gs && gd && ga is a one-liner that I came up with after doing a number of conflict resolutions of the Podfile.lock and becoming tired of entering the commands one by one. It checks out the Podfile.lock from the merged, upstream branch (here, master), adds that version, pod installs whatever is necessary, prints git status, shows me git diff to verify the changes and finally asks to add the changes with git add -p.

Updated commit message hook

Here’s the updated prepare-commit-msg hook that has a special case for such PR branches. Specifically, if the PR 123’s branch is named task/TA1234/foo, locally it will be pr/123, but the hook is able to insert the TA1234: ticket number from the branch name.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env bash
#
# A hook script to ensure that the commit message is prepended with a ticket
# number from the current branch name. The hook runs before the user edits
# the message.
#

COMMIT_MSG_FILE="$1"
# Can be: <empty>, `message`, `template`, `merge`, `squash`, `commit`.
# See `man githooks`.
#COMMIT_SOURCE="$2"
#SHA1="$3"

BRANCH="$( git rev-parse --abbrev-ref HEAD )"
# A special case for PR branches created with `g cpr`, which have the original
# branch name in the push specification.
if [[ "${BRANCH:0:3}" == "pr/" ]]; then
  BRANCH="$( git config --local branch."$BRANCH".merge )"
fi

TICKET_NUMBER="$( grep -Eo '(US|DE|TA|F)\d+' <<< "$BRANCH" )"
[[ -z "$TICKET_NUMBER" ]] || perl -pi -e "$.==1 && s/^(?!$TICKET_NUMBER:|fixup!|squash!)/$TICKET_NUMBER: /" "$COMMIT_MSG_FILE"

Automation for the win!

Comments