Files
knox/internal/ingest/gitea.go
T
david 2c0f8fa257 feat: M1 determinism fixes for gossip
Refs #1

- F1: canonical fingerprints (git identity by remote URL, log by
  basename, obsidian by relative vault path) so identical facts get
  identical ids across machines
- F2: node_id (persisted in settings) + Hybrid Logical Clock in all
  observation "when" columns; removed time.Now() fact-time fallbacks
- F3: stable TF-IDF tie-break sort (score desc, term asc)
- F4: observations carry (node_id, hcl) locator; dedup ordered by hcl
2026-08-29 03:51:02 -07:00

248 lines
6.1 KiB
Go

package ingest
import (
"encoding/json"
"fmt"
"os/exec"
"time"
)
type GiteaIngester struct {
Version string
}
func NewGiteaIngester() *GiteaIngester {
return &GiteaIngester{Version: "gitea-tea/v1"}
}
func (g *GiteaIngester) SourceID() string { return "gitea" }
func (g *GiteaIngester) IngestAll() ([]*IngestResult, error) {
var results []*IngestResult
repos, err := g.fetchRepos()
if err == nil {
for _, r := range repos {
if result := g.repoToResult(r); result != nil {
results = append(results, result)
}
}
}
issues, err := g.fetchIssues()
if err == nil {
for _, i := range issues {
if result := g.issueToResult(i); result != nil {
results = append(results, result)
}
}
}
pulls, err := g.fetchPulls()
if err == nil {
for _, p := range pulls {
if result := g.pullToResult(p); result != nil {
results = append(results, result)
}
}
}
return results, nil
}
type teaRepo struct {
Owner string `json:"owner"`
Name string `json:"name"`
Description string `json:"description"`
Updated string `json:"updated"`
}
type teaIssue struct {
Index string `json:"index"`
Title string `json:"title"`
State string `json:"state"`
Owner string `json:"owner"`
Repo string `json:"repo"`
Labels string `json:"labels"`
Author string `json:"author"`
Updated string `json:"updated"`
}
type teaPull struct {
Index string `json:"index"`
Title string `json:"title"`
State string `json:"state"`
Owner string `json:"owner"`
Repo string `json:"repo"`
Updated string `json:"updated"`
}
func (g *GiteaIngester) teaJSON(args ...string) ([][]byte, error) {
cmd := exec.Command("tea", append(args, "--output", "json")...)
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("tea %s: %w", args[0], err)
}
var raw json.RawMessage
if err := json.Unmarshal(output, &raw); err != nil {
return nil, err
}
// Handle both array and object responses
var arr []json.RawMessage
if err := json.Unmarshal(raw, &arr); err == nil {
bytes := make([][]byte, len(arr))
for i, a := range arr {
bytes[i] = a
}
return bytes, nil
}
// Single object — wrap in array
return [][]byte{raw}, nil
}
func (g *GiteaIngester) fetchRepos() ([]teaRepo, error) {
items, err := g.teaJSON("repos", "list", "--limit", "100", "--fields", "owner,name,description,updated")
if err != nil {
return nil, err
}
var repos []teaRepo
for _, item := range items {
var r teaRepo
if err := json.Unmarshal(item, &r); err == nil {
repos = append(repos, r)
}
}
return repos, nil
}
func (g *GiteaIngester) fetchIssues() ([]teaIssue, error) {
items, err := g.teaJSON("issues", "list", "--state", "open", "--limit", "100", "--fields", "index,title,state,author,milestone,labels,owner,repo,updated")
if err != nil {
return nil, err
}
var issues []teaIssue
for _, item := range items {
var i teaIssue
if err := json.Unmarshal(item, &i); err == nil {
issues = append(issues, i)
}
}
return issues, nil
}
func (g *GiteaIngester) fetchPulls() ([]teaPull, error) {
items, err := g.teaJSON("pulls", "list", "--state", "open", "--limit", "100", "--fields", "index,title,state,owner,repo,updated")
if err != nil {
return nil, err
}
var pulls []teaPull
for _, item := range items {
var p teaPull
if err := json.Unmarshal(item, &p); err == nil {
pulls = append(pulls, p)
}
}
return pulls, nil
}
// signalTime returns the item's last-activity time, or "" when unknown. An
// empty value is an explicit unknown — downstream uses it as a missing signal,
// never fabricates a "now" timestamp that would differ across nodes.
func signalTime(updated string) string {
if updated != "" {
if _, err := time.Parse(time.RFC3339, updated); err == nil {
return updated
}
}
return ""
}
func (g *GiteaIngester) repoToResult(r teaRepo) *IngestResult {
fullName := r.Owner + "/" + r.Name
fp := Fingerprint([]byte("gitea:repo:" + fullName))
return &IngestResult{
Fingerprint: fp,
SourceID: g.SourceID(),
SourcePath: fullName,
Project: r.Name,
ContentType: "repo",
Title: fullName,
Summary: r.Description,
Confidence: 0.9,
IngesterVersion: g.Version,
CreatedAt: signalTime(r.Updated),
Provenance: map[string]any{
"type": "repository",
"owner": r.Owner,
"name": r.Name,
"description": r.Description,
},
}
}
func (g *GiteaIngester) issueToResult(i teaIssue) *IngestResult {
title := fmt.Sprintf("#%s %s", i.Index, i.Title)
fp := Fingerprint([]byte(fmt.Sprintf("gitea:issue:%s/%s:%s", i.Owner, i.Repo, i.Index)))
summary := fmt.Sprintf("[%s/%s#%s] %s", i.Owner, i.Repo, i.Index, i.Title)
if i.Labels != "" {
summary += " [" + i.Labels + "]"
}
return &IngestResult{
Fingerprint: fp,
SourceID: g.SourceID(),
SourcePath: fmt.Sprintf("%s/%s#%s", i.Owner, i.Repo, i.Index),
Project: i.Repo,
ContentType: "issue",
Title: title,
Summary: summary,
Confidence: 0.85,
IngesterVersion: g.Version,
CreatedAt: signalTime(i.Updated),
Provenance: map[string]any{
"type": "issue",
"owner": i.Owner,
"repo": i.Repo,
"index": i.Index,
"state": i.State,
"labels": i.Labels,
},
}
}
func (g *GiteaIngester) pullToResult(p teaPull) *IngestResult {
title := fmt.Sprintf("!%s %s", p.Index, p.Title)
fp := Fingerprint([]byte(fmt.Sprintf("gitea:pull:%s/%s:%s", p.Owner, p.Repo, p.Index)))
return &IngestResult{
Fingerprint: fp,
SourceID: g.SourceID(),
SourcePath: fmt.Sprintf("%s/%s!%s", p.Owner, p.Repo, p.Index),
Project: p.Repo,
ContentType: "pull",
Title: title,
Summary: fmt.Sprintf("[PR %s/%s] %s", p.Owner, p.Repo, p.Title),
Confidence: 0.85,
IngesterVersion: g.Version,
CreatedAt: signalTime(p.Updated),
Provenance: map[string]any{
"type": "pull_request",
"owner": p.Owner,
"repo": p.Repo,
"index": p.Index,
"state": p.State,
},
}
}
func (g *GiteaIngester) Ingest(_ string) (*IngestResult, error) {
return nil, fmt.Errorf("use IngestAll() for gitea")
}
var _ Ingester = (*GiteaIngester)(nil)