#!/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/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 git_clone() { local direction=$1 url=$2 if ! test -d $TMPDIR/$direction; then git -c credential.helper="store --file=$TMPDIR/$direction.git-credentials" clone $url $TMPDIR/$direction fi ( cd $TMPDIR/$direction 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 git_checkout() { local direction=$1 ref="$3" local remote=origin ( cd $TMPDIR/$direction if [[ "$ref" =~ ^refs/ ]] ; then git fetch ${remote} +$ref:$ref else ref=${remote}/$ref fi git checkout -b prbranch $ref ) } function git_remote() { local direction=$1 remote=$2 url=$3 ( cd $TMPDIR/$direction git remote add $remote $url ) } function git_reset_branch() { local direction=$1 remote=$2 branch=$3 ( cd $TMPDIR/$direction if git ls-remote --exit-code --heads ${remote} $branch ; then git fetch --quiet ${remote} $branch git reset --hard ${remote}/$branch fi ) } function sha_pushed() { local direction=$1 if test -f $TMPDIR/$direction.sha ; then cat $TMPDIR/$direction.sha fi } function push() { local direction=$1 remote=$2 branch=$3 ( cd $TMPDIR/$direction git add . if git commit -m 'cascading-pr update'; then git push --force ${remote} prbranch:$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 upsert_fork() { if repo_curl ${options[destination_repo]} api_json ${options[destination_fork_api]} > $TMPDIR/fork.json 2> /dev/null ; then if test "$(jq --raw-output .fork)" != true ; then log_error "the destination fork already exists but is not a fork ${options[destination_fork]}" return 1 fi local forked_from_repo=$(jq --raw-output .parent.full_name) if test "$forked_from_repo" != "${options[destination_repo]}" ; then log_error "${options[destination_fork]} must be a fork of ${options[destination_repo]} but is a fork of $forked_from_repo instead" return 1 fi else local fork_owner=$(owner ${options[destination_fork]}) local data="{}" if repo_curl ${options[destination_repo]} api_json ${options[destination_url]}/api/v1/orgs/${fork_owner} >& /dev/null ; then data='{"organization":"'$fork_owner'"}' fi repo_curl ${options[destination_repo]} api_json --data "$data" ${options[destination_url]}/api/v1/${options[destination_repo]}/forks fi } function checkout() { # # origin # git_clone origin ${options[origin_clone]} git_checkout origin "${options[origin_head]}" # # destination # git_clone destination ${options[destination_clone]} git_checkout destination "${options[destination_base]}" # # fork # local head_remote=origin if ${options[destination_is_fork]} ; then upsert_fork git_remote destination fork ${options[destination_fetch_fork]} head_remote=fork fi git_reset_branch destination $head_remote "${options[destination_head]}" } function update() { ( 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 ) local remote_head=origin if ${options[destination_is_fork]} ; then remote_head=fork fi push destination $remote_head ${options[destination_head]} } 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 fork_sanity_check() { local fork_repo=${options[destination_fork_repo]} local repo=${options[destination_repo]} if test "$(repository $fork_repo)" != "$(repository $repo)"; then echo "$repo and its fork $fork_repo must have the same repository name (see https://codeberg.org/forgejo/forgejo/issues/1707)" return 1 fi } 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]} if test "${options[destination_fork_repo]}"; then fork_sanity_check options[destination_is_fork]=true options[destination_fork_api]=${options[destination_url]}/api/v1/repos/${options[destination_fork_repo]} options[destination_is_fork]=false fi : ${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" checkout 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 checkout 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-fork-repo) shift options[destination_fork_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