From d558d6d9a7a4b2981ba8006ee992c3d28ad0f824 Mon Sep 17 00:00:00 2001 From: Marwin Moellers Date: Tue, 29 Sep 2026 15:31:42 +0200 Subject: [PATCH] Normalize relative paths before writing add rules nebel add field ./test.yaml and nebel add file ./*.yaml wrote the raw, un-normalized path/glob into .gitattributes and .nebel.yaml. git never reports a leading "./" for tracked files, so the pattern silently failed to match and the file was left unencrypted. Fixes #8 Co-Authored-By: Claude Sonnet 5 --- cmd/nebel/add.go | 14 ++++++--- cmd/nebel/addfield_e2e_test.go | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/cmd/nebel/add.go b/cmd/nebel/add.go index 3ad8a7b..24e9178 100644 --- a/cmd/nebel/add.go +++ b/cmd/nebel/add.go @@ -61,7 +61,7 @@ func runAddFile(args []string) error { if len(args) != 1 { return fmt.Errorf("usage: nebel add file ") } - glob := args[0] + glob := normalizePath(args[0]) root, configPath, cfg, err := openConfig() if err != nil { @@ -103,15 +103,15 @@ func runAddField(args []string) error { if len(args) == 0 { return fmt.Errorf("usage: nebel add field [path...]") } - target, paths := args[0], args[1:] + target, paths := normalizePath(args[0]), args[1:] root, configPath, cfg, err := openConfig() if err != nil { return err } - // The rule pattern is stored as given, but the document is read from - // the working tree, so the argument has to name a real file. + // The document is read from the working tree, so the argument has to + // name a real file. source, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(target))) if err != nil { return fmt.Errorf("reading %s: %w", target, err) @@ -154,6 +154,12 @@ func runAddField(args []string) error { return nil } +// normalizePath gives a CLI path or glob the repo-relative, slash-separated +// form git reports, so it matches what MatchRule later compares it against. +func normalizePath(path string) string { + return filepath.ToSlash(filepath.Clean(path)) +} + // existingFields returns the paths already configured for pattern. func existingFields(cfg *config.Config, pattern string) map[string]bool { fields := map[string]bool{} diff --git a/cmd/nebel/addfield_e2e_test.go b/cmd/nebel/addfield_e2e_test.go index 62a663e..e2b3847 100644 --- a/cmd/nebel/addfield_e2e_test.go +++ b/cmd/nebel/addfield_e2e_test.go @@ -121,6 +121,62 @@ func TestAddFieldRejectsMissingPath(t *testing.T) { } } +// issue #8: a leading "./" on the path must be normalized away before it +// reaches .gitattributes and the config, or the pattern silently fails to +// match the file git reports without it. +func TestAddFieldNormalizesRelativePath(t *testing.T) { + pathEnv := pathEnvWithBin(t) + repo := setupValueRepo(t, pathEnv) + + runIn(t, repo, pathEnv, "nebel", "add", "field", "./config/staging.yaml", "database.password") + + attrs, err := os.ReadFile(filepath.Join(repo, gitattributesName)) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(attrs), "./config/staging.yaml") { + t.Errorf("%s still has an un-normalized ./ prefix:\n%s", gitattributesName, attrs) + } + if !strings.Contains(string(attrs), "config/staging.yaml filter=nebel") { + t.Errorf("%s is missing the normalized pattern:\n%s", gitattributesName, attrs) + } + + cfg, err := os.ReadFile(filepath.Join(repo, ".nebel.yaml")) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(cfg), "./config/staging.yaml") { + t.Errorf(".nebel.yaml still has an un-normalized ./ prefix:\n%s", cfg) + } + + runIn(t, repo, pathEnv, "git", "add", ".") + runIn(t, repo, pathEnv, "git", "commit", "-q", "-m", "encrypt fields") + stored := runIn(t, repo, pathEnv, "git", "show", "HEAD:config/staging.yaml") + if strings.Contains(stored, "s3cr3t") { + t.Errorf("path given with a ./ prefix was not actually encrypted:\n%s", stored) + } +} + +// issue #8, mode: file variant: the same normalization applies to +// `add file`'s glob argument. +func TestAddFileNormalizesRelativePath(t *testing.T) { + pathEnv := pathEnvWithBin(t) + repo := setupValueRepo(t, pathEnv) + + runIn(t, repo, pathEnv, "nebel", "add", "file", "./config/*.yaml") + + attrs, err := os.ReadFile(filepath.Join(repo, gitattributesName)) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(attrs), "./config/*.yaml") { + t.Errorf("%s still has an un-normalized ./ prefix:\n%s", gitattributesName, attrs) + } + if !strings.Contains(string(attrs), "config/*.yaml filter=nebel") { + t.Errorf("%s is missing the normalized pattern:\n%s", gitattributesName, attrs) + } +} + // The two subcommands write different rules for the same path, so mixing // them is refused rather than silently reinterpreting one as the other. func TestAddFileAndFieldConflict(t *testing.T) {