diff --git a/action.yml b/action.yml index 789f367..4fc807c 100644 --- a/action.yml +++ b/action.yml @@ -21,7 +21,7 @@ description: | The `update` script is expected to be found in the origin repository running the PR. It is given four arguments: - * A directory in which the destination repository is checked-out + * A directory in which the destination repository (or a fork) is checked-out on the base branch * A file with the JSON describing the pull request in the destination repository @@ -36,6 +36,9 @@ description: | When the PR is from a forked repository, the `update` script is checked out from the default branch instead of the head branch of the fork. + If the fork of the destination repository is specified and it does + not exist, it is created. + inputs: origin-url: description: 'URL of the Forgejo instance where the PR that triggers the action is located (e.g. https://code.forgejo.org)' @@ -55,6 +58,8 @@ inputs: destination-repo: description: 'the repository in which the cascading PR is created or updated' required: true + destination-fork-repo: + description: 'the fork of {desitnation-repo} in which the {destination-branch} will be created or updated' destination-branch: description: 'the base branch of the destination repository for the cascading PR' required: true @@ -102,6 +107,7 @@ runs: --origin-pr "${{ inputs.origin-pr }}" \ --destination-url "${{ inputs.destination-url }}" \ --destination-repo "${{ inputs.destination-repo }}" \ + --destination-fork-repo "${{ inputs.destination-fork-repo }}" \ --destination-token "@$destination_token" \ --destination-branch "${{ inputs.destination-branch }}" \ --update "${{ inputs.update }}" \ diff --git a/cascading-pr-lib.sh b/cascading-pr-lib.sh index 5cf0c39..3a8d00e 100644 --- a/cascading-pr-lib.sh +++ b/cascading-pr-lib.sh @@ -115,6 +115,18 @@ function scheme() { echo "${url%%://*}" } +function owner() { + local repo="$1" + + echo "${repo%%/*}" +} + +function repository() { + local repo="$1" + + echo "${repo##*/}" +} + function get_status() { local api="$1" local sha="$2" diff --git a/cascading-pr.sh b/cascading-pr.sh index 2f6ed9f..5f8fec7 100755 --- a/cascading-pr.sh +++ b/cascading-pr.sh @@ -64,21 +64,6 @@ EOF 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]}" } @@ -94,13 +79,18 @@ function upsert_destination_pr() { log_info "an open PR already exists $url" return fi + if ${options[destination_is_fork]} ; then + head="$(owner ${options[destination_fork_repo]}):${options[destination_head]}" + else + head=${options[destination_head]} + 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]}" + "head":"$head" } EOF retry repo_curl ${options[destination_repo]} api_json --data @$TMPDIR/data ${options[destination_api]}/pulls > $TMPDIR/destination-pr.json @@ -165,26 +155,55 @@ function pr_from_fork() { pr $1 | jq --raw-output .head.repo.fork } -function upsert_clone() { - local direction=$1 ref="$2" clone=$3 +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 $clone $TMPDIR/$direction + git -c credential.helper="store --file=$TMPDIR/$direction.git-credentials" clone $url $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 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 @@ -193,13 +212,13 @@ function sha_pushed() { } function push() { - local direction=$1 branch=$2 clone=$3 + local direction=$1 remote=$2 branch=$3 ( cd $TMPDIR/$direction git add . if git commit -m 'cascading-pr update'; then - git push --force origin $direction:$branch + git push --force ${remote} prbranch:$branch git rev-parse HEAD > ../$direction.sha log_info "pushed" else @@ -214,9 +233,53 @@ function wait_destination_ci() { 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() { - 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 @@ -234,7 +297,11 @@ function update() { cd $TMPDIR $update $TMPDIR/destination $TMPDIR/destination-pr.json $TMPDIR/origin $TMPDIR/origin-pr.json ) - push destination ${options[destination_head]} ${options[destination_clone]} + local remote_head=origin + if ${options[destination_is_fork]} ; then + remote_head=fork + fi + push destination $remote_head ${options[destination_head]} } function set_clone() { @@ -254,12 +321,22 @@ function set_clone() { 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]}) @@ -267,6 +344,14 @@ function finalize_options() { 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} } @@ -279,7 +364,7 @@ function run() { case "$state" in open) log_info "PR is open, update or create the cascade branch and PR" - upsert_destination_branch + checkout update local sha=$(sha_pushed destination) if test "$sha" ; then @@ -297,6 +382,7 @@ function run() { log_info "PR was merged, update the cascade PR" pr_get origin pr_get destination + checkout update fi else @@ -351,6 +437,11 @@ function main() { options[destination_repo]=$1 shift ;; + --destination-fork-repo) + shift + options[destination_fork_repo]=$1 + shift + ;; --destination-token) shift options[destination_token]=$1