// Package status defines the domain model shared across gitflow: how a Git // repository's state is captured, classified, and summarised after a scan. package status import "time" // RepoStatus classifies the overall health of a repository at scan time. // // The zero value is StatusUnknown, an invalid/unset state, so a zero-valued // RepoInfo can never be mistaken for a real scan result. type RepoStatus int const ( StatusUnknown RepoStatus = iota // 0 — invalid/unset StatusClean // 1 — working tree clean, in sync StatusModified // 2 — staged, modified, or untracked files StatusAhead // 3 — local commits not yet pushed StatusBehind // 4 — remote commits not yet pulled StatusDiverged // 5 — both ahead of and behind upstream StatusDetached // 6 — HEAD points at a commit, not a branch StatusBare // 7 — bare repository, no working tree StatusError // 8 — could not be scanned ) // String returns a lowercase, human-readable label for the status. func (s RepoStatus) String() string { switch s { case StatusClean: return "clean" case StatusModified: return "modified" case StatusAhead: return "ahead" case StatusBehind: return "behind" case StatusDiverged: return "diverged" case StatusDetached: return "detached" case StatusBare: return "bare" case StatusError: return "error" default: return "unknown" } } // NeedsAttention reports whether the repository asks for human action. func (s RepoStatus) NeedsAttention() bool { return s != StatusClean && s != StatusUnknown } // RepoInfo is a full snapshot of a single repository at scan time. type RepoInfo struct { Path string // absolute path to the repository root Name string // base directory name, for display RemoteURL string // fetch URL of the first remote, if any Branch string // current branch name; "(detached)" when detached Detached bool // HEAD is detached from any branch Status RepoStatus StagedFiles []string // files with staged changes ModifiedFiles []string // files with unstaged changes UntrackedFiles []string // untracked files or directories AheadBy int // commits ahead of upstream BehindBy int // commits behind upstream StashCount int // number of stashes Error string // scan error detail; non-empty when Status is StatusError } // FileCount returns the total number of files with any local change. func (r RepoInfo) FileCount() int { return len(r.StagedFiles) + len(r.ModifiedFiles) + len(r.UntrackedFiles) } // ScanResult is the outcome of one scan pass over a set of repositories. type ScanResult struct { ScannedAt time.Time // when the scan ran ParentDir string // the directory that was scanned Repos []RepoInfo } // Summary aggregates per-repository counters for the whole result. type Summary struct { Total int // repositories scanned Clean int // status clean, no action needed Attention int // status not clean (modified, ahead, behind, diverged, detached, bare) Errored int // status error (scan failed) Staged int // files staged across all repos Modified int // files modified across all repos Untracked int // untracked files across all repos } // Summary computes the aggregate counters for the result. func (r ScanResult) Summary() Summary { s := Summary{Total: len(r.Repos)} for _, repo := range r.Repos { switch repo.Status { case StatusClean: s.Clean++ case StatusError: s.Errored++ case StatusUnknown: // Neither clean nor actionable; not counted. default: s.Attention++ } s.Staged += len(repo.StagedFiles) s.Modified += len(repo.ModifiedFiles) s.Untracked += len(repo.UntrackedFiles) } return s } // NeedsAttention lists repositories whose status is anything but clean. func (r ScanResult) NeedsAttention() []RepoInfo { out := make([]RepoInfo, 0, len(r.Repos)) for _, repo := range r.Repos { if repo.Status.NeedsAttention() { out = append(out, repo) } } return out }