May 28, 2024

Exploring GitHub Repos and PRs with gh, fzf, and a touch of GraphQL

Have you ever been awestruck by someone’s workflow and wanted to replicate it? I certainly have. My recent awe moment was when I saw my senior colleague, Professor Peter, navigating through GitHub repositories and pull requests from the comfort of his terminal. Moreover, he maintain multiple working directories within a single Git reposioty with git worktree. He’s a real wizard.

I asked him to show me the way of the wizard, and he agreed to take me under his tutelage on the condition that he wouldn’t show me his code but would only guide me.

Starting with gh cli

gh is a GitHub CLI tool that brings GitHub to your terminal. It’s a Swiss army knife for GitHub. You can create repositories, issues, pull requests, and more. Two of the most useful features of gh that I use are gh pr and gh repo. However, there are some limitations to gh pr as you need to be in the repository directory to perform any action.

~ gh pr list 
failed to run git: fatal: not a git repository (or any of the parent directories): .git 

Once you move to your repository directory, you can list all the pull requests in the repository and also filter PRs using search syntax.


Pull requests for owner/repo

#14  Upgrade to Prettier 1.19                           prettier
#14  Extend arrow navigation in lists for MacOS         arrow-nav 
#13  Add Support for Windows Automatic Dark Mode        dark-mode 
#8   Create and use keyboard shortcut react component   shortcut

~/Github/my-project$ # Filter PRs using search syntax 
~/Github/my-project$ gh pr list --search "status:success review:required" 

You can view the details of a PR using gh pr view <pr-number> in the terminal, which is not pretty, or you can open the PR in the browser using the web flag like this: gh pr view <pr-number> --web.

Limitations of gh pr

  • What happens if you deal with multiple repositories and want to view the PRs across all of them?
    • You have to navigate to each repository and list the PRs.
  • What if you want to view the PRs:
    • assigned to you?
    • that you have created?
    • that have requested your review?

Luckily, GitHub has developed GraphQL API endpoints. You can go to https://github.com/pulls or click on the Pull requests tab on GitHub. You can filter PRs using the search parameters or enter your own search query. But then, you have to open the browser and navigate to the URL.

What if you could do all of this from the comfort of your terminal?

GraphQL API for GitHub

My professor urged me to learn GraphQL first and use the GitHub GraphQL API to perform queries. I started with GitHub’s guide on Introduction to GraphQL.

I also learned from other blogs:

Here’s my query to get all the PRs created by me, which returns the title, URL, and repository name of the PRs:

{
    search(query: "is:pr is:open author:@me", type: ISSUE, first: 100) {
      edges {
        node {
          ... on PullRequest {
            title
            url
            repository {
              nameWithOwner
            }
          }
        }
      }
    }
  }

Just by changing the query parameter:

  • you can get the PRs assigned to you,
  • the PRs that you have created,
  • the PRs that you have requested review,
  • the PRs created by other users, etc.

Using gh api graphql -f you can run the query in the terminal and get the result.

~ gh api graphql -f query='
{
    search(query: "is:pr is:open author:@me", type: ISSUE, first: 100) {
      edges {
        node {
          ... on PullRequest {
            title
            url
            repository {
              nameWithOwner
            }
          }
        }
      }
    }
  }

Enter fzf and jq

Once you get the result of your query, you can use jq to parse the JSON response and extract the fields that you want to display. Here, I’m choosing the title and the repository name of the PRs. Then you can pipe the PR information to fzf to allow the user to interactively select a PR.

fzf is a picker. It lets you select an item from a list of items and if the result is not piped anywhere, it is echoed in the terminal. It is commonly used as a command-line fuzzy finder.

When a PR is selected, jq is used again to find the corresponding URL of the selected PR. The URL is then opened in the browser.

Putting it all together

Here’s my script which allows me to filter and view PRs:

#!/bin/env bash

# Helper function to display usage information
show_usage() {
  echo "Usage: $0 [option]"
  echo "Options:"
  echo "  - rv: List open pull requests where a review is requested"
  echo "  - someAuthor: List all open pull requests from a specific author (replace 'someAuthor' with the actual username)"
  echo "  - all: List all open pull requests from MyOrg"
  exit 1
}

