#!/usr/bin/env sh
# # A GitHub API client library written in POSIX sh
#
# https://github.com/whiteinge/ok.sh
# BSD licensed.
#
# ## Requirements
#
# * A POSIX environment (tested against Busybox v1.19.4)
# * curl (tested against 7.32.0)
#
# ## Optional requirements
#
# * jq <http://stedolan.github.io/jq/> (tested against 1.3)
#   If jq is not installed commands will output raw JSON; if jq is installed
#   the output will be formatted and filtered for use with other shell tools.
#
# ## Setup
#
# Authentication credentials are read from a `$HOME/.netrc` file on UNIX
# machines or a `_netrc` file in `%HOME%` for UNIX environments under Windows.
# [Generate the token on GitHub](https://github.com/settings/tokens) under
# "Account Settings -> Applications".
# Restrict permissions on that file with `chmod 600 ~/.netrc`!
#
#     machine api.github.com
#         login <username>
#         password <token>
#
#     machine uploads.github.com
#         login <username>
#         password <token>
#
# ## Configuration
#
# The following environment variables may be set to customize ${NAME}.
#
# * OK_SH_URL=${OK_SH_URL}
#   Base URL for GitHub or GitHub Enterprise.
# * OK_SH_ACCEPT=${OK_SH_ACCEPT}
#   The 'Accept' header to send with each request.
# * OK_SH_JQ_BIN=${OK_SH_JQ_BIN}
#   The name of the jq binary, if installed.
# * OK_SH_VERBOSE=${OK_SH_VERBOSE}
#   The debug logging verbosity level. Same as the verbose flag.
# * OK_SH_RATE_LIMIT=${OK_SH_RATE_LIMIT}
#   Output current GitHub rate limit information to stderr.
# * OK_SH_DESTRUCTIVE=${OK_SH_DESTRUCTIVE}
#   Allow destructive operations without prompting for confirmation.
# * OK_SH_MARKDOWN=${OK_SH_MARKDOWN}
#   Output some text in Markdown format.

export NAME=$(basename "$0")
export VERSION='0.1.0'

export OK_SH_URL=${OK_SH_URL:-'https://api.github.com'}
export OK_SH_ACCEPT=${OK_SH_ACCEPT:-'application/vnd.github.v3+json'}
export OK_SH_JQ_BIN="${OK_SH_JQ_BIN:-jq}"
export OK_SH_VERBOSE="${OK_SH_VERBOSE:-0}"
export OK_SH_RATE_LIMIT="${OK_SH_RATE_LIMIT:-0}"
export OK_SH_DESTRUCTIVE="${OK_SH_DESTRUCTIVE:-0}"
export OK_SH_MARKDOWN="${OK_SH_MARKDOWN:-0}"

# Detect if jq is installed.
command -v "$OK_SH_JQ_BIN" 1>/dev/null 2>/dev/null
NO_JQ=$?

# Customizable logging output.
exec 4>/dev/null
exec 5>/dev/null
exec 6>/dev/null
export LINFO=4      # Info-level log messages.
export LDEBUG=5     # Debug-level log messages.
export LSUMMARY=6   # Summary output.

# We need this path for when we reset our env.
awk_bin=$(command -v awk)

# Generate a carriage return so we can match on it.
# Using a variable because these are tough to specify in a portable way.
cr=$(printf '\r')

# ## Main
# Generic functions not necessarily specific to working with GitHub.

# ### Help
# Functions for fetching and formatting help text.

 _cols() { sort | pr -t -3; }
 _links() { awk '{ print "* [" $0 "](#" $0 ")" }'; }
 _funcsfmt() { if [ "$OK_SH_MARKDOWN" -eq 0 ]; then _cols; else _links; fi; }

help() {
    # Output the help text for a command
    #
    # Usage:
    #
    #     help commandname
    #
    # Positional arguments
    #
    local fname="$1"
    #   Function name to search for; if omitted searches whole file.

    # Short-circuit if only producing help for a single function.
    if [ $# -gt 0 ]; then
        awk -v fname="^$fname\\\(\\\) {$" '$0 ~ fname, /^}/ { print }' "$0" \
            | _helptext
        return
    fi

    _helptext < "$0"
    printf '\n'
    help __main
    printf '\n'

    printf '## Table of Contents\n'
    printf '\n### Utility and request/response commands\n\n'
    _all_funcs public=0 | _funcsfmt
    printf '\n### GitHub commands\n\n'
    _all_funcs private=0 | _funcsfmt
    printf '\n## Commands\n\n'

    for cmd in $(_all_funcs public=0); do
        printf '### %s\n\n' "$cmd"
        help "$cmd"
        printf '\n'
    done

    for cmd in $(_all_funcs private=0); do
        printf '### %s\n\n' "$cmd"
        help "$cmd"
        printf '\n'
    done
}

_all_funcs() {
    # List all functions found in the current file in the order they appear
    #
    # Keyword arguments
    #
    local public=1
    #   `0` do not output public functions.
    local private=1
    #   `0` do not output private functions.

    for arg in "$@"; do
        case $arg in
            (public=*) public="${arg#*=}";;
            (private=*) private="${arg#*=}";;
        esac
    done

    awk -v public="$public" -v private="$private" '
        $1 !~ /^__/ && /^[a-zA-Z0-9_]+\s*\(\)/ {
            sub(/\(\)$/, "", $1)
            if (!public && substr($1, 1, 1) != "_") next
            if (!private && substr($1, 1, 1) == "_") next
            print $1
        }
    ' "$0"
}

__main() {
    # ## Usage
    #
    # `${NAME} [<flags>] (command [<arg>, <name=value>...])`
    #
    #       ${NAME} -h              # Short, usage help text.
    #       ${NAME} help            # All help text. Warning: long!
    #       ${NAME} help command    # Command-specific help text.
    #       ${NAME} command         # Run a command with and without args.
    #       ${NAME} command foo bar baz=Baz qux='Qux arg here'
    #
    # Flag | Description
    # ---- | -----------
    # -V   | Show version.
    # -h   | Show this screen.
    # -j   | Output raw JSON; don't process with jq.
    # -q   | Quiet; don't print to stdout.
    # -r   | Print current GitHub API rate limit to stderr.
    # -v   | Logging output; specify multiple times: info, debug, trace.
    # -x   | Enable xtrace debug logging.
    # -y   | Answer 'yes' to any prompts.
    #
    # Flags _must_ be the first argument to `${NAME}`, before `command`.

    local cmd
    local ret
    local opt
    local OPTARG
    local OPTIND
    local quiet=0
    local temp_dir="${TMPDIR-/tmp}/${NAME}.${$}.$(awk \
        'BEGIN {srand(); printf "%d\n", rand() * 10^10}')"
    local summary_fifo="${temp_dir}/oksh_summary.fifo"

    # shellcheck disable=SC2154
    trap '
        excode=$?; trap - EXIT;
        exec 4>&-
        exec 5>&-
        exec 6>&-
        rm -rf '"$temp_dir"'
        exit $excode
    ' INT TERM EXIT

    while getopts Vhjqrvxy opt; do
        case $opt in
        V)  printf 'Version: %s\n' $VERSION
            exit;;
        h) help __main
            printf '\nAvailable commands:\n\n'
            _all_funcs private=0 | _cols
            printf '\n'
            exit;;
        j)  NO_JQ=1;;
        q)  quiet=1;;
        r)  OK_SH_RATE_LIMIT=1;;
        v)  OK_SH_VERBOSE=$(( OK_SH_VERBOSE + 1 ));;
        x)  set -x;;
        y)  OK_SH_DESTRUCTIVE=1;;
        esac
    done
    shift $(( OPTIND - 1 ))

    if [ -z "$1" ] ; then
        printf 'No command given. Available commands:\n\n%s\n' \
            "$(_all_funcs private=0 | _cols)" 1>&2
        exit 1
    fi

    [ $OK_SH_VERBOSE -gt 0 ] && exec 4>&2
    [ $OK_SH_VERBOSE -gt 1 ] && exec 5>&2
    if [ $quiet -eq 1 ]; then
        exec 1>/dev/null 2>/dev/null
    fi

    if [ "$OK_SH_RATE_LIMIT" -eq 1 ] ; then
        mkdir -m 700 "$temp_dir" || {
            printf 'failed to create temp_dir\n' >&2; exit 1;
        }
        mkfifo "$summary_fifo"
        # Hold the fifo open so it will buffer input until emptied.
        exec 6<>"$summary_fifo"
    fi

    # Run the command.
    cmd="$1" && shift
    _log debug "Running command ${cmd}."
    "$cmd" "$@"
    ret=$?
    _log debug "Command ${cmd} exited with ${?}."

    # Output any summary messages.
    if [ "$OK_SH_RATE_LIMIT" -eq 1 ] ; then
        cat "$summary_fifo" 1>&2 &
        exec 6>&-
    fi

    exit $ret
}

