#!/bin/bash # SPDX-License-Identifier: MIT set -e set -o posix SELF=${BASH_SOURCE[0]} SELF_DIR="$( cd "$( dirname "$SELF" )" && pwd )" source $SELF_DIR/cascading-pr-lib.sh trap "rm -fr $TMPDIR" EXIT function repo_login() { local direction="$1" local repo=${options[${direction}_repo]} ( export DOT=$TMPDIR/$repo forgejo-curl.sh logout forgejo-curl.sh --token "${options[${direction}_token]}" login "${options[${direction}_url]}" ) } function repo_curl() { local repo=$1 shift DOT=$TMPDIR/$repo forgejo-curl.sh "$@" } function default_branch() { local direction=$1 repo_curl ${options[${direction}_repo]} api_json ${options[${direction}_api]} > $TMPDIR/$direction.json jq --raw-output .default_branch < $TMPDIR/$direction.json } function exists_branch() { local direction=$1 repo_curl ${options[${direction}_repo]} api_json ${options[${direction}_api]}/branches/${options[${direction}_head]} >& /dev/null } function delete_branch() { local direction=$1 if ! $(exists_branch $direction) ; then log_info "branch ${options[${direction}_head]} does not exists" return fi repo_curl ${options[${direction}_repo]} api_json -X DELETE ${options[${direction}_api]}/branches/${options[${direction}_head]} log_info "branch ${options[${direction}_head]} deleted" } function pr_origin_comment_body() { echo "cascading-pr updated at ${options[destination_url]}/${options[destination_repo]}/pulls/$(pr_number destination)" } function comment_origin_pr() { cat > $TMPDIR/data < $TMPDIR/data < $TMPDIR/data < $TMPDIR/destination-pr.json log_info "PR created $(pr_url destination)" } function close_pr() { local direction=destination if test "$(pr_state ${direction})" = "open"; then log_info "closing $(pr_url ${direction})" local number=$(pr_number $direction) repo_curl ${options[${direction}_repo]} api_json -X PATCH --data '{"state":"closed"}' ${options[${direction}_api]}/issues/$number delete_branch ${direction} else log_info "no open PR found" fi } function pr_get_origin() { repo_curl ${options[origin_repo]} api_json ${options[origin_api]}/pulls/${options[origin_pr]} > $TMPDIR/origin-pr.json } function pr_get_destination() { local title=$(pr_destination_title) repo_curl ${options[destination_repo]} api --get --data state=open --data type=pulls --data-urlencode q="$title" ${options[destination_api]}/issues | jq --raw-output .[0] > $TMPDIR/destination-pr.json } function pr_get() { local direction=$1 if ! test -f $TMPDIR/${direction}-pr.json; then pr_get_$direction fi } function pr() { cat $TMPDIR/$1-pr.json } function pr_state() { pr_get $1 pr $1 | jq --raw-output .state } function pr_url() { pr_get $1 pr $1 | jq --raw-output .url } function pr_number() { pr_get $1 pr $1 | jq --raw-output .number } function pr_merged() { pr_get $1 pr $1 | jq --raw-output .merged } function pr_from_fork() { pr_get $1 pr $1 | jq --raw-output .head.repo.fork } function upsert_clone() { local direction=$1 ref="$2" clone=$3 if ! test -d $TMPDIR/$direction; then git -c credential.helper="store --file=$TMPDIR/$direction.git-credentials" clone $clone $TMPDIR/$direction fi ( cd $TMPDIR/$direction if [[ "$ref" =~ ^refs/ ]] ; then git fetch origin +$ref:$ref else ref=origin/$ref fi git checkout -b $direction $ref git config credential.helper "store --file=$TMPDIR/$direction.git-credentials" git config user.email cascading-pr@example.com git config user.name cascading-pr ) } function sha_pushed() { local direction=$1 if test -f $TMPDIR/$direction.sha ; then cat $TMPDIR/$direction.sha fi } function push() { local direction=$1 branch=$2 clone=$3 ( cd $TMPDIR/$direction git add . if git commit -m 'cascading-pr update'; then git push --force origin $direction:$branch git rev-parse HEAD > ../$direction.sha log_info "pushed" else log_info "nothing to push" fi ) } function wait_destination_ci() { local sha="$1" local repo_api=${options[destination_url]}/api/v1/repos/${options[destination_repo]} wait_success $repo_api $sha } function update() { upsert_clone origin "${options[origin_head]}" ${options[origin_clone]} upsert_clone destination "${options[destination_head]}" ${options[destination_clone]} ( local update=${options[update]} if ! [[ "$update" =~ ^/ ]] ; then local d if $(pr_from_fork origin); then local default_branch=$(default_branch origin) log_info "PR is from a forked repository, using the default branch $default_branch to obtain the update script" d=$TMPDIR/update git -C $TMPDIR/origin worktree add $d $default_branch else d=$TMPDIR/origin fi update=$d/$update fi cd $TMPDIR $update $TMPDIR/destination $TMPDIR/destination-pr.json $TMPDIR/origin $TMPDIR/origin-pr.json ) push destination ${options[destination_head]} ${options[destination_clone]} } function set_clone() { local direction=$1 local token=${options[${direction}_token]} if [[ "$token" =~ ^@ ]] ; then local file=${token##@} ( echo -n ${options[${direction}_scheme]}://any: cat $file echo @${options[${direction}_host_port]}/${options[${direction}_repo]} ) > $TMPDIR/$direction.git-credentials else echo ${options[${direction}_scheme]}://any:${options[${direction}_token]}@${options[${direction}_host_port]}/${options[${direction}_repo]} > $TMPDIR/$direction.git-credentials fi options[${direction}_clone]=${options[${direction}_scheme]}://${options[${direction}_host_port]}/${options[${direction}_repo]} } function finalize_options() { options[origin_api]=${options[origin_url]}/api/v1/repos/${options[origin_repo]} options[origin_scheme]=$(scheme ${options[origin_url]}) options[origin_host_port]=$(host_port ${options[origin_url]}) set_clone origin options[origin_head]=refs/pull/${options[origin_pr]}/head options[destination_api]=${options[destination_url]}/api/v1/repos/${options[destination_repo]} options[destination_scheme]=$(scheme ${options[destination_url]}) options[destination_host_port]=$(host_port ${options[destination_url]}) set_clone destination options[destination_base]=${options[destination_branch]} : ${options[prefix]:=${options[origin_repo]}} options[destination_head]=${options[prefix]}-${options[origin_pr]} : ${options[close_merge]:=false} } function run() { local state=$(pr_state origin) repo_login origin repo_login destination case "$state" in open) log_info "PR is open, update or create the cascade branch and PR" upsert_destination_branch update local sha=$(sha_pushed destination) if test "$sha" ; then upsert_destination_pr comment_origin_pr wait_destination_ci "$sha" fi ;; closed) if "$(pr_merged origin)"; then if "${options[close_merge]}" ; then log_info "PR is merged, close the cascade PR and remove the branch" close_pr else log_info "PR was merged, update the cascade PR" pr_get origin pr_get destination update fi else log_info "PR is closed, close the cascade PR and remove the branch" close_pr fi ;; *) log_info "state '$state', do nothing" ;; esac } function main() { while true; do case "$1" in --verbose) shift verbose ;; --debug) shift debug ;; --origin-url) shift options[origin_url]=$1 shift ;; --origin-repo) shift options[origin_repo]=$1 shift ;; --origin-token) shift options[origin_token]=$1 shift ;; --origin-pr) shift options[origin_pr]=$1 shift ;; --destination-url) shift options[destination_url]=$1 shift ;; --destination-repo) shift options[destination_repo]=$1 shift ;; --destination-token) shift options[destination_token]=$1 shift ;; --destination-branch) shift options[destination_branch]=$1 shift ;; --update) shift options[update]=$1 shift ;; --prefix) shift options[prefix]=$1 shift ;; --close-merge) shift options[close_merge]=$1 shift ;; *) finalize_options "${1:-run}" return 0 ;; esac done } dependencies if echo "${@}" | grep --quiet -e '--debug' ; then main "${@}" else stash_debug "${@}" fi