diff --git a/changelog.d/236.md b/changelog.d/236.md new file mode 100644 index 0000000..098c379 --- /dev/null +++ b/changelog.d/236.md @@ -0,0 +1,3 @@ +### Fixed + +- Forgejo drafts and fast-forward conflict-check windows no longer surface as merge conflicts when the API has not distinguished one (#236). diff --git a/lib/forge-forgejo.sh b/lib/forge-forgejo.sh index 4331cc6..cfdf887 100644 --- a/lib/forge-forgejo.sh +++ b/lib/forge-forgejo.sh @@ -429,9 +429,12 @@ forge_pr_view() { status="$(forge_api "repos/$REPO/commits/$sha/status")" || return 1 jq -n --argjson pr "$pr" --argjson st "$status" ' { - mergeable: (if $pr.mergeable == true then "MERGEABLE" - elif $pr.mergeable == false then "CONFLICTING" - else "UNKNOWN" end), + # Forgejo folds checking, conflict, check error, and WIP into false. + # Draft must win because WIP makes the boolean carry no merge result (#236). + mergeable: (if $pr.draft == true then "UNKNOWN" + elif $pr.mergeable == true then "MERGEABLE" + elif $pr.merge_base == $pr.base.sha then "UNKNOWN" + else "CONFLICTING" end), statusCheckRollup: [ $st.statuses[]? | { __typename: "StatusContext", diff --git a/test/forge-backends.test.sh b/test/forge-backends.test.sh index d304218..fd2c1c7 100644 --- a/test/forge-backends.test.sh +++ b/test/forge-backends.test.sh @@ -599,7 +599,7 @@ pr_view_stub() { printf 'HTTP/1.1 200 OK\r\nX-Total-Count: 1\r\n\r\n' >"$hdr" case "$url" in */status) printf '%s' "$FAKE_STATUS" >"$out" ;; - *) printf '{"head":{"sha":"abc"},"mergeable":true}' >"$out" ;; + *) printf '%s' "$FAKE_PR" >"$out" ;; esac return 0 } @@ -608,6 +608,7 @@ pr_view_stub() { FAKE_STATUS='{"state":"failure","statuses":[ {"context":"ci / check","status":"success","created_at":"2026-08-02T10:00:00Z","updated_at":"2026-08-02T10:00:00Z"}, {"context":"ci / check","status":"failure","created_at":"2026-08-02T09:00:00Z","updated_at":"2026-08-02T09:00:00Z"}]}' +FAKE_PR='{"head":{"sha":"abc"},"base":{"sha":"base"},"merge_base":"ancestor","draft":false,"mergeable":true}' pr_view_stub view_json="$(REPO=o/r forge_pr_view 5)" check "pr_view maps createdAt" 0 "" \ @@ -616,6 +617,31 @@ check "pr_view maps completedAt" 0 "" \ grep -q '"completedAt":' <<<"$view_json" check "pr_view maps mergeable to the UI string" 0 "" \ grep -q '"mergeable": "MERGEABLE"' <<<"$view_json" + +# Forgejo folds conflict-checking, conflict-check errors, real conflicts, and +# WIP into one false boolean. These fixtures pin the distinctions the API +# object still lets the backend report honestly (#236). +FAKE_PR='{"head":{"sha":"abc"},"base":{"sha":"base"},"merge_base":"ancestor","draft":true,"mergeable":false}' +view_json="$(REPO=o/r forge_pr_view 5)" +check "pr_view reports draft mergeability as unknown" 0 "" \ + grep -q '"mergeable": "UNKNOWN"' <<<"$view_json" + +FAKE_PR='{"head":{"sha":"abc"},"base":{"sha":"base"},"merge_base":"base","draft":false,"mergeable":false}' +view_json="$(REPO=o/r forge_pr_view 5)" +check "pr_view reports a fast-forward false mergeability as unknown" 0 "" \ + grep -q '"mergeable": "UNKNOWN"' <<<"$view_json" + +FAKE_PR='{"head":{"sha":"abc"},"base":{"sha":"base"},"merge_base":"ancestor","draft":false,"mergeable":false}' +view_json="$(REPO=o/r forge_pr_view 5)" +check "pr_view preserves a distinguishable real conflict" 0 "" \ + grep -q '"mergeable": "CONFLICTING"' <<<"$view_json" + +# Unreachable on Forgejo: Mergeable() includes !IsWorkInProgress(). This +# ordering fixture exists only to pin draft ahead of mergeable true (#236). +FAKE_PR='{"head":{"sha":"abc"},"base":{"sha":"base"},"merge_base":"ancestor","draft":true,"mergeable":true}' +view_json="$(REPO=o/r forge_pr_view 5)" +check "pr_view reads draft before the mergeable boolean" 0 "" \ + grep -q '"mergeable": "UNKNOWN"' <<<"$view_json" # The real proof: feed it to the production classifier and confirm the newer # SUCCESS wins over the older FAILURE regardless of array order. # shellcheck source=actions/labels-reconcile/labels-reconcile.sh