_log() {
    # A lightweight logging system based on file descriptors
    #
    # Usage:
    #
    #     _log debug 'Starting the combobulator!'
    #
    # Positional arguments
    #
    local level="${1:?Level is required.}"
    #   The level for a given log message. (info or debug)
    local message="${2:?Message is required.}"
    #   The log message.

    shift 2

    local lname

    case "$level" in
        info) lname='INFO'; level=$LINFO ;;
        debug) lname='DEBUG'; level=$LDEBUG ;;
        *) printf 'Invalid logging level: %s\n' "$level" ;;
    esac

    printf '%s %s: %s\n' "$NAME" "$lname" "$message" 1>&$level
}

_helptext() {
    # Extract contiguous lines of comments and function params as help text
    #
    # Indentation will be ignored. She-bangs will be ignored. Local variable
    # declarations and their default values can also be pulled in as
    # documentation. Exits upon encountering the first blank line.
    #
    # Exported environment variables can be used for string interpolation in
    # the extracted commented text.
    #
    # Input
    #
    # * (stdin)
    #   The text of a function body to parse.

    awk '
    NR != 1 && /^\s*#/ {
        line=$0
        while(match(line, "[$]{[^}]*}")) {
            var=substr(line, RSTART+2, RLENGTH -3)
            gsub("[$]{"var"}", ENVIRON[var], line)
        }
        gsub(/^\s*#\s?/, "", line)
        print line
    }
    /^\s*local/ {
        sub(/^\s*local /, "")
        # sub(/"{0,1}\${/, "$", $0)
        sub(/\${/, "$", $0)
        sub(/:.*}/, "", $0)
        print "* `" $0 "`\n"
    }
    !NF { exit }'
}

# ### Request-response
# Functions for making HTTP requests and processing HTTP responses.

_awk_map() {
    # Invoke awk with a function that will empty the ENVIRON map
    #
    # Positional arguments
    #
    local prg="${1:?awk program string required}"
    # The body of an awk program to run

    shift 1

    local env_bin=$(command -v env)
    local env_blacklist=$(env -i "$env_bin" | while read -r env_var; do
        printf '%s\n' "${env_var%=*}"
    done)

    env -i "$@" "$awk_bin" \
        -v env_blacklist="${env_blacklist}" \
        '
        function clear_envrion() {
            for (name in ENVIRON) {
                if (substr(name, 0, 3) == "AWK") delete ENVIRON[name]
            }

            split(env_blacklist, bl, "\n")
            for (name in bl) { delete ENVIRON[bl[name]] }
        }
        '"$prg"
}

_format_json() {
    # Create formatted JSON from name=value pairs
    #
    # Usage:
    # ```
    # _format_json foo=Foo bar=123 baz=true qux=Qux=Qux quux='Multi-line
    # string' quuz=\'5.20170918\' corge=$(ok.sh _format_json grault=Grault)
    # ```
    #
    # Return:
    # ```
    # {
    #   "foo": "Foo",
    #   "corge": {
    #     "grault": "Grault"
    #   },
    #   "baz": true,
    #   "qux": "Qux=Qux",
    #   "quux": "Multi-line\nstring",
    #   "bar": 123,
    #   "quuz": "5.20170918"
    # }
    # ```
    #
    # Tries not to quote numbers, booleans, nulls, or nested structures.
    # If jq is installed it will also validate the output.
    #
    # Positional arguments
    #
    # * $1 - $9
    #
    #   Each positional arg must be in the format of `name=value` which will be
    #   added to a single, flat JSON object.

    _log debug "Formatting ${#} parameters as JSON."

    _awk_map '
    function isnum(x){ return (x == x + 0) }
    function isnull(x){ return (x == "null" ) }
    function isbool(x){ if (x == "true" || x == "false") return 1 }
    function isnested(x) { if (substr(x, 0, 1) == "[" \
        || substr(x, 0, 1) == "{") return 1 }

    BEGIN {

        clear_envrion()

        printf("{")

        for (name in ENVIRON) {
            val = ENVIRON[name]

            # If not castable, then quote it.
            if (!isbool(val) && !isnum(val) && !isnull(val) && !isnested(val)) {
                sub(/^('\''|")/, "", val) # Remove surrounding quotes
                sub(/('\''|")$/, "", val)

                gsub(/"/, "\\\"", val)  # Escape double-quotes.
                gsub(/\n/, "\\n", val)  # Replace newlines with \n text.
                val = "\"" val "\""
            }

            printf("%s\"%s\": %s", sep, name, val)
            sep = ", "
        }

        printf("}\n")
    }
    ' "$@" | _filter_json
}

_format_urlencode() {
    # URL encode and join name=value pairs
    #
    # Usage:
    # ```
    # _format_urlencode foo='Foo Foo' bar='<Bar>&/Bar/'
    # ```
    #
    # Return:
    # ```
    # foo=Foo%20Foo&bar=%3CBar%3E%26%2FBar%2F
    # ```
    #
    # Ignores pairs if the value begins with an underscore.

    _log debug "Formatting ${#} parameters as urlencoded"

    _awk_map '
    function escape(str, c, len, res) {
        len = length(str)
        res = ""
        for (i = 1; i <= len; i += 1) {
            c = substr(str, i, 1);
            if (c ~ /[0-9A-Za-z]/)
                res = res c
            else
                res = res "%" sprintf("%02X", ord[c])
        }
        return res
    }

    BEGIN {
        clear_envrion()

        for (i = 0; i <= 255; i += 1) ord[sprintf("%c", i)] = i;

        for (name in ENVIRON) {
            if (substr(name, 1, 1) == "_") continue
            val = ENVIRON[name]

            printf("%s%s=%s", sep, name, escape(val))
            sep = "&"
        }
    }
    ' "$@"
}

_filter_json() {
    # Filter JSON input using jq; outputs raw JSON if jq is not installed
    #
    # Usage:
    #
    #     printf '[{"foo": "One"}, {"foo": "Two"}]' | \
    #         ok.sh _filter_json '.[] | "\(.foo)"'
    #
    # * (stdin)
    #   JSON input.
    local _filter="$1"
    #   A string of jq filters to apply to the input stream.

    _log debug 'Filtering JSON.'

    if [ $NO_JQ -ne 0 ] ; then
        _log debug 'Bypassing jq processing.'
        cat
        return
    fi

    "${OK_SH_JQ_BIN}" -c -r "${_filter}"
    [ $? -eq 0 ] || printf 'jq parse error; invalid JSON.\n' 1>&2
}

_get_mime_type() {
    # Guess the mime type for a file based on the file extension
    #
    # Usage:
    #
    #     local mime_type
    #     _get_mime_type "foo.tar"; printf 'mime is: %s' "$mime_type"
    #
    # Sets the global variable `mime_type` with the result. (If this function
    # is called from within a function that has declared a local variable of
    # that name it will update the local copy and not set a global.)
    #
    # Positional arguments
    #
    local filename="${1:?Filename is required.}"
    #   The full name of the file, with extension.

    # Taken from Apache's mime.types file (public domain).
    case "$filename" in
        *.bz2) mime_type=application/x-bzip2 ;;
        *.exe) mime_type=application/x-msdownload ;;
        *.tar.gz | *.gz | *.tgz) mime_type=application/x-gzip ;;
        *.jpg | *.jpeg | *.jpe | *.jfif) mime_type=image/jpeg ;;
        *.json) mime_type=application/json ;;
        *.pdf) mime_type=application/pdf ;;
        *.png) mime_type=image/png ;;
        *.rpm) mime_type=application/x-rpm ;;
        *.svg | *.svgz) mime_type=image/svg+xml ;;
        *.tar) mime_type=application/x-tar ;;
        *.yaml) mime_type=application/x-yaml ;;
        *.zip) mime_type=application/zip ;;
    esac

    _log debug "Guessed mime type of '${mime_type}' for '${filename}'."
}