# Display usage if --help is provided
if [[ "$1" == "help" || "$1" == "h" ]]; then
  show_usage
fi

# Determine the query based on the selected option and author
if [[ $# -eq 0 ]]; then
  # Default to listing open pull requests authored by you (MyOrg)
  query='{
    search(query: "is:pr is:open author:@me user:MyOrg", type: ISSUE, first: 100) {
      edges {
        node {
          ... on PullRequest {
            title
            url
            repository {
              nameWithOwner
            }
          }
        }
      }
    }
  }'
elif [[ "$1" == "all" ]]; then
  query='{
    search(query: "is:pr is:open user:MyOrg", type: ISSUE, first: 100) {
      edges {
        node {
          ... on PullRequest {
            title
            url
            repository {
              nameWithOwner
            }
          }
        }
      }
    }
  }'
elif [[ "$1" == "rv" ]]; then
  query='{
    search(query: "is:pr is:open review-requested:@me user:MyOrg", type: ISSUE, first: 100) {
      edges {
        node {
          ... on PullRequest {
            title
            url
            repository {
              nameWithOwner
            }
          }
        }
      }
    }
  }'
else
  # Use the argument as the author name
  query='{
    search(query: "is:pr is:open author:'"$1"' user:MyOrg", type: ISSUE, first: 100) {
      edges {
        node {
          ... on PullRequest {
            title
            url
            repository {
              nameWithOwner
            }
          }
        }
      }
    }
  }'
fi

# Fetch data using gh CLI and pipe directly to jq
response=$(gh api graphql -f query="$query")

# Extract information using jq
pr_info=$(echo "$response" | jq -r '.data.search.edges[] | "\(.node.title) (\(.node.repository.nameWithOwner))"')

# Use fzf to select a PR
selected_pr=$(echo "$pr_info" | fzf --prompt="Select a Pull Request: ")

# Extract the URL from the selected PR
selected_url=$(echo "$response" | jq -r --arg selected_pr "$selected_pr" '.data.search.edges[] | select(.node.title + " (" + .node.repository.nameWithOwner + ")" == $selected_pr) | .node.url')

# Check if a URL was selected
if [[ -n "$selected_url" ]]; then
  # Open the selected URL in the default browser
  # xdg-open "$selected_url"  # For Linux
  open "$selected_url"  # For macOS
else
  echo "No URL selected."
fi

Here’s a screenshot of the script in action. I modified the script a little to search the whole GitHub instead of just MyOrg. I queried all the PRs that are open by the user mitchellh.

~ pr mitchellh 

pr in action

Other use cases of gh and fzf

I’ve created two aliases sr and wr which are short for search-repo and web-repo respectively.

  • Alias sr: Allows interactive selection of a directory within ~/Github and navigates to it.
sr='cd ~/Github && cd $(find * -type d -maxdepth 0|
fzf --multi --height=60% --margin=5%,2%,2%,5% --layout=reverse --border=double
--info=hidden --prompt='\''>'\'' --pointer='\''→'\'' --marker='\''♡'\''
--header='\''CTRL-c or ESC to quit'\''
--color='\''dark,fg:green,prompt:green'\'')' 
  • Alias wr: Does the same as sr but also opens the corresponding GitHub repository page in the browser using the gh browse command.
wr='cd ~/Github && cd $(find * -type d -maxdepth 0| fzf --multi --height=60%
--margin=5%,2%,2%,5% --layout=reverse --border=double --info=hidden
--prompt='\''>'\'' --pointer='\''→'\'' --marker='\''♡'\'' --header='\''CTRL-c
or ESC to quit'\'' --color='\''dark,fg:green,prompt:green'\'') && gh browse'

sr in action

Here’s a guide on how to customize the fzf display.

By integrating gh, GraphQL, fzf, and jq, I’ve managed to replicate a fraction of Professor Peter’s wizardry. This approach saves time and brings powerful GitHub functionalities directly to the terminal.

Powered by Hugo & Kiss.