Initial implementation of Gitea Wrapped TUI

- Gitea API client with repository and commit fetching
- Interactive credential input screen with masked token input
- Statistics analyzer for commits, languages, and activity patterns
- Multi-page report screen with ASCII charts and visualizations
- Integration of all components in main app coordinator
- Comprehensive README with usage instructions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-01 09:43:09 -06:00
commit fec14022cd
12 changed files with 1152 additions and 0 deletions
+175
View File
@@ -0,0 +1,175 @@
package gitea
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
type Client struct {
BaseURL string
Token string
Client *http.Client
}
func NewClient(baseURL, token string) *Client {
baseURL = strings.TrimRight(baseURL, "/")
if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
baseURL = "https://" + baseURL
}
if strings.HasSuffix(baseURL, "/api/v1") {
baseURL = strings.TrimSuffix(baseURL, "/api/v1")
}
if strings.HasSuffix(baseURL, "/api") {
baseURL = strings.TrimSuffix(baseURL, "/api")
}
return &Client{
BaseURL: baseURL,
Token: token,
Client: &http.Client{Timeout: 60 * time.Second},
}
}
type GiteaRepo struct {
ID int64 `json:"id"`
Name string `json:"name"`
FullName string `json:"full_name"`
HTMLURL string `json:"html_url"`
Language string `json:"language"`
Topics []string `json:"topics"`
}
type GiteaCommit struct {
SHA string `json:"sha"`
Commit struct {
Author struct {
Name string `json:"name"`
Email string `json:"email"`
Date time.Time `json:"date"`
} `json:"author"`
Message string `json:"message"`
} `json:"commit"`
}
// do makes an authenticated HTTP request to Gitea API
func (c *Client) do(method, path string) ([]byte, error) {
url := fmt.Sprintf("%s/api/v1%s", c.BaseURL, path)
req, err := http.NewRequest(method, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request for %s: %w", url, err)
}
req.Header.Set("Authorization", fmt.Sprintf("token %s", c.Token))
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "gitea-wrapped/1.0")
resp, err := c.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("API request failed (%s %s): %w", method, url, err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
errMsg := string(body)
if len(errMsg) > 500 {
errMsg = errMsg[:500]
}
return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, errMsg)
}
return body, nil
}
func (c *Client) GetUserRepos() ([]GiteaRepo, error) {
var repos []GiteaRepo
page := 1
pageSize := 50
maxPages := 100 // Safety limit to prevent infinite loops
for page <= maxPages {
path := fmt.Sprintf("/user/repos?page=%d&limit=%d", page, pageSize)
body, err := c.do("GET", path)
if err != nil {
if len(repos) > 0 {
return repos, nil
}
return nil, err
}
var pageRepos []GiteaRepo
if err := json.Unmarshal(body, &pageRepos); err != nil {
return nil, err
}
if len(pageRepos) == 0 {
break
}
repos = append(repos, pageRepos...)
if len(pageRepos) < pageSize {
break
}
page++
}
return repos, nil
}
func (c *Client) GetRepoCommits(owner, repo string) ([]GiteaCommit, error) {
var commits []GiteaCommit
page := 1
pageSize := 50
maxPages := 1000 // Safety limit to prevent infinite loops
for page <= maxPages {
path := fmt.Sprintf("/repos/%s/%s/commits?page=%d&limit=%d", owner, repo, page, pageSize)
body, err := c.do("GET", path)
if err != nil {
if len(commits) > 0 {
return commits, nil
}
return nil, err
}
var pageCommits []GiteaCommit
if err := json.Unmarshal(body, &pageCommits); err != nil {
return nil, err
}
if len(pageCommits) == 0 {
break
}
commits = append(commits, pageCommits...)
if len(pageCommits) < pageSize {
break
}
page++
}
return commits, nil
}
func (c *Client) TestConnection() error {
_, err := c.do("GET", "/user")
if err != nil {
return err
}
return nil
}