Skip to content

Commit cdaa508

Browse files
julianknutsenclaude
andcommitted
feat: per-submission accept/reject/close buttons and fix stale-fork filter
Add reject-upstream and close-upstream endpoints so posters can act on each competing submission individually. Reject closes the fork's DoltHub PR without modifying local state; close adopts the fork's completion without creating a stamp. Fix the stale-fork filter that was hiding submissions (e.g. nullpriest): the claimed_by != author check now only applies to "claimed" status, since in_review/completed items represent intentional action. Synthesize main-branch completions into the submissions list when an item is in_review, so all submissions appear in one place. The separate Completion section now only renders for completed (accepted) items. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 99d291d commit cdaa508

14 files changed

Lines changed: 322 additions & 47 deletions

File tree

cmd/wl/cmd_review.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -965,3 +965,30 @@ func branchURLCallback(cfg *federation.Config) func(string) string {
965965
return nil
966966
}
967967
}
968+
969+
// closeUpstreamPRCallback returns a callback that closes an upstream PR by its web URL.
970+
func closeUpstreamPRCallback(cfg *federation.Config) func(string) error {
971+
switch cfg.ResolveProviderType() {
972+
case "dolthub":
973+
token := os.Getenv("DOLTHUB_TOKEN")
974+
if token == "" {
975+
return nil
976+
}
977+
upstreamOrg, db, err := federation.ParseUpstream(cfg.Upstream)
978+
if err != nil {
979+
return nil
980+
}
981+
provider := remote.NewDoltHubProvider(token)
982+
return func(prURL string) error {
983+
// Extract PR ID from URL like ".../pulls/123"
984+
idx := strings.LastIndex(prURL, "/pulls/")
985+
if idx < 0 {
986+
return fmt.Errorf("cannot extract PR ID from URL: %s", prURL)
987+
}
988+
prID := prURL[idx+len("/pulls/"):]
989+
return provider.ClosePR(upstreamOrg, db, prID)
990+
}
991+
default:
992+
return nil
993+
}
994+
}

cmd/wl/cmd_serve.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ func runServe(cmd *cobra.Command, stdout, stderr io.Writer) error {
206206
},
207207
ListPendingItems: listPendingItemsFromPRs(cfg),
208208
BranchURL: branchURLCallback(cfg),
209+
CloseUpstreamPR: closeUpstreamPRCallback(cfg),
209210
})
210211

211212
server := api.New(client)

cmd/wl/cmd_tui.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ func runTUI(cmd *cobra.Command, _, stderr io.Writer) error {
123123
},
124124
ListPendingItems: listPendingItemsFromPRs(cfg),
125125
BranchURL: branchURLCallback(cfg),
126+
CloseUpstreamPR: closeUpstreamPRCallback(cfg),
126127
})
127128

128129
m := tui.New(tui.Config{

cmd/wl/sdk_factory.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,6 @@ var newSDKClient = func(cfg *federation.Config, noPush bool) (*sdk.Client, error
3333
},
3434
ListPendingItems: listPendingItemsFromPRs(cfg),
3535
BranchURL: branchURLCallback(cfg),
36+
CloseUpstreamPR: closeUpstreamPRCallback(cfg),
3637
}), nil
3738
}

internal/api/handlers.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,53 @@ func (s *Server) handleAcceptUpstream(w http.ResponseWriter, r *http.Request) {
428428
writeJSON(w, http.StatusOK, toMutationResponse(result, client.Mode()))
429429
}
430430

