package gitea import ( "fmt" "strconv" "strings" sdk "code.gitea.io/sdk/gitea" ) // 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() ([]*sdk.Label, error) { owner, repo := c.owned() return paginate(func(page, limit int) ([]*sdk.Label, error) { got, resp, err := c.api.ListRepoLabels(owner, repo, sdk.ListLabelsOptions{ListOptions: listOptions(page, limit)}) return got, fail(resp, err) }, 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(opt sdk.CreateLabelOption) (*sdk.Label, error) { owner, repo := c.owned() c.dump.label("label-" + opt.Name) got, resp, err := c.api.CreateLabel(owner, repo, opt) if err := fail(resp, err); err != nil { return nil, err } if got == nil || got.ID == 0 { return nil, fmt.Errorf("creating label %q: the tracker's answer carries no id", opt.Name) } return got, nil } // EditLabel patches an existing label by id. // // It takes the same spec a create takes, and sends every field of it. The SDK // spells an edit with pointers, where nil means "leave it alone" — but a label // edit is rare enough that sending the unchanged name and description along // costs nothing and removes a way to lose them on a server that reads an absent // field as empty. The caller decides what the label should BE; this makes the // tracker say that and nothing less. func (c *Client) EditLabel(id int64, opt sdk.CreateLabelOption) (*sdk.Label, error) { owner, repo := c.owned() c.dump.label("label-" + opt.Name) got, resp, err := c.api.EditLabel(owner, repo, id, sdk.EditLabelOption{ Name: &opt.Name, Color: &opt.Color, Description: &opt.Description, Exclusive: &opt.Exclusive, }) if err := fail(resp, err); 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() ([]*sdk.Milestone, error) { owner, repo := c.owned() return paginate(func(page, limit int) ([]*sdk.Milestone, error) { got, resp, err := c.api.ListRepoMilestones(owner, repo, sdk.ListMilestoneOption{ ListOptions: listOptions(page, limit), State: sdk.StateAll, }) return got, fail(resp, err) }, 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". // // Against the whole listing rather than the SDK's GetMilestoneByName, because a // failure has to say what the repository actually HAS — and because that helper // matches case-insensitively, which would resolve two different milestones to // one on a repository that has both. func (c *Client) ResolveMilestone(value string) (*sdk.Milestone, error) { got, err := c.ListMilestones() if err != nil { return nil, err } for _, m := range got { if m.Title == value || strconv.FormatInt(m.ID, 10) == value { return m, 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) (*sdk.Milestone, error) { if title == "" || title == "none" { return nil, nil } got, err := c.ListMilestones() if err != nil { return nil, err } for _, m := range got { if m.Title == title { return m, nil } } return nil, nil }