go-scm/repos/gitstate.go
Virgil a0fac1341b
Some checks failed
Security Scan / security (push) Failing after 10s
Test / test (push) Successful in 2m11s
chore(ax): add usage docs to exported APIs
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-30 14:11:15 +00:00

189 lines
5 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package repos
import (
filepath "dappco.re/go/core/scm/internal/ax/filepathx"
"time"
"dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
"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.
// Usage: LoadGitState(...)
func LoadGitState(m io.Medium, root string) (*GitState, error) {
path := filepath.Join(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.
// Usage: SaveGitState(...)
func SaveGitState(m io.Medium, root string, gs *GitState) error {
coreDir := filepath.Join(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 := filepath.Join(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.
// Usage: NewGitState(...)
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.
// Usage: TouchPull(...)
func (gs *GitState) TouchPull(name string) {
gs.ensureRepo(name).LastPull = time.Now()
}
// TouchPush records a push timestamp for the named repo.
// Usage: TouchPush(...)
func (gs *GitState) TouchPush(name string) {
gs.ensureRepo(name).LastPush = time.Now()
}
// UpdateRepo records the current git status for a repo.
// Usage: UpdateRepo(...)
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.
// Usage: Heartbeat(...)
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.
// Usage: StaleAgents(...)
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.
// Usage: ActiveAgentsFor(...)
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.
// Usage: NeedsPull(...)
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
}