unfinished

package
v1.42.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 13, 2026 License: AGPL-3.0 Imports: 39 Imported by: 0

Documentation

Overview

Package unfinished provides background scanning for git worktrees that have uncommitted changes, commits ahead of the default branch, or commits behind.

Index

Constants

View Source
const (
	// EventUnfinishedWorkUpdated is published when a worktree scan result changes.
	EventUnfinishedWorkUpdated pkgevents.EventType = "unfinished.work_updated"
	// EventUnfinishedWorkRemoved is published when a worktree is dismissed/snoozed/gone.
	EventUnfinishedWorkRemoved pkgevents.EventType = "unfinished.work_removed"
	// EventUnfinishedScanCompleted is published after each full scan pass.
	EventUnfinishedScanCompleted pkgevents.EventType = "unfinished.scan_completed"
)

Variables

This section is empty.

Functions

func ComputeDiffHash

func ComputeDiffHash(worktreePath string) (string, error)

ComputeDiffHash runs `git -C path diff HEAD` and SHA256-hashes the output.

func LinesDiff added in v1.35.0

func LinesDiff(old, newContent string) (insertions, deletions int)

LinesDiff returns inserted and deleted line counts between old and new using LCS. Exported so tests can exercise the algorithm directly.

func RegisterMetrics added in v1.39.0

func RegisterMetrics() error

RegisterMetrics wires blobCache effectiveness (see BlobCacheStatsSnapshot) into the process's OTel MeterProvider as observable gauges, so it shows up in Datadog/OTLP alongside every other metric — not just the /debug/blob-cache JSON endpoint (profiling.StartProfiling). Both read the same snapshot function, so they can never disagree with each other.

Safe to call even when telemetry is disabled or before telemetry.Initialize has run: the OTel global Meter is a delegating proxy — instruments created against it now start exporting retroactively once a real MeterProvider is installed later (see telemetry.Initialize). Call once per process; calling it more than once registers duplicate instruments.

func SortByLastModified

func SortByLastModified(results []ScanResult)

SortByLastModified sorts a slice of ScanResult descending by LastModified. Equal times are broken by RepoPath+Branch for stability.

Types

type BlobCacheStats added in v1.39.0

type BlobCacheStats struct {
	Hits               int64
	Misses             int64
	EstimatedTimeSaved time.Duration
}

BlobCacheStats reports blobCache effectiveness across every repo this reader has touched: hit/miss counts and an estimated amount of wall-clock packfile decompression time avoided by cache hits (hits * the average observed miss duration). A low hit rate relative to misses suggests effectiveBlobCacheMaxBytes is sized too small for this workload (or that HEAD/blobs are churning too fast for caching to help at all); a high hit rate with a large EstimatedTimeSaved means the cache is earning its keep.

func BlobCacheStatsSnapshot added in v1.39.0

func BlobCacheStatsSnapshot() BlobCacheStats

BlobCacheStatsSnapshot returns BlobCacheStats for the process's registered reader (see currentReader), or a zero value if none has been registered yet — e.g. queried before the scanner starts, or in tests that construct a *GoGitVCSReader directly without going through NewScanner/ NewScannerWithReader.

type DiffStat added in v1.35.0

type DiffStat struct {
	Files, Insertions, Deletions int
}

DiffStat holds a summary of changed lines.

type GitVCSReader added in v1.35.0

type GitVCSReader struct{}

GitVCSReader implements VCSReader using CLI git subprocesses. --no-optional-locks is injected by gitCmd so no call site needs to remember it.

func (*GitVCSReader) AheadBehind added in v1.35.0

func (g *GitVCSReader) AheadBehind(worktreePath, base string) (int, int, error)

func (*GitVCSReader) CommitMessages added in v1.35.0

func (g *GitVCSReader) CommitMessages(worktreePath, base string, max int) ([]string, error)

func (*GitVCSReader) DiffShortstat added in v1.35.0

func (g *GitVCSReader) DiffShortstat(worktreePath string) (DiffStat, error)

func (*GitVCSReader) HasUncommitted added in v1.35.0

func (g *GitVCSReader) HasUncommitted(worktreePath string) (bool, error)

func (*GitVCSReader) ListWorktrees added in v1.35.0

func (g *GitVCSReader) ListWorktrees(repoPath string) ([]WorktreeInfo, error)

