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
}