diff --git a/internal/cli/issue_list.go b/internal/cli/issue_list.go index df6eb92..93a9186 100644 --- a/internal/cli/issue_list.go +++ b/internal/cli/issue_list.go @@ -1,12 +1,8 @@ package cli import ( - "context" - "encoding/json" "fmt" - "strings" - "github.com/alyraffauf/tg/internal/gitutil" "github.com/alyraffauf/tg/tangled" "github.com/spf13/cobra" ) @@ -32,110 +28,16 @@ If no argument is given, the command detects the repository from the return err } - issues, err := client.ListIssues(ctx, repoDid, tangled.IssueListOpts{ + issues, err := client.ListIssues(ctx, repoDid, tangled.ListOpts{ Limit: defaultListLimit, }) if err != nil { return fmt.Errorf("list issues for %q: %w", repo, err) } - items := buildIssueItems(ctx, issues.Items) - return output(items, renderIssueList) - }, -} - -func parseHandleRepo(arg string) (string, string, error) { - parts := strings.SplitN(arg, "/", 2) - if len(parts) != 2 || parts[0] == "" || parts[1] == "" { - return "", "", fmt.Errorf("expected handle/repo, got %q", arg) - } - return parts[0], parts[1], nil -} - -// resolveTarget returns the handle and repo name from an explicit -// "handle/repo" argument or by detecting the git remote in the CWD. -func resolveTarget(ctx context.Context, args []string) (string, string, error) { - if len(args) == 1 { - return parseHandleRepo(args[0]) - } - - rc, err := gitutil.DetectRepoFromCWD(ctx) - if err != nil { - return "", "", fmt.Errorf("detect repo from current directory: %w", err) - } - return rc.Handle, rc.Repo, nil -} - -// findRepoDid resolves handle/repo to the repo's repoDid, which listIssues is -// keyed by. It looks the record up directly by name (current schema uses the -// name as the rkey), falling back to a listing for legacy repos whose rkey is a -// TID with the name in the body. -func findRepoDid(ctx context.Context, handle, repo string) (string, error) { - ident, err := resolver.ResolveHandle(ctx, handle) - if err != nil { - return "", fmt.Errorf("resolve handle %q: %w", handle, err) - } - - repoURI := fmt.Sprintf("at://%s/sh.tangled.repo/%s", ident.DID, repo) - if got, err := client.GetRepo(ctx, repoURI); err == nil { - return got.Value.RepoDid, nil - } - - if repos, err := client.ListRepos(ctx, ident.DID.String()); err == nil { - for _, item := range repos.Items { - if item.Value.Name == repo || strings.HasSuffix(item.URI, "/"+repo) { - return item.Value.RepoDid, nil - } - } - } - - return "", fmt.Errorf("repo %q not found for handle %q", repo, handle) -} - -func buildIssueItems(ctx context.Context, items []tangled.IssueListItem) []issueItem { - result := make([]issueItem, 0, len(items)) - - for _, item := range items { - var record tangled.IssueRecord - if err := json.Unmarshal(item.Value, &record); err != nil { - continue - } - - updated := item.StateUpdatedAt - if updated == "" { - updated = record.CreatedAt - } - - title := record.Title - if title == "" { - title = "(no title)" - } - - result = append(result, issueItem{ - Rkey: extractRKey(item.URI), - URI: item.URI, - Title: title, - State: item.State, - Author: resolveAuthor(ctx, extractDID(item.URI)), - CreatedAt: record.CreatedAt, - UpdatedAt: updated, - CommentCount: item.CommentCount, - }) - } - - return result -} - -func renderIssueList(items []issueItem) { - rows := make([]listRow, 0, len(items)) - for _, item := range items { - rows = append(rows, listRow{ - rkey: item.Rkey, - title: item.Title, - state: item.State, - author: item.Author.Handle, - updated: shortDate(item.UpdatedAt), + items := buildItems(ctx, issues.Items, decodeIssue) + return output(items, func(items []item) { + renderList(items, "No issues found.") }) - } - renderRows(rows, "No issues found.") + }, } diff --git a/internal/cli/issue_view.go b/internal/cli/issue_view.go index f89c96e..66c6465 100644 --- a/internal/cli/issue_view.go +++ b/internal/cli/issue_view.go @@ -1,9 +1,7 @@ package cli import ( - "encoding/json" "fmt" - "strings" "github.com/alyraffauf/tg/tangled" "github.com/spf13/cobra" @@ -37,26 +35,30 @@ directory's git origin remote.`, return err } - issues, err := client.ListIssues(ctx, repoDid, tangled.IssueListOpts{ + issues, err := client.ListIssues(ctx, repoDid, tangled.ListOpts{ Limit: defaultListLimit, }) if err != nil { return fmt.Errorf("list issues for %s/%s: %w", handle, repo, err) } - issue, authorDID, err := findIssueByRKey(issues.Items, rkey) + found, err := findByRKey(issues.Items, rkey, "issue") if err != nil { return err } + decoded, err := decodeIssue(found.Value) + if err != nil { + return fmt.Errorf("decode issue %q: %w", rkey, err) + } - result := issueViewResult{ + result := viewResult{ Rkey: rkey, - Title: issue.Title, - Body: issue.Body, - Author: resolveAuthor(ctx, authorDID), - CreatedAt: issue.CreatedAt, + Title: decoded.Title, + Body: decoded.Body, + Author: resolveAuthor(ctx, extractDID(found.URI)), + CreatedAt: decoded.CreatedAt, } - return output(result, func(view issueViewResult) { + return output(result, func(view viewResult) { fmt.Printf("Title: %s\n", view.Title) fmt.Printf("Author: %s\n", view.Author.Handle) fmt.Printf("Created: %s\n", view.CreatedAt) @@ -70,17 +72,3 @@ directory's git origin remote.`, func init() { issueViewCmd.Flags().StringVarP(&issueViewRepo, "repo", "R", "", "Target repository as handle/repo") } - -func findIssueByRKey(items []tangled.IssueListItem, rkey string) (*tangled.IssueRecord, string, error) { - for _, item := range items { - if !strings.HasSuffix(item.URI, "/"+rkey) { - continue - } - var issue tangled.IssueRecord - if err := json.Unmarshal(item.Value, &issue); err != nil { - return nil, "", fmt.Errorf("decode issue %q: %w", rkey, err) - } - return &issue, extractDID(item.URI), nil - } - return nil, "", fmt.Errorf("issue %q not found", rkey) -} diff --git a/internal/cli/output.go b/internal/cli/output.go index a0ab942..308ba5d 100644 --- a/internal/cli/output.go +++ b/internal/cli/output.go @@ -22,18 +22,9 @@ type author struct { Handle string `json:"handle"` } -type issueItem struct { - Rkey string `json:"rkey"` - URI string `json:"uri"` - Title string `json:"title"` - State string `json:"state"` - Author author `json:"author"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt,omitempty"` - CommentCount int64 `json:"commentCount"` -} - -type pullItem struct { +// item is a listing entry for an issue or a pull request. SourceBranch and +// TargetBranch are only populated (and only emitted as JSON) for pulls. +type item struct { Rkey string `json:"rkey"` URI string `json:"uri"` Title string `json:"title"` @@ -43,7 +34,7 @@ type pullItem struct { UpdatedAt string `json:"updatedAt,omitempty"` CommentCount int64 `json:"commentCount"` SourceBranch string `json:"sourceBranch,omitempty"` - TargetBranch string `json:"targetBranch"` + TargetBranch string `json:"targetBranch,omitempty"` } type repoItem struct { @@ -63,22 +54,16 @@ type sshKeyItem struct { URI string `json:"uri"` } -type issueViewResult struct { - Rkey string `json:"rkey"` - Title string `json:"title"` - Body string `json:"body,omitempty"` - Author author `json:"author"` - CreatedAt string `json:"createdAt"` -} - -type prViewResult struct { +// viewResult is a single issue or pull request. SourceBranch and +// TargetBranch are only populated (and only emitted as JSON) for pulls. +type viewResult struct { Rkey string `json:"rkey"` Title string `json:"title"` Body string `json:"body,omitempty"` Author author `json:"author"` CreatedAt string `json:"createdAt"` SourceBranch string `json:"sourceBranch,omitempty"` - TargetBranch string `json:"targetBranch"` + TargetBranch string `json:"targetBranch,omitempty"` } type repoCreateResult struct { diff --git a/internal/cli/pr_checkout.go b/internal/cli/pr_checkout.go index b79f262..9623df2 100644 --- a/internal/cli/pr_checkout.go +++ b/internal/cli/pr_checkout.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "os" - "strings" "github.com/alyraffauf/tg/internal/gitutil" "github.com/alyraffauf/tg/tangled" @@ -34,17 +33,23 @@ Must be run from inside a cloned Tangled repository.`, return err } - pulls, err := client.ListPulls(ctx, repoDid, tangled.PullListOpts{ + pulls, err := client.ListPulls(ctx, repoDid, tangled.ListOpts{ Limit: defaultListLimit, }) if err != nil { return fmt.Errorf("list pulls for %q: %w", repo, err) } - pr, authorDID, err := findPullByRKey(pulls.Items, prRKey) + found, err := findByRKey(pulls.Items, prRKey, "pull request") if err != nil { return err } + var pr tangled.PullRecord + if err := json.Unmarshal(found.Value, &pr); err != nil { + return fmt.Errorf("decode pull request %q: %w", prRKey, err) + } + authorDID := extractDID(found.URI) + if len(pr.Rounds) == 0 { return fmt.Errorf("pull request %q has no rounds", prRKey) } @@ -87,18 +92,3 @@ Must be run from inside a cloned Tangled repository.`, }) }, } - -func findPullByRKey(items []tangled.PullListItem, rkey string) (*tangled.PullRecord, string, error) { - for _, item := range items { - if !strings.HasSuffix(item.URI, "/"+rkey) { - continue - } - - var pr tangled.PullRecord - if err := json.Unmarshal(item.Value, &pr); err != nil { - return nil, "", fmt.Errorf("decode pull record %q: %w", rkey, err) - } - return &pr, extractDID(item.URI), nil - } - return nil, "", fmt.Errorf("pull request %q not found", rkey) -} diff --git a/internal/cli/pr_list.go b/internal/cli/pr_list.go index 3dc5550..5aa5208 100644 --- a/internal/cli/pr_list.go +++ b/internal/cli/pr_list.go @@ -1,8 +1,6 @@ package cli import ( - "context" - "encoding/json" "fmt" "github.com/alyraffauf/tg/tangled" @@ -30,64 +28,16 @@ If no argument is given, the command detects the repository from the return err } - pulls, err := client.ListPulls(ctx, repoDid, tangled.PullListOpts{ + pulls, err := client.ListPulls(ctx, repoDid, tangled.ListOpts{ Limit: defaultListLimit, }) if err != nil { return fmt.Errorf("list PRs for %q: %w", repo, err) } - items := buildPullItems(ctx, pulls.Items) - return output(items, renderPullList) - }, -} - -func buildPullItems(ctx context.Context, items []tangled.PullListItem) []pullItem { - result := make([]pullItem, 0, len(items)) - - for _, item := range items { - var record tangled.PullRecord - if err := json.Unmarshal(item.Value, &record); err != nil { - continue - } - - updated := item.StateUpdatedAt - if updated == "" { - updated = record.CreatedAt - } - - title := record.Title - if title == "" { - title = "(no title)" - } - - result = append(result, pullItem{ - Rkey: extractRKey(item.URI), - URI: item.URI, - Title: title, - State: item.State, - Author: resolveAuthor(ctx, extractDID(item.URI)), - CreatedAt: record.CreatedAt, - UpdatedAt: updated, - CommentCount: item.CommentCount, - SourceBranch: record.Source.Branch, - TargetBranch: record.Target.Branch, + items := buildItems(ctx, pulls.Items, decodePull) + return output(items, func(items []item) { + renderList(items, "No pull requests found.") }) - } - - return result -} - -func renderPullList(items []pullItem) { - rows := make([]listRow, 0, len(items)) - for _, item := range items { - rows = append(rows, listRow{ - rkey: item.Rkey, - title: item.Title, - state: item.State, - author: item.Author.Handle, - updated: shortDate(item.UpdatedAt), - }) - } - renderRows(rows, "No pull requests found.") + }, } diff --git a/internal/cli/pr_view.go b/internal/cli/pr_view.go index ea4a8f3..a8a127b 100644 --- a/internal/cli/pr_view.go +++ b/internal/cli/pr_view.go @@ -35,28 +35,32 @@ directory's git origin remote.`, return err } - pulls, err := client.ListPulls(ctx, repoDid, tangled.PullListOpts{ + pulls, err := client.ListPulls(ctx, repoDid, tangled.ListOpts{ Limit: defaultListLimit, }) if err != nil { return fmt.Errorf("list PRs for %s/%s: %w", handle, repo, err) } - pr, authorDID, err := findPullByRKey(pulls.Items, rkey) + found, err := findByRKey(pulls.Items, rkey, "pull request") if err != nil { return err } + decoded, err := decodePull(found.Value) + if err != nil { + return fmt.Errorf("decode pull request %q: %w", rkey, err) + } - result := prViewResult{ + result := viewResult{ Rkey: rkey, - Title: pr.Title, - Body: pr.Body, - Author: resolveAuthor(ctx, authorDID), - CreatedAt: pr.CreatedAt, - SourceBranch: pr.Source.Branch, - TargetBranch: pr.Target.Branch, + Title: decoded.Title, + Body: decoded.Body, + Author: resolveAuthor(ctx, extractDID(found.URI)), + CreatedAt: decoded.CreatedAt, + SourceBranch: decoded.SourceBranch, + TargetBranch: decoded.TargetBranch, } - return output(result, func(view prViewResult) { + return output(result, func(view viewResult) { fmt.Printf("Title: %s\n", view.Title) fmt.Printf("Author: %s\n", view.Author.Handle) fmt.Printf("Created: %s\n", view.CreatedAt) diff --git a/internal/cli/repo_list.go b/internal/cli/repo_list.go index 61a2515..4546e48 100644 --- a/internal/cli/repo_list.go +++ b/internal/cli/repo_list.go @@ -3,9 +3,7 @@ package cli import ( "context" "fmt" - "os" "strings" - "text/tabwriter" "github.com/alyraffauf/tg/internal/gitutil" "github.com/alyraffauf/tg/tangled" @@ -76,23 +74,23 @@ func resolveHandleOrSelf(ctx context.Context, args []string) (string, error) { func buildRepoItems(items []tangled.Repo, author string) []repoItem { result := make([]repoItem, 0, len(items)) - for _, item := range items { - name := item.Value.Name + for _, tangledRepo := range items { + name := tangledRepo.Value.Name if name == "" { // Fall back to the rkey segment of the at:// URI. - if idx := strings.LastIndex(item.URI, "/"); idx != -1 { - name = item.URI[idx+1:] + if idx := strings.LastIndex(tangledRepo.URI, "/"); idx != -1 { + name = tangledRepo.URI[idx+1:] } } result = append(result, repoItem{ Name: name, - URI: item.URI, + URI: tangledRepo.URI, Author: author, - Knot: item.Value.Knot, - Description: item.Value.Description, - CreatedAt: item.Value.CreatedAt, - RepoDid: item.Value.RepoDid, + Knot: tangledRepo.Value.Knot, + Description: tangledRepo.Value.Description, + CreatedAt: tangledRepo.Value.CreatedAt, + RepoDid: tangledRepo.Value.RepoDid, }) } @@ -100,16 +98,9 @@ func buildRepoItems(items []tangled.Repo, author string) []repoItem { } func renderRepoList(items []repoItem) { - if len(items) == 0 { - fmt.Println("No repositories found.") - return + rows := make([][]string, 0, len(items)) + for _, repo := range items { + rows = append(rows, []string{repo.Name, repo.Knot, repo.Description, shortDate(repo.CreatedAt)}) } - - tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', tabwriter.TabIndent) - fmt.Fprintln(tw, "NAME\tKNOT\tDESCRIPTION\tCREATED") - - for _, item := range items { - fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", item.Name, item.Knot, item.Description, shortDate(item.CreatedAt)) - } - tw.Flush() + renderTable([]string{"NAME", "KNOT", "DESCRIPTION", "CREATED"}, rows, "No repositories found.") } diff --git a/internal/cli/repo_view.go b/internal/cli/repo_view.go index 2246e16..05b4e29 100644 --- a/internal/cli/repo_view.go +++ b/internal/cli/repo_view.go @@ -11,7 +11,7 @@ var repoViewCmd = &cobra.Command{ Use: "view ", Short: "View a Tangled repository", Long: `View details for a Tangled repository.`, - Args: cobra.ExactArgs(1), + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/internal/cli/rows.go b/internal/cli/rows.go index c4f6b55..fb3ae3a 100644 --- a/internal/cli/rows.go +++ b/internal/cli/rows.go @@ -2,23 +2,17 @@ package cli import ( "context" + "encoding/json" "fmt" "os" "strings" "text/tabwriter" + + "github.com/alyraffauf/tg/tangled" ) const defaultListLimit int64 = 100 -// listRow is display-ready data for one issue or PR. -type listRow struct { - rkey string - title string - state string - author string - updated string -} - // shortDate trims an ISO 8601 timestamp to its YYYY-MM-DD prefix. func shortDate(timestamp string) string { if len(timestamp) > 10 { @@ -27,19 +21,19 @@ func shortDate(timestamp string) string { return timestamp } -// renderRows writes a table of rows to stdout. emptyMessage is shown -// when rows has no entries. -func renderRows(rows []listRow, emptyMessage string) { +// renderTable writes a tab-aligned table of rows to stdout under header. +// emptyMessage is shown when rows has no entries. Every renderer in this +// package (issues, pulls, repos, SSH keys) goes through this. +func renderTable(header []string, rows [][]string, emptyMessage string) { if len(rows) == 0 { fmt.Println(emptyMessage) return } tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', tabwriter.TabIndent) - fmt.Fprintln(tw, "RKEY\tTITLE\tSTATE\tAUTHOR\tUPDATED") - + fmt.Fprintln(tw, strings.Join(header, "\t")) for _, row := range rows { - fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n", row.rkey, row.title, row.state, row.author, row.updated) + fmt.Fprintln(tw, strings.Join(row, "\t")) } tw.Flush() } @@ -68,3 +62,94 @@ func resolveAuthor(ctx context.Context, did string) author { } return result } + +// recordView is the fields common to an issue or pull-request record, +// as decoded from a tangled.ListItem's raw Value. +type recordView struct { + Title string + Body string + CreatedAt string + SourceBranch string + TargetBranch string +} + +func decodeIssue(raw json.RawMessage) (recordView, error) { + var r tangled.IssueRecord + if err := json.Unmarshal(raw, &r); err != nil { + return recordView{}, err + } + return recordView{Title: r.Title, Body: r.Body, CreatedAt: r.CreatedAt}, nil +} + +func decodePull(raw json.RawMessage) (recordView, error) { + var r tangled.PullRecord + if err := json.Unmarshal(raw, &r); err != nil { + return recordView{}, err + } + return recordView{ + Title: r.Title, + Body: r.Body, + CreatedAt: r.CreatedAt, + SourceBranch: r.Source.Branch, + TargetBranch: r.Target.Branch, + }, nil +} + +// buildItems decodes a listing's items into display/JSON-ready items, +// silently skipping any whose Value fails to decode. decode is +// decodeIssue or decodePull depending on the resource being listed. +func buildItems(ctx context.Context, items []tangled.ListItem, decode func(json.RawMessage) (recordView, error)) []item { + result := make([]item, 0, len(items)) + + for _, listItem := range items { + decoded, err := decode(listItem.Value) + if err != nil { + continue + } + + updated := listItem.StateUpdatedAt + if updated == "" { + updated = decoded.CreatedAt + } + + title := decoded.Title + if title == "" { + title = "(no title)" + } + + result = append(result, item{ + Rkey: extractRKey(listItem.URI), + URI: listItem.URI, + Title: title, + State: listItem.State, + Author: resolveAuthor(ctx, extractDID(listItem.URI)), + CreatedAt: decoded.CreatedAt, + UpdatedAt: updated, + CommentCount: listItem.CommentCount, + SourceBranch: decoded.SourceBranch, + TargetBranch: decoded.TargetBranch, + }) + } + + return result +} + +// renderList renders issue or pull-request items as a table. +func renderList(items []item, emptyMessage string) { + rows := make([][]string, 0, len(items)) + for _, it := range items { + rows = append(rows, []string{it.Rkey, it.Title, it.State, it.Author.Handle, shortDate(it.UpdatedAt)}) + } + renderTable([]string{"RKEY", "TITLE", "STATE", "AUTHOR", "UPDATED"}, rows, emptyMessage) +} + +// findByRKey finds the listing item whose URI ends in "/"+rkey. what names +// the resource kind (e.g. "issue", "pull request") for the error message. +func findByRKey(items []tangled.ListItem, rkey, what string) (*tangled.ListItem, error) { + for i := range items { + if strings.HasSuffix(items[i].URI, "/"+rkey) { + return &items[i], nil + } + } + return nil, fmt.Errorf("%s %q not found", what, rkey) +} diff --git a/internal/cli/ssh_key_list.go b/internal/cli/ssh_key_list.go index beaa794..5de9a56 100644 --- a/internal/cli/ssh_key_list.go +++ b/internal/cli/ssh_key_list.go @@ -3,8 +3,6 @@ package cli import ( "encoding/json" "fmt" - "os" - "text/tabwriter" "github.com/alyraffauf/tg/atproto" "github.com/bluesky-social/indigo/atproto/atclient" @@ -70,16 +68,9 @@ func buildSSHKeyItems(records []atproto.RecordItem) []sshKeyItem { } func renderSSHKeyList(items []sshKeyItem) { - if len(items) == 0 { - fmt.Println("No SSH keys found.") - return + rows := make([][]string, 0, len(items)) + for _, key := range items { + rows = append(rows, []string{key.Name, key.Key, shortDate(key.CreatedAt)}) } - - tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', tabwriter.TabIndent) - fmt.Fprintln(tw, "NAME\tKEY\tADDED") - - for _, item := range items { - fmt.Fprintf(tw, "%s\t%s\t%s\n", item.Name, item.Key, shortDate(item.CreatedAt)) - } - tw.Flush() + renderTable([]string{"NAME", "KEY", "ADDED"}, rows, "No SSH keys found.") } diff --git a/internal/cli/target.go b/internal/cli/target.go new file mode 100644 index 0000000..c95d8de --- /dev/null +++ b/internal/cli/target.go @@ -0,0 +1,57 @@ +package cli + +import ( + "context" + "fmt" + "strings" + + "github.com/alyraffauf/tg/internal/gitutil" +) + +func parseHandleRepo(arg string) (string, string, error) { + parts := strings.SplitN(arg, "/", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf("expected handle/repo, got %q", arg) + } + return parts[0], parts[1], nil +} + +// resolveTarget returns the handle and repo name from an explicit +// "handle/repo" argument or by detecting the git remote in the CWD. +func resolveTarget(ctx context.Context, args []string) (string, string, error) { + if len(args) == 1 { + return parseHandleRepo(args[0]) + } + + rc, err := gitutil.DetectRepoFromCWD(ctx) + if err != nil { + return "", "", fmt.Errorf("detect repo from current directory: %w", err) + } + return rc.Handle, rc.Repo, nil +} + +// findRepoDid resolves handle/repo to the repo's repoDid, which listIssues is +// keyed by. It looks the record up directly by name (current schema uses the +// name as the rkey), falling back to a listing for legacy repos whose rkey is a +// TID with the name in the body. +func findRepoDid(ctx context.Context, handle, repo string) (string, error) { + ident, err := resolver.ResolveHandle(ctx, handle) + if err != nil { + return "", fmt.Errorf("resolve handle %q: %w", handle, err) + } + + repoURI := fmt.Sprintf("at://%s/sh.tangled.repo/%s", ident.DID, repo) + if got, err := client.GetRepo(ctx, repoURI); err == nil { + return got.Value.RepoDid, nil + } + + if repos, err := client.ListRepos(ctx, ident.DID.String()); err == nil { + for _, candidate := range repos.Items { + if candidate.Value.Name == repo || strings.HasSuffix(candidate.URI, "/"+repo) { + return candidate.Value.RepoDid, nil + } + } + } + + return "", fmt.Errorf("repo %q not found for handle %q", repo, handle) +} diff --git a/tangled/list.go b/tangled/list.go new file mode 100644 index 0000000..ccb2907 --- /dev/null +++ b/tangled/list.go @@ -0,0 +1,47 @@ +package tangled + +import "encoding/json" + +// ListItem is one item in an issue or pull-request listing. +type ListItem struct { + URI string `json:"uri"` + CID string `json:"cid,omitempty"` + Value json.RawMessage `json:"value"` + State string `json:"state"` + StateUpdatedAt string `json:"stateUpdatedAt,omitempty"` + CommentCount int64 `json:"commentCount"` +} + +// List is a page of issues or pull requests. +type List struct { + Items []ListItem `json:"items"` + Cursor *string `json:"cursor"` +} + +// ListOpts are the query parameters shared by ListIssues and ListPulls. +type ListOpts struct { + Author string // only items by this DID + State string // "open" or "closed" + Limit int64 // 1-1000, default 50 + Order string // "asc" or "desc" +} + +// params builds the XRPC query parameters for subject. +func (o ListOpts) params(subject string) map[string]any { + params := map[string]any{"subject": subject} + if o.Author != "" { + params["author"] = o.Author + } + if o.State != "" { + params["state"] = o.State + } + if o.Limit > 0 { + params["limit"] = o.Limit + } else { + params["limit"] = 50 + } + if o.Order != "" { + params["order"] = o.Order + } + return params +} diff --git a/tangled/list_issues.go b/tangled/list_issues.go index 6cfafea..b48081f 100644 --- a/tangled/list_issues.go +++ b/tangled/list_issues.go @@ -2,7 +2,6 @@ package tangled import ( "context" - "encoding/json" "fmt" "github.com/bluesky-social/indigo/atproto/syntax" @@ -18,49 +17,9 @@ type IssueRecord struct { References []string `json:"references,omitempty"` } -type IssueListItem struct { - URI string `json:"uri"` - CID string `json:"cid,omitempty"` - Value json.RawMessage `json:"value"` - State string `json:"state"` - StateUpdatedAt string `json:"stateUpdatedAt,omitempty"` - CommentCount int64 `json:"commentCount"` -} - -type IssueList struct { - Items []IssueListItem `json:"items"` - Cursor *string `json:"cursor"` -} - -type IssueListOpts struct { - Author string // only issues by this DID - State string // "open" or "closed" - Limit int64 // 1-1000, default 50 - Order string // "asc" or "desc" -} - -func (t *Tangled) ListIssues(ctx context.Context, repoDid string, opts IssueListOpts) (*IssueList, error) { - params := map[string]any{ - "subject": repoDid, - } - if opts.Author != "" { - params["author"] = opts.Author - } - if opts.State != "" { - params["state"] = opts.State - } - if opts.Limit > 0 { - params["limit"] = opts.Limit - } else { - params["limit"] = 50 - } - if opts.Order != "" { - params["order"] = opts.Order - } - - var out IssueList - err := t.Client.Get(ctx, syntax.NSID("sh.tangled.repo.listIssues"), params, &out) - if err != nil { +func (t *Tangled) ListIssues(ctx context.Context, repoDid string, opts ListOpts) (*List, error) { + var out List + if err := t.Client.Get(ctx, syntax.NSID("sh.tangled.repo.listIssues"), opts.params(repoDid), &out); err != nil { return nil, fmt.Errorf("list issues for %q: %w", repoDid, err) } return &out, nil diff --git a/tangled/list_pulls.go b/tangled/list_pulls.go index a5d9372..7257f4b 100644 --- a/tangled/list_pulls.go +++ b/tangled/list_pulls.go @@ -2,7 +2,6 @@ package tangled import ( "context" - "encoding/json" "fmt" "github.com/bluesky-social/indigo/atproto/atdata" @@ -45,49 +44,9 @@ type PatchBlob struct { Size int64 `json:"size"` } -type PullListItem struct { - URI string `json:"uri"` - CID string `json:"cid,omitempty"` - Value json.RawMessage `json:"value"` - State string `json:"state"` - StateUpdatedAt string `json:"stateUpdatedAt,omitempty"` - CommentCount int64 `json:"commentCount"` -} - -type PullList struct { - Items []PullListItem `json:"items"` - Cursor *string `json:"cursor"` -} - -type PullListOpts struct { - Author string // only pulls by this DID - State string // "open" or "closed" - Limit int64 // 1-1000, default 50 - Order string // "asc" or "desc" -} - -func (t *Tangled) ListPulls(ctx context.Context, repoDid string, opts PullListOpts) (*PullList, error) { - params := map[string]any{ - "subject": repoDid, - } - if opts.Author != "" { - params["author"] = opts.Author - } - if opts.State != "" { - params["state"] = opts.State - } - if opts.Limit > 0 { - params["limit"] = opts.Limit - } else { - params["limit"] = 50 - } - if opts.Order != "" { - params["order"] = opts.Order - } - - var out PullList - err := t.Client.Get(ctx, syntax.NSID("sh.tangled.repo.listPulls"), params, &out) - if err != nil { +func (t *Tangled) ListPulls(ctx context.Context, repoDid string, opts ListOpts) (*List, error) { + var out List + if err := t.Client.Get(ctx, syntax.NSID("sh.tangled.repo.listPulls"), opts.params(repoDid), &out); err != nil { return nil, fmt.Errorf("list PRs for %q: %w", repoDid, err) } return &out, nil