_get_confirm() {
    # Prompt the user for confirmation
    #
    # Usage:
    #
    #     local confirm; _get_confirm
    #     [ "$confirm" -eq 1 ] && printf 'Good to go!\n'
    #
    # If global confirmation is set via `$OK_SH_DESTRUCTIVE` then the user
    # is not prompted. Assigns the user's confirmation to the `confirm` global
    # variable. (If this function is called within a function that has a local
    # variable of that name, the local variable will be updated instead.)
    #
    # Positional arguments
    #
    local message="${1:-Are you sure?}"
    #   The message to prompt the user with.

    local answer

    if [ "$OK_SH_DESTRUCTIVE" -eq 1 ] ; then
        confirm=$OK_SH_DESTRUCTIVE
        return
    fi

    printf '%s ' "$message"
    read -r answer

    ! printf '%s\n' "$answer" | grep -Eq "$(locale yesexpr)"
    confirm=$?
}

_opts_filter() {
    # Extract common jq filter keyword options and assign to vars
    #
    # Usage:
    #
    #       local filter
    #       _opts_filter "$@"

    for arg in "$@"; do
        case $arg in
            (_filter=*) _filter="${arg#*=}";;
        esac
    done
}

_opts_pagination() {
    # Extract common pagination keyword options and assign to vars
    #
    # Usage:
    #
    #       local _follow_next
    #       _opts_pagination "$@"

    for arg in "$@"; do
        case $arg in
            (_follow_next=*) _follow_next="${arg#*=}";;
            (_follow_next_limit=*) _follow_next_limit="${arg#*=}";;
        esac
    done
}

_opts_qs() {
    # Extract common query string keyword options and assign to vars
    #
    # Usage:
    #
    #       local qs
    #       _opts_qs "$@"
    #       _get "/some/path${qs}"

    local querystring=$(_format_urlencode "$@")
    qs="${querystring:+?$querystring}"
}

_request() {
    # A wrapper around making HTTP requests with curl
    #
    # Usage:
    # ```
    # # Get JSON for all issues:
    # _request /repos/saltstack/salt/issues
    #
    # # Send a POST request; parse response using jq:
    # printf '{"title": "%s", "body": "%s"}\n' "Stuff" "Things" \
    #   | _request /some/path | jq -r '.[url]'
    #
    # # Send a PUT request; parse response using jq:
    # printf '{"title": "%s", "body": "%s"}\n' "Stuff" "Things" \
    #   | _request /repos/:owner/:repo/issues method=PUT | jq -r '.[url]'
    #
    # # Send a conditional-GET request:
    # _request /users etag=edd3a0d38d8c329d3ccc6575f17a76bb
    # ```
    #
    # Input
    #
    # * (stdin)
    #   Data that will be used as the request body.
    #
    # Positional arguments
    #
    local path="${1:?Path is required.}"
    #   The URL path for the HTTP request.
    #   Must be an absolute path that starts with a `/` or a full URL that
    #   starts with http(s). Absolute paths will be append to the value in
    #   `$OK_SH_URL`.
    #
    # Keyword arguments
    #
    local method='GET'
    #   The method to use for the HTTP request.
    local content_type='application/json'
    #   The value of the Content-Type header to use for the request.
    local etag
    #   An optional Etag to send as the If-None-Match header.

    shift 1

    local cmd
    local arg
    local has_stdin
    local trace_curl

    case $path in
        (http*) : ;;
        *) path="${OK_SH_URL}${path}" ;;
    esac

    for arg in "$@"; do
        case $arg in
            (method=*) method="${arg#*=}";;
            (content_type=*) content_type="${arg#*=}";;
            (etag=*) etag="${arg#*=}";;
        esac
    done

    case "$method" in
        POST | PUT | PATCH) has_stdin=1;;
    esac

    [ $OK_SH_VERBOSE -eq 3 ] && trace_curl=1

    [ "$OK_SH_VERBOSE" -eq 1 ] && set -x
    # shellcheck disable=SC2086
    curl -nsSig \
        -H "Accept: ${OK_SH_ACCEPT}" \
        -H "Content-Type: ${content_type}" \
        ${etag:+-H "If-None-Match: \"${etag}\""} \
        ${has_stdin:+--data-binary @-} \
        ${trace_curl:+--trace-ascii /dev/stderr} \
        -X "${method}" \
        "${path}"
    set +x
}

