package gitea import ( "context" "encoding/json" "fmt" "strconv" "time" ) // Comment mirrors the slice of POST .../comments response that projax needs // for round-trip display. Body + URL are enough; comment edits are out of // scope at v1. type Comment struct { ID int64 `json:"id"` Body string `json:"body"` HTMLURL string `json:"html_url"` CreatedAt time.Time `json:"created_at"` } // CloseIssue sets state=closed on an open issue. Idempotent — closing an // already-closed issue is a no-op upstream (Gitea returns 201 with the // current state echoed back). func (c *Client) CloseIssue(ctx context.Context, owner, repo string, number int) error { return c.patchIssueState(ctx, owner, repo, number, "closed") } // ReopenIssue sets state=open. Same idempotency notes as CloseIssue. func (c *Client) ReopenIssue(ctx context.Context, owner, repo string, number int) error { return c.patchIssueState(ctx, owner, repo, number, "open") } func (c *Client) patchIssueState(ctx context.Context, owner, repo string, number int, state string) error { body, _ := json.Marshal(map[string]string{"state": state}) resp, err := c.do(ctx, "PATCH", "/repos/"+owner+"/"+repo+"/issues/"+strconv.Itoa(number), nil, body) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != 200 && resp.StatusCode != 201 { return readErr(resp, fmt.Sprintf("patch issue state=%s", state)) } return nil } // CreateIssue files a new issue. Returns the upstream Issue shape so the UI // can prepend it to the list without a refetch. func (c *Client) CreateIssue(ctx context.Context, owner, repo, title, body string) (*Issue, error) { payload, _ := json.Marshal(map[string]string{"title": title, "body": body}) resp, err := c.do(ctx, "POST", "/repos/"+owner+"/"+repo+"/issues", nil, payload) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != 201 && resp.StatusCode != 200 { return nil, readErr(resp, "create issue") } var r rawIssue if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { return nil, err } out := Issue{ Number: r.Number, Title: r.Title, State: r.State, HTMLURL: r.HTMLURL, UpdatedAt: r.UpdatedAt, ClosedAt: r.ClosedAt, } for _, l := range r.Labels { out.Labels = append(out.Labels, l.Name) } for _, a := range r.Assignees { out.Assignees = append(out.Assignees, a.Login) } if r.Milestone != nil { out.Milestone = r.Milestone.Title } return &out, nil } // AddComment posts a comment on an issue and returns the created comment. func (c *Client) AddComment(ctx context.Context, owner, repo string, number int, body string) (*Comment, error) { payload, _ := json.Marshal(map[string]string{"body": body}) resp, err := c.do(ctx, "POST", "/repos/"+owner+"/"+repo+"/issues/"+strconv.Itoa(number)+"/comments", nil, payload) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != 201 && resp.StatusCode != 200 { return nil, readErr(resp, "add comment") } var out Comment if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return nil, err } return &out, nil }