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