package gitea import ( "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) // Repo represents a Gitea repository. type Repo struct { Name string `json:"name"` Description string `json:"description"` DefaultBranch string `json:"default_branch"` } // TreeEntry represents a file in a Gitea repo tree. type TreeEntry struct { Path string `json:"path"` Type string `json:"type"` // "blob" or "tree" } // TreeResponse is the Gitea API response for a recursive tree listing. type TreeResponse struct { Tree []TreeEntry `json:"tree"` } // CommitMeta holds minimal commit info for a file. type CommitMeta struct { SHA string `json:"sha"` Date time.Time `json:"-"` } // RepoCommit is used to extract the latest commit SHA for a repo. type RepoCommit struct { SHA string `json:"sha"` Commit repoCommitInfo `json:"commit"` } type repoCommitInfo struct { Committer commitPerson `json:"committer"` } type commitPerson struct { Date time.Time `json:"date"` } // Client fetches content from a Gitea instance. type Client struct { baseURL string org string httpClient *http.Client } // NewClient creates a Gitea API client. func NewClient(baseURL, org string, timeout time.Duration) *Client { return &Client{ baseURL: strings.TrimRight(baseURL, "/"), org: org, httpClient: &http.Client{ Timeout: timeout, }, } } // ListRepos returns all repositories in the configured organization. func (c *Client) ListRepos(ctx context.Context) ([]Repo, error) { var allRepos []Repo page := 1 for { url := fmt.Sprintf("%s/api/v1/orgs/%s/repos?page=%d&limit=50", c.baseURL, c.org, page) var repos []Repo if err := c.getJSON(ctx, url, &repos); err != nil { return nil, fmt.Errorf("list repos page %d: %w", page, err) } if len(repos) == 0 { break } allRepos = append(allRepos, repos...) page++ } return allRepos, nil } // ListMarkdownFiles returns paths to all .md files in a repo's default branch. func (c *Client) ListMarkdownFiles(ctx context.Context, repo, branch string) ([]string, error) { url := fmt.Sprintf("%s/api/v1/repos/%s/%s/git/trees/%s?recursive=true", c.baseURL, c.org, repo, branch) var tree TreeResponse if err := c.getJSON(ctx, url, &tree); err != nil { return nil, fmt.Errorf("list tree for %s: %w", repo, err) } var files []string for _, entry := range tree.Tree { if entry.Type == "blob" && strings.HasSuffix(strings.ToLower(entry.Path), ".md") { files = append(files, entry.Path) } } return files, nil } // FetchFileContent returns the raw content of a file in a repo. func (c *Client) FetchFileContent(ctx context.Context, repo, branch, filepath string) ([]byte, error) { url := fmt.Sprintf("%s/api/v1/repos/%s/%s/raw/%s?ref=%s", c.baseURL, c.org, repo, filepath, branch) return c.getRaw(ctx, url) } // LatestCommitSHA returns the SHA of the latest commit on a branch. func (c *Client) LatestCommitSHA(ctx context.Context, repo, branch string) (string, time.Time, error) { url := fmt.Sprintf("%s/api/v1/repos/%s/%s/commits?sha=%s&limit=1", c.baseURL, c.org, repo, branch) var commits []RepoCommit if err := c.getJSON(ctx, url, &commits); err != nil { return "", time.Time{}, fmt.Errorf("latest commit for %s: %w", repo, err) } if len(commits) == 0 { return "", time.Time{}, fmt.Errorf("no commits found for %s/%s", repo, branch) } return commits[0].SHA, commits[0].Commit.Committer.Date, nil } func (c *Client) getJSON(ctx context.Context, url string, target interface{}) error { body, err := c.getRaw(ctx, url) if err != nil { return err } return json.Unmarshal(body, target) } func (c *Client) getRaw(ctx context.Context, url string) ([]byte, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("create request: %w", err) } resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("fetch %s: %w", url, err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("fetch %s: status %d", url, resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response from %s: %w", url, err) } return body, nil }