_response() {
    # Process an HTTP response from curl
    #
    # Output only headers of interest followed by the response body. Additional
    # processing is performed on select headers to make them easier to parse
    # using shell tools.
    #
    # Usage:
    # ```
    # # Send a request; output the response and only select response headers:
    # _request /some/path | _response status_code ETag Link_next
    #
    # # Make request using curl; output response with select response headers;
    # # assign response headers to local variables:
    # curl -isS example.com/some/path | _response status_code status_text | {
    #   local status_code status_text
    #   read -r status_code
    #   read -r status_text
    # }
    # ```
    #
    # Header reformatting
    #
    # * HTTP Status
    #
    #   The HTTP line is split into separate `http_version`, `status_code`, and
    #   `status_text` variables.
    #
    # * ETag
    #
    #   The surrounding quotes are removed.
    #
    # * Link
    #
    #   Each URL in the Link header is expanded with the URL type appended to
    #   the name. E.g., `Link_first`, `Link_last`, `Link_next`.
    #
    # Positional arguments
    #
    # * $1 - $9
    #
    #   Each positional arg is the name of an HTTP header. Each header value is
    #   output in the same order as each argument; each on a single line. A
    #   blank line is output for headers that cannot be found.

    local hdr
    local val
    local http_version
    local status_code
    local status_text
    local headers output

    _log debug 'Processing response.'

    read -r http_version status_code status_text
    status_text="${status_text%${cr}}"
    http_version="${http_version#HTTP/}"

    _log debug "Response status is: ${status_code} ${status_text}"

    headers="http_version: ${http_version}
status_code: ${status_code}
status_text: ${status_text}
"
    while IFS=": " read -r hdr val; do
        # Headers stop at the first blank line.
        [ "$hdr" = "$cr" ] && break
        val="${val%${cr}}"

        # Process each header; reformat some to work better with sh tools.
        case "$hdr" in
            # Update the GitHub rate limit trackers.
            X-RateLimit-Remaining)
                printf 'GitHub remaining requests: %s\n' "$val" 1>&$LSUMMARY ;;
            X-RateLimit-Reset)
                awk -v gh_reset="$val" 'BEGIN {
                    srand(); curtime = srand()
                    print "GitHub seconds to reset: " gh_reset - curtime
                }' 1>&$LSUMMARY ;;

            # Remove quotes from the etag header.
            ETag) val="${val#\"}"; val="${val%\"}" ;;

            # Split the URLs in the Link header into separate pseudo-headers.
            Link) headers="${headers}$(printf '%s' "$val" | awk '
                BEGIN { RS=", "; FS="; "; OFS=": " }
                {
                    sub(/^rel="/, "", $2); sub(/"$/, "", $2)
                    sub(/^[[:space:]]*</, "", $1); sub(/>$/, "", $1)
                    print "Link_" $2, $1
                }')
"  # need trailing newline
            ;;
        esac

        headers="${headers}${hdr}: ${val}
"  # need trailing newline

    done

    # Output requested headers in deterministic order.
    for arg in "$@"; do
        _log debug "Outputting requested header '${arg}'."
        output=$(printf '%s' "$headers" | while IFS=": " read -r hdr val; do
            [ "$hdr" = "$arg" ] && printf '%s' "$val"
        done)
        printf '%s\n' "$output"
    done

    # Output the response body.
    cat
}

_get() {
    # A wrapper around _request() for common GET patterns
    #
    # Will automatically follow 'next' pagination URLs in the Link header.
    #
    # Usage:
    #
    #     _get /some/path
    #     _get /some/path _follow_next=0
    #     _get /some/path _follow_next_limit=200 | jq -c .
    #
    # Positional arguments
    #
    local path="${1:?Path is required.}"
    #   The HTTP path or URL to pass to _request().
    #
    # Keyword arguments
    #
    # * _follow_next=1
    #
    #   Automatically look for a 'Links' header and follow any 'next' URLs.
    #
    # * _follow_next_limit=50
    #
    #   Maximum number of 'next' URLs to follow before stopping.

    shift 1
    local status_code
    local status_text
    local next_url

    # If the variable is unset or empty set it to a default value. Functions
    # that call this function can pass these parameters in one of two ways:
    # explicitly as a keyword arg or implicitly by setting variables of the same
    # names within the local scope.
    # shellcheck disable=SC2086
    if [ -z ${_follow_next+x} ] || [ -z "${_follow_next}" ]; then
        local _follow_next=1
    fi
    # shellcheck disable=SC2086
    if [ -z ${_follow_next_limit+x} ] || [ -z "${_follow_next_limit}" ]; then
        local _follow_next_limit=50
    fi

    _opts_pagination "$@"

    _request "$path" | _response status_code status_text Link_next | {
        read -r status_code
        read -r status_text
        read -r next_url

        case "$status_code" in
            20*) : ;;
            4*) printf 'Client Error: %s %s\n' \
                "$status_code" "$status_text" 1>&2; exit 1 ;;
            5*) printf 'Server Error: %s %s\n' \
                "$status_code" "$status_text" 1>&2; exit 1 ;;
        esac

        # Output response body.
        cat

        [ "$_follow_next" -eq 1 ] || return

        _log info "Remaining next link follows: ${_follow_next_limit}"
        if [ -n "$next_url" ] && [ $_follow_next_limit -gt 0 ] ; then
            _follow_next_limit=$(( _follow_next_limit - 1 ))

            _get "$next_url" "_follow_next_limit=${_follow_next_limit}"
        fi
    }
}

_post() {
    # A wrapper around _request() for commoon POST / PUT patterns
    #
    # Usage:
    #
    #     _format_json foo=Foo bar=Bar | _post /some/path
    #     _format_json foo=Foo bar=Bar | _post /some/path method='PUT'
    #     _post /some/path filename=somearchive.tar
    #     _post /some/path filename=somearchive.tar mime_type=application/x-tar
    #     _post /some/path filename=somearchive.tar \
    #       mime_type=$(file -b --mime-type somearchive.tar)
    #
    # Input
    #
    # * (stdin)
    #   Optional. See the `filename` argument also.
    #   Data that will be used as the request body.
    #
    # Positional arguments
    #
    local path="${1:?Path is required.}"
    #   The HTTP path or URL to pass to _request().
    #
    # Keyword arguments
    #
    local method='POST'
    #   The method to use for the HTTP request.
    local filename
    #   Optional. See the `stdin` option above also.
    #   Takes precedence over any data passed as stdin and loads a file off the
    #   file system to serve as the request body.
    local mime_type
    #   The value of the Content-Type header to use for the request.
    #   If the `filename` argument is given this value will be guessed from the
    #   file extension. If the `filename` argument is not given (i.e., using
    #   stdin) this value defaults to `application/json`. Specifying this
    #   argument overrides all other defaults or guesses.

    shift 1

    for arg in "$@"; do
        case $arg in
            (method=*) method="${arg#*=}";;
            (filename=*) filename="${arg#*=}";;
            (mime_type=*) mime_type="${arg#*=}";;
        esac
    done

    # Make either the file or stdin available as fd7.
    if [ -n "$filename" ] ; then
        if [ -r "$filename" ] ; then
            _log debug "Using '${filename}' as POST data."
            [ -n "$mime_type" ] || _get_mime_type "$filename"
            : ${mime_type:?The MIME type could not be guessed.}
            exec 7<"$filename"
        else
            printf 'File could not be found or read.\n' 1>&2
            exit 1
        fi
    else
        _log debug "Using stdin as POST data."
        mime_type='application/json'
        exec 7<&0
    fi

    _request "$path" method="$method" content_type="$mime_type" 0<&7 \
            | _response status_code status_text \
            | {
        read -r status_code
        read -r status_text

        case "$status_code" in
            20*) : ;;
            4*) printf 'Client Error: %s %s\n' \
                "$status_code" "$status_text" 1>&2; exit 1 ;;
            5*) printf 'Server Error: %s %s\n' \
                "$status_code" "$status_text" 1>&2; exit 1 ;;
        esac

        # Output response body.
        cat
    }
}

