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) SplitPath(raw string) (string, string, bool) { clean := strings.Trim(strings.TrimSpace(raw), "/") if clean == "" { return "", "", false } parts := strings.Split(clean, "/") s.mu.RLock() defer s.mu.RUnlock() for i := len(parts); i >= 1; i-- { candidate := strings.Join(parts[:i], "/") if _, ok := s.repos[candidate]; !ok { continue } if i == len(parts) { return candidate, "", true } return candidate, "/" + strings.Join(parts[i:], "/"), true } return "", "", false } 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.Trim(filepath.ToSlash(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 } found, err := appendRepo(repos, hidden, root, entry.Name()) if err != nil { return nil, err } if found { continue } children, err := os.ReadDir(filepath.Join(root, entry.Name())) if err != nil { if errors.Is(err, os.ErrNotExist) { continue } return nil, err } for _, child := range children { if !child.IsDir() { continue } if _, err := appendRepo(repos, hidden, root, entry.Name(), child.Name()); err != nil { return nil, err } } } return repos, nil } func appendRepo(repos map[string]Info, hidden map[string]struct{}, root string, parts ...string) (bool, error) { name := filepath.ToSlash(filepath.Join(parts...)) repoPath := filepath.Join(append([]string{root}, parts...)...) if _, err := git.PlainOpen(repoPath); err != nil { if errors.Is(err, git.ErrRepositoryNotExists) { return false, nil } return false, err } if _, ok := hidden[name]; ok { return true, nil } repos[name] = Info{Name: name, Path: repoPath} return true, nil }