From 1c6dad088d65e6ca52fcfae7d1162c4350849142 Mon Sep 17 00:00:00 2001 From: Vitor Py Date: Mon, 5 Jan 2026 15:03:20 +0100 Subject: [PATCH] Add XRPC endpoint to list all PRs targeting a repository Implements sh.tangled.repo.listPulls XRPC method to allow querying all pull requests targeting a specific repository, regardless of author. Changes: - Add ListPulls() handler in appview/pulls/pulls.go - Register /xrpc/sh.tangled.repo.listPulls endpoint in router - Create delegation method in appview/state/xrpc.go - Add lexicon schema for sh.tangled.repo.listPulls This enables the CLI to list all PRs for a repo with: tangled pr list --repo Instead of only showing PRs created by the current user. --- appview/pulls/pulls.go | 89 ++++++++++++++++++++++++++++++++++++ appview/state/router.go | 3 ++ appview/state/xrpc.go | 26 +++++++++++ lexicons/repo/listPulls.json | 80 ++++++++++++++++++++++++++++++++ 4 files changed, 198 insertions(+) create mode 100644 appview/state/xrpc.go create mode 100644 lexicons/repo/listPulls.json diff --git a/appview/pulls/pulls.go b/appview/pulls/pulls.go index 25b3abd7..155a150d 100644 --- a/appview/pulls/pulls.go +++ b/appview/pulls/pulls.go @@ -691,6 +691,95 @@ func (s *Pulls) RepoPulls(w http.ResponseWriter, r *http.Request) { }) } +// ListPulls is an XRPC method that lists all pull requests targeting a repository +func (s *Pulls) ListPulls(w http.ResponseWriter, r *http.Request) { + l := s.logger.With("handler", "ListPulls") + + // Parse query parameters + params := r.URL.Query() + repoAtStr := params.Get("repo") // AT-URI of target repo + stateParam := params.Get("state") // "open", "closed", "merged", or empty for all + + if repoAtStr == "" { + http.Error(w, "repo parameter is required", http.StatusBadRequest) + return + } + + // Parse AT-URI and validate + repoAt, err := syntax.ParseATURI(repoAtStr) + if err != nil { + l.Error("invalid repo AT-URI", "err", err, "repoAt", repoAtStr) + http.Error(w, "invalid repo AT-URI", http.StatusBadRequest) + return + } + + // Determine state filter + var stateFilter orm.Filter + switch stateParam { + case "open": + stateFilter = orm.FilterEq("state", models.PullOpen) + case "closed": + stateFilter = orm.FilterEq("state", models.PullClosed) + case "merged": + stateFilter = orm.FilterEq("state", models.PullMerged) + case "": + // No state filter - include all states + stateFilter = orm.FilterIn("state", []models.PullState{ + models.PullOpen, + models.PullClosed, + models.PullMerged, + }) + default: + http.Error(w, "invalid state parameter", http.StatusBadRequest) + return + } + + // Query database + pulls, err := db.GetPulls( + s.db, + orm.FilterEq("repo_at", repoAt.String()), + stateFilter, + ) + if err != nil { + l.Error("failed to list pulls", "err", err) + http.Error(w, "failed to list pulls", http.StatusInternalServerError) + return + } + + l.Debug("listed pulls", "count", len(pulls), "repo_at", repoAt.String()) + + // Build response + type PullSummary struct { + Rkey string `json:"rkey"` + OwnerDid string `json:"ownerDid"` + PullId int `json:"pullId"` + Title string `json:"title"` + State int `json:"state"` + TargetBranch string `json:"targetBranch"` + CreatedAt string `json:"createdAt"` + } + + type Response struct { + Pulls []PullSummary `json:"pulls"` + } + + summaries := make([]PullSummary, 0, len(pulls)) + for _, p := range pulls { + summaries = append(summaries, PullSummary{ + Rkey: p.Rkey, + OwnerDid: p.OwnerDid, + PullId: p.PullId, + Title: p.Title, + State: int(p.State), + TargetBranch: p.TargetBranch, + CreatedAt: p.Created.Format(time.RFC3339), + }) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(Response{Pulls: summaries}) +} + func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) { user := s.oauth.GetUser(r) f, err := s.repoResolver.Resolve(r) diff --git a/appview/state/router.go b/appview/state/router.go index 6b14e647..84808ee0 100644 --- a/appview/state/router.go +++ b/appview/state/router.go @@ -143,6 +143,9 @@ func (s *State) StandardRouter(mw *middleware.Middleware) http.Handler { r.With(middleware.Paginate).Get("/goodfirstissues", s.GoodFirstIssues) + // XRPC endpoints + r.Get("/xrpc/sh.tangled.repo.listPulls", s.ListPulls) + r.With(middleware.AuthMiddleware(s.oauth)).Route("/follow", func(r chi.Router) { r.Post("/", s.Follow) r.Delete("/", s.Follow) diff --git a/appview/state/xrpc.go b/appview/state/xrpc.go new file mode 100644 index 00000000..ec996982 --- /dev/null +++ b/appview/state/xrpc.go @@ -0,0 +1,26 @@ +package state + +import ( + "net/http" + + "tangled.org/core/appview/pulls" +) + +// ListPulls delegates to the pulls.ListPulls XRPC handler +func (s *State) ListPulls(w http.ResponseWriter, r *http.Request) { + pullsHandler := pulls.New( + s.oauth, + s.repoResolver, + s.pages, + s.idResolver, + s.mentionsResolver, + s.db, + s.config, + s.notifier, + s.enforcer, + s.validator, + s.indexer.Pulls, + s.logger.With("handler", "pulls-xrpc"), + ) + pullsHandler.ListPulls(w, r) +} diff --git a/lexicons/repo/listPulls.json b/lexicons/repo/listPulls.json new file mode 100644 index 00000000..7a8dce59 --- /dev/null +++ b/lexicons/repo/listPulls.json @@ -0,0 +1,80 @@ +{ + "lexicon": 1, + "id": "sh.tangled.repo.listPulls", + "defs": { + "main": { + "type": "query", + "parameters": { + "type": "params", + "required": [ + "repo" + ], + "properties": { + "repo": { + "type": "string", + "format": "at-uri" + }, + "state": { + "type": "string", + "enum": ["open", "closed", "merged"] + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "pulls" + ], + "properties": { + "pulls": { + "type": "array", + "items": { + "type": "ref", + "ref": "#pull" + } + } + } + } + } + }, + "pull": { + "type": "object", + "required": [ + "rkey", + "ownerDid", + "pullId", + "title", + "state", + "targetBranch", + "createdAt" + ], + "properties": { + "rkey": { + "type": "string" + }, + "ownerDid": { + "type": "string", + "format": "did" + }, + "pullId": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "state": { + "type": "integer" + }, + "targetBranch": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + } +} -- 2.43.0