Something went wrong. Try again.
A command-line interface for tangled, the decentralized Git collaboration platform built on the AT Protocol. eric-wien.tngl.io/tng-cli
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202package api
import ( "context" "encoding/json" "fmt" "time")
const ( // IssueRecordNSID is the AT Protocol collection for Tangled issues. IssueRecordNSID = "sh.tangled.repo.issue" // IssueCommentNSID is the AT Protocol collection for issue comments. IssueCommentNSID = "sh.tangled.repo.issue.comment" // IssueStateNSID is the AT Protocol collection for issue state changes. IssueStateNSID = "sh.tangled.repo.issue.state"
IssueStateOpen = "sh.tangled.repo.issue.state.open" IssueStateClosed = "sh.tangled.repo.issue.state.closed")
// issueRecord is written to the user's PDS when creating an issue.type issueRecord struct { Type string `json:"$type"` Repo string `json:"repo"` // AT URI of the target repo Title string `json:"title"` Body *string `json:"body,omitempty"` CreatedAt string `json:"createdAt"`}
// issueCommentRecord is written to the user's PDS when commenting on an issue.type issueCommentRecord struct { Type string `json:"$type"` Issue string `json:"issue"` // AT URI of the issue Body string `json:"body"` CreatedAt string `json:"createdAt"` ReplyTo *string `json:"replyTo,omitempty"` // AT URI of parent comment}
// issueStateRecord is written to change an issue's state.type issueStateRecord struct { Type string `json:"$type"` Issue string `json:"issue"` // AT URI of the issue State string `json:"state"` // IssueStateOpen or IssueStateClosed}
// IssueEntry represents a parsed issue record for display.type IssueEntry struct { URI string Rkey string AuthorDID string Repo string // AT URI of the target repo Title string Body string CreatedAt string}
// CreateIssue creates a new issue by writing an AT Protocol record to the user's PDS.// repoURI is the AT URI of the target repository (at://did/sh.tangled.repo/rkey).// Returns the rkey of the created issue.func (c *Client) CreateIssue(ctx context.Context, repoURI, title, body string) (string, error) { rkey := newTID()
var bodyPtr *string if body != "" { bodyPtr = &body }
record := &issueRecord{ Type: IssueRecordNSID, Repo: repoURI, Title: title, Body: bodyPtr, CreatedAt: time.Now().Format(time.RFC3339), }
_, err := c.PutRecord(ctx, IssueRecordNSID, rkey, record) if err != nil { return "", err } return rkey, nil}
// ListOwnIssues lists issues created by the current user. If repoURI is// non-empty, only issues targeting that repo are returned.func (c *Client) ListOwnIssues(ctx context.Context, repoURI string) ([]IssueEntry, error) { records, err := Paginate(ctx, func(cursor string) ([]RecordEntry, string, error) { rl, err := c.ListRecords(ctx, c.did.String(), IssueRecordNSID, cursor, 100) if err != nil { return nil, "", err } return rl.Records, rl.Cursor, nil }) if err != nil { return nil, err }
var issues []IssueEntry for _, rec := range records { var r struct { Repo string `json:"repo"` Title string `json:"title"` Body *string `json:"body"` CreatedAt string `json:"createdAt"` } if err := json.Unmarshal(rec.Value, &r); err != nil { continue }
if repoURI != "" && r.Repo != repoURI { continue }
body := "" if r.Body != nil { body = *r.Body }
issues = append(issues, IssueEntry{ URI: rec.URI, Rkey: RkeyFromURI(rec.URI), AuthorDID: c.did.String(), Repo: r.Repo, Title: r.Title, Body: body, CreatedAt: r.CreatedAt, }) } return issues, nil}
// GetIssueRecord fetches a specific issue record from a user's PDS.func (c *Client) GetIssueRecord(ctx context.Context, authorDID, rkey string) (*IssueEntry, error) { rec, err := c.GetRecord(ctx, authorDID, IssueRecordNSID, rkey) if err != nil { return nil, err }
var r struct { Repo string `json:"repo"` Title string `json:"title"` Body *string `json:"body"` CreatedAt string `json:"createdAt"` } if err := json.Unmarshal(rec.Value, &r); err != nil { return nil, fmt.Errorf("failed to parse issue record: %w", err) }
body := "" if r.Body != nil { body = *r.Body }
return &IssueEntry{ URI: rec.URI, Rkey: rkey, AuthorDID: authorDID, Repo: r.Repo, Title: r.Title, Body: body, CreatedAt: r.CreatedAt, }, nil}
// CloseIssue writes an issue state record to close the issue.// issueURI is the AT URI of the issue (at://did/sh.tangled.repo.issue/rkey).func (c *Client) CloseIssue(ctx context.Context, issueURI string) error { rkey := newTID()
record := &issueStateRecord{ Type: IssueStateNSID, Issue: issueURI, State: IssueStateClosed, }
_, err := c.PutRecord(ctx, IssueStateNSID, rkey, record) return err}
// CreateIssueComment adds a comment to an issue.// issueURI is the AT URI of the issue.func (c *Client) CreateIssueComment(ctx context.Context, issueURI, body string) error { rkey := newTID()
record := &issueCommentRecord{ Type: IssueCommentNSID, Issue: issueURI, Body: body, CreatedAt: time.Now().Format(time.RFC3339), }
_, err := c.PutRecord(ctx, IssueCommentNSID, rkey, record) return err}
// FormatIssueURL returns the web URL for an issue on tangled.sh.// Since the appview uses sequential numbers we can't map directly,// so we link to the repo's issues page.func FormatIssueURL(repo string) string { return fmt.Sprintf("https://tangled.sh/%s/issues", repo)}