package repository import ( "errors" "os" "path/filepath" "sort" "strings" "sync" "time" "github.com/go-git/go-git/v5" ) type Info struct { Name string Path string } type Store struct { root string hidden map[string]struct{} mu sync.RWMutex repos map[string]Info cache map[string]*Repository refreshing bool } func Discover(root string, hiddenRepos ...string) (*Store, error) { hidden := normalizeHiddenRepos(hiddenRepos) repos, err := discoverRepos(root, hidden) if err != nil { return nil, err } return &Store{ root: root, hidden: hidden, repos: repos, cache: make(map[string]*Repository), }, nil } func (s *Store) Root() string { return s.root } func (s *Store) List() []Info { s.mu.RLock() defer s.mu.RUnlock() items := make([]Info, 0, len(s.repos)) for _, repo := range s.repos { items = append(items, repo) } sort.Slice(items, func(i, j int) bool { return items[i].Name < items[j].Name }) return items } func (s *Store) Open(name string) (*Repository, error) { s.mu.RLock() info, ok := s.repos[name] if !ok { s.mu.RUnlock() return nil, os.ErrNotExist } if repo, ok := s.cache[name]; ok { s.mu.RUnlock() return repo, nil } s.mu.RUnlock() repo, err := Open(info.Name, info.Path) if err != nil { return nil, err } s.mu.Lock() defer s.mu.Unlock() if cached, ok := s.cache[name]; ok { return cached, nil } if _, ok := s.repos[name]; !ok { return nil, os.ErrNotExist } s.cache[name] = repo return repo, nil } func (s *Store) Refresh() error { repos, err := discoverRepos(s.root, s.hidden) if err != nil { return err } s.mu.Lock() defer s.mu.Unlock() s.repos = repos s.cache = make(map[string]*Repository) return nil } func (s *Store) RefreshAsync(onError func(error)) bool { s.mu.Lock() if s.refreshing { s.mu.Unlock() return false } s.refreshing = true s.mu.Unlock() go func() { err := s.Refresh() s.mu.Lock() s.refreshing = false s.mu.Unlock() if err != nil && onError != nil { onError(err) } }() return true } func (s *Store) StartPeriodicRefresh(interval time.Duration, onError func(error)) func() { if interval <= 0 { return func() {} } done := make(chan struct{}) ticker := time.NewTicker(interval) go func() { defer ticker.Stop() for { select { case <-ticker.C: s.RefreshAsync(onError) case <-done: return } } }() var once sync.Once return func() { once.Do(func() { close(done) }) } } func normalizeHiddenRepos(hiddenRepos []string) map[string]struct{} { hidden := make(map[string]struct{}, len(hiddenRepos)) for _, repo := range hiddenRepos { name := strings.TrimSpace(repo) if name == "" { continue } hidden[name] = struct{}{} } return hidden } func discoverRepos(root string, hidden map[string]struct{}) (map[string]Info, error) { entries, err := os.ReadDir(root) if err != nil { return nil, err } repos := make(map[string]Info) for _, entry := range entries { if !entry.IsDir() { continue } name := entry.Name() if _, ok := hidden[name]; ok { continue } path := filepath.Join(root, name) if _, err := git.PlainOpen(path); err != nil { if errors.Is(err, git.ErrRepositoryNotExists) { continue } return nil, err } repos[name] = Info{Name: name, Path: path} } return repos, nil }