feat: example env files are listed hidden, not skipped
CI / Checks (pull_request) Successful in 37s
PR title / Conventional title (pull_request) Successful in 0s

.env.example, .sample, .template and the like show with H: someone may
have put a real value in one. They're left out of sharing a repo's env
files.
This commit is contained in:
keb committed 2026-10-15 14:06:16 +02:00
1 parent 4bac06ae5b
commit 96d9f24082
4 files changed
+35 -17

No files matched your search

+3 -2
View File
@@ -29,8 +29,9 @@ anything by hand.
## Which files ## Which files
**Env files**: `.env`, `.env.local`, `prod.env` and the like, also when **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; git-ignored (as they usually are), in the folders git tracks files in.
not `.env.example`, `.sample`, `.template` and the like. They're read `.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 directly; a file with SOPS metadata is always treated as SOPS, whatever
its name. its name.
+25 -11
View File
@@ -333,8 +333,9 @@ func (p *Provider) Containers(ctx context.Context, src provider.Source) ([]provi
c.Detail = "sops · " + strings.Join(meta.methods, ", ") c.Detail = "sops · " + strings.Join(meta.methods, ", ")
case isSops: case isSops:
return // a SOPS file where .sops.yaml says SOPS files don't go return // a SOPS file where .sops.yaml says SOPS files don't go
case IsEnv(f): case isEnvNamed(f):
c.Detail = provider.DetailPlainEnv c.Detail = provider.DetailPlainEnv
c.Hidden = isExample(f)
case IsSpring(f): case IsSpring(f):
c.Detail = "spring · " + springFormat(f) c.Detail = "spring · " + springFormat(f)
c.Hidden = p.springHidden(r.root, path) c.Hidden = p.springHidden(r.root, path)
@@ -717,7 +718,7 @@ func envFiles(root string, tracked []string) []string {
dirs := map[string]bool{".": true} dirs := map[string]bool{".": true}
for _, f := range tracked { for _, f := range tracked {
isTracked[f] = true isTracked[f] = true
if IsEnv(f) { if isEnvNamed(f) {
out = append(out, f) out = append(out, f)
} }
if d := path.Dir(f); !dirs[d] && !inPackages(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 { for _, e := range entries {
f := path.Join(d, e.Name()) 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) 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 // .env, .env.local, prod.env; not .env.example and the like, nor one in
// dependencies. // dependencies.
func IsEnv(f string) bool { func IsEnv(f string) bool {
if inPackages(f) { return isEnvNamed(f) && !isExample(f)
return false }
}
// 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)) base := strings.ToLower(path.Base(f))
for _, ext := range []string{".example", ".sample", ".template", ".dist", ".tmpl"} { return !inPackages(f) && (base == ".env" || strings.HasPrefix(base, ".env.") || strings.HasSuffix(base, ".env"))
if strings.HasSuffix(base, ext) { }
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 false return false
}
}
return base == ".env" || strings.HasPrefix(base, ".env.") || strings.HasSuffix(base, ".env")
} }
// plainValue reads a key from a plain env file. // plainValue reads a key from a plain env file.
+6 -3
View File
@@ -292,9 +292,12 @@ func TestPlainEnvFiles(t *testing.T) {
got := map[string]string{} got := map[string]string{}
for _, c := range files { for _, c := range files {
got[c.Name] = c.Detail 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 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) 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() exec.Command("git", "-C", root, "add", ".sops.yaml").Run()
files, _ = New(Settings{Dir: root}).Containers(ctx, srcs[0]) 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) t.Errorf("with rules elsewhere: %v", names)
} }
} }
+1 -1
View File
@@ -71,7 +71,7 @@ func (m *Model) shareFiles(src provider.Source, cs []provider.Container) tea.Cmd
return msg return msg
} }
for _, c := range all { for _, c := range all {
if c.Detail == provider.DetailPlainEnv { if c.Detail == provider.DetailPlainEnv && !c.Hidden { // not .env.example
cs = append(cs, c) cs = append(cs, c)
} }
} }