diff --git a/cmd/lazysecrets/main.go b/cmd/lazysecrets/main.go index 38afd40..23a50db 100644 --- a/cmd/lazysecrets/main.go +++ b/cmd/lazysecrets/main.go @@ -287,7 +287,7 @@ Flags: if start == "" { start, _ = os.Getwd() } - roots := []string{"~/git"} + roots := repo.DefaultRoots if pc.Repos.Roots != nil { roots = *pc.Repos.Roots } diff --git a/docs/repos.md b/docs/repos.md index d8ec9d0..68c9790 100644 --- a/docs/repos.md +++ b/docs/repos.md @@ -7,10 +7,11 @@ lazysecrets shows git repositories under **Repos**: their ## Which repos -**Repos** lists every repo with secret files under the `roots` (`~/git` -by default; one or two levels down, `repo` or `org/repo`), any listed in -`paths`, and any you starred something in (the repo, a file or a key), -wherever it is. They're checked in the background. +**Repos** lists every repo with secret files under the `roots` (by +default whichever of `~/git`, `~/repo`, `~/repos`, `~/projects`, `~/code`, +`~/source` and `~/src` exist; one or two levels down, `repo` or +`org/repo`), any listed in `paths`, and any you starred something in +(the repo, a file or a key), wherever it is. They're checked in the background. Started inside a repo, that repo gets a row of its own at the top, `Repo · `, and lazysecrets opens on it, with the files under the @@ -49,7 +50,7 @@ Folders of dependencies and caches are never looked in, tracked or not: ```yaml providers: repos: - roots: [~/git] # folders of repos (the default; [] turns it off) + roots: [~/git] # folders of repos (replaces the defaults; [] turns it off) paths: [~/work/infra] # single repos elsewhere include: ["secrets/**", "**/*.sops.yaml"] # globs from the repo root; ** for any depth exclude: ["**/testdata/**"] diff --git a/internal/config/config.go b/internal/config/config.go index d44885c..e8a59df 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -115,8 +115,8 @@ type Repos struct { // Enabled defaults to true. Enabled *bool `yaml:"enabled"` Paths []string `yaml:"paths"` - // Roots are folders of repos. Defaults to ~/git; an empty list - // turns it off. + // Roots are folders of repos. Defaults to repo.DefaultRoots (~/git, + // ~/src, ~/code and the like); an empty list turns it off. Roots *[]string `yaml:"roots"` // Include and Exclude are globs relative to a repo's root (** for any // depth), for repos without their own .lazysecrets.yaml. Without them, diff --git a/internal/provider/repo/repo.go b/internal/provider/repo/repo.go index 1f53ad7..d9261eb 100644 --- a/internal/provider/repo/repo.go +++ b/internal/provider/repo/repo.go @@ -80,6 +80,10 @@ type repoInfo struct { here bool // the repo lazysecrets started in } +// DefaultRoots are the usual folders of repos, used when the config +// doesn't set roots. Ones that don't exist cost a stat each. +var DefaultRoots = []string{"~/git", "~/repo", "~/repos", "~/projects", "~/code", "~/source", "~/src"} + func New(s Settings) *Provider { if s.Sops == "" { s.Sops = "sops" @@ -116,10 +120,18 @@ func (p *Provider) load() { } add(root) } + // Missing roots are skipped (most of the defaults are), and one + // that's a symlink to another is scanned once. + scanned := map[string]bool{} for _, dir := range p.s.Roots { - base, _ := filepath.EvalSymlinks(expandHome(dir)) - for _, root := range reposIn(expandHome(dir), 2) { - if name, err := filepath.Rel(base, root); err == nil && !strings.HasPrefix(name, "..") { + base, err := filepath.EvalSymlinks(expandHome(dir)) + if err != nil || scanned[base] { + continue + } + scanned[base] = true + for _, root := range reposIn(base, 2) { + // A repo under two roots keeps the first one's name. + if name, err := filepath.Rel(base, root); err == nil && !strings.HasPrefix(name, "..") && names[root] == "" { names[root] = filepath.ToSlash(name) } add(root) diff --git a/internal/provider/repo/repo_test.go b/internal/provider/repo/repo_test.go index 55b11dd..e22001a 100644 --- a/internal/provider/repo/repo_test.go +++ b/internal/provider/repo/repo_test.go @@ -336,6 +336,30 @@ func TestRoots(t *testing.T) { } } +func TestRootsDeduped(t *testing.T) { + root, _ := filepath.EvalSymlinks(t.TempDir()) + os.Rename(gitRepo(t, map[string]string{"secrets/a.yaml": sopsYAML}), filepath.Join(root, "infra")) + os.MkdirAll(filepath.Join(root, "acme"), 0o755) + os.Rename(gitRepo(t, map[string]string{"prod.env": sopsEnv}), filepath.Join(root, "acme", "billing")) + + // ~/src → ~/git, and a root reaching one repo at another depth. + links := t.TempDir() + alias := filepath.Join(links, "src") + os.Symlink(root, alias) + missing := filepath.Join(links, "nope") + acme := filepath.Join(root, "acme") + + srcs, err := New(Settings{Dir: links, Roots: []string{missing, root, alias, acme}}).Sources(context.Background()) + if err != nil { + t.Fatal(err) + } + got := names(srcs, func(s provider.Source) string { return s.Name }) + slices.Sort(got) + if !slices.Equal(got, []string{"acme/billing", "infra"}) { + t.Errorf("repos: %v, want each once, named from the first root", got) + } +} + func TestStarredReposAlongside(t *testing.T) { here := gitRepo(t, map[string]string{"secrets/a.yaml": sopsYAML}) starred := gitRepo(t, map[string]string{"prod.env": sopsEnv})