From 8f8f4e150df95008385594155d7f8396fddc7cf1 Mon Sep 17 00:00:00 2001 From: Anthony Vitacco Date: Mon, 27 Jul 2026 08:02:33 -0400 Subject: [PATCH] Fixing a bug with template dump When template dump is use to dump into the current directory, it tries to back up the current directory but this does not work correctly. This fix detects the cwd and skips the backup check if the backup dir is the same as the cwd. --- commands/templates.go | 35 ++++++++++++++++-- commands/templates_test.go | 75 ++++++++++++++++++++++++++++++++++++++ docs/commands/templates.md | 4 +- 3 files changed, 109 insertions(+), 5 deletions(-) diff --git a/commands/templates.go b/commands/templates.go index 3434289..9e4b6dc 100644 --- a/commands/templates.go +++ b/commands/templates.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "path/filepath" "github.com/spf13/cobra" "github.com/voxpupuli/jig/internal/scaffold" @@ -99,6 +100,26 @@ func explainTemplate(w io.Writer, src *templateSource, name string) error { return nil } +// isCwd reports whether destination resolves to the current working +// directory. Backing up the destination is skipped in that case: renaming +// the current directory out from under the process is unreliable across +// platforms (fails outright when destination is "."), so templates are +// written directly into the existing directory instead, overwriting only +// the files jig manages. +func isCwd(destination string) (bool, error) { + cwd, err := os.Getwd() + if err != nil { + return false, fmt.Errorf("failed to determine current directory: %w", err) + } + + abs, err := filepath.Abs(destination) + if err != nil { + return false, fmt.Errorf("failed to resolve destination %s: %w", destination, err) + } + + return abs == cwd, nil +} + func (a *App) templatesDumpCmd() *cobra.Command { cmd := &cobra.Command{ Use: "dump ", @@ -106,11 +127,17 @@ func (a *App) templatesDumpCmd() *cobra.Command { Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { destination := args[0] - if _, err := os.Stat(destination); err == nil { - if err := scaffold.BackupDir(destination); err != nil { - return fmt.Errorf("failed to back up existing directory: %w", err) + destinationIsCwd, err := isCwd(destination) + if err != nil { + return err + } + if !destinationIsCwd { + if _, err := os.Stat(destination); err == nil { + if err := scaffold.BackupDir(destination); err != nil { + return fmt.Errorf("failed to back up existing directory: %w", err) + } + fmt.Printf("backed up existing directory %s\n", destination) } - fmt.Printf("backed up existing directory %s\n", destination) } return template.DumpTemplates(destination) }, diff --git a/commands/templates_test.go b/commands/templates_test.go index 0c048e1..df3fced 100644 --- a/commands/templates_test.go +++ b/commands/templates_test.go @@ -108,6 +108,81 @@ func TestTemplatesResolve_NotFound(t *testing.T) { } } +// runTemplatesDump executes `jig templates dump ` and returns +// the command output. +func runTemplatesDump(t *testing.T, a *App, destination string) (string, error) { + t.Helper() + cmd := a.templatesCmd() + var buf bytes.Buffer + cmd.SetOut(&buf) + cmd.SetErr(&buf) + cmd.SetArgs([]string{"dump", destination}) + err := cmd.Execute() + return buf.String(), err +} + +// Dumping to "." must write into the current directory in place rather than +// trying to rename it out of the way: renaming "." fails on every platform, +// and dumping is expected to behave like unpacking an archive alongside +// existing files. See https://github.com/voxpupuli/jig/issues/82. +func TestTemplatesDump_CurrentDirectory(t *testing.T) { + dir := t.TempDir() + t.Chdir(dir) + if err := os.WriteFile(filepath.Join(dir, "another-file"), []byte("keep me"), 0o644); err != nil { + t.Fatal(err) + } + + a := testApp(config.Config{}) + if _, err := runTemplatesDump(t, a, "."); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if _, err := os.Stat(filepath.Join(dir, "another-file")); err != nil { + t.Errorf("expected pre-existing file to survive the dump: %v", err) + } + if _, err := os.Stat(filepath.Join(dir, "class", "class.pp.tmpl")); err != nil { + t.Errorf("expected templates to be written into the current directory: %v", err) + } + + entries, err := os.ReadDir(filepath.Dir(dir)) + if err != nil { + t.Fatal(err) + } + for _, e := range entries { + if strings.Contains(e.Name(), ".bak.") { + t.Errorf("expected no backup directory to be created, found %q", e.Name()) + } + } +} + +// Dumping to a non-cwd destination that already exists must still back it up +// with a timestamp suffix before writing. +func TestTemplatesDump_BacksUpExistingDestination(t *testing.T) { + t.Chdir(t.TempDir()) + if err := os.Mkdir("existing", 0o755); err != nil { + t.Fatal(err) + } + + a := testApp(config.Config{}) + if _, err := runTemplatesDump(t, a, "existing"); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + entries, err := os.ReadDir(".") + if err != nil { + t.Fatal(err) + } + var sawBackup bool + for _, e := range entries { + if strings.Contains(e.Name(), "existing.bak.") { + sawBackup = true + } + } + if !sawBackup { + t.Error("expected a backup directory to be created") + } +} + // Inside a module directory, the [template] section of jig.toml must feed // resolution, matching what component commands do. func TestTemplatesResolve_ModuleConfigURL(t *testing.T) { diff --git a/docs/commands/templates.md b/docs/commands/templates.md index 4474def..04a6a1a 100644 --- a/docs/commands/templates.md +++ b/docs/commands/templates.md @@ -9,7 +9,9 @@ remote template repositories work. Extracts all embedded default templates to a directory on disk. This is useful as a starting point for creating your own custom templates. If the destination directory already exists it will be renamed with a timestamp -suffix before writing. +suffix before writing, unless the destination is the current directory (e.g. +`.`), in which case templates are written into it in place alongside any +existing files. ``` jig templates dump