141 lines
3.0 KiB
Go
141 lines
3.0 KiB
Go
package agents
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
|
|
"g.fc.run/ai/byre/internal/api"
|
|
"g.fc.run/ai/byre/internal/git"
|
|
"g.fc.run/ai/byre/internal/store"
|
|
)
|
|
|
|
const allTasks = "*"
|
|
|
|
func (m *Manager) observeFacts(ev api.Event) {
|
|
switch ev.Kind {
|
|
case "task.created", "turn.ended", "snapshot.created", "review.started", "review.finished", "review.finding", "user.message", "board.posted":
|
|
if ev.TaskId != nil {
|
|
m.MarkFacts(*ev.TaskId)
|
|
}
|
|
case "task.merged", "task.merged_to_main":
|
|
if ev.ProjectId != nil {
|
|
m.MarkFacts("project:" + *ev.ProjectId)
|
|
}
|
|
case "user.updated":
|
|
m.MarkFacts(allTasks)
|
|
}
|
|
}
|
|
|
|
func (m *Manager) MarkFacts(key string) {
|
|
m.factsMu.Lock()
|
|
m.factsDirty[key] = true
|
|
m.factsMu.Unlock()
|
|
select {
|
|
case m.factsPoke <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (m *Manager) factsLoop() {
|
|
for {
|
|
select {
|
|
case <-m.watchStop:
|
|
return
|
|
case <-m.factsPoke:
|
|
}
|
|
m.factsMu.Lock()
|
|
dirty := m.factsDirty
|
|
m.factsDirty = map[string]bool{}
|
|
m.factsMu.Unlock()
|
|
err := m.settleFacts(context.Background(), dirty)
|
|
if err != nil {
|
|
slog.Error("settle merge facts", "err", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *Manager) settleFacts(ctx context.Context, dirty map[string]bool) error {
|
|
tasks, err := m.store.ActiveTasks(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
projects := map[string]api.Project{}
|
|
for _, t := range tasks {
|
|
if !dirty[allTasks] && !dirty[t.Id] && !dirty["project:"+t.ProjectId] {
|
|
continue
|
|
}
|
|
p, ok := projects[t.ProjectId]
|
|
if !ok {
|
|
p, err = m.store.GetProject(ctx, t.ProjectId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
projects[t.ProjectId] = p
|
|
}
|
|
err = m.settleTaskFacts(ctx, t, p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) refreshFacts(ctx context.Context, taskID string) {
|
|
t, err := m.store.GetTask(ctx, taskID)
|
|
if err == nil {
|
|
var p api.Project
|
|
p, err = m.store.GetProject(ctx, t.ProjectId)
|
|
if err == nil {
|
|
err = m.settleTaskFacts(ctx, t, p)
|
|
}
|
|
}
|
|
if err != nil {
|
|
slog.Error("refresh merge facts", "task", taskID, "err", err)
|
|
}
|
|
}
|
|
|
|
func (m *Manager) settleTaskFacts(ctx context.Context, t api.Task, p api.Project) error {
|
|
m.factsRun.Lock()
|
|
defer m.factsRun.Unlock()
|
|
g, err := m.gitFacts(ctx, t, p)
|
|
if err != nil {
|
|
slog.Error("compute merge facts", "task", t.Id, "err", err)
|
|
return nil
|
|
}
|
|
err = m.store.SettleMergeFacts(ctx, t.Id, g)
|
|
if errors.Is(err, store.ErrNotFound) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (m *Manager) gitFacts(ctx context.Context, t api.Task, p api.Project) (store.GitFacts, error) {
|
|
g := store.GitFacts{}
|
|
if t.WorktreePath == "" {
|
|
return g, nil
|
|
}
|
|
tree, err := git.Snapshot(t.WorktreePath)
|
|
if err != nil {
|
|
return g, err
|
|
}
|
|
g.UnmergedCommits, err = git.Ahead(t.WorktreePath, p.DefaultBranch)
|
|
if err != nil {
|
|
return g, err
|
|
}
|
|
all, err := git.Compare(t.WorktreePath, p.DefaultBranch, "HEAD", tree, "")
|
|
if err != nil {
|
|
return g, err
|
|
}
|
|
g.UnmergedFiles = len(all.Files)
|
|
latest, err := m.store.LastReviewable(ctx, t.Id)
|
|
if errors.Is(err, store.ErrNotFound) {
|
|
return g, nil
|
|
}
|
|
if err != nil {
|
|
return g, err
|
|
}
|
|
g.UnreviewedChanges = latest.TreeSha != tree
|
|
return g, nil
|
|
}
|