#!/usr/bin/env bash
set -e

usage() {
    cat <<EOF
usage: $0 [--merge] PULL_REQUEST_ID [REMOTE]

Force-push our HEAD to the given GitHub pull request branch.

Useful for a maintainer to run just before pushing to main,
after tweaking the branch and/or rebasing to latest.  This causes
GitHub to see the subsequent push to main as representing a
merge of the PR, rather than requiring the PR to be manually
(and to the casual observer misleadingly) closed instead.

With --merge, also go ahead and merge the PR.

REMOTE defaults to the value of the Git config variable
\`zulip.zulipRemote\` if set, else to \`upstream\`.

If we have a pseudo-remote-tracking branch for the PR (as created
by \`reset-to-pull-request\`, like \`pr/1234\`), then the tracking
branch is updated to reflect the pushed version.

See also \`reset-to-pull-request\`.
EOF
}

merge=
args=()

while ((OPTIND <= $#)); do
    if getopts ":-:" opt; then
        case $opt in
            -)
                case "$OPTARG" in
                    help)
                        usage
                        exit 0
                        ;;
                    merge)
                        merge=t
                        ;;
                    *)
                        echo "Invalid option: --$OPTARG" >&2
                        exit 1
                        ;;
                esac
                ;;
            \?)
                echo "Invalid option: -$OPTARG" >&2
                exit 1
                ;;
        esac
    else
        args+=("${!OPTIND}")
        ((OPTIND++))
    fi
done

set -- "${args[@]}"

remote_default="$(git config zulip.zulipRemote || echo upstream)"
pseudo_remote="$(git config zulip.prPseudoRemote || echo)"

pr_id="$1"
remote="${2:-"$remote_default"}"

if [ -z "$pr_id" ]; then
    usage >&2
    exit 1
fi

if ! jq --version >/dev/null 2>&1; then
    cat >&2 <<EOF
error: not found: jq

push-to-pull-request requires the \`jq\` utility; you should install it.
Try:

  sudo apt install jq
EOF
    exit 1
fi

remote_url="$(git remote get-url --push "$remote")"
repo_fq="$(echo "$remote_url" | perl -lne 'print $1 if (
    m, ^ git\@github\.com:
         ([^/]+ / [^/]+?)
         (?:\.git)?
       $ ,x )')"

if [ -z "$repo_fq" ]; then
    # We're pretty specific about what we expect the URL to look like;
    # there are probably more cases we could legitimately cover, which
    # we can add if/when they come up for someone.
    echo "error: couldn't parse remote URL as GitHub repo: $remote_url" >&2
    exit 1
fi

# See https://developer.github.com/v3/pulls/#get-a-single-pull-request .
# This is the old REST API; the new GraphQL API does look neat, but it
# seems to require authentication even for simple lookups of public data,
# and that'd be a pain for a simple script like this.
pr_url=https://api.github.com/repos/"${repo_fq}"/pulls/"${pr_id}"
pr_details="$(curl -fLsS --retry 3 "$pr_url")"

pr_jq() {
    echo "$pr_details" | jq "$@"
}

if [ "$(pr_jq -r .message)" = "Not Found" ]; then
    echo "error: invalid PR URL: $pr_url" >&2
    exit 1
fi

if [ "$(pr_jq .maintainer_can_modify)" != "true" ]; then
    # This happens when the PR has already been merged or closed, or
    # if the contributor has turned off the (default) setting to allow
    # maintainers of the target repo to push to their PR branch.
    #
    # The latter seems to be rare (in Greg's experience doing the
    # manual equivalent of this script for many different
    # contributors, none have ever chosen this setting), but give a
    # decent error message if it does happen.
    echo "error: PR already closed, or contributor has disallowed pushing to branch" >&2
    exit 1
fi

if [ "$merge" ]; then
    pr_base_ref="$(pr_jq -r .base.ref)"
    git fetch -- "$remote"
    if ! git merge-base --is-ancestor -- "$remote/$pr_base_ref" @; then
        echo "error: You need to rebase on $remote/$pr_base_ref first" >&2
        exit 1
    fi
fi

pr_head_repo_fq="$(pr_jq -r .head.repo.full_name)"
pr_head_refname="$(pr_jq -r .head.ref)"

tracking_ref=
if [ -n "$pseudo_remote" ]; then
    tracking_ref=$(git rev-parse -q --verify --symbolic refs/remotes/"$pseudo_remote"/"$pr_id" || echo)
fi

set -x
git push git@github.com:"$pr_head_repo_fq" +@:"$pr_head_refname"

{ set +x; } 2>&-
if [ -n "$tracking_ref" ]; then
    set -x
    git update-ref "$tracking_ref" @
fi

if [ "$merge" ]; then
    tries=10
    sha="$(git rev-parse @)"
    while
        out=$(git ls-remote -- "$remote" "refs/pull/$pr_id/head")
        [ "$out" != "$sha	refs/pull/$pr_id/head" ]
    do
        if ! ((--tries)); then
            echo 'error: Push was not observed in PR' >&2
            exit 1
        fi
        sleep 1
    done
    git push -- "$remote" "@:$pr_base_ref"
fi
