package gitea import ( "encoding/json" "fmt" "io" "net/http" "time" ) // Repo represents a Gitea repository. type Repo struct { ID int64 `json:"id"` FullName string `json:"full_name"` CloneURL string `json:"clone_url"` DefaultBranch string `json:"default_branch"` Empty bool `json:"empty"` Archived bool `json:"archived"` UpdatedAt string `json:"updated_at"` } // Client is a Gitea API client. type Client struct { baseURL string token string httpClient *http.Client } // NewClient creates a new Gitea API client. func NewClient(baseURL, token string) *Client { return &Client{ baseURL: baseURL, token: token, httpClient: &http.Client{ Timeout: 30 * time.Second, }, } } // ListAllRepos returns all repositories accessible to the authenticated user. // It paginates through all results automatically. func (c *Client) ListAllRepos() ([]Repo, error) { var allRepos []Repo page := 1 limit := 50 for { url := fmt.Sprintf("%s/api/v1/repos/search?page=%d&limit=%d", c.baseURL, page, limit) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, fmt.Errorf("creating request: %w", err) } req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", "token "+c.token) resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("fetching repos page %d: %w", page, err) } if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) resp.Body.Close() return nil, fmt.Errorf("gitea API returned %d: %s", resp.StatusCode, string(body)) } var result struct { Data []Repo `json:"data"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { resp.Body.Close() return nil, fmt.Errorf("decoding response: %w", err) } resp.Body.Close() if len(result.Data) == 0 { break } allRepos = append(allRepos, result.Data...) if len(result.Data) < limit { break } page++ } // Filter out empty repos filtered := make([]Repo, 0, len(allRepos)) for _, r := range allRepos { if !r.Empty { filtered = append(filtered, r) } } return filtered, nil } // GetRepo returns a single repository by owner/name. func (c *Client) GetRepo(fullName string) (*Repo, error) { url := fmt.Sprintf("%s/api/v1/repos/%s", c.baseURL, fullName) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, fmt.Errorf("creating request: %w", err) } req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", "token "+c.token) resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("fetching repo %s: %w", fullName, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("gitea API returned %d: %s", resp.StatusCode, string(body)) } var repo Repo if err := json.NewDecoder(resp.Body).Decode(&repo); err != nil { return nil, fmt.Errorf("decoding response: %w", err) } return &repo, nil } // AuthenticatedCloneURL returns the clone URL with the token embedded for private repos. func (c *Client) AuthenticatedCloneURL(repo Repo) string { // Insert token into https URL: https://token@host/path.git if len(c.baseURL) > 8 { return fmt.Sprintf("%s://%s@%s", c.baseURL[:5], // "https" c.token, repo.CloneURL[8:]) // strip "https://" } return repo.CloneURL }