diff --git a/README.md b/README.md index 399fd8f13..e9f0a1ab1 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,12 @@ anon-access: read-write # You can grant read-only access to users without private keys. allow-keyless: false +# You can select the order in which repositories are shown: +# 'alphabetical': alphabetical order +# 'commit': repositories with latest commits show first +# 'config': keep the order of config.yaml -> this is the default option +repos-order: config + # Customize repos in the menu repos: - name: Home diff --git a/config/config.go b/config/config.go index 7046a52b7..1f3d8880a 100644 --- a/config/config.go +++ b/config/config.go @@ -41,6 +41,7 @@ type Config struct { AllowKeyless bool `yaml:"allow-keyless" json:"allow-keyless"` Users []User `yaml:"users" json:"users"` Repos []RepoConfig `yaml:"repos" json:"repos"` + ReposOrder string `yaml:"repos-order" json:"repos-order"` Source *RepoSource `yaml:"-" json:"-"` Cfg *config.Config `yaml:"-" json:"-"` mtx sync.Mutex @@ -89,7 +90,10 @@ func NewConfig(cfg *config.Config) (*Config, error) { pks = append(pks, pk) } - rs := NewRepoSource(cfg.RepoPath) + rs, err := NewRepoSource(cfg.RepoPath) + if err != nil { + return nil, fmt.Errorf("failed to get repository source folder: %w\n", err) + } c := &Config{ Cfg: cfg, } @@ -123,10 +127,11 @@ func NewConfig(cfg *config.Config) (*Config, error) { yamlUsers = fmt.Sprintf(hasKeyUserConfig, result) } yaml := fmt.Sprintf("%s%s%s", yamlConfig, yamlUsers, exampleUserConfig) - err := c.createDefaultConfigRepo(yaml) + err = c.createDefaultConfigRepo(yaml) if err != nil { return nil, err } + return c, nil } @@ -179,6 +184,7 @@ func (cfg *Config) Reload() error { if err := cfg.readConfig("config", cfg); err != nil { return fmt.Errorf("error reading config: %w", err) } + cfg.Source.Sort(cfg.ReposOrder, cfg.Repos) // sanitize repo configs repos := make(map[string]RepoConfig, 0) for _, r := range cfg.Repos { diff --git a/config/defaults.go b/config/defaults.go index a471fab58..b8b68f3a2 100644 --- a/config/defaults.go +++ b/config/defaults.go @@ -19,6 +19,12 @@ anon-access: %s # will be accepted. allow-keyless: %t +# You can select the order in which repositories are shown: +# 'alphabetical': alphabetical order +# 'commit': repositories with latest commits show first +# 'config': keep the order of config.yaml -> this is the default option +repos-order: config + # Customize repo display in the menu. repos: - name: Home diff --git a/config/git.go b/config/git.go index d1d4d6a3d..b90d71798 100644 --- a/config/git.go +++ b/config/git.go @@ -5,6 +5,7 @@ import ( "log" "os" "path/filepath" + "sort" "sync" "github.com/charmbracelet/soft-serve/git" @@ -15,6 +16,12 @@ import ( // ErrMissingRepo indicates that the requested repository could not be found. var ErrMissingRepo = errors.New("missing repo") +const ( + AscendingAlphabetical = "alphabetical" + DescendingLatestCommit = "commit" + MimicConfig = "config" +) + // Repo represents a Git repository. type Repo struct { name string @@ -174,31 +181,71 @@ func (r *Repo) Push(remote, branch string) error { // RepoSource is a reference to an on-disk repositories. type RepoSource struct { - Path string - mtx sync.Mutex - repos map[string]*Repo + Path string + mtx sync.Mutex + repos map[string]*Repo + sorted []*Repo } // NewRepoSource creates a new RepoSource. -func NewRepoSource(repoPath string) *RepoSource { +func NewRepoSource(repoPath string) (*RepoSource, error) { err := os.MkdirAll(repoPath, os.ModeDir|os.FileMode(0700)) if err != nil { - log.Fatal(err) + return nil, err } rs := &RepoSource{Path: repoPath} rs.repos = make(map[string]*Repo, 0) - return rs + + return rs, nil +} + +// sort sorts the slice of repositories in the RepoSource +func (rs *RepoSource) Sort(order string, rc []RepoConfig) { + repos := make([]*Repo, 0, len(rs.repos)) + switch order { + case AscendingAlphabetical: + for _, v := range rs.repos { + repos = append(repos, v) + } + sort.Slice(repos, func(i, j int) bool { + rci := repos[i].Repo() + rcj := repos[j].Repo() + return rci < rcj + }) + case DescendingLatestCommit: + for _, v := range rs.repos { + repos = append(repos, v) + } + sort.Slice(repos, func(i, j int) bool { + lci, err := repos[i].Commit("HEAD") + lcj, err := repos[j].Commit("HEAD") + if err != nil { + log.Fatal(err) + } + return lci.Committer.When.After(lcj.Committer.When) + }) + case MimicConfig: + isInConfig := map[string]bool{} + for _, r := range rc { + repos = append(repos, rs.repos[r.Repo]) + isInConfig[r.Repo] = true + } + for _, r := range rs.repos { + if !isInConfig[r.Repo()] { + repos = append(repos, r) + } + } + } + + rs.sorted = repos } // AllRepos returns all repositories for the given RepoSource. func (rs *RepoSource) AllRepos() []*Repo { rs.mtx.Lock() defer rs.mtx.Unlock() - repos := make([]*Repo, 0, len(rs.repos)) - for _, r := range rs.repos { - repos = append(repos, r) - } - return repos + + return rs.sorted } // GetRepo returns a repository by name. diff --git a/config/git_test.go b/config/git_test.go new file mode 100644 index 000000000..878e4ae53 --- /dev/null +++ b/config/git_test.go @@ -0,0 +1,105 @@ +package config + +import ( + "testing" + + "github.com/matryer/is" +) + +func TestNewRepoSource(t *testing.T) { + repoPath := "./testdata" + rs, err := NewRepoSource(repoPath) + repos := []string{ + "z-repo", + "1-repo", + "a-repo", + "m-repo", + "b-repo", + } + for _, r := range repos { + rs.InitRepo(r, true) + } + is := is.New(t) + is.NoErr(err) + is.Equal(len(rs.repos), 5) // there should be 5 repos +} + +func TestOrderReposAlphabetically(t *testing.T) { + repoPath := "./testdata" + rs, err := NewRepoSource(repoPath) + repos := []string{ + "z-repo", + "1-repo", + "a-repo", + "m-repo", + "b-repo", + } + for _, r := range repos { + rs.InitRepo(r, true) + } + rs.Sort("alphabetical", []RepoConfig{}) + expected := map[string]int{ // repos and their expected index + "1-repo": 0, + "a-repo": 1, + "b-repo": 2, + "m-repo": 3, + "z-repo": 4, + } + result := rs.AllRepos() // as returned from ls command + is := is.New(t) + is.NoErr(err) + for i, repo := range result { + is.Equal(expected[repo.Repo()], i) // repos should be alphabetically ordered + } +} + +func TestOrderReposConfig(t *testing.T) { + repoPath := "./testdata" + rs, err := NewRepoSource(repoPath) + config := []RepoConfig{ + { + Name: "test-repo-a", + Repo: "a-repo", + }, + { + Name: "test-repo-1", + Repo: "1-repo", + }, + { + Name: "test-repo-z", + Repo: "z-repo", + }, + { + Name: "test-repo-b", + Repo: "b-repo", + }, + { + Name: "test-repo-m", + Repo: "m-repo", + }, + } + repos := []string{ + "z-repo", + "1-repo", + "a-repo", + "m-repo", + "b-repo", + } + for _, r := range repos { + rs.InitRepo(r, true) + } + rs.Sort("config", config) + expected := map[string]int{ // repos and their expected index + "a-repo": 0, + "1-repo": 1, + "z-repo": 2, + "b-repo": 3, + "m-repo": 4, + } + result := rs.AllRepos() // as returned from ls command + is := is.New(t) + is.NoErr(err) + for i, repo := range result { + is.Equal(expected[repo.Repo()], i) // repos should be ordered as in config file + } +} diff --git a/ui/pages/selection/selection.go b/ui/pages/selection/selection.go index d2c46b1bd..4834baee0 100644 --- a/ui/pages/selection/selection.go +++ b/ui/pages/selection/selection.go @@ -187,21 +187,6 @@ func (s *Selection) Init() tea.Cmd { items := make([]selector.IdentifiableItem, 0) cfg := s.cfg pk := s.pk - // Put configured repos first - for _, r := range cfg.Repos { - acc := cfg.AuthRepo(r.Repo, pk) - if r.Private && acc < wgit.ReadOnlyAccess { - continue - } - repo, err := cfg.Source.GetRepo(r.Repo) - if err != nil { - continue - } - items = append(items, Item{ - repo: repo, - cmd: git.RepoURL(cfg.Host, cfg.Port, r.Repo), - }) - } for _, r := range cfg.Source.AllRepos() { if r.Repo() == "config" { rm, rp := r.Readme() @@ -234,7 +219,7 @@ func (s *Selection) Init() tea.Cmd { items = append(items, Item{ repo: r, lastUpdate: lastUpdate, - cmd: git.RepoURL(cfg.Host, cfg.Port, r.Name()), + cmd: git.RepoURL(cfg.Host, cfg.Port, r.Repo()), }) } }