_delete() {
    # A wrapper around _request() for common DELETE patterns
    #
    # Usage:
    #
    #     _delete '/some/url'
    #
    # Return: 0 for success; 1 for failure.
    #
    # Positional arguments
    #
    local url="${1:?URL is required.}"
    #   The URL to send the DELETE request to.

    local status_code

    _request "${url}" method='DELETE' | _response status_code | {
        read -r status_code
        [ "$status_code" = "204" ]
        exit $?
    }
}

# ## GitHub
# Friendly functions for common GitHub tasks.

# ### Authorization
# Perform authentication and authorization.

show_scopes() {
    # Show the permission scopes for the currently authenticated user
    #
    # Usage:
    #
    #     show_scopes

    local oauth_scopes

    _request '/' | _response X-OAuth-Scopes | {
        read -r oauth_scopes

        printf '%s\n' "$oauth_scopes"

        # Dump any remaining response body.
        cat >/dev/null
    }
}

# ### Repository
# Create, update, delete, list repositories.

org_repos() {
    # List organization repositories
    #
    # Usage:
    #
    #     org_repos myorg
    #     org_repos myorg type=private per_page=10
    #     org_repos myorg _filter='.[] | "\(.name)\t\(.owner.login)"'
    #
    # Positional arguments
    #
    local org="${1:?Org name required.}"
    #   Organization GitHub login or id for which to list repos.
    #
    # Keyword arguments
    #
    local _follow_next
    #   Automatically look for a 'Links' header and follow any 'next' URLs.
    local _follow_next_limit
    #   Maximum number of 'next' URLs to follow before stopping.
    local _filter='.[] | "\(.name)\t\(.ssh_url)"'
    #   A jq filter to apply to the return data.
    #
    # Querystring arguments may also be passed as keyword arguments:
    #
    # * `per_page`
    # * `type`

    shift 1
    local qs

    _opts_pagination "$@"
    _opts_filter "$@"
    _opts_qs "$@"

    _get "/orgs/${org}/repos${qs}" | _filter_json "${_filter}"
}

org_teams() {
    # List teams
    #
    # Usage:
    #
    #     org_teams org
    #
    # Positional arguments
    #
    local org="${1:?Org name required.}"
    #   Organization GitHub login or id.
    #
    # Keyword arguments
    #
    local _filter='.[] | "\(.name)\t\(.id)\t\(.permission)"'
    #   A jq filter to apply to the return data.

    shift 1

    _opts_filter "$@"

    _get "/orgs/${org}/teams" \
        | _filter_json "${_filter}"
}

org_members() {
    # List organization members
    #
    # Usage:
    #
    #     org_members org
    #
    # Positional arguments
    #
    local org="${1:?Org name required.}"
    #   Organization GitHub login or id.
    #
    # Keyword arguments
    #
    local _filter='.[] | "\(.login)\t\(.id)"'
    #   A jq filter to apply to the return data.

    shift 1

    _opts_filter "$@"

    _get "/orgs/${org}/members" \
        | _filter_json "${_filter}"
}

list_repos() {
    # List user repositories
    #
    # Usage:
    #
    #     list_repos
    #     list_repos user
    #
    # Positional arguments
    #
    local user="$1"
    #   Optional GitHub user login or id for which to list repos.
    #
    # Keyword arguments
    #
    local _filter='.[] | "\(.name)\t\(.html_url)"'
    #   A jq filter to apply to the return data.
    #
    # Querystring arguments may also be passed as keyword arguments:
    #
    # * `direction`
    # * `per_page`
    # * `sort`
    # * `type`


    shift 1
    local qs

    _opts_filter "$@"
    _opts_qs "$@"

    if [ -n "$user" ] ; then
        url="/users/${user}/repos"
    else
        url='/user/repos'
    fi

    _get "${url}${qs}" | _filter_json "${_filter}"
}

list_branches() {
    # List branches of a specified repository.
    # ( https://developer.github.com/v3/repos/#list_branches )
    #
    # Usage:
    #
    #     list_branches user repo
    #
    # Positional arguments
    #
    #   GitHub user login or id for which to list branches
    #   Name of the repo for which to list branches
    #
    local user="${1:?User name required.}"
    local repo="${2:?Repo name required.}"
    shift 2
    #
    # Keyword arguments
    #
    local _filter='.[] | "\(.name)"'
    #   A jq filter to apply to the return data.
    #
    # Querystring arguments may also be passed as keyword arguments:
    #
    # * `direction`
    # * `per_page`
    # * `sort`
    # * `type`

    local qs

    _opts_filter "$@"
    _opts_qs "$@"

    url="/repos/${user}/${repo}/branches"

    _get "${url}${qs}" | _filter_json "${_filter}"
}

list_contributors() {
    # List contributors to the specified repository, sorted by the number of commits per contributor in descending order.
    # ( https://developer.github.com/v3/repos/#list-contributors )
    #
    # Usage:
    #
    #     list_contributors user repo
    #
    # Positional arguments
    #   
    local user="${1:?User name required.}"
    #   GitHub user login or id for which to list contributors
    local repo="${2:?Repo name required.}"
    #   Name of the repo for which to list contributors
    #
    # Keyword arguments
    #
    local _filter='.[] | "\(.login)\t\(.type)\tType:\(.type)\tContributions:\(.contributions)"'
    #   A jq filter to apply to the return data.
    #
    # Querystring arguments may also be passed as keyword arguments:
    #
    # * `direction`
    # * `per_page`
    # * `sort`
    # * `type`

    shift 2

    local qs

    _opts_filter "$@"
    _opts_qs "$@"

    url="/repos/${user}/${repo}/contributors"

    _get "${url}${qs}" | _filter_json "${_filter}"
}

list_collaborators() {
    # List collaborators to the specified repository, sorted by the number of commits per collaborator in descending order.
    # ( https://developer.github.com/v3/repos/#list-collaborators )
    #
    # Usage:
    #
    #     list_collaborators someuser/somerepo
    #
    # Positional arguments
    #   GitHub user login or id for which to list collaborators
    #   Name of the repo for which to list collaborators
    #
    local repo="${1:?Repo name required.}"
    #
    # Keyword arguments
    #
    local _filter='.[] | "\(.login)\t\(.type)\tType:\(.type)\tPermissions:\(.permissions)"'
    #   A jq filter to apply to the return data.
    #
    # Querystring arguments may also be passed as keyword arguments:
    #
    # * `direction`
    # * `per_page`
    # * `sort`
    # * `type`
 
    shift 1

    local qs

    _opts_filter "$@"
    _opts_qs "$@"

    url="/repos/${repo}/collaborators"

    _get "${url}${qs}" | _filter_json "${_filter}"
}

add_collaborator() {
    # Add a collaborator to a repository
    #
    # Usage:
    #       add_collaborator someuser/somerepo collaboratoruser permission
    #
    # Positional arguments
    #
    local repo="${1:?Repo name required.}"
    #   A GitHub repository.
    local collaborator="${2:?Collaborator name required.}"
    #   A new collaborator.
    local permission="${3:?Permission required. One of: push pull admin}"
    #   The permission level for this collaborator. One of `push`, `pull`,
    #   `admin`. The `pull` and `admin` permissions are valid for organization
    #   repos only.
    case $permission in
        push|pull|admin) :;;
        *) printf 'Permission invalid: %s\nMust be one of: push pull admin\n' \
                "$permission" 1>&2; exit 1 ;;
    esac
    #
    # Keyword arguments
    #
    local _filter='"\(.name)\t\(.color)"'
    #   A jq filter to apply to the return data.

    _opts_filter "$@"

    _format_json permission="$permission" \
        | _post "/repos/${repo}/collaborators/${collaborator}" method='PUT' \
        | _filter_json "$_filter"
}

