package gitea import ( "fmt" "net/http" "strconv" "strings" "git.noodles.cam/claude-skills/marketplace/cli/internal/wire" ) // ListLabels is every label in the repository, every page of it. // // A bootstrap decides its plan against this and never against a cache: a cache // answers "what did we create last time", and the question is "what does the // repository have right now". func (c *Client) ListLabels() ([]wire.Label, error) { return paginate[wire.Label](c, c.repoPath("labels"), 100) } // CreateLabel adds a label to the repository. // // Through the API rather than through any CLI wrapper, because `exclusive` — // the flag that makes `type/*` behave like a single choice — is not something // the `tea` client could set. // // What a label MEANS is not decided here either: this creates what it is // handed. func (c *Client) CreateLabel(req wire.LabelRequest) (*wire.Label, error) { var got wire.Label body := &Body{Name: "label-" + req.Name, Data: req} if err := c.Call(http.MethodPost, c.repoPath("labels"), body, &got); err != nil { return nil, err } if got.ID == 0 { return nil, fmt.Errorf("creating label %q: the tracker's answer carries no id", req.Name) } return &got, nil } // EditLabel patches an existing label by id. func (c *Client) EditLabel(id int64, req wire.LabelRequest) (*wire.Label, error) { var got wire.Label body := &Body{Name: "label-" + req.Name, Data: req} if err := c.Call(http.MethodPatch, c.repoPathf("labels/%d", id), body, &got); err != nil { return nil, err } return &got, nil } // ListMilestones is every milestone in the repository, open and closed. // // Both states, always: a milestone is closed the moment its work is done, and a // listing that hid those would fail to resolve exactly the filter somebody // types when they want to see what was in it. func (c *Client) ListMilestones() ([]wire.Milestone, error) { return paginate[wire.Milestone](c, c.repoPath("milestones?state=all"), 100) } // ResolveMilestone finds a milestone by id or by title, and fails when there is // none. // // It fails LOUDLY, and that is the whole point of resolving before filtering: // Gitea silently ignores a `milestones=` filter it cannot resolve and answers // with the entire backlog. A typo in a milestone name would otherwise read as // "your milestone has 300 issues in it". func (c *Client) ResolveMilestone(value string) (*wire.Milestone, error) { got, err := c.ListMilestones() if err != nil { return nil, err } for i := range got { if got[i].Title == value || strconv.FormatInt(got[i].ID, 10) == value { return &got[i], nil } } have := make([]string, 0, len(got)) for _, m := range got { have = append(have, fmt.Sprintf("%s (id %d)", m.Title, m.ID)) } if len(have) == 0 { have = []string{"none"} } return nil, fmt.Errorf("no milestone %q in %s — have: %s", value, c.repo, strings.Join(have, ", ")) } // FindMilestone is the milestone with this title, or nil when the repository // has no such milestone. // // The quiet counterpart of ResolveMilestone, for a push: an issue naming a // milestone the tracker does not have is filed without one, because refusing // the whole push over a field the tracker will happily accept as empty helps // nobody. "none" and "" are both "no milestone". func (c *Client) FindMilestone(title string) (*wire.Milestone, error) { if title == "" || title == "none" { return nil, nil } got, err := c.ListMilestones() if err != nil { return nil, err } for i := range got { if got[i].Title == title { return &got[i], nil } } return nil, nil }