Federated via graft
0

Configure Feed

Select the types of activity you want to include in your feed.

1// Package forgejo is a minimal client for the subset of the Forgejo REST 2// API the sync daemon needs: reading/creating issues and pull requests, 3// and resolving a repository's clone URL and default branch. 4package forgejo 5 6import ( 7 "bytes" 8 "encoding/base64" 9 "encoding/json" 10 "fmt" 11 "io" 12 "net/http" 13 "strings" 14 "time" 15) 16 17// Client talks to one Forgejo instance on behalf of one repository. 18type Client struct { 19 baseURL string 20 owner string 21 repo string 22 token string 23 http *http.Client 24} 25 26func New(baseURL, owner, repo, token string) *Client { 27 return &Client{ 28 baseURL: strings.TrimRight(baseURL, "/"), 29 owner: owner, 30 repo: repo, 31 token: token, 32 http: &http.Client{Timeout: 30 * time.Second}, 33 } 34} 35 36func (c *Client) do(method, path string, body any, out any) error { 37 var reqBody io.Reader 38 if body != nil { 39 b, err := json.Marshal(body) 40 if err != nil { 41 return fmt.Errorf("encode request: %w", err) 42 } 43 reqBody = bytes.NewReader(b) 44 } 45 46 url := c.baseURL + "/api/v1" + path 47 req, err := http.NewRequest(method, url, reqBody) 48 if err != nil { 49 return fmt.Errorf("build request: %w", err) 50 } 51 req.Header.Set("Authorization", "token "+c.token) 52 req.Header.Set("Content-Type", "application/json") 53 54 resp, err := c.http.Do(req) 55 if err != nil { 56 return fmt.Errorf("%s %s: %w", method, path, err) 57 } 58 defer resp.Body.Close() 59 60 if resp.StatusCode >= 300 { 61 respBody, _ := io.ReadAll(resp.Body) 62 return fmt.Errorf("%s %s: HTTP %d: %s", method, path, resp.StatusCode, string(respBody)) 63 } 64 65 if out != nil { 66 if err := json.NewDecoder(resp.Body).Decode(out); err != nil { 67 return fmt.Errorf("decode response for %s %s: %w", method, path, err) 68 } 69 } 70 return nil 71} 72 73// Repository is the subset of Forgejo's repository object the daemon uses. 74type Repository struct { 75 CloneURL string `json:"clone_url"` 76 DefaultBranch string `json:"default_branch"` 77} 78 79func (c *Client) GetRepository() (*Repository, error) { 80 var repo Repository 81 path := fmt.Sprintf("/repos/%s/%s", c.owner, c.repo) 82 if err := c.do(http.MethodGet, path, nil, &repo); err != nil { 83 return nil, err 84 } 85 return &repo, nil 86} 87 88// Issue is the subset of Forgejo's issue object the daemon reads and writes. 89type Issue struct { 90 Index int64 `json:"number"` 91 Title string `json:"title"` 92 Body string `json:"body"` 93 State string `json:"state"` 94} 95 96func (c *Client) ListIssues() ([]Issue, error) { 97 var issues []Issue 98 path := fmt.Sprintf("/repos/%s/%s/issues?type=issues&state=all&limit=50", c.owner, c.repo) 99 if err := c.do(http.MethodGet, path, nil, &issues); err != nil { 100 return nil, err 101 } 102 return issues, nil 103} 104 105func (c *Client) CreateIssue(title, body string) (*Issue, error) { 106 var issue Issue 107 path := fmt.Sprintf("/repos/%s/%s/issues", c.owner, c.repo) 108 req := map[string]string{"title": title, "body": body} 109 if err := c.do(http.MethodPost, path, req, &issue); err != nil { 110 return nil, err 111 } 112 return &issue, nil 113} 114 115// PullRequest is the subset of Forgejo's pull request object the daemon 116// reads and writes. 117type PullRequest struct { 118 Index int64 `json:"number"` 119 Title string `json:"title"` 120 Body string `json:"body"` 121 State string `json:"state"` 122 Head Branch `json:"head"` 123 Base Branch `json:"base"` 124} 125 126type Branch struct { 127 Ref string `json:"ref"` 128} 129 130func (c *Client) ListPullRequests() ([]PullRequest, error) { 131 var prs []PullRequest 132 path := fmt.Sprintf("/repos/%s/%s/pulls?state=all&limit=50", c.owner, c.repo) 133 if err := c.do(http.MethodGet, path, nil, &prs); err != nil { 134 return nil, err 135 } 136 return prs, nil 137} 138 139func (c *Client) CreatePullRequest(title, body, head, base string) (*PullRequest, error) { 140 var pr PullRequest 141 path := fmt.Sprintf("/repos/%s/%s/pulls", c.owner, c.repo) 142 req := map[string]string{"title": title, "body": body, "head": head, "base": base} 143 if err := c.do(http.MethodPost, path, req, &pr); err != nil { 144 return nil, err 145 } 146 return &pr, nil 147} 148 149// CreateIssueComment adds a comment to an issue or pull request — Forgejo 150// (like Gitea) treats PR comments as issue comments under the same 151// endpoint, verified against a live PR on f1. 152func (c *Client) CreateIssueComment(index int64, body string) error { 153 path := fmt.Sprintf("/repos/%s/%s/issues/%d/comments", c.owner, c.repo, index) 154 req := map[string]string{"body": body} 155 return c.do(http.MethodPost, path, req, nil) 156} 157 158// contentEntry is the subset of Forgejo's contents API response this 159// client reads, shared by both directory listings and single-file fetches. 160type contentEntry struct { 161 Name string `json:"name"` 162 Type string `json:"type"` // "file" or "dir" 163 Content string `json:"content,omitempty"` 164 Encoding string `json:"encoding,omitempty"` 165} 166 167// ListWorkflowFiles lists file names directly under .forgejo/workflows/. 168// Returns an empty slice (not an error) if the directory doesn't exist — 169// most repos have no workflows at all, which is a normal, expected state. 170func (c *Client) ListWorkflowFiles() ([]string, error) { 171 var entries []contentEntry 172 path := fmt.Sprintf("/repos/%s/%s/contents/.forgejo/workflows", c.owner, c.repo) 173 if err := c.do(http.MethodGet, path, nil, &entries); err != nil { 174 if strings.Contains(err.Error(), "HTTP 404") { 175 return nil, nil 176 } 177 return nil, err 178 } 179 names := make([]string, 0, len(entries)) 180 for _, e := range entries { 181 if e.Type == "file" { 182 names = append(names, e.Name) 183 } 184 } 185 return names, nil 186} 187 188// GetFileContent fetches and decodes one file's text content. 189func (c *Client) GetFileContent(path string) (string, error) { 190 var entry contentEntry 191 apiPath := fmt.Sprintf("/repos/%s/%s/contents/%s", c.owner, c.repo, path) 192 if err := c.do(http.MethodGet, apiPath, nil, &entry); err != nil { 193 return "", err 194 } 195 if entry.Encoding == "base64" { 196 decoded, err := base64.StdEncoding.DecodeString(entry.Content) 197 if err != nil { 198 return "", fmt.Errorf("decode %s: %w", path, err) 199 } 200 return string(decoded), nil 201 } 202 return entry.Content, nil 203}