delete_collaborator() {
    # Delete a collaborator to a repository
    #
    # Usage:
    #       delete_collaborator someuser/somerepo collaboratoruser permission
    #
    # Positional arguments
    #
    local repo="${1:?Repo name required.}"
    #   A GitHub repository.
    local collaborator="${2:?Collaborator name required.}"
    #   A new collaborator.

    shift 2

    local confirm

    _get_confirm 'This will permanently delete the collaborator from this repo. Continue?'
    [ "$confirm" -eq 1 ] || exit 0

    _delete "/repos/${repo}/collaborators/${collaborator}"
    exit $?
}

create_repo() {
    # Create a repository for a user or organization
    #
    # Usage:
    #
    #     create_repo foo
    #     create_repo bar description='Stuff and things' homepage='example.com'
    #     create_repo baz organization=myorg
    #
    # Positional arguments
    #
    local name="${1:?Repo name required.}"
    #   Name of the new repo
    #
    # Keyword arguments
    #
    local _filter='"\(.name)\t\(.html_url)"'
    #   A jq filter to apply to the return data.
    #
    # POST data may also be passed as keyword arguments:
    #
    # * `auto_init`,
    # * `description`
    # * `gitignore_template`
    # * `has_downloads`
    # * `has_issues`
    # * `has_wiki`,
    # * `homepage`
    # * `organization`
    # * `private`
    # * `team_id`

    shift 1

    _opts_filter "$@"

    local url
    local organization

    for arg in "$@"; do
        case $arg in
            (organization=*) organization="${arg#*=}";;
        esac
    done

    if [ -n "$organization" ] ; then
        url="/orgs/${organization}/repos"
    else
        url='/user/repos'
    fi

    _format_json "name=${name}" "$@" | _post "$url" | _filter_json "${_filter}"
}

delete_repo() {
    # Create a repository for a user or organization
    #
    # Usage:
    #
    #     delete_repo owner repo
    #
    # The currently authenticated user must have the `delete_repo` scope. View
    # current scopes with the `show_scopes()` function.
    #
    # Positional arguments
    #
    local owner="${1:?Owner name required.}"
    #   Name of the new repo
    local repo="${2:?Repo name required.}"
    #   Name of the new repo

    shift 2

    local confirm

    _get_confirm 'This will permanently delete a repository! Continue?'
    [ "$confirm" -eq 1 ] || exit 0

    _delete "/repos/${owner}/${repo}"
    exit $?
}

fork_repo() {
    # Fork a repository from a user or organization to own account
    #
    # Usage:
    #
    #     fork_repo owner repo
    #
    # Positional arguments
    #
    local owner="${1:?Owner name required.}"
    #   Name of existing user or organization
    local repo="${2:?Repo name required.}"
    #   Name of the existing repo
    #
    # Keyword arguments
    #
    local _filter='"\(.clone_url)\t\(.ssh_url)"'
    #   A jq filter to apply to the return data.

    shift 2

    _opts_filter "$@"

    _format_json foo=Foo | _post "/repos/${owner}/${repo}/forks" \
        | _filter_json "${_filter}"
    exit $?  # might take a bit time...
}

# ### Releases
# Create, update, delete, list releases.

list_releases() {
    # List releases for a repository
    #
    # Usage:
    #
    #     list_releases org repo '\(.assets[0].name)\t\(.name.id)'
    #
    # Positional arguments
    #
    local owner="${1:?Owner name required.}"
    #   A GitHub user or organization.
    local repo="${2:?Repo name required.}"
    #   A GitHub repository.
    #
    # Keyword arguments
    #
    local _filter='.[] | "\(.name)\t\(.id)\t\(.html_url)"'
    #   A jq filter to apply to the return data.

    shift 2

    _opts_filter "$@"

    _get "/repos/${owner}/${repo}/releases" \
        | _filter_json "${_filter}"
}

release() {
    # Get a release
    #
    # Usage:
    #
    #     release user repo 1087855
    #
    # Positional arguments
    #
    local owner="${1:?Owner name required.}"
    #   A GitHub user or organization.
    local repo="${2:?Repo name required.}"
    #   A GitHub repository.
    local release_id="${3:?Release ID required.}"
    #   The unique ID of the release; see list_releases.
    #
    # Keyword arguments
    #
    local _filter='"\(.author.login)\t\(.published_at)"'
    #   A jq filter to apply to the return data.

    shift 3

    _opts_filter "$@"

    _get "/repos/${owner}/${repo}/releases/${release_id}" \
        | _filter_json "${_filter}"
}

create_release() {
    # Create a release
    #
    # Usage:
    #
    #     create_release org repo v1.2.3
    #     create_release user repo v3.2.1 draft=true
    #
    # Positional arguments
    #
    local owner="${1:?Owner name required.}"
    #   A GitHub user or organization.
    local repo="${2:?Repo name required.}"
    #   A GitHub repository.
    local tag_name="${3:?Tag name required.}"
    #   Git tag from which to create release.
    #
    # Keyword arguments
    #
    local _filter='"\(.name)\t\(.id)\t\(.html_url)"'
    #   A jq filter to apply to the return data.
    #
    # POST data may also be passed as keyword arguments:
    #
    # * `body`
    # * `draft`
    # * `name`
    # * `prerelease`
    # * `target_commitish`

    shift 3

    _opts_filter "$@"

    _format_json "tag_name=${tag_name}" "$@" \
        | _post "/repos/${owner}/${repo}/releases" \
        | _filter_json "${_filter}"
}

delete_release() {
    # Delete a release
    #
    # Usage:
    #
    #     delete_release org repo 1087855
    #
    # Return: 0 for success; 1 for failure.
    #
    # Positional arguments
    #
    local owner="${1:?Owner name required.}"
    #   A GitHub user or organization.
    local repo="${2:?Repo name required.}"
    #   A GitHub repository.
    local release_id="${3:?Release ID required.}"
    #   The unique ID of the release; see list_releases.

    shift 3

    local confirm

    _get_confirm 'This will permanently delete a release. Continue?'
    [ "$confirm" -eq 1 ] || exit 0

    _delete "/repos/${owner}/${repo}/releases/${release_id}"
    exit $?
}

release_assets() {
    # List release assets
    #
    # Usage:
    #
    #     release_assets user repo 1087855
    #
    # Positional arguments
    #
    local owner="${1:?Owner name required.}"
    #   A GitHub user or organization.
    local repo="${2:?Repo name required.}"
    #   A GitHub repository.
    local release_id="${3:?Release ID required.}"
    #   The unique ID of the release; see list_releases.
    #
    # Keyword arguments
    #
    local _filter='.[] | "\(.id)\t\(.name)\t\(.updated_at)"'
    #   A jq filter to apply to the return data.

    shift 3

    _opts_filter "$@"

    _get "/repos/${owner}/${repo}/releases/${release_id}/assets" \
        | _filter_json "$_filter"
}

