From 580c7ef985599b98277d80be38e39abe169b2dc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Einar=20Bergstr=C3=B8m?= Date: Thu, 15 Oct 2026 14:07:02 +0200 Subject: [PATCH] feat: example env files are listed hidden, not skipped (#83) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Karl-Einar Bergstrøm --- docs/repos.md | 5 +++-- internal/provider/repo/repo.go | 34 ++++++++++++++++++++--------- internal/provider/repo/repo_test.go | 9 +++++--- internal/ui/app/sharefiles.go | 2 +- 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/docs/repos.md b/docs/repos.md index 041b9e4..d84c110 100644 --- a/docs/repos.md +++ b/docs/repos.md @@ -29,8 +29,9 @@ anything by hand. ## Which files **Env files**: `.env`, `.env.local`, `prod.env` and the like, also when -git-ignored (as they usually are), in the folders git tracks files in; -not `.env.example`, `.sample`, `.template` and the like. They're read +git-ignored (as they usually are), in the folders git tracks files in. +`.env.example`, `.sample`, `.template` and the like are listed hidden +(`H` shows them): someone may have put a real value in one. They're read directly; a file with SOPS metadata is always treated as SOPS, whatever its name. diff --git a/internal/provider/repo/repo.go b/internal/provider/repo/repo.go index db2f510..ed1cf0e 100644 --- a/internal/provider/repo/repo.go +++ b/internal/provider/repo/repo.go @@ -333,8 +333,9 @@ func (p *Provider) Containers(ctx context.Context, src provider.Source) ([]provi c.Detail = "sops · " + strings.Join(meta.methods, ", ") case isSops: return // a SOPS file where .sops.yaml says SOPS files don't go - case IsEnv(f): + case isEnvNamed(f): c.Detail = provider.DetailPlainEnv + c.Hidden = isExample(f) case IsSpring(f): c.Detail = "spring · " + springFormat(f) c.Hidden = p.springHidden(r.root, path) @@ -717,7 +718,7 @@ func envFiles(root string, tracked []string) []string { dirs := map[string]bool{".": true} for _, f := range tracked { isTracked[f] = true - if IsEnv(f) { + if isEnvNamed(f) { out = append(out, f) } if d := path.Dir(f); !dirs[d] && !inPackages(f) { @@ -731,7 +732,7 @@ func envFiles(root string, tracked []string) []string { } for _, e := range entries { f := path.Join(d, e.Name()) - if e.Type().IsRegular() && !isTracked[f] && IsEnv(f) { + if e.Type().IsRegular() && !isTracked[f] && isEnvNamed(f) { out = append(out, f) } } @@ -766,16 +767,29 @@ func inPackages(f string) bool { // .env, .env.local, prod.env; not .env.example and the like, nor one in // dependencies. func IsEnv(f string) bool { - if inPackages(f) { - return false - } + return isEnvNamed(f) && !isExample(f) +} + +// isEnvNamed reports whether a file outside dependencies is named like an +// env file, examples too. +func isEnvNamed(f string) bool { base := strings.ToLower(path.Base(f)) - for _, ext := range []string{".example", ".sample", ".template", ".dist", ".tmpl"} { - if strings.HasSuffix(base, ext) { - return false + return !inPackages(f) && (base == ".env" || strings.HasPrefix(base, ".env.") || strings.HasSuffix(base, ".env")) +} + +var exampleWords = []string{"example", "sample", "template", "dist", "tmpl"} + +// isExample reports whether a file is an example or template of another: +// .env.example, docker-compose.example.yml. They're listed hidden: someone +// may have put a real value in one. +func isExample(f string) bool { + base := strings.ToLower(path.Base(f)) + for _, w := range exampleWords { + if strings.HasSuffix(base, "."+w) || strings.Contains(base, "."+w+".") { + return true } } - return base == ".env" || strings.HasPrefix(base, ".env.") || strings.HasSuffix(base, ".env") + return false } // plainValue reads a key from a plain env file. diff --git a/internal/provider/repo/repo_test.go b/internal/provider/repo/repo_test.go index 8c6e60d..feb2c32 100644 --- a/internal/provider/repo/repo_test.go +++ b/internal/provider/repo/repo_test.go @@ -292,9 +292,12 @@ func TestPlainEnvFiles(t *testing.T) { got := map[string]string{} for _, c := range files { got[c.Name] = c.Detail + if c.Hidden != (c.Name == ".env.example") { + t.Errorf("%s: hidden %t", c.Name, c.Hidden) + } } - if len(got) != 2 || got[".env"] != "env · plain text" || got["secrets/app.yaml"] != "sops · age" { - t.Fatalf("files %v (want .env and the sops file; not the example or node_modules)", got) + if len(got) != 3 || got[".env"] != "env · plain text" || got[".env.example"] != "env · plain text" || got["secrets/app.yaml"] != "sops · age" { + t.Fatalf("files %v (want .env, the example, hidden, and the sops file; not node_modules)", got) } var env provider.Container @@ -319,7 +322,7 @@ func TestPlainEnvFiles(t *testing.T) { os.WriteFile(filepath.Join(root, ".sops.yaml"), []byte("creation_rules:\n - path_regex: nowhere/.*\n"), 0o644) exec.Command("git", "-C", root, "add", ".sops.yaml").Run() files, _ = New(Settings{Dir: root}).Containers(ctx, srcs[0]) - if names := names(files, func(c provider.Container) string { return c.Name }); !slices.Equal(names, []string{".env"}) { + if names := names(files, func(c provider.Container) string { return c.Name }); !slices.Equal(names, []string{".env", ".env.example"}) { t.Errorf("with rules elsewhere: %v", names) } } diff --git a/internal/ui/app/sharefiles.go b/internal/ui/app/sharefiles.go index fb29af5..2eda082 100644 --- a/internal/ui/app/sharefiles.go +++ b/internal/ui/app/sharefiles.go @@ -71,7 +71,7 @@ func (m *Model) shareFiles(src provider.Source, cs []provider.Container) tea.Cmd return msg } for _, c := range all { - if c.Detail == provider.DetailPlainEnv { + if c.Detail == provider.DetailPlainEnv && !c.Hidden { // not .env.example cs = append(cs, c) } }