cascading-pr/cascading-pr.sh
Earl Warren 5c0fc66c82
use sha for origin instead of head
because the head ref may be either a branch or a reference, depending
on wether the PR was merged and the branch deleted. The SHA (either
from the head or the merged commit) always represents the most up to
date state of the PR.
2023-10-24 18:31:18 +02:00

370 lines
9.1 KiB
Bash
Executable file

#!/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 repo="$1"
(
export DOT=$TMPDIR/$repo
forgejo-curl.sh logout
forgejo-curl.sh --token "${options[destination_token]}" login "${options[destination_url]}"
)
}
function repo_curl() {
local repo=$1
shift
DOT=$TMPDIR/$repo forgejo-curl.sh "$@"
}
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 <<EOF
{
"body":"$(pr_origin_comment_body)"
}
EOF
repo_curl ${options[origin_repo]} api_json --data @$TMPDIR/data ${options[origin_api]}/issues/${options[origin_pr]}/comments
log_info "comment added to $(pr_url origin)"
}
function upsert_destination_branch() {
if $(exists_branch destination) ; then
log_info "branch ${options[destination_head]} already exists"
return
fi
cat > $TMPDIR/data <<EOF
{
"new_branch_name":"${options[destination_head]}",
"old_branch_name":"${options[destination_base]}"
}
EOF
repo_curl ${options[destination_repo]} api_json --data @$TMPDIR/data ${options[destination_api]}/branches
log_info "branch ${options[destination_head]} created"
}
function pr_destination_title() {
echo "cascading-pr from ${options[origin_url]}/${options[origin_repo]}/pulls/${options[origin_pr]}"
}
function pr_destination_body() {
echo "cascading-pr from ${options[origin_url]}/${options[origin_repo]}/pulls/${options[origin_pr]}"
}
function upsert_destination_pr() {
url=$(pr_url destination)
state=$(pr_state destination)
if test "$url" != "null" -a "$state" = "open"; then
log_info "an open PR already exists $url"
return
fi
local title=$(pr_destination_title)
cat > $TMPDIR/data <<EOF
{
"title":"$(pr_destination_title)",
"body":"$(pr_destination_body)",
"base":"${options[destination_base]}",
"head":"${options[destination_head]}"
}
EOF
retry repo_curl ${options[destination_repo]} api_json --data @$TMPDIR/data ${options[destination_api]}/pulls > $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_sha() {
pr_get $1
merged=$(pr $1 | jq --raw-output .merged)
if "$merged"; then
pr $1 | jq --raw-output .merge_commit_sha
else
pr $1 | jq --raw-output .head.sha
fi
}
function pr_merged() {
pr_get $1
pr $1 | jq --raw-output .merged
}
function upsert_clone() {
local direction=$1 ref="$2" sha="$3" clone=$4
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 test "$ref"; then
git switch -c $direction origin/$ref
fi
if test "$sha"; then
git switch -c $direction $sha
fi
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_sha]}" ${options[origin_clone]}
upsert_clone destination "${options[destination_head]}" "${options[destination_sha]}" ${options[destination_clone]}
(
cd $TMPDIR/origin
${options[update]} $TMPDIR/destination $TMPDIR/destination-pr.json $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]=
options[origin_sha]=$(pr_sha origin)
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[destination_sha]=
}
function run() {
local state=$(pr_state origin)
case "$state" in
open)
log_info "PR is open, update or create the cascade branch and PR"
repo_login ${options[destination_repo]}
upsert_destination_branch
update
local sha=$(sha_pushed destination)
if test "$sha" ; then
upsert_destination_pr
repo_login ${options[origin_repo]}
comment_origin_pr
wait_destination_ci "$sha"
fi
;;
closed)
if "$(pr_merged origin)"; then
log_info "PR was merged, update the cascade PR"
repo_login ${options[destination_repo]}
pr_get origin
pr_get destination
update
else
log_info "PR is closed, close the cascade PR and remove the branch"
repo_login ${options[destination_repo]}
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
;;
*)
finalize_options
"${1:-run}"
return 0
;;
esac
done
}
dependencies
if echo "${@}" | grep --quiet -e '--debug' ; then
main "${@}"
else
stash_debug "${@}"
fi