Monorepo for Tangled tangled.org

Add sh.tangled.repo.listPulls XRPC endpoint for querying all PRs by repository #930

closed opened by vitorpy.com targeting master from vitorpy.com/tangled-core: feature/list-all-pulls

Summary#

Adds a new XRPC endpoint sh.tangled.repo.listPulls that allows listing all pull requests targeting a specific repository, regardless of author. This enables the CLI to show all PRs for a repo instead of only the user's own PRs.

Changes#

  • appview/pulls/pulls.go: Add ListPulls() handler that queries the database for all PRs targeting a given repository AT-URI, with optional state filtering (open/closed/merged)
  • appview/state/router.go: Register the new XRPC endpoint at /xrpc/sh.tangled.repo.listPulls
  • appview/state/xrpc.go: Add delegation method to route requests to the pulls handler
  • lexicons/repo/listPulls.json: Define the lexicon schema for the new XRPC method

API#

Endpoint: GET /xrpc/sh.tangled.repo.listPulls Parameters:

  • repo (required): AT-URI of the target repository
  • state (optional): Filter by state - "open", "closed", or "merged" Response: JSON array of pull request summaries with owner DID, pull ID, title, state, target branch, and creation timestamp

Use Case#

This enables CLI commands like:

tangled pr list --repo tangled-cli --state open

To show all open PRs targeting the repository, not just those created by the current user.

Backwards Compatibility

