Danny Brown

A Blog on Code and Occasionally Other Things

Open Pull Requests from the Terminal (One of My Favorite Dotfiles Scripts)

Danny BrownDecember 23, 2023

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.

Posted In Bash | Bitbucket | code | Git | GitHub | tech
Tagged dotfiles

Post navigation

PreviousDotfiles Script for a New TypeScript/Node Project

Danny Brown

A Dev Blog with Some Tangents

About

Categories

  • code
    • APIs
    • Bash
    • CSS
    • Django
    • HTML
    • JavaScript
    • Python
    • S3
    • Selenium
    • Serverless
    • TypeScript
  • games
  • music
    • concert reviews
    • synthesizers
  • opinion
  • sports
  • tech
    • Bitbucket
    • Git
    • GitHub
    • MS Teams
    • WordPress
  • theater

Recent Posts

  • Open Pull Requests from the Terminal (One of My Favorite Dotfiles Scripts)
  • Dotfiles Script for a New TypeScript/Node Project
  • So I Told You to Go See a Broadway Play? Tips for Theater in New York
  • Build a Simple Microsoft Teams Bot Easily, No SDK Required
  • Creating a GUI for Conway’s Game of Life Using Pygame and Numpy

External Links

  • GitHub
  • LinkedIn

Recent Posts

  • Open Pull Requests from the Terminal (One of My Favorite Dotfiles Scripts)
  • Dotfiles Script for a New TypeScript/Node Project
  • So I Told You to Go See a Broadway Play? Tips for Theater in New York
  • Build a Simple Microsoft Teams Bot Easily, No SDK Required
  • Creating a GUI for Conway’s Game of Life Using Pygame and Numpy

Categories

  • code
    • APIs
    • Bash
    • CSS
    • Django
    • HTML
    • JavaScript
    • Python
    • S3
    • Selenium
    • Serverless
    • TypeScript
  • games
  • music
    • concert reviews
    • synthesizers
  • opinion
  • sports
  • tech
    • Bitbucket
    • Git
    • GitHub
    • MS Teams
    • WordPress
  • theater
Copyright © 2025. Danny Brown
Powered By WordPress and Meritorious