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) {