210 lines
5.1 KiB
Go
210 lines
5.1 KiB
Go
package gitea
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"sgg-ai-skill-manager/internal/domain"
|
|
)
|
|
|
|
const (
|
|
AuthPassword = "password"
|
|
AuthToken = "token"
|
|
)
|
|
|
|
type Auth struct {
|
|
Type string
|
|
Username string
|
|
Secret string
|
|
}
|
|
|
|
type Client struct {
|
|
baseURL string
|
|
auth Auth
|
|
httpClient *http.Client
|
|
}
|
|
|
|
type apiRepo struct {
|
|
Name string `json:"name"`
|
|
FullName string `json:"full_name"`
|
|
Description string `json:"description"`
|
|
CloneURL string `json:"clone_url"`
|
|
SSHURL string `json:"ssh_url"`
|
|
DefaultBranch string `json:"default_branch"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
type apiUser struct {
|
|
UserName string `json:"username"`
|
|
}
|
|
|
|
func NewClient(baseURL string, auth Auth) *Client {
|
|
return &Client{
|
|
baseURL: strings.TrimRight(baseURL, "/"),
|
|
auth: auth,
|
|
httpClient: &http.Client{Timeout: 20 * time.Second},
|
|
}
|
|
}
|
|
|
|
func (c *Client) ListOrgSkills(ctx context.Context, org string) ([]domain.RemoteSkill, error) {
|
|
var skills []domain.RemoteSkill
|
|
for page := 1; ; page++ {
|
|
var repos []apiRepo
|
|
endpoint := fmt.Sprintf("/api/v1/orgs/%s/repos?page=%d&limit=50", url.PathEscape(org), page)
|
|
if err := c.getJSON(ctx, endpoint, &repos); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(repos) == 0 {
|
|
break
|
|
}
|
|
for _, repo := range repos {
|
|
hasSkill, err := c.HasRootSkill(ctx, org, repo.Name, repo.DefaultBranch)
|
|
if err != nil {
|
|
skills = append(skills, domain.RemoteSkill{
|
|
Name: repo.Name,
|
|
FullName: repo.FullName,
|
|
Status: "check_failed",
|
|
Error: err.Error(),
|
|
})
|
|
continue
|
|
}
|
|
if !hasSkill {
|
|
continue
|
|
}
|
|
skills = append(skills, domain.RemoteSkill{
|
|
Name: repo.Name,
|
|
FullName: repo.FullName,
|
|
Description: repo.Description,
|
|
CloneURL: normalizeCloneURL(repo.CloneURL, c.baseURL),
|
|
SSHURL: repo.SSHURL,
|
|
DefaultBranch: repo.DefaultBranch,
|
|
UpdatedAt: repo.UpdatedAt,
|
|
Status: "remote",
|
|
})
|
|
}
|
|
if len(repos) < 50 {
|
|
break
|
|
}
|
|
}
|
|
return skills, nil
|
|
}
|
|
|
|
func (c *Client) HasRootSkill(ctx context.Context, org, repo, ref string) (bool, error) {
|
|
endpoint := fmt.Sprintf(
|
|
"/api/v1/repos/%s/%s/contents/SKILL.md",
|
|
url.PathEscape(org),
|
|
url.PathEscape(repo),
|
|
)
|
|
if ref != "" {
|
|
endpoint += "?ref=" + url.QueryEscape(ref)
|
|
}
|
|
req, err := c.newRequest(ctx, http.MethodGet, endpoint)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return false, nil
|
|
}
|
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
return true, nil
|
|
}
|
|
return false, fmt.Errorf("check SKILL.md failed: %s", resp.Status)
|
|
}
|
|
|
|
func (c *Client) TestConnection(ctx context.Context, org string) (domain.TestConnectionResult, error) {
|
|
var user apiUser
|
|
if err := c.getJSON(ctx, "/api/v1/user", &user); err != nil {
|
|
return domain.TestConnectionResult{}, err
|
|
}
|
|
req, err := c.newRequest(ctx, http.MethodGet, "/api/v1/orgs/"+url.PathEscape(org))
|
|
if err != nil {
|
|
return domain.TestConnectionResult{}, err
|
|
}
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return domain.TestConnectionResult{}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return domain.TestConnectionResult{}, fmt.Errorf("org access failed: %s", resp.Status)
|
|
}
|
|
return domain.TestConnectionResult{
|
|
OK: true,
|
|
Message: "connected",
|
|
Username: user.UserName,
|
|
Org: org,
|
|
}, nil
|
|
}
|
|
|
|
func (c *Client) getJSON(ctx context.Context, endpoint string, target any) error {
|
|
req, err := c.newRequest(ctx, http.MethodGet, endpoint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return fmt.Errorf("gitea request failed: %s", resp.Status)
|
|
}
|
|
return json.NewDecoder(resp.Body).Decode(target)
|
|
}
|
|
|
|
func (c *Client) newRequest(ctx context.Context, method, endpoint string) (*http.Request, error) {
|
|
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+endpoint, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if c.auth.Type == AuthToken {
|
|
req.Header.Set("Authorization", "token "+c.auth.Secret)
|
|
} else if c.auth.Username != "" || c.auth.Secret != "" {
|
|
req.SetBasicAuth(c.auth.Username, c.auth.Secret)
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
return req, nil
|
|
}
|
|
|
|
func normalizeCloneURL(cloneURL, baseURL string) string {
|
|
clone, err := url.Parse(cloneURL)
|
|
if err != nil || clone.Scheme == "" || clone.Host == "" {
|
|
return cloneURL
|
|
}
|
|
if clone.Scheme != "http" && clone.Scheme != "https" {
|
|
return cloneURL
|
|
}
|
|
if !isLoopbackHost(clone.Hostname()) {
|
|
return cloneURL
|
|
}
|
|
base, err := url.Parse(baseURL)
|
|
if err != nil || base.Scheme == "" || base.Host == "" {
|
|
return cloneURL
|
|
}
|
|
if clone.Scheme == base.Scheme && strings.EqualFold(clone.Host, base.Host) {
|
|
return cloneURL
|
|
}
|
|
clone.Scheme = base.Scheme
|
|
clone.Host = base.Host
|
|
return clone.String()
|
|
}
|
|
|
|
func isLoopbackHost(host string) bool {
|
|
if strings.EqualFold(host, "localhost") {
|
|
return true
|
|
}
|
|
ip := net.ParseIP(host)
|
|
return ip != nil && ip.IsLoopback()
|
|
}
|