โœ… No breaking changes - this is a new additive endpoint
Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:5rtpn23tmq5jocptcbkooj4b/sh.tangled.repo.pull/3mbomhj7crp22
+198
Diff #0
+89
appview/pulls/pulls.go
··· 691 691 }) 692 692 } 693 693 694 + // ListPulls is an XRPC method that lists all pull requests targeting a repository 695 + func (s *Pulls) ListPulls(w http.ResponseWriter, r *http.Request) { 696 + l := s.logger.With("handler", "ListPulls") 697 + 698 + // Parse query parameters 699 + params := r.URL.Query() 700 + repoAtStr := params.Get("repo") // AT-URI of target repo 701 + stateParam := params.Get("state") // "open", "closed", "merged", or empty for all 702 + 703 + if repoAtStr == "" { 704 + http.Error(w, "repo parameter is required", http.StatusBadRequest) 705 + return 706 + } 707 + 708 + // Parse AT-URI and validate 709 + repoAt, err := syntax.ParseATURI(repoAtStr) 710 + if err != nil { 711 + l.Error("invalid repo AT-URI", "err", err, "repoAt", repoAtStr) 712 + http.Error(w, "invalid repo AT-URI", http.StatusBadRequest) 713 + return 714 + } 715 + 716 + // Determine state filter 717 + var stateFilter orm.Filter 718 + switch stateParam { 719 + case "open": 720 + stateFilter = orm.FilterEq("state", models.PullOpen) 721 + case "closed": 722 + stateFilter = orm.FilterEq("state", models.PullClosed) 723 + case "merged": 724 + stateFilter = orm.FilterEq("state", models.PullMerged) 725 + case "": 726 + // No state filter - include all states 727 + stateFilter = orm.FilterIn("state", []models.PullState{ 728 + models.PullOpen, 729 + models.PullClosed, 730 + models.PullMerged, 731 + }) 732 + default: 733 + http.Error(w, "invalid state parameter", http.StatusBadRequest) 734 + return 735 + } 736 + 737 + // Query database 738 + pulls, err := db.GetPulls( 739 + s.db, 740 + orm.FilterEq("repo_at", repoAt.String()), 741 + stateFilter, 742 + ) 743 + if err != nil { 744 + l.Error("failed to list pulls", "err", err) 745 + http.Error(w, "failed to list pulls", http.StatusInternalServerError) 746 + return 747 + } 748 + 749 + l.Debug("listed pulls", "count", len(pulls), "repo_at", repoAt.String()) 750 + 751 + // Build response 752 + type PullSummary struct { 753 + Rkey string `json:"rkey"` 754 + OwnerDid string `json:"ownerDid"` 755 + PullId int `json:"pullId"` 756 + Title string `json:"title"` 757 + State int `json:"state"` 758 + TargetBranch string `json:"targetBranch"` 759 + CreatedAt string `json:"createdAt"` 760 + } 761 + 762 + type Response struct { 763 + Pulls []PullSummary `json:"pulls"` 764 + } 765 + 766 + summaries := make([]PullSummary, 0, len(pulls)) 767 + for _, p := range pulls { 768 + summaries = append(summaries, PullSummary{ 769 + Rkey: p.Rkey, 770 + OwnerDid: p.OwnerDid, 771 + PullId: p.PullId, 772 + Title: p.Title, 773 + State: int(p.State), 774 + TargetBranch: p.TargetBranch, 775 + CreatedAt: p.Created.Format(time.RFC3339), 776 + }) 777 + } 778 + 779 + w.Header().Set("Content-Type", "application/json") 780 + json.NewEncoder(w).Encode(Response{Pulls: summaries}) 781 + } 782 + 694 783 func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) { 695 784 user := s.oauth.GetUser(r) 696 785 f, err := s.repoResolver.Resolve(r)
+3
appview/state/router.go
··· 143 143 144 144 r.With(middleware.Paginate).Get("/goodfirstissues", s.GoodFirstIssues) 145 145 146 + // XRPC endpoints 147 + r.Get("/xrpc/sh.tangled.repo.listPulls", s.ListPulls) 148 + 146 149 r.With(middleware.AuthMiddleware(s.oauth)).Route("/follow", func(r chi.Router) { 147 150 r.Post("/", s.Follow) 148 151 r.Delete("/", s.Follow)
+26
appview/state/xrpc.go
··· 1 + package state 2 + 3 + import ( 4 + "net/http" 5 + 6 + "tangled.org/core/appview/pulls" 7 + ) 8 + 9 + // ListPulls delegates to the pulls.ListPulls XRPC handler 10 + func (s *State) ListPulls(w http.ResponseWriter, r *http.Request) { 11 + pullsHandler := pulls.New( 12 + s.oauth, 13 + s.repoResolver, 14 + s.pages, 15 + s.idResolver, 16 + s.mentionsResolver, 17 + s.db, 18 + s.config, 19 + s.notifier, 20 + s.enforcer, 21 + s.validator, 22 + s.indexer.Pulls, 23 + s.logger.With("handler", "pulls-xrpc"), 24 + ) 25 + pullsHandler.ListPulls(w, r) 26 + }
+80
lexicons/repo/listPulls.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.repo.listPulls", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "parameters": { 8 + "type": "params", 9 + "required": [ 10 + "repo" 11 + ], 12 + "properties": { 13 + "repo": { 14 + "type": "string", 15 + "format": "at-uri" 16 + }, 17 + "state": { 18 + "type": "string", 19 + "enum": ["open", "closed", "merged"] 20 + } 21 + } 22 + }, 23 + "output": { 24 + "encoding": "application/json", 25 + "schema": { 26 + "type": "object", 27 + "required": [ 28 + "pulls" 29 + ], 30 + "properties": { 31 + "pulls": { 32 + "type": "array", 33 + "items": { 34 + "type": "ref", 35 + "ref": "#pull" 36 + } 37 + } 38 + } 39 + } 40 + } 41 + }, 42 + "pull": { 43 + "type": "object", 44 + "required": [ 45 + "rkey", 46 + "ownerDid", 47 + "pullId", 48 + "title", 49 + "state", 50 + "targetBranch", 51 + "createdAt" 52 + ], 53 + "properties": { 54 + "rkey": { 55 + "type": "string" 56 + }, 57 + "ownerDid": { 58 + "type": "string", 59 + "format": "did" 60 + }, 61 + "pullId": { 62 + "type": "integer" 63 + }, 64 + "title": { 65 + "type": "string" 66 + }, 67 + "state": { 68 + "type": "integer" 69 + }, 70 + "targetBranch": { 71 + "type": "string" 72 + }, 73 + "createdAt": { 74 + "type": "string", 75 + "format": "datetime" 76 + } 77 + } 78 + } 79 + } 80 + }

History

1 round 2 comments
sign up or login to add to the discussion
vitorpy.com submitted #0
1 commit
expand
Add XRPC endpoint to list all PRs targeting a repository
expand 2 comments

Thank you for the contribution!

About having sh.tangled.repo.listPulls xrpc method...

I agree that can be useful, but I don't think it is a necessary api to have. All repo/PR records are public and it is pretty easy to get backlinked records via services like constellation.

So for now, in current state, I think maintaining an extra endpoint for that is not worth doing.

We can definitely have this kind of endpoints for convenience in future, but not now as long as they are doable with constellation.

Well, sure, it would be fairly easy if I maintained a specific appview for tangled-cli... I'll try to find an workaround.

I had pinged @oppi.li on bsky, happy to maintain it myself.

closed without merging