workspace

package
v1.16.0 Latest Latest
Warning

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

Go to latest
Published: Apr 21, 2026 License: AGPL-3.0 Imports: 10 Imported by: 0

Documentation

Overview

Package workspace provides workspace tracking and status management for stapler-squad sessions. It supports multi-pod deployments via distributed locking and cache invalidation.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrLockTimeout indicates lock acquisition timed out
	ErrLockTimeout = &LockError{Op: "acquire", Err: context.DeadlineExceeded}

	// ErrLockNotHeld indicates trying to release a lock that isn't held
	ErrLockNotHeld = &LockError{Op: "release", Err: context.Canceled}

	// ErrNotSupported indicates the operation isn't supported by this implementation
	ErrNotSupported = &LockError{Op: "extend", Err: context.Canceled}
)

Common lock errors

View Source
var (
	// ErrNotFound is returned when a workspace is not found.
	ErrNotFound = fmt.Errorf("workspace not found")
)

Functions

This section is empty.

Types

type BatchStatusResult

type BatchStatusResult struct {
	Path      string        // Workspace path
	Status    *vc.VCSStatus // VCS status (nil if error)
	Error     error         // Error (nil if success)
	Duration  time.Duration // How long the operation took
	IsPartial bool          // True if status is incomplete
}

BatchStatusResult contains the result of a batch status operation

type BatchVCSProvider

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

BatchVCSProvider collects VCS status for multiple workspaces concurrently. It provides efficient batch operations with timeout handling and graceful degradation.

func NewBatchVCSProvider

func NewBatchVCSProvider(maxConcurrent int, timeout time.Duration) *BatchVCSProvider

NewBatchVCSProvider creates a new batch VCS provider.

func (*BatchVCSProvider) GetStatuses

func (b *BatchVCSProvider) GetStatuses(ctx context.Context, paths []string) []BatchStatusResult

GetStatuses collects VCS status for multiple paths concurrently. It returns results for all paths, including errors for failed operations.

func (*BatchVCSProvider) GetStatusesByRepository

func (b *BatchVCSProvider) GetStatusesByRepository(ctx context.Context, workspaces []*TrackedWorkspace) map[string][]BatchStatusResult

GetStatusesByRepository groups workspaces by repository root and collects status. This is more efficient when multiple workspaces share the same repository.

type CacheInvalidationNotifier

type CacheInvalidationNotifier interface {
	// Subscribe starts listening for invalidation events.
	// The handler is called for each invalidation with the workspace path.
	// Runs until context is cancelled.
	Subscribe(ctx context.Context, handler func(workspacePath string)) error

	// Publish broadcasts an invalidation event to all subscribers.
	// workspacePath can be empty string to invalidate all caches.
	Publish(ctx context.Context, workspacePath string) error

	// Close stops listening and releases resources.
	Close() error
}

CacheInvalidationNotifier handles cross-pod cache invalidation. Implementations: - PostgresListenNotify: Uses LISTEN/NOTIFY for real-time notifications - PollingNotifier: Poll-based invalidation for SQLite (fallback)

type ChangesSummary

type ChangesSummary struct {
	TotalRepositories  int // Distinct repository roots
	TotalWorkspaces    int // All tracked workspaces
	TotalUncommitted   int // Files with uncommitted changes
	TotalUntracked     int // Untracked files
	TotalStaged        int // Staged files
	TotalConflicts     int // Conflicted files
	WorkspacesWithWork int // Workspaces with any pending changes
	OrphanedWorkspaces int // Workspaces without active sessions
}

ChangesSummary provides aggregated statistics across all workspaces

func SummaryFromResults

func SummaryFromResults(results []BatchStatusResult) *ChangesSummary

SummaryFromResults creates a ChangesSummary from batch results.

type DistributedLock

type DistributedLock interface {
	// Acquire obtains an exclusive lock for the given resource.
	// Blocks until the lock is acquired or context is cancelled/times out.
	// Returns a handle that must be released when done.
	Acquire(ctx context.Context, resource string, timeout time.Duration) (LockHandle, error)

	// TryAcquire attempts to acquire lock without blocking.
	// Returns (handle, true) if acquired, (nil, false) if unavailable.
	TryAcquire(ctx context.Context, resource string) (LockHandle, bool, error)

	// Close releases any resources held by the lock implementation
	Close() error
}

DistributedLock provides coordination across multiple pods/processes. Implementations: - PostgresAdvisoryLock: Uses pg_advisory_lock() for multi-pod deployments - SQLiteTransactionLock: Uses BEGIN IMMEDIATE for single-pod deployments - NoOpLock: For testing or single-instance deployments without contention

type LockError

type LockError struct {
	Op       string // Operation: "acquire", "release", "extend"
	Resource string // Resource being locked
	Err      error  // Underlying error
}

LockError represents errors from distributed lock operations

func (*LockError) Error

func (e *LockError) Error() string

func (*LockError) Unwrap

func (e *LockError) Unwrap() error

type LockHandle

type LockHandle interface {
	// Release releases the lock. Must be called when done.
	// Safe to call multiple times (subsequent calls are no-ops).
	Release() error

	// Extend extends the lock TTL for long-running operations.
	// Not all implementations support this (may return ErrNotSupported).
	Extend(duration time.Duration) error

	// IsValid checks if the lock is still held.
	// Returns false if lock was released or expired.
	IsValid() bool

	// Resource returns the resource name this lock is for.
	Resource() string
}

LockHandle represents an acquired distributed lock. The lock is held until Release() is called or the context is cancelled.

type NoOpLock

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

NoOpLock is a no-operation lock for single-instance deployments or testing where distributed coordination is not needed. It still provides in-memory locking for thread safety.

func NewNoOpLock

func NewNoOpLock() *NoOpLock

NewNoOpLock creates a new no-operation lock.

func (*NoOpLock) Acquire

func (l *NoOpLock) Acquire(ctx context.Context, resource string, timeout time.Duration) (LockHandle, error)

Acquire obtains a lock for the given resource. For NoOpLock, this provides in-memory thread safety only.

func (*NoOpLock) Close

func (l *NoOpLock) Close() error

Close releases all locks.

func (*NoOpLock) TryAcquire

func (l *NoOpLock) TryAcquire(ctx context.Context, resource string) (LockHandle, bool, error)

TryAcquire attempts to acquire lock without blocking.

type NoOpNotifier

type NoOpNotifier struct{}

NoOpNotifier is a notifier that does nothing. Used when cache invalidation is not needed (single-pod deployments).

func NewNoOpNotifier

func NewNoOpNotifier() *NoOpNotifier

NewNoOpNotifier creates a new no-operation notifier.

func (*NoOpNotifier) Close

func (n *NoOpNotifier) Close() error

Close is a no-op.

func (*NoOpNotifier) Publish

func (n *NoOpNotifier) Publish(ctx context.Context, workspacePath string) error

Publish is a no-op.

func (*NoOpNotifier) Subscribe

func (n *NoOpNotifier) Subscribe(ctx context.Context, handler func(workspacePath string)) error

Subscribe is a no-op.

type PollingNotifier

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

PollingNotifier implements CacheInvalidationNotifier using polling. This is used for SQLite deployments where LISTEN/NOTIFY is not available. It's less efficient but works with any database backend.

func NewPollingNotifier

func NewPollingNotifier(pollInterval time.Duration) *PollingNotifier

NewPollingNotifier creates a new polling-based cache invalidation notifier. Default poll interval is 5 seconds.

func (*PollingNotifier) Close

func (n *PollingNotifier) Close() error

Close stops the polling notifier.

func (*PollingNotifier) Publish

func (n *PollingNotifier) Publish(ctx context.Context, workspacePath string) error

Publish broadcasts an invalidation event to all local subscribers. For polling notifier, this only notifies local subscribers.

func (*PollingNotifier) Subscribe

func (n *PollingNotifier) Subscribe(ctx context.Context, handler func(workspacePath string)) error

Subscribe starts listening for invalidation events. With polling notifier, this is primarily for local invalidation within the same process.

type PostgresAdvisoryLock

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

PostgresAdvisoryLock implements distributed locking using PostgreSQL advisory locks. Advisory locks are application-level locks that don't conflict with row-level locks. They are ideal for distributed coordination across multiple application instances.

Key characteristics: - Reentrant within the same database connection - Automatically released when the connection closes - No risk of deadlock with row-level operations - Efficient: no disk I/O, fully in-memory on the PostgreSQL server

func NewPostgresAdvisoryLock

func NewPostgresAdvisoryLock(db *sql.DB, namespace int64) *PostgresAdvisoryLock

NewPostgresAdvisoryLock creates a new PostgreSQL advisory lock manager. The namespace should be unique to this application to avoid conflicts. Recommended: use a consistent hash of the application name.

func (*PostgresAdvisoryLock) Acquire

func (l *PostgresAdvisoryLock) Acquire(ctx context.Context, resource string, timeout time.Duration) (LockHandle, error)

Acquire obtains an exclusive advisory lock for the given resource. The lock is held on a dedicated connection and released when Release() is called or when the connection closes.

func (*PostgresAdvisoryLock) Close

func (l *PostgresAdvisoryLock) Close() error

Close releases all locks.

func (*PostgresAdvisoryLock) TryAcquire

func (l *PostgresAdvisoryLock) TryAcquire(ctx context.Context, resource string) (LockHandle, bool, error)

TryAcquire attempts to acquire lock without blocking. Uses pg_try_advisory_lock which returns immediately.

type PostgresListenNotifyNotifier

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

PostgresListenNotifyNotifier implements CacheInvalidationNotifier using PostgreSQL's LISTEN/NOTIFY mechanism for real-time cross-pod cache invalidation.

Note: This is a simplified implementation. For production use with high-throughput requirements, consider using github.com/jackc/pgx directly for native LISTEN/NOTIFY support.

func NewPostgresListenNotifyNotifier

func NewPostgresListenNotifyNotifier(db *sql.DB, channel string) *PostgresListenNotifyNotifier

NewPostgresListenNotifyNotifier creates a new PostgreSQL LISTEN/NOTIFY notifier. The channel name should be unique to this application's workspace invalidation.

func (*PostgresListenNotifyNotifier) Close

Close stops listening and releases resources.

func (*PostgresListenNotifyNotifier) Publish

func (n *PostgresListenNotifyNotifier) Publish(ctx context.Context, workspacePath string) error

Publish sends a notification to all listening pods via PostgreSQL NOTIFY.

func (*PostgresListenNotifyNotifier) Subscribe

func (n *PostgresListenNotifyNotifier) Subscribe(ctx context.Context, handler func(workspacePath string)) error

Subscribe starts listening for invalidation events on the PostgreSQL channel. This spawns a goroutine that listens for notifications.

type Registry

type Registry interface {
	// Registration
	Register(ctx context.Context, workspace *TrackedWorkspace) error
	Unregister(ctx context.Context, path string) error
	MarkOrphaned(ctx context.Context, path string) error

	// Query
	Get(ctx context.Context, path string) (*TrackedWorkspace, error)
	List(ctx context.Context, filter *WorkspaceFilter) ([]*TrackedWorkspace, error)
	ListByRepository(ctx context.Context, repoRoot string) ([]*TrackedWorkspace, error)

	// Status
	GetStatus(ctx context.Context, path string, opts StatusRefreshOptions) (*WorkspaceStatus, error)
	GetAllStatuses(ctx context.Context, opts StatusRefreshOptions) ([]*WorkspaceStatus, error)
	GetSummary(ctx context.Context) (*ChangesSummary, error)

	// Batch operations
	RefreshStatuses(ctx context.Context, paths []string, opts StatusRefreshOptions) ([]*WorkspaceStatus, error)

	// Lifecycle
	Close() error
}

Registry tracks all known workspaces and their status. It provides a central point for workspace discovery, status caching, and coordination across multiple pods via distributed locking.

type RegistryConfig

type RegistryConfig struct {
	// Distributed lock for multi-pod coordination
	Lock DistributedLock

	// Cache invalidation for cross-pod coordination
	Notifier CacheInvalidationNotifier

	// Cache settings
	CacheTTL         time.Duration // How long to cache status (default: 30s)
	MaxCacheSize     int           // Maximum cached entries (default: 1000)
	RefreshBatchSize int           // Max concurrent status refreshes (default: 10)

	// Timeouts
	LockTimeout      time.Duration // Lock acquisition timeout (default: 10s)
	OperationTimeout time.Duration // Individual operation timeout (default: 5s)
}

RegistryConfig configures the workspace registry

func DefaultRegistryConfig

func DefaultRegistryConfig() RegistryConfig

DefaultRegistryConfig returns sensible defaults

type SQLiteTransactionLock

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

SQLiteTransactionLock implements distributed locking using SQLite transactions. It uses BEGIN IMMEDIATE to acquire an exclusive lock on the database. This is suitable for single-pod deployments or when using a shared filesystem.

Note: SQLite locks are database-wide, not per-resource. For fine-grained locking, use the in-memory lock map to simulate per-resource locks within a transaction.

func NewSQLiteTransactionLock

func NewSQLiteTransactionLock(db *sql.DB) *SQLiteTransactionLock

NewSQLiteTransactionLock creates a new SQLite-based distributed lock. The db connection should already be configured with WAL mode and appropriate timeouts.

func (*SQLiteTransactionLock) Acquire

func (l *SQLiteTransactionLock) Acquire(ctx context.Context, resource string, timeout time.Duration) (LockHandle, error)

Acquire obtains an exclusive lock for the given resource. This starts a SQLite transaction with BEGIN IMMEDIATE to acquire the write lock.

func (*SQLiteTransactionLock) Close

func (l *SQLiteTransactionLock) Close() error

Close releases all locks and closes resources.

func (*SQLiteTransactionLock) TryAcquire

func (l *SQLiteTransactionLock) TryAcquire(ctx context.Context, resource string) (LockHandle, bool, error)

TryAcquire attempts to acquire lock without blocking.

type StatusRefreshOptions

type StatusRefreshOptions struct {
	// Force refresh even if cache is fresh
	Force bool

	// Maximum age for cached status to be considered fresh
	MaxAge time.Duration

	// Timeout for individual git operations
	Timeout time.Duration

	// Continue on errors (return partial results)
	ContinueOnError bool
}

StatusRefreshOptions controls how status refresh behaves

func DefaultStatusRefreshOptions

func DefaultStatusRefreshOptions() StatusRefreshOptions

DefaultStatusRefreshOptions returns sensible defaults

type TrackedWorkspace

type TrackedWorkspace struct {
	// Identity
	Path           string // Absolute path to workspace
	RepositoryRoot string // Root of the git/jj repository
	WorktreePath   string // Path if this is a worktree (empty if not)
	MainRepoPath   string // Main repo path if this is a worktree

	// Session association
	SessionTitle  string         // Title of associated session (empty if orphaned)
	SessionStatus session.Status // Status of associated session
	IsOrphaned    bool           // True if no active session owns this workspace

	// VCS information
	VCSType vc.VCSType // Git or Jujutsu

	// Timestamps
	LastChecked  time.Time // When VCS status was last refreshed
	LastActivity time.Time // Last file modification detected

	// Attention flags
	NeedsAttention  bool   // Has issues requiring action
	AttentionReason string // Why attention is needed
}

TrackedWorkspace represents a directory tracked by stapler-squad

type WorkspaceFilter

type WorkspaceFilter struct {
	// Path filters
	RepositoryRoot string // Filter by repository root
	PathPrefix     string // Filter by path prefix

	// Status filters
	IncludeOrphaned  bool // Include orphaned workspaces
	OnlyWithChanges  bool // Only workspaces with uncommitted changes
	OnlyWithConflict bool // Only workspaces with conflicts

	// Session filters
	SessionStatus *session.Status // Filter by session status
}

WorkspaceFilter defines criteria for filtering workspace queries

type WorkspaceRegistry

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

WorkspaceRegistry is the default implementation of Registry. It uses in-memory caching with optional distributed locking.

func NewRegistry

func NewRegistry(config RegistryConfig) *WorkspaceRegistry

NewRegistry creates a new workspace registry with the given configuration.

func (*WorkspaceRegistry) Close

func (r *WorkspaceRegistry) Close() error

Close stops background processes and releases resources.

func (*WorkspaceRegistry) Get

Get retrieves a tracked workspace by path.

func (*WorkspaceRegistry) GetAllStatuses

func (r *WorkspaceRegistry) GetAllStatuses(ctx context.Context, opts StatusRefreshOptions) ([]*WorkspaceStatus, error)

GetAllStatuses retrieves VCS status for all tracked workspaces.

func (*WorkspaceRegistry) GetStatus

GetStatus retrieves the VCS status for a workspace.

func (*WorkspaceRegistry) GetSummary

func (r *WorkspaceRegistry) GetSummary(ctx context.Context) (*ChangesSummary, error)

GetSummary returns aggregated statistics across all workspaces.

func (*WorkspaceRegistry) List

List returns all tracked workspaces matching the filter.

func (*WorkspaceRegistry) ListByRepository

func (r *WorkspaceRegistry) ListByRepository(ctx context.Context, repoRoot string) ([]*TrackedWorkspace, error)

ListByRepository returns all workspaces for a given repository root.

func (*WorkspaceRegistry) MarkOrphaned

func (r *WorkspaceRegistry) MarkOrphaned(ctx context.Context, path string) error

MarkOrphaned marks a workspace as orphaned (no active session).

func (*WorkspaceRegistry) RefreshStatuses

func (r *WorkspaceRegistry) RefreshStatuses(ctx context.Context, paths []string, opts StatusRefreshOptions) ([]*WorkspaceStatus, error)

RefreshStatuses refreshes VCS status for the given workspace paths.

func (*WorkspaceRegistry) Register

func (r *WorkspaceRegistry) Register(ctx context.Context, workspace *TrackedWorkspace) error

Register adds a workspace to the registry.

func (*WorkspaceRegistry) Unregister

func (r *WorkspaceRegistry) Unregister(ctx context.Context, path string) error

Unregister removes a workspace from the registry.

type WorkspaceStatus

type WorkspaceStatus struct {
	// Embedded VCS status
	VCSStatus *vc.VCSStatus

	// Workspace context
	WorkspacePath string         // Path to this workspace
	SessionTitle  string         // Associated session (if any)
	SessionStatus session.Status // Session state
	IsOrphaned    bool           // No active session
	IsWorktree    bool           // Is a git worktree

	// Activity tracking
	LastActivity time.Time // Last file modification
	LastChecked  time.Time // When status was collected

	// Attention flags
	NeedsAttention  bool   // Has issues requiring action
	AttentionReason string // Why attention is needed

	// Error state
	Error     error  // Error during status collection (nil if success)
	ErrorMsg  string // Human-readable error message
	IsPartial bool   // True if status is incomplete due to errors
}

WorkspaceStatus combines VCS status with workspace-specific information

Jump to

Keyboard shortcuts

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