func (*GitVCSReader) ResolveDefaultBranch added in v1.35.0

func (g *GitVCSReader) ResolveDefaultBranch(repoPath string) string

type GoGitVCSReader added in v1.35.0

type GoGitVCSReader struct {
	// contains filtered or unexported fields
}

GoGitVCSReader implements VCSReader using the go-git library. No subprocesses are spawned; all operations run in-process. Prefer this in environments where spawning git subprocesses is undesirable or where index.lock contention is a concern.

All fields are zero-value safe — GoGitVCSReader{} is valid without a constructor.

func (*GoGitVCSReader) AheadBehind added in v1.35.0

func (g *GoGitVCSReader) AheadBehind(worktreePath, base string) (int, int, error)

AheadBehind returns the number of commits by which worktreePath's HEAD is ahead of and behind the given base ref.

Strategy (no subprocess): find the merge base with a BFS over each side, then count commits between each tip and the merge base. This bounds the walk to the diverged portion of history rather than the full reachable set.

func (*GoGitVCSReader) BlobCacheStats added in v1.39.0

func (g *GoGitVCSReader) BlobCacheStats() BlobCacheStats

func (*GoGitVCSReader) ClearCache added in v1.35.0

func (g *GoGitVCSReader) ClearCache()

ClearCache evicts ALL cached *git.Repository entries, allowing their internal go-git object LRU caches (~96 MB per repo, approxBytesPerCachedRepo) to be garbage-collected. Callers already holding a *cachedRepo reference (mid-scan) are unaffected — their reference keeps the object alive until the operation finishes. The next operation for an evicted repo re-opens it from disk (fast: only the pack index is read, not all objects).

