go-scm/repos/gitstate.go
Claude d67ac486ff
Some checks failed
Security Scan / security (push) Failing after 8s
Test / test (push) Successful in 4m5s
feat: upgrade to core v0.8.0-alpha.1, replace banned stdlib imports
Migrate git/service.go to Action-based API. Replace fmt, strings,
path/filepath with Core primitives across 77 files (~400 call sites).
Keep encoding/json, strings.EqualFold/SplitSeq/Fields, filepath.Abs/Rel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 13:44:13 +00:00

177 lines
4.7 KiB
Go

package repos
import (
"time"
core "dappco.re/go/core"
coreerr "dappco.re/go/core/log"
"dappco.re/go/core/io"
"gopkg.in/yaml.v3"
)
// GitState holds per-machine git sync state for a workspace.
// Stored at .core/git.yaml and .gitignored (not shared across machines).
type GitState struct {
Version int `yaml:"version"`
Repos map[string]*RepoGitState `yaml:"repos,omitempty"`
Agents map[string]*AgentState `yaml:"agents,omitempty"`
}
// RepoGitState tracks the last known git state for a single repo.
type RepoGitState struct {
LastPull time.Time `yaml:"last_pull,omitempty"`
LastPush time.Time `yaml:"last_push,omitempty"`
Branch string `yaml:"branch,omitempty"`
Remote string `yaml:"remote,omitempty"`
Ahead int `yaml:"ahead,omitempty"`
Behind int `yaml:"behind,omitempty"`
}
// AgentState tracks which agent last touched which repos.
type AgentState struct {
LastSeen time.Time `yaml:"last_seen"`
Active []string `yaml:"active,omitempty"`
}
// LoadGitState reads .core/git.yaml from the given workspace root directory.
// Returns a new empty GitState if the file does not exist.
func LoadGitState(m io.Medium, root string) (*GitState, error) {
path := core.JoinPath(root, ".core", "git.yaml")
if !m.Exists(path) {
return NewGitState(), nil
}
content, err := m.Read(path)
if err != nil {
return nil, coreerr.E("repos.LoadGitState", "failed to read git state", err)
}
var gs GitState
if err := yaml.Unmarshal([]byte(content), &gs); err != nil {
return nil, coreerr.E("repos.LoadGitState", "failed to parse git state", err)
}
if gs.Repos == nil {
gs.Repos = make(map[string]*RepoGitState)
}
if gs.Agents == nil {
gs.Agents = make(map[string]*AgentState)
}
return &gs, nil
}
// SaveGitState writes .core/git.yaml to the given workspace root directory.
func SaveGitState(m io.Medium, root string, gs *GitState) error {
coreDir := core.JoinPath(root, ".core")
if err := m.EnsureDir(coreDir); err != nil {
return coreerr.E("repos.SaveGitState", "failed to create .core directory", err)
}
data, err := yaml.Marshal(gs)
if err != nil {
return coreerr.E("repos.SaveGitState", "failed to marshal git state", err)
}
path := core.JoinPath(coreDir, "git.yaml")
if err := m.Write(path, string(data)); err != nil {
return coreerr.E("repos.SaveGitState", "failed to write git state", err)
}
return nil
}
// NewGitState returns a new empty GitState with version 1.
func NewGitState() *GitState {
return &GitState{
Version: 1,
Repos: make(map[string]*RepoGitState),
Agents: make(map[string]*AgentState),
}
}
// Touch records a pull timestamp for the named repo.
func (gs *GitState) TouchPull(name string) {
gs.ensureRepo(name).LastPull = time.Now()
}
// TouchPush records a push timestamp for the named repo.
func (gs *GitState) TouchPush(name string) {
gs.ensureRepo(name).LastPush = time.Now()
}
// UpdateRepo records the current git status for a repo.
func (gs *GitState) UpdateRepo(name, branch, remote string, ahead, behind int) {
r := gs.ensureRepo(name)
r.Branch = branch
r.Remote = remote
r.Ahead = ahead
r.Behind = behind
}
// Heartbeat records an agent's presence and active packages.
func (gs *GitState) Heartbeat(agentName string, active []string) {
if gs.Agents == nil {
gs.Agents = make(map[string]*AgentState)
}
gs.Agents[agentName] = &AgentState{
LastSeen: time.Now(),
Active: active,
}
}
// StaleAgents returns agent names whose last heartbeat is older than the given duration.
func (gs *GitState) StaleAgents(staleAfter time.Duration) []string {
cutoff := time.Now().Add(-staleAfter)
var stale []string
for name, agent := range gs.Agents {
if agent.LastSeen.Before(cutoff) {
stale = append(stale, name)
}
}
return stale
}
// ActiveAgentsFor returns agent names that have the given repo in their active list
// and are not stale.
func (gs *GitState) ActiveAgentsFor(repoName string, staleAfter time.Duration) []string {
cutoff := time.Now().Add(-staleAfter)
var agents []string
for name, agent := range gs.Agents {
if agent.LastSeen.Before(cutoff) {
continue
}
for _, r := range agent.Active {
if r == repoName {
agents = append(agents, name)
break
}
}
}
return agents
}
// NeedsPull returns true if the repo has never been pulled or was pulled before the given duration.
func (gs *GitState) NeedsPull(name string, maxAge time.Duration) bool {
r, ok := gs.Repos[name]
if !ok {
return true
}
if r.LastPull.IsZero() {
return true
}
return time.Since(r.LastPull) > maxAge
}
func (gs *GitState) ensureRepo(name string) *RepoGitState {
if gs.Repos == nil {
gs.Repos = make(map[string]*RepoGitState)
}
r, ok := gs.Repos[name]
if !ok {
r = &RepoGitState{}
gs.Repos[name] = r
}
return r
}