- gitea pkg: CloseIssue, ReopenIssue, CreateIssue, AddComment + ErrForbidden
classification on 401/403. Client.do sets Content-Type on non-empty bodies.
- web handler: POST /i/{path}/issues/{close|reopen|comment|create}
- authorisation guard: repo form value must match a gitea-repo item_link
on the target item (rejects form-crafted writes to unrelated repos)
- HTMX re-renders issues_section partial after each action
- busts gitea per-repo cache (open + closed-recent) and dashboard 60s TTL
- templates: ✓ close button + reopen + collapsible comment box on every
issue row; "+ new issue" disclosure per repo
- design.md §6 retitled "Phase 2.d read; 3h writeback" with auth/perm
semantics + parked list
- 5 unit tests in gitea/, 5 integration tests in web/ covering happy paths
+ 403 → inline banner fallback
99 lines
3.1 KiB
Go
99 lines
3.1 KiB
Go
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
|
|
}
|