This is intentionally NOT called on a routine ticker — PruneToMemoryBudget is the gentle, budget-respecting path for normal operation. ClearCache is reserved as an emergency escape valve for severe memory pressure (see Scanner.Start's prune ticker), since it evicts hot repos along with cold ones and forces needless re-opens for anything still actively in use.

func (*GoGitVCSReader) CommitMessages added in v1.35.0

func (g *GoGitVCSReader) CommitMessages(worktreePath, base string, max int) ([]string, error)

func (*GoGitVCSReader) DiffShortstat added in v1.35.0

func (g *GoGitVCSReader) DiffShortstat(worktreePath string) (DiffStat, error)

DiffShortstat returns changed-file and line counts for the given worktree. Results are cached for diffStatCacheTTL (30s) to avoid repeated wt.Status() calls from concurrent scanner workers, which was the top mutex hotspot (537M cycles, 13,941 events in profiling).

func (*GoGitVCSReader) HasUncommitted added in v1.35.0

func (g *GoGitVCSReader) HasUncommitted(worktreePath string) (bool, error)

func (*GoGitVCSReader) ListWorktrees added in v1.35.0

func (g *GoGitVCSReader) ListWorktrees(repoPath string) ([]WorktreeInfo, error)

func (*GoGitVCSReader) PruneToMemoryBudget added in v1.38.0

func (g *GoGitVCSReader) PruneToMemoryBudget()

PruneToMemoryBudget runs a gentle, budget-respecting prune pass: evicts idle-past-TTL entries, then LRU-trims any remaining excess down to the current memory-derived budget. Intended to run proactively on a short ticker (Scanner does this every minute) rather than only reactively on overflow, so cache pressure never has a chance to build up between polls. Unlike ClearCache, entries that are still hot (recently accessed, within budget) are left alone — this is the normal-operation degradation path; ClearCache remains available as a rarely-used emergency escape valve.

func (*GoGitVCSReader) ResolveDefaultBranch added in v1.35.0

func (g *GoGitVCSReader) ResolveDefaultBranch(repoPath string) string

func (*GoGitVCSReader) UnderSeverePressure added in v1.38.0

func (g *GoGitVCSReader) UnderSeverePressure() bool

UnderSeverePressure reports whether this process's heap is currently at or above severeMemoryPressureThreshold. Scanner callers use this to skip a scan cycle for a repo (staying on its last-known-good cached result) instead of piling on more allocation while already under pressure.

type JJVCSReader added in v1.35.0

type JJVCSReader struct{}

JJVCSReader implements VCSReader for Jujutsu (jj) repositories. jj uses a different model than git: there are no traditional "worktrees" (each checkout is a separate repo), and change tracking works differently. This implementation maps jj concepts onto the VCSReader interface as closely as possible so the scanner can surface unfinished work in jj repos.

func (*JJVCSReader) AheadBehind added in v1.35.0

func (j *JJVCSReader) AheadBehind(worktreePath, base string) (int, int, error)

AheadBehind counts revisions between the working copy and base. "ahead" = commits in @:: that are not ancestors of base (exclusive of base). "behind" = commits in base:: that are not ancestors of @ (exclusive of @).

func (*JJVCSReader) CommitMessages added in v1.35.0

func (j *JJVCSReader) CommitMessages(worktreePath, base string, max int) ([]string, error)

CommitMessages returns up to max commit descriptions from @ back to base.

func (*JJVCSReader) DiffShortstat added in v1.35.0

func (j *JJVCSReader) DiffShortstat(worktreePath string) (DiffStat, error)

DiffShortstat returns file-change counts for the working-copy change (@).

func (*JJVCSReader) HasUncommitted added in v1.35.0

func (j *JJVCSReader) HasUncommitted(worktreePath string) (bool, error)

HasUncommitted reports whether the working-copy change (@) has any modified files.

func (*JJVCSReader) ListWorktrees added in v1.35.0

func (j *JJVCSReader) ListWorktrees(repoPath string) ([]WorktreeInfo, error)

ListWorktrees returns a single WorktreeInfo for a jj repo. jj does not have git-style linked worktrees; the repo path is the only tree.

func (*JJVCSReader) ResolveDefaultBranch added in v1.35.0

func (j *JJVCSReader) ResolveDefaultBranch(repoPath string) string

ResolveDefaultBranch returns the trunk bookmark for the jj repo. Tries "trunk()" revset first, then falls back to common bookmark names.

type ScanResult

type ScanResult struct {
	RepoPath     string
	Branch       string
	WorktreePath string
	RepoName     string
	DisplayPath  string

	HasUncommitted bool
	AheadCount     int
	BehindCount    int
	DefaultBranch  string

	ChangedFiles  int
	LinesAdded    int
	LinesRemoved  int
	AheadMessages []string

	LastModified time.Time
	ScanTime     time.Time

	Status   ScanResultStatus
	ErrorMsg string

	// SessionIDs holds the UUIDs of all active stapler-squad sessions whose Path
	// matches this worktree. Multiple sessions can target the same worktree.
	SessionIDs []string
}

ScanResult holds the complete unfinished-work state for a single git worktree.

func (ScanResult) IsUnfinished

func (r ScanResult) IsUnfinished() bool

IsUnfinished returns true when at least one unfinished-work criterion is met.

type ScanResultStatus

type ScanResultStatus int

ScanResultStatus describes the quality of a scan result.

const (
	ScanResultStatusOK         ScanResultStatus = 0
	ScanResultStatusTimeout    ScanResultStatus = 1
	ScanResultStatusPermission ScanResultStatus = 2
	ScanResultStatusError      ScanResultStatus = 3
)

type Scanner

type Scanner struct {
	// contains filtered or unexported fields
}

Scanner is the central coordinator for the unfinished-work background scan.

func NewScanner

func NewScanner(eventBus *pkgevents.EventBus, stateStore *StateStore) *Scanner

NewScanner constructs a Scanner. Call Start(ctx) to begin background processing.

func NewScannerWithReader added in v1.35.0

func NewScannerWithReader(eventBus *pkgevents.EventBus, stateStore *StateStore, reader VCSReader) *Scanner

NewScannerWithReader constructs a Scanner with an explicit VCSReader. Used in tests to inject a fake or alternative implementation.

func (*Scanner) AddPinnedRepo

func (s *Scanner) AddPinnedRepo(repoPath string) error

AddPinnedRepo validates that path is a git repo, then adds it.

func (*Scanner) AddRepo

func (s *Scanner) AddRepo(repoPath string)

AddRepo adds a repo path to the scan set, registers it with the fsnotify watcher (the sole choke point every repo-discovery path — pinned, watch-dir walk, or session auto-spider — funnels through, so every tracked repo gets event-driven scanning regardless of how it was discovered), and immediately enqueues it for an initial scan.

func (*Scanner) EnqueueRepo

func (s *Scanner) EnqueueRepo(repoPath string)

EnqueueRepo queues a repo for scanning if it's not cached recently.

func (*Scanner) GetAllResults

func (s *Scanner) GetAllResults() []ScanResult

GetAllResults returns a snapshot of all stored scan results (excluding dismissed/snoozed).

func (*Scanner) GetResultByKey

func (s *Scanner) GetResultByKey(repoPath, branch string) (ScanResult, bool)

GetResultByKey returns a single stored result by (repoPath, branch).

func (*Scanner) InvalidateCache

func (s *Scanner) InvalidateCache(worktreePath string)

InvalidateCache invalidates the cache for a given worktree path.

func (*Scanner) RemovePinnedRepo

func (s *Scanner) RemovePinnedRepo(repoPath string)

RemovePinnedRepo removes a pinned repo from scanning.

func (*Scanner) RemoveRepo

func (s *Scanner) RemoveRepo(repoPath string)

RemoveRepo removes a repo from the scan set, purges its results, and unregisters its fsnotify watch.

func (*Scanner) RemoveResult

func (s *Scanner) RemoveResult(repoPath, branch string)

RemoveResult removes a result from the store (called after dismiss/snooze).

func (*Scanner) ResolveDefaultBranch added in v1.35.0

func (s *Scanner) ResolveDefaultBranch(repoPath string) string

ResolveDefaultBranch delegates to the underlying VCSReader.

func (*Scanner) ScanDone

func (s *Scanner) ScanDone() <-chan time.Time

ScanDone returns a channel that receives the completion time of each full scan.

func (*Scanner) SetAutoSpider

func (s *Scanner) SetAutoSpider(enabled bool)

SetAutoSpider enables or disables auto-spider of session paths.

func (*Scanner) SetTickInterval

func (s *Scanner) SetTickInterval(d time.Duration)

SetTickInterval overrides the default 30-second scan tick (for tests).

func (*Scanner) Start

func (s *Scanner) Start(ctx context.Context)

Start launches the coordinator goroutine, 4 worker goroutines, and (when available) the fsnotify watch loop that makes scanning event-driven rather than purely tick-driven. All goroutines exit cleanly when ctx is cancelled.

func (*Scanner) TriggerScan

func (s *Scanner) TriggerScan()

TriggerScan signals the coordinator to run a full scan immediately.

type StateStore

type StateStore struct {
	// contains filtered or unexported fields
}

StateStore manages persistent state for the unfinished-work feature. All public methods are thread-safe.

func NewStateStore

func NewStateStore(path string) (*StateStore, error)

NewStateStore loads (or creates) the state file at the given path.

func (*StateStore) AutoSpiderEnabled

func (s *StateStore) AutoSpiderEnabled() bool

AutoSpiderEnabled returns whether auto-spider is enabled.

func (*StateStore) CacheSummary

func (s *StateStore) CacheSummary(repoPath, branch, diffHash, summary string) error

CacheSummary stores an AI summary in the cache.

func (*StateStore) Dismiss

func (s *StateStore) Dismiss(repoPath, branch string) error

Dismiss permanently hides (repoPath, branch) from unfinished-work results.

func (*StateStore) GetCachedSummary

func (s *StateStore) GetCachedSummary(repoPath, branch, diffHash string) (string, bool)

GetCachedSummary returns a cached AI summary for (repoPath, branch, diffHash). Returns ("", false) on cache miss or expiry.

func (*StateStore) IsDismissed

func (s *StateStore) IsDismissed(repoPath, branch string) bool

IsDismissed returns true when (repoPath, branch) has been permanently dismissed.

func (*StateStore) IsSnoozed

func (s *StateStore) IsSnoozed(repoPath, branch, currentHeadSHA string) bool

IsSnoozed returns true if the worktree is snoozed and the HEAD SHA has not changed. When currentSHA differs from the snooze-time SHA, the snooze is auto-cleared. Note: currentSHA here is the worktree path (we'll look up HEAD SHA on demand if needed). For simplicity, we accept the HEAD SHA directly.

func (*StateStore) Load

func (s *StateStore) Load() error

Load reads the state file from disk. Returns os.ErrNotExist if the file is missing.

func (*StateStore) PinnedRepos

func (s *StateStore) PinnedRepos() []string

PinnedRepos returns the configured pinned repos.

func (*StateStore) SetConfig

func (s *StateStore) SetConfig(autoSpider bool, watchDirs, pinnedRepos []string) error

SetConfig atomically replaces config fields and saves.

func (*StateStore) Snooze

func (s *StateStore) Snooze(repoPath, branch, headSHA string) error

Snooze hides (repoPath, branch) until its HEAD SHA changes.

func (*StateStore) Undismiss

func (s *StateStore) Undismiss(repoPath, branch string) error

Undismiss removes the dismiss record for (repoPath, branch).

func (*StateStore) Unsnooze

func (s *StateStore) Unsnooze(repoPath, branch string) error

Unsnooze removes the snooze record.

func (*StateStore) WatchDirs

func (s *StateStore) WatchDirs() []string

WatchDirs returns the configured watch directories.

type UnfinishedEvent

type UnfinishedEvent struct {
	pkgevents.Event
	ScanResult  ScanResult
	CompletedAt time.Time
}

UnfinishedEvent extends pkgevents.Event with extra fields for unfinished-work events. It reuses the existing pkgevents.Event type by embedding the ScanResult in Context.

type VCSReader added in v1.35.0

type VCSReader interface {
	// ListWorktrees returns all worktrees registered in the repo at repoPath.
	ListWorktrees(repoPath string) ([]WorktreeInfo, error)

	// ResolveDefaultBranch returns the ref to compare unfinished work against
	// (e.g. "origin/main"). Returns "" if no default branch can be determined.
	ResolveDefaultBranch(repoPath string) string

	// HasUncommitted reports whether worktreePath has uncommitted changes.
	HasUncommitted(worktreePath string) (bool, error)

	// AheadBehind returns how many commits worktreePath is ahead of and behind base.
	AheadBehind(worktreePath, base string) (ahead, behind int, err error)

	// CommitMessages returns up to max one-line commit messages that are in
	// worktreePath but not in base.
	CommitMessages(worktreePath, base string, max int) ([]string, error)

	// DiffShortstat returns a summary of the diff between HEAD and the working tree.
	DiffShortstat(worktreePath string) (DiffStat, error)
}

VCSReader is the read-only interface the Scanner uses to interrogate repositories. All methods must be safe to call concurrently. The interface is intentionally narrow — only what the scanner needs.

type WatchDirWatcher

type WatchDirWatcher struct {
	// contains filtered or unexported fields
}

WatchDirWatcher discovers git repos under configured watch directories and triggers scans via fsnotify on .git/ changes.

func NewWatchDirWatcher

func NewWatchDirWatcher(scanner *Scanner, stateStore *StateStore) *WatchDirWatcher

NewWatchDirWatcher creates a WatchDirWatcher. It attempts to create an fsnotify watcher and falls back to polling if the system doesn't support it.

func (*WatchDirWatcher) AddPinnedRepo

func (w *WatchDirWatcher) AddPinnedRepo(repo string)

AddPinnedRepo adds a pinned repo and triggers an immediate scan.

func (*WatchDirWatcher) AddWatchDir

func (w *WatchDirWatcher) AddWatchDir(dir string)

AddWatchDir adds a new watch directory at runtime and walks it immediately.

func (*WatchDirWatcher) RemoveWatchDir

func (w *WatchDirWatcher) RemoveWatchDir(dir string)

RemoveWatchDir removes a watch directory (repos only removed if not covered by other sources).

func (*WatchDirWatcher) Start

func (w *WatchDirWatcher) Start(ctx context.Context)

Start begins watching all configured watch dirs and pinned repos. It performs an initial walk then starts the event loop.

type WorktreeInfo

type WorktreeInfo struct {
	Path       string
	HEAD       string
	Branch     string
	IsBare     bool
	IsDetached bool
	IsPrunable bool
	IsLocked   bool
}

WorktreeInfo is parsed from `git worktree list --porcelain`.

func ParseAllWorktrees

func ParseAllWorktrees(output string) []WorktreeInfo

ParseAllWorktrees parses `git worktree list --porcelain` output into WorktreeInfo slices. It does NOT filter—the caller decides what to skip.

Directories

Path Synopsis
mmapindex.go implements the mmap-backed .idx loader described in session/unfinished/design/pluggable-gitstore.md §5 ("mmap for the index — designed, not built").
mmapindex.go implements the mmap-backed .idx loader described in session/unfinished/design/pluggable-gitstore.md §5 ("mmap for the index — designed, not built").

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL