package scanner import ( "context" "path/filepath" "golang.org/x/sync/errgroup" "gitea.oblak.solutions/dimitar/gitFlow/internal/git" "gitea.oblak.solutions/dimitar/gitFlow/pkg/status" ) // Scanner produces status snapshots for a set of repositories, scanning them // concurrently with a bounded worker pool. type Scanner struct { git *git.Executor workers int } // ScannerOption configures a Scanner (functional options pattern). type ScannerOption func(*Scanner) // WithGitExecutor overrides the git executor used for scanning. func WithGitExecutor(e *git.Executor) ScannerOption { return func(s *Scanner) { s.git = e } } // WithWorkers sets the maximum number of concurrent git scans. func WithWorkers(n int) ScannerOption { return func(s *Scanner) { if n > 0 { s.workers = n } } } // NewScanner builds a Scanner with sane defaults. func NewScanner(opts ...ScannerOption) *Scanner { s := &Scanner{git: git.NewExecutor(), workers: 8} for _, opt := range opts { opt(s) } return s } // Scan snapshots every repository path. A repository that fails to scan is // reported with StatusError rather than aborting the pass. The returned // error is non-nil only when the context is cancelled mid-scan. func (s *Scanner) Scan(ctx context.Context, paths []string) ([]status.RepoInfo, error) { infos := make([]status.RepoInfo, len(paths)) g, ctx := errgroup.WithContext(ctx) g.SetLimit(s.workers) for i, path := range paths { i, path := i, path g.Go(func() error { info, err := s.git.Status(ctx, path) if err != nil { if ctx.Err() != nil { return ctx.Err() // cancelled mid-scan, not a repo failure } info = status.RepoInfo{ Path: path, Name: filepath.Base(path), Status: status.StatusError, Error: err.Error(), } } infos[i] = info return nil }) } return infos, g.Wait() }