From 40ebcea462cf95a648cf351f1c113a6e1d1c6930 Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl Date: Mon, 24 Aug 2026 17:53:13 +0000 Subject: [PATCH] fix: exhaust Forgejo timeline pagination --- changelog.d/240.md | 3 ++ lib/forge-forgejo.sh | 64 +++++++++++++++++++++++++++---------- test/forge-backends.test.sh | 4 ++- 3 files changed, 53 insertions(+), 18 deletions(-) create mode 100644 changelog.d/240.md diff --git a/changelog.d/240.md b/changelog.d/240.md new file mode 100644 index 0000000..3b2f6f3 --- /dev/null +++ b/changelog.d/240.md @@ -0,0 +1,3 @@ +### Fixed + +- Read Forgejo timelines to exhaustion so busy issues retain their newest label events despite dishonest total-count headers (#240). diff --git a/lib/forge-forgejo.sh b/lib/forge-forgejo.sh index 1b05275..247c760 100644 --- a/lib/forge-forgejo.sh +++ b/lib/forge-forgejo.sh @@ -84,7 +84,7 @@ forgejo_page_url() { esac } -# forge_api [--paginate] [--jq ] +# forge_api [--paginate | --paginate-exhaustive] [--jq ] # # --paginate walks page= until a short page, then PROVES the walk was # complete by comparing what it collected against the server's declared @@ -94,11 +94,21 @@ forgejo_page_url() { # make the completeness check compare null to a number — the guard itself # degrading silently, which is the failure class re-entering through the # door built to stop it. +# +# --paginate-exhaustive is the narrow alternative for an endpoint whose +# x-total-count is known not to describe the collection. It proves completion +# by reading through the first short page and never consults that header. forge_api() { - local paginate=false endpoint="" jqexpr="" have_jq=false + local paginate=false paginate_exhaustive=false method=GET endpoint="" jqexpr="" have_jq=false while [ $# -gt 0 ]; do case "$1" in --paginate) paginate=true ;; + --paginate-exhaustive) paginate_exhaustive=true ;; + -X | --method) + [ "$#" -ge 2 ] || { echo "forge_api: $1 requires a value" >&2; return 1; } + method="$2" + shift + ;; --jq) jqexpr="$2"; have_jq=true; shift ;; -*) ;; *) [ -n "$endpoint" ] || endpoint="$1" ;; @@ -106,6 +116,14 @@ forge_api() { shift done [ -n "$endpoint" ] || { echo "forge_api: endpoint required" >&2; return 1; } + if [ "$paginate" = true ] && [ "$paginate_exhaustive" = true ]; then + echo "forge_api: --paginate and --paginate-exhaustive are mutually exclusive" >&2 + return 1 + fi + if { [ "$paginate" = true ] || [ "$paginate_exhaustive" = true ]; } && [ "$method" != GET ]; then + echo "forge_api: pagination is available only for GET requests" >&2 + return 1 + fi local base token base="$(forgejo_api_base)" || return 1 @@ -116,7 +134,7 @@ forge_api() { # shellcheck disable=SC2064 # the paths are fixed at trap time on purpose trap "rm -f '$hdr' '$body'" RETURN - if [ "$paginate" = false ]; then + if [ "$paginate" = false ] && [ "$paginate_exhaustive" = false ]; then if ! curl -sS -D "$hdr" -o "$body" \ -H "Authorization: token $token" -H 'Accept: application/json' \ "$base/$endpoint"; then @@ -142,21 +160,23 @@ forge_api() { fi forgejo_http_ok "$hdr" "GET $endpoint" || return 1 - # Re-read on EVERY page, not once (#4712). A board that changes size - # under the walk was invisible: page 1 declaring 4 and page 2 declaring - # 9 stopped at 4 believing itself whole. A moving total means the read - # cannot have been atomic, so it is refused rather than reconciled. - local page_total - page_total="$(forgejo_total_count "$hdr")" || return 1 - if [ -z "$total" ]; then - total="$page_total" - elif [ "$page_total" != "$total" ]; then - cat >&2 <&2 <&2 <