Add forge pr close command #65
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "agent/pr-close"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #59
Adds
forge pr close {<number> | <url> | <branch>} [-c <text>] [-d] [-R <owner/repo>]-c, --commentposts a closing comment before closing-d, --delete-branchdeletes the remote head branch after closingCode Review: PR #65 —
forge pr closeReviewed the full diff against
master, the completeforgescript for context, all test files, and the existingissue closeimplementation for consistency comparison.What's done well
pr_closefollows the same arg-parsing pattern as every other subcommand —contains_help_flag, while-case loop,dieon bad input, guard clause on missing target. Easy to read.resolve_pr_targetfunction. Clean separation between "figure out what PR we're talking about" and "do the thing." This will serve future commands (reopen, merge) without duplication.IFS=/ readdecomposition with theextracheck catches trailing path segments. The$resource == "pulls"check meansissues/12URLs correctly fail. Good.jq -n --arg, which handles all quoting. No injection there.gh pr closesemantics.Issues found
1. Branch name not URL-encoded in API query string — MAJOR
The same pattern exists in
pr_find_by_head(line 577), so this is a pre-existing issue, butpr closeinherits it and should be noted. A branch namedfeature/foo&state=closedor containing%,+,#, or spaces will produce a malformed query string. The Forgejo API may misparse the parameters or return unexpected results.The
head=value should be percent-encoded before interpolation. A minimal fix:(Alternatively
curl --data-urlencodein a GET, but that changes theapi()call shape.)The test suite only uses the branch name
topic— a single ASCII word — so this is completely invisible to the current tests.2.
--delete-branchwill delete any branch name the API returns, includingmaster— MAJORIf someone accidentally runs
forge pr close 42 -don a PR whose head ismasterormain, this will delete the default branch.gh pr close --delete-branchrefuses to delete the base branch. There is no guard here.At minimum, fetch the base ref too and refuse if
head_ref == base_ref:3. No test for
-R/--repooverride withpr close— MINOREvery other test uses the implicit
-R acme/widgetfromrun_ok. There is no test thatpr close 12 -R acme/gadgetactually targets the right repo, or that URL-based close overrides an earlier-R. Theresolve_pr_targetURL path setsREPOdirectly, but the interaction with a pre-set-Ris untested.4.
head_refcontaining/or special chars reachesDELETEURL unencoded — MINORBranch names like
feature/close-buttoncontain slashes. The Forgejo branch API endpoint uses{branch}as a path parameter and expects it URL-encoded (e.g.,feature%2Fclose-button). The current code would produce/repos/acme/widget/branches/feature/close-button, which is a 404.The mock only returns
"ref":"topic"(no slashes), so this is invisible in tests.Fix:
head_ref=$(printf '%s' "$head_ref" | jq -sRr @uri)5. No error handling if the PATCH (close) call fails but the comment already posted — MINOR
If the PATCH fails (403, network error, etc.),
set -ewill abort the script, but the comment is already posted. The PR is now open with a stray "closing" comment on it. This is also howissue closeworks, so it is at least consistent, but worth noting as a known limitation.ghhas the same behavior, so this is arguably acceptable.6. Closing an already-closed PR silently succeeds — NITPICK
The Forgejo API will happily PATCH a closed PR to
state:"closed"and return 200.gh pr closeon an already-closed PR prints a warning. This command will succeed silently. Whether that matters depends on how agentic callers use it (idempotent close is arguably a feature), but it is a behavioral difference worth documenting.7. Branch deletion result is not checked — NITPICK
If the DELETE returns a 404 (branch already deleted, or encoded wrong per issue #4),
set -ewill kill the script after the PR is already closed. The user sees no "PR closed" message at all — just a silent exit 1. Catching the DELETE error and printing a warning ("PR closed but branch deletion failed") would be more user-friendly.8. Mock returns empty body for branch DELETE — NITPICK
The mock returns an empty response for branch deletion. The real Forgejo API returns 204 No Content with an empty body. This is fine since the code does
>/dev/null, but if anyone adds response checking later, the mock should at minimum be commented explaining the 204 behavior.9.
resolve_pr_targetsets globalPR_NUMBER— NITPICKThe function communicates its result via the global
PR_NUMBER(and possiblyREPO). This is consistent withresolve_issue_targetwhich usesISSUE_NUMBER, so it is not a bug, but it would be worth a comment noting thatresolve_pr_targetmay modify bothREPOandPR_NUMBERas side effects — especially since a URL target silently overrides any-Rflag.10. Empty comment string edge case — NITPICK
This guards against an empty target, but:
forge pr close 12 -c ""will setcomment_set=truewith an empty comment string. The API call will then POST{"body":""}— an empty comment.gh pr close -c ""rejects this. Consider:[[ -n "$comment" ]] || die "--comment cannot be empty".Summary
The core logic is solid and well-structured. The arg parsing, URL validation, and operation ordering are all correct. The two MAJOR items (URL-encoding of branch names in query parameters, and the missing guard against deleting protected/base branches) are the ones I would want fixed before merge, since one silently breaks for common branch naming conventions and the other is a data-loss footgun. Everything else is polish or defense-in-depth.
Good work overall. The
resolve_pr_targetabstraction in particular will pay off whenpr mergeorpr reopenland.9a28519578to8f36bccc048f36bccc04toce53dab9e5ce53dab9e5to8224bc3459Second-Pass Review: PR #65 —
forge pr closeReviewed by a greybeard bash scripter who dreams in POSIX.
Prior Review Fixes — Status
All MAJOR findings from the first review were properly addressed:
jq @uri)--comment ""now dies)Items intentionally left as-is (consistent with
issue closeandgh): comment-before-failed-PATCH, idempotent close of already-closed PR, global-settingresolve_pr_target.New Findings
MAJOR
M1.
urlencode()usesjq -Rinstead ofjq -sRjq -Rreads one line at a time. If input contains a newline, only the first line is encoded; the rest becomes a separate unencoded output line. Git branch names can't contain newlines so this is low practical risk, butjq -sRr @uriis strictly safer and a one-character fix.MINOR
m1.
pr close --helpnot tested inhelp.shEvery other subcommand has a
--helptest.pr closeis the only one missing.m2. No test for
--delete-branchwith a branch-name targetAll
-dtests use numeric targets. The full resolve-by-branch + fetch-refs + close + delete pipeline (the most complex code path) has no dedicated test.m3. No test for duplicate
--commentflag rejectionThe code guards against
pr close 12 -c "first" -c "second"but no test exercises it. Compare with the duplicate--titletest forpr create.m4.
-Roverride test usesrun_captureinstead ofrun_okThe repo-override test doesn't assert exit code 0, unlike every other happy-path test. The
assert_requestprovides indirect validation but the exit code isn't checked.NITPICK
n1. Warning message for head==base guard is not asserted in tests
The test checks request count but not that the stderr warning actually appears.
n2. Minor formatting inconsistency: top-of-file comment uses
{<number>|<url>|<branch>}(no spaces) while the die/usage messages use spaces around pipes.Verdict
Solid work. The code is well-structured, follows existing conventions, and the prior MAJOR issues are properly resolved. None of the new findings are merge-blockers. The
urlencodefix (M1) is worth doing before merge; the test gaps are good follow-up items.