Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions commands/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io"
"os"
"path/filepath"

"github.com/spf13/cobra"
"github.com/voxpupuli/jig/internal/scaffold"
Expand Down Expand Up @@ -99,18 +100,44 @@ 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 <destination>",
Short: "Dump all available templates to a directory",
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)
},
Expand Down
75 changes: 75 additions & 0 deletions commands/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,81 @@ func TestTemplatesResolve_NotFound(t *testing.T) {
}
}

// runTemplatesDump executes `jig templates dump <destination>` 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) {
Expand Down
4 changes: 3 additions & 1 deletion docs/commands/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <destination>
Expand Down