431+
func (s *Server) handleRejectUpstream(w http.ResponseWriter, r *http.Request) {
432+
client, ok := s.resolveClient(w, r)
433+
if !ok {
434+
return
435+
}
436+
id := r.PathValue("id")
437+
var req RejectUpstreamRequest
438+
if err := decodeJSON(r, &req); err != nil {
439+
writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
440+
return
441+
}
442+
if req.RigHandle == "" {
443+
writeError(w, http.StatusBadRequest, "rig_handle is required")
444+
return
445+
}
446+
if err := client.RejectUpstream(id, req.RigHandle); err != nil {
447+
writeMutationError(w, err)
448+
return
449+
}
450+
s.invalidateReadCaches(id)
451+
writeJSON(w, http.StatusOK, map[string]string{"status": "rejected"})
452+
}
453+
454+
func (s *Server) handleCloseUpstream(w http.ResponseWriter, r *http.Request) {
455+
client, ok := s.resolveClient(w, r)
456+
if !ok {
457+
return
458+
}
459+
id := r.PathValue("id")
460+
var req CloseUpstreamRequest
461+
if err := decodeJSON(r, &req); err != nil {
462+
writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
463+
return
464+
}
465+
if req.RigHandle == "" {
466+
writeError(w, http.StatusBadRequest, "rig_handle is required")
467+
return
468+
}
469+
result, err := client.CloseUpstream(id, req.RigHandle)
470+
if err != nil {
471+
writeMutationError(w, err)
472+
return
473+
}
474+
s.invalidateReadCaches(id)
475+
writeJSON(w, http.StatusOK, toMutationResponse(result, client.Mode()))
476+
}
477+
431478
func (s *Server) handleReject(w http.ResponseWriter, r *http.Request) {
432479
client, ok := s.resolveClient(w, r)
433480
if !ok {

internal/api/routes.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ func (s *Server) registerRoutes() {
1818
s.mux.HandleFunc("POST /api/wanted/{id}/done", s.handleDone)
1919
s.mux.HandleFunc("POST /api/wanted/{id}/accept", s.handleAccept)
2020
s.mux.HandleFunc("POST /api/wanted/{id}/accept-upstream", s.handleAcceptUpstream)
21+
s.mux.HandleFunc("POST /api/wanted/{id}/reject-upstream", s.handleRejectUpstream)
22+
s.mux.HandleFunc("POST /api/wanted/{id}/close-upstream", s.handleCloseUpstream)
2123
s.mux.HandleFunc("POST /api/wanted/{id}/reject", s.handleReject)
2224
s.mux.HandleFunc("POST /api/wanted/{id}/close", s.handleClose)
2325

internal/api/types.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,16 @@ type AcceptUpstreamRequest struct {
199199
Message string `json:"message"`
200200
}
201201

202+
// RejectUpstreamRequest is the JSON body for POST /api/wanted/{id}/reject-upstream.
203+
type RejectUpstreamRequest struct {
204+
RigHandle string `json:"rig_handle"`
205+
}
206+
207+
// CloseUpstreamRequest is the JSON body for POST /api/wanted/{id}/close-upstream.
208+
type CloseUpstreamRequest struct {
209+
RigHandle string `json:"rig_handle"`
210+
}
211+
202212
// RejectRequest is the JSON body for POST /api/wanted/{id}/reject.
203213
type RejectRequest struct {
204214
Reason string `json:"reason"`
@@ -284,6 +294,18 @@ func toDetailResponse(d *sdk.DetailResult, mode string) *DetailResponse {
284294
actions[i] = commons.TransitionName(t)
285295
}
286296
var upstreamPRs []UpstreamPRJSON
297+
298+
// If the item is in_review and has a completion on main, include it as
299+
// the first entry so the poster sees all submissions in one place.
300+
if d.Item != nil && d.Item.Status == "in_review" && d.Completion != nil {
301+
upstreamPRs = append(upstreamPRs, UpstreamPRJSON{
302+
RigHandle: d.Completion.CompletedBy,
303+
Status: "in_review",
304+
CompletedBy: d.Completion.CompletedBy,
305+
Evidence: d.Completion.Evidence,
306+
})
307+
}
308+
287309
for _, p := range d.UpstreamPRs {
288310
delta := ""
289311
if p.Status != "" && d.Item != nil && p.Status != d.Item.Status {

internal/commons/commons.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,27 @@ func AcceptUpstreamDML(wantedID, completionID, completedBy, evidence, rigHandle,
693693
return []string{deleteCompletion, insertCompletion, updateWanted, insertStamp, updateCompletion}
694694
}
695695

696+
// CloseUpstreamDML returns the pure DML statements for adopting a fork submission
697+
// without creating a stamp. Statements: DELETE existing completion, INSERT fork
698+
// completion, UPDATE wanted to completed.
699+
func CloseUpstreamDML(wantedID, completionID, completedBy, evidence, hopURI string) []string {
700+
hopField := "NULL"
701+
if hopURI != "" {
702+
hopField = fmt.Sprintf("'%s'", EscapeSQL(hopURI))
703+
}
704+
705+
deleteCompletion := fmt.Sprintf(`DELETE FROM completions WHERE wanted_id='%s'`,
706+
EscapeSQL(wantedID))
707+
708+
insertCompletion := fmt.Sprintf(`INSERT IGNORE INTO completions (id, wanted_id, completed_by, evidence, hop_uri, completed_at) VALUES ('%s', '%s', '%s', '%s', %s, NOW())`,
709+
EscapeSQL(completionID), EscapeSQL(wantedID), EscapeSQL(completedBy), EscapeSQL(evidence), hopField)
710+
711+
updateWanted := fmt.Sprintf(`UPDATE wanted SET status='completed', claimed_by='%s', evidence_url='%s', updated_at=NOW() WHERE id='%s'`,
712+
EscapeSQL(completedBy), EscapeSQL(evidence), EscapeSQL(wantedID))
713+
714+
return []string{deleteCompletion, insertCompletion, updateWanted}
715+
}
716+
696717
// AcceptCompletion validates a completion, creates a stamp, and marks the item completed.
697718
func AcceptCompletion(db DB, wantedID, completionID, rigHandle, hopURI string, stamp *Stamp, signed bool) error {
698719
stmts := AcceptCompletionDML(wantedID, completionID, rigHandle, hopURI, stamp)

internal/hosted/resolver.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,13 @@ func (wr *WorkspaceResolver) buildClient(wl *WastelandConfig, rigHandle, connect
243243
}
244244
return provider.ClosePR(upOrg, upDB, prID)
245245
},
246+
CloseUpstreamPR: func(prURL string) error {
247+
prID := extractPRID(prURL)
248+
if prID == "" {
249+
return fmt.Errorf("cannot extract PR ID from URL: %s", prURL)
250+
}
251+
return provider.ClosePR(upOrg, upDB, prID)
252+
},
246253
ListPendingItems: wr.getOrCreatePendingCache(provider, upOrg, upDB).Get,
247254
BranchURL: branchURL,
248255
Signing: wl.Signing,
@@ -277,3 +284,13 @@ func extractWantedIDFromBranch(branch string) string {
277284
}
278285
return branch
279286
}
287+
288+
// extractPRID extracts the pull request ID from a DoltHub PR URL like
289+
// "https://www.dolthub.com/repositories/org/db/pulls/123".
290+
func extractPRID(prURL string) string {
291+
idx := strings.LastIndex(prURL, "/pulls/")
292+
if idx < 0 {
293+
return ""
294+
}
295+
return prURL[idx+len("/pulls/"):]
296+
}

internal/remote/dolthub.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -794,12 +794,14 @@ func (d *DoltHubProvider) ListPendingWantedIDs(upstreamOrg, db string) (map[stri
794794
//
795795
// Filter rules:
796796
// 1. status "open" = untouched item (stale copy)
797-
// 2. claimed_by set to someone other than the PR author =
798-
// inherited claim from a previous upstream state
797+
// 2. status "claimed" with claimed_by set to someone other than
798+
// the PR author = inherited claim from a previous upstream state
799+
// Items at "in_review" or "completed" always pass — those statuses
800+
// require intentional action (submitting evidence / accepting).
799801
if e.state.Status == "open" {
800802
continue
801803
}
802-
if e.state.ClaimedBy != "" && e.state.ClaimedBy != e.author {
804+
if e.state.Status == "claimed" && e.state.ClaimedBy != "" && e.state.ClaimedBy != e.author {
803805
continue
804806
}
805807
ids[e.wantedID] = append(ids[e.wantedID], e.state)

0 commit comments

Comments
 (0)