One of my favorite small scripts I’ve written this year is my pr
script, which allows the user to simply enter pr
into their terminal of choice and open a pull request without having to specify anything else.
Repo Home and Secrets
if git remote -v | grep -q "bitbucket"; then repo_home="bitbucket" if [[ -z $BITBUCKET_TOKEN || -z $BITBUCKET_BASE_URL ]]; then echo "Error: You must set your BITBUCKET_TOKEN and BITBUCKET_BASE_URL in the environment" echo "Hint: The base URL should end with \".com\", the rest will be constructed in-script" return fi elif git remote -v | grep -q "github"; then repo_home="github" if [[ -z $GITHUB_TOKEN ]]; then echo "Error: You must set your GITHUB_TOKEN in the environment" return fi else echo "Error: Repo's remote URL is not supported. Add it or stick with GitHub or Bitbucket." git remote -v return fi
This first block uses the git remote
command to determine if the Git remote URL is on GitHub or Bitbucket, then in turn ensures that an appropriate token is present in the environment. To add said token, which you can get from your account section from each website in question, just export
it to your environment:
export GITHUB_TOKEN=
Note that for Bitbucket I also included a BITBUCKET_BASE_URL
. This allows one to use an enterprise instance of Bitbucket that doesn’t follow the typical www.bitbucket.com URL pattern.
Getting Default and Target Branches
Next we’ll use Git to get the repo’s default branch (where PRs should target) as well as the current branch.
default_branch=$(git remote show origin | grep 'HEAD branch' | awk '{print $NF}') current_branch=$(git branch | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/') pull_request_title="$current_branch -> $default_branch"
Note that we also generate a pull_request_title
from these two branches.
Creating a Pull Request Description from Commit Messages
non_current_branches=$(git for-each-ref --format='%(refname)' refs/heads/ | grep -v "refs/heads/$current_branch") commit_messages=$(git log $current_branch --oneline --not $non_current_branches) readarray -t commit_messages <<< "$commit_messages" commit_message="Commits in pull request:\n" for message in "${commit_messages[@]}"; do commit_message+="\n$message" done
The above code pulls the commit message and commit hash for each commit in the current branch and creates a commit message string that looks like this:
Commits in pull request:
47440b0 chore: add ts-node to .npm global installs
65c2977 chore: add further typescript config
ad0c6c5 chore: install typescript and make index.ts
9db17e6 chore: add defaultBranch of main to initial git config
Note that the downside to this approach is that if you add new commits or rebase existing ones (and, oh, do I do those things), the changes will not be picked up and the original PR description will stand (unless manually edited or deleted/reopened).
Collect Final Repo Details
Just a couple more things to collect here. First, we need the repo name. Second, we need what I've called the "repo_parent", which in GitHub is simply the user name of the repo owner, but in enterprise Bitbucket it is the parent project. Luckily, the same command will give us the necessary info for both services:
repo_name=$(basename "$(git rev-parse --show-toplevel)") repo_parent=$(git remote -v | grep push | cut -d'/' -f4)
Creating the Pull Request for Bitbucket
Since the JSON content of GitHub and Bitbucket PRs differs, we have a couple if blocks to build the necessary variables. Here's the Bitbucket version, which as you can see uses most of the variables we've been setting so far:
if [ $repo_home = "bitbucket" ]; then json_content="{ \"title\": \"$pull_request_title\", \"description\": \"$commit_message\", \"fromRef\": { \"id\": \"refs/heads/$current_branch\", \"repository\": \"$repo_name\", \"project\": {\"key\": \"$repo_parent\"} }, \"toRef\": { \"id\": \"refs/heads/$default_branch\", \"repository\": \"$repo_name\", \"project\": {\"key\": \"$repo_parent\"} } }" echo "$json_content" > temp_pr.json url="$BITBUCKET_BASE_URL/rest/api/1.0/projects/$repo_parent/repos/$repo_name/pull-requests" data_type_header="Content-Type: application/json" token=$BITBUCKET_TOKEN
Creating the Pull Request for GitHub
And the comparatively simpler variable construction for GitHub:
elif [ $repo_home = "github" ]; then json_content="{ \"title\": \"$pull_request_title\", \"body\": \"$commit_message\", \"head\": \"$current_branch\", \"base\": \"$default_branch\" }" echo "$json_content" > temp_pr.json url="https://api.github.com/repos/$repo_parent/$repo_name/pulls" data_type_header="Accept: application/vnd.github.v3+json" token=$GITHUB_TOKEN fi
Finally: Opening the PR
After all that, we're just going to curl
up the token, header, and JSON file we created, then remove the JSON file for good measure.
curl -X POST \ -H "Authorization: Bearer $token" \ -H "$data_type_header" \ -d @temp_pr.json \ "$url" rm -f temp_pr.json
For the full script, visit my dotfiles repo rather than have me sub-optimally copy-paste the full thing here.
Final Thoughts
For such a simple script, this has had a hugely positive impact on my workflow this year. I'm often churning through several small PRs a day and it would throw me out of my flow to have to leave my terminal/code editor, go to a web UI, find the branch, manually write a title and description, open the PR, etc. Of course there are situations where I still go in and edit some of the title/description details, but when you have a build that takes 10, 30, or even more minutes to complete, it's really nice to get the build agent spinning up as soon as possible.