upload_asset() {
    # Upload a release asset
    #
    # Note, this command requires `jq` to find the release `upload_url`.
    #
    # Usage:
    #
    #     upload_asset username reponame 1087938 \
    #         foo.tar application/x-tar < foo.tar
    #
    # * (stdin)
    #   The contents of the file to upload.
    #
    # Positional arguments
    #
    local owner="${1:?Owner name required.}"
    #   A GitHub user or organization.
    local repo="${2:?Repo name required.}"
    #   A GitHub repository.
    local release_id="${3:?Release ID required.}"
    #   The unique ID of the release; see list_releases.
    local name="${4:?File name is required.}"
    #   The file name of the asset.
    #
    # Keyword arguments
    #
    local _filter='"\(.state)\t\(.browser_download_url)"'
    #   A jq filter to apply to the return data.

    shift 4

    if [ $NO_JQ -ne 0 ] ; then
        printf 'upload_asset requires jq\n' 1>&2
        exit 1
    fi

    _opts_filter "$@"

    local upload_url=$(release "$owner" "$repo" "$release_id" _filter="(.upload_url)" \
        | sed -e 's/{?name,label}/?name='"$name"'/g')

    : "${upload_url:?Upload URL could not be retrieved.}"

    _post "$upload_url" filename="$name" \
        | _filter_json "$_filter"
}

# ### Issues
# Create, update, edit, delete, list issues and milestones.

list_milestones() {
    # List milestones for a repository
    #
    # Usage:
    #
    #       list_milestones someuser/somerepo
    #       list_milestones someuser/somerepo state=closed
    #
    # Positional arguments
    #
    local repository="${1:?Repo name required.}"
    #   A GitHub repository.
    #
    # Keyword arguments
    #
    local _follow_next
    #   Automatically look for a 'Links' header and follow any 'next' URLs.
    local _follow_next_limit
    #   Maximum number of 'next' URLs to follow before stopping.
    local _filter='.[] | "\(.number)\t\(.open_issues)/\(.closed_issues)\t\(.title)"'
    #   A jq filter to apply to the return data.
    #
    # GitHub querystring arguments may also be passed as keyword arguments:
    #
    # * `direction`
    # * `per_page`
    # * `sort`
    # * `state`

    shift 1
    local qs

    _opts_pagination "$@"
    _opts_filter "$@"
    _opts_qs "$@"

    _get "/repos/${repository}/milestones${qs}" | _filter_json "$_filter"
}

create_milestone() {
    # Create a milestone for a repository
    #
    # Usage:
    #
    #       create_milestone someuser/somerepo MyMilestone
    #
    #       create_milestone someuser/somerepo MyMilestone \
    #           due_on=2015-06-16T16:54:00Z \
    #           description='Long description here
    #       that spans multiple lines.'
    #
    # Positional arguments
    #
    local repo="${1:?Repo name required.}"
    #   A GitHub repository.
    local title="${2:?Milestone name required.}"
    #   A unique title.
    #
    # Keyword arguments
    #
    local _filter='"\(.number)\t\(.html_url)"'
    #   A jq filter to apply to the return data.
    #
    # Milestone options may also be passed as keyword arguments:
    #
    # * `description`
    # * `due_on`
    # * `state`

    shift 2

    _opts_filter "$@"

    _format_json title="$title" "$@" \
        | _post "/repos/${repo}/milestones" \
        | _filter_json "$_filter"
}

add_comment() {
    # Add a comment to an issue
    #
    # Usage:
    #   add_comment someuser/somerepo 123 'This is a comment'
    #
    # Positional arguments
    #
    local repository="${1:?Repo name required}"
    #   A GitHub repository
    local number="${2:?Issue number required}"
    #   Issue Number
    local comment="${3:?Comment required}"
    #   Comment to be added
    #
    # Keyword arguments
    #
    local _filter='"\(.id)\t\(.html_url)"'
    #   A jq filter to apply to the return data.

    shift 3
    _opts_filter "$@"

    _format_json body="$comment" \
        | _post "/repos/${repository}/issues/${number}/comments" \
        | _filter_json "${_filter}"
}

close_issue() {
    # Close an issue
    #
    # Usage:
    #   close_issue someuser/somerepo 123
    #
    # Positional arguments
    #
    local repository="${1:?Repo name required}"
    #   A GitHub repository
    local number="${2:?Issue number required}"
    #   Issue Number
    #
    # Keyword arguments
    #
    local _filter='"\(.id)\t\(.state)\t\(.html_url)"'
    #   A jq filter to apply to the return data.
    #
    # POST data may also be passed as keyword arguments:
    #
    # * `assignee`
    # * `labels`
    # * `milestone`

    shift 2
    _opts_filter "$@"

    _format_json state="closed" "$@" \
        | _post "/repos/${repository}/issues/${number}" method='PATCH' \
        | _filter_json "${_filter}"
}

list_issues() {
    # List issues for the authenticated user or repository
    #
    # Usage:
    #
    #       list_issues
    #       list_issues someuser/somerepo
    #       list_issues <any of the above> state=closed labels=foo,bar
    #
    # Positional arguments
    #
    # user or user/repository
    #
    # Keyword arguments
    #
    local _follow_next
    #   Automatically look for a 'Links' header and follow any 'next' URLs.
    local _follow_next_limit
    #   Maximum number of 'next' URLs to follow before stopping.
    local _filter='.[] | "\(.number)\t\(.title)"'
    #   A jq filter to apply to the return data.
    #
    # GitHub querystring arguments may also be passed as keyword arguments:
    #
    # * `assignee`
    # * `creator`
    # * `direction`
    # * `labels`
    # * `mentioned`
    # * `milestone`
    # * `per_page`
    # * `since`
    # * `sort`
    # * `state`

    local url
    local qs

    case $1 in
        ('') url='/user/issues' ;;
        (*=*) url='/user/issues' ;;
        (*/*) url="/repos/${1}/issues"; shift 1 ;;
    esac

    _opts_pagination "$@"
    _opts_filter "$@"
    _opts_qs "$@"

    _get "${url}${qs}" | _filter_json "$_filter"
}

user_issues() {
    # List all issues across owned and member repositories for the authenticated user
    #
    # Usage:
    #
    #       user_issues
    #       user_issues since=2015-60-11T00:09:00Z
    #
    # Keyword arguments
    #
    local _follow_next
    #   Automatically look for a 'Links' header and follow any 'next' URLs.
    local _follow_next_limit
    #   Maximum number of 'next' URLs to follow before stopping.
    local _filter='.[] | "\(.repository.full_name)\t\(.number)\t\(.title)"'
    #   A jq filter to apply to the return data.
    #
    # GitHub querystring arguments may also be passed as keyword arguments:
    #
    # * `direction`
    # * `filter`
    # * `labels`
    # * `per_page`
    # * `since`
    # * `sort`
    # * `state`

    local qs

    _opts_pagination "$@"
    _opts_filter "$@"
    _opts_qs "$@"

    _get "/issues${qs}" | _filter_json "$_filter"
}

org_issues() {
    # List all issues for a given organization for the authenticated user
    #
    # Usage:
    #
    #       org_issues someorg
    #
    # Positional arguments
    #
    local org="${1:?Organization name required.}"
    #   Organization GitHub login or id.
    #
    # Keyword arguments
    #
    local _follow_next
    #   Automatically look for a 'Links' header and follow any 'next' URLs.
    local _follow_next_limit
    #   Maximum number of 'next' URLs to follow before stopping.
    local _filter='.[] | "\(.number)\t\(.title)"'
    #   A jq filter to apply to the return data.
    #
    # GitHub querystring arguments may also be passed as keyword arguments:
    #
    # * `direction`
    # * `filter`
    # * `labels`
    # * `per_page`
    # * `since`
    # * `sort`
    # * `state`

    shift 1
    local qs

    _opts_pagination "$@"
    _opts_filter "$@"
    _opts_qs "$@"

    _get "/orgs/${org}/issues${qs}" | _filter_json "$_filter"
}

labels() {
    # List available labels for a repository
    #
    # Usage:
    #
    #       labels someuser/somerepo
    #
    # Positional arguments
    #
    local repo="$1"
    #   A GitHub repository.
    #
    # Keyword arguments
    #
    local _follow_next
    #   Automatically look for a 'Links' header and follow any 'next' URLs.
    local _follow_next_limit
    #   Maximum number of 'next' URLs to follow before stopping.
    local _filter='.[] | "\(.name)\t\(.color)"'
    #   A jq filter to apply to the return data.

    _opts_pagination "$@"
    _opts_filter "$@"

    _get "/repos/${repo}/labels" | _filter_json "$_filter"
}

add_label() {
    # Add a label to a repository
    #
    # Usage:
    #       add_label someuser/somerepo LabelName color
    #
    # Positional arguments
    #
    local repo="${1:?Repo name required.}"
    #   A GitHub repository.
    local label="${2:?Label name required.}"
    #   A new label.
    local color="${3:?Hex color required.}"
    #   A color, in hex, without the leading `#`.
    #
    # Keyword arguments
    #
    local _filter='"\(.name)\t\(.color)"'
    #   A jq filter to apply to the return data.

    _opts_filter "$@"

    _format_json name="$label" color="$color" \
        | _post "/repos/${repo}/labels" \
        | _filter_json "$_filter"
}

update_label() {
    # Update a label
    #
    # Usage:
    #       update_label someuser/somerepo OldLabelName \
    #           label=NewLabel color=newcolor
    #
    # Positional arguments
    #
    local repo="${1:?Repo name required.}"
    #   A GitHub repository.
    local label="${2:?Label name required.}"
    #   The name of the label which will be updated
    #
    # Keyword arguments
    #
    local _filter='"\(.name)\t\(.color)"'
    #   A jq filter to apply to the return data.
    #
    # Label options may also be passed as keyword arguments, these will update
    # the existing values:
    #
    # * `color`
    # * `name`

    shift 2

    _opts_filter "$@"

    _format_json "$@" \
        | _post "/repos/${repo}/labels/${label}" method='PATCH' \
        | _filter_json "$_filter"
}

add_team_repo() {
    # Add a team repository
    #
    # Usage:
    #
    #     add_team_repo team_id organization repository_name permission
    #
    # Positional arguments
    #
    local team_id="${1:?Team id required.}"
    #   Team id to add repository to
    local organization="${2:?Organization required.}"
    #   Organization to add repository to
    local repository_name="${3:?Repository name required.}"
    #   Repository name to add
    local permission="${4:?Permission required.}"
    #   Permission to grant: pull, push, admin
    #
    local url="/teams/${team_id}/repos/${organization}/${repository_name}"

    export OK_SH_ACCEPT="application/vnd.github.ironman-preview+json"

    _format_json "name=${name}" "permission=${permission}" | _post "$url" method='PUT' | _filter_json "${_filter}"
}

list_pull() {
    # Lists the pull requests for a repository
    #
    # Usage:
    #
    #       list_pulls user repo
    #
    # Positional arguments
    #
    local owner="${1:?Owner required.}"
    #   A GitHub owner.
    local repo="${2:?Repo name required.}"
    #   A GitHub repository.
    local pull="${3:?Pull request number required.}"
    #   A GitHub pull request number.
    #
    # Keyword arguments
    #
    local _follow_next
    #   Automatically look for a 'Links' header and follow any 'next' URLs.
    local _follow_next_limit
    #   Maximum number of 'next' URLs to follow before stopping.
    local _filter='. | "\(.number)\t\(.user.login)\t\(.head.repo.clone_url)\t\(.head.ref)"'
    #   A jq filter to apply to the return data.

    _opts_pagination "$@"
    _opts_filter "$@"

    _get "/repos/${owner}/${repo}/pulls/${pull}" | _filter_json "$_filter"
}

list_pulls() {
    # Lists the pull requests for a repository
    #
    # Usage:
    #
    #       list_pulls user repo
    #
    # Positional arguments
    #
    local owner="${1:?Owner required.}"
    #   A GitHub owner.
    local repo="${2:?Repo name required.}"
    #   A GitHub repository.
    #
    # Keyword arguments
    #
    local _follow_next
    #   Automatically look for a 'Links' header and follow any 'next' URLs.
    local _follow_next_limit
    #   Maximum number of 'next' URLs to follow before stopping.
    local _filter='.[] | "\(.number)\t\(.user.login)\t\(.head.repo.clone_url)\t\(.head.ref)"'
    #   A jq filter to apply to the return data.

    _opts_pagination "$@"
    _opts_filter "$@"

    _get "/repos/${owner}/${repo}/pulls" | _filter_json "$_filter"
}

create_pull_request() {
    # Create a pull request for a repository
    #
    # Usage:
    #
    #       create_pull_request someuser/somerepo title head base
    #
    #       create_pull_request someuser/somerepo title head base body='Description here.'
    #
    # Positional arguments
    #
    local repo="${1:?Repo name required.}"
    #   A GitHub repository.
    local title="${2:?Pull request title required.}"
    #   A title.
    local head="${3:?Pull request head required.}"
    #   A head.
    local base="${4:?Pull request base required.}"
    #   A base.
    #
    # Keyword arguments
    #
    local _filter='"\(.number)\t\(.html_url)"'
    #   A jq filter to apply to the return data.
    #
    # Pull request options may also be passed as keyword arguments:
    #
    # * `body`
    # * `maintainer_can_modify`

    shift 4

    _opts_filter "$@"

    _format_json title="$title" head="$head" base="$base" "$@" \
        | _post "/repos/${repo}/pulls" \
        | _filter_json "$_filter"
}

update_pull_request() {
    # Update a pull request for a repository
    #
    # Usage:
    #
    #       update_pull_request someuser/somerepo number title='New title' body='New body'
    #
    # Positional arguments
    #
    local repo="${1:?Repo name required.}"
    #   A GitHub repository.
    local number="${2:?Pull request number required.}"
    #   A pull request number.
    #
    # Keyword arguments
    #
    local _filter='"\(.number)\t\(.html_url)"'
    #   A jq filter to apply to the return data.
    #
    # Pull request options may also be passed as keyword arguments:
    #
    # * `base`
    # * `body`
    # * `maintainer_can_modify`
    # * `state` (either open or closed)
    # * `title`

    shift 2

    _opts_filter "$@"

    _format_json "$@" \
        | _post "/repos/${repo}/pulls/${number}" method='PATCH' \
        | _filter_json "$_filter"
}

__main "$@"
