Skip to content
Open
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
13 changes: 11 additions & 2 deletions internal/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,10 @@ func createCmd(dir string, startTime time.Time, format string, name string, ext

dir = filepath.Clean(dir)
ext = "." + strings.TrimPrefix(ext, ".")
globDir := escapeGlobPath(dir)

if seq {
matches, err := filepath.Glob(filepath.Join(dir, "*"+ext))
matches, err := filepath.Glob(filepath.Join(globDir, "*"+ext))

if err != nil {
return err
Expand All @@ -103,7 +104,7 @@ func createCmd(dir string, startTime time.Time, format string, name string, ext
}
}

versionGlob := filepath.Join(dir, version+"_*"+ext)
versionGlob := filepath.Join(globDir, version+"_*"+ext)
matches, err := filepath.Glob(versionGlob)

if err != nil {
Expand Down Expand Up @@ -135,6 +136,14 @@ func createCmd(dir string, startTime time.Time, format string, name string, ext
return nil
}

// escapeGlobPath keeps directory names literal while leaving the filename pattern separate.
func escapeGlobPath(dir string) string {
if filepath.Separator != '\\' {
dir = strings.ReplaceAll(dir, "\\", "\\\\")
}
return strings.NewReplacer("*", "[*]", "?", "[?]", "[", "[[]").Replace(dir)
}

func createFile(filename string) error {
// create exclusive (fails if file already exists)
// os.Create() specifies 0666 as the FileMode, so we're doing the same
Expand Down
72 changes: 72 additions & 0 deletions internal/cli/literal_directory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package cli

import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)

func TestCreateLiteralDirectory(t *testing.T) {
for _, name := range []string{"plain", "migrations[dev]", "migrations[", "migrations*", "migrations?", `migrations\dev[local]`} {
t.Run(name, func(t *testing.T) {
if runtime.GOOS == "windows" && strings.ContainsAny(name, `*?\`) {
t.Skip("not a valid literal Windows directory name")
}
dir := filepath.Join(t.TempDir(), name)
// A similarly named sibling must not contribute versions to this directory.
if strings.ContainsAny(name, "*?[") {
sibling := filepath.Join(filepath.Dir(dir), strings.NewReplacer("*", "prod", "?", "p", "[dev]", "d", "[", "x").Replace(name))
if err := os.MkdirAll(sibling, 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(sibling, "000009_other.up.sql"), nil, 0644); err != nil {
t.Fatal(err)
}
}
for _, migration := range []string{"first", "second"} {
if err := createCmd(dir, time.Time{}, defaultTimeFormat, migration, "sql", true, 6, false); err != nil {
t.Fatalf("create %s: %v", migration, err)
}
}
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatal(err)
}
want := []string{"000001_first.down.sql", "000001_first.up.sql", "000002_second.down.sql", "000002_second.up.sql"}
if len(entries) != len(want) {
t.Fatalf("entries: %v", entries)
}
for i, entry := range entries {
if entry.Name() != want[i] {
t.Errorf("entry %d: got %q want %q", i, entry.Name(), want[i])
}
}
})
}
}

func TestCreateLiteralDirectoryDuplicate(t *testing.T) {
for _, name := range []string{"plain", "migrations[dev]"} {
t.Run(name, func(t *testing.T) {
dir := filepath.Join(t.TempDir(), name)
ts := time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC)
if err := createCmd(dir, ts, defaultTimeFormat, "first", "sql", false, 6, false); err != nil {
t.Fatal(err)
}
err := createCmd(dir, ts, defaultTimeFormat, "second", "sql", false, 6, false)
if err == nil || !strings.Contains(err.Error(), "duplicate migration version") {
t.Fatalf("duplicate create: %v", err)
}
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatal(err)
}
if len(entries) != 2 {
t.Fatalf("duplicate created extra files: %v", entries)
}
})
}
}
Loading