Repository: supabase/cli
Pull request #337 (2022-08-06 09:44:05)

feat: support stdin when creating new migration
## What kind of change does this PR introduce?

feature

## What is the current behavior?

`supabase migration new <name>` only creates a new file

## What is the new behavior?

supports reading from stdin so pipe works, for eg.
```bash
echo "create table pet;" | supabase migration new create_pet_table
supabase db changes | supabase migration new my_diff
```

## Additional context

Add any other context or screenshots.


coveralls: ## Pull Request Test Coverage Report for [Build 2808484667](https://coveralls.io/builds/51466999)

* **13** of **16**   **(81.25%)**  changed or added relevant lines in **3** files are covered.
* **6** unchanged lines in **1** file lost coverage.
* Overall coverage decreased (**-0.003%**) to **22.568%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [internal/db/commit/migra.go](https://coveralls.io/builds/51466999/source?filename=internal%2Fdb%2Fcommit%2Fmigra.go#L103) | 2 | 3 | 66.67%
| [internal/migration/new/new.go](https://coveralls.io/builds/51466999/source?filename=internal%2Fmigration%2Fnew%2Fnew.go#L28) | 10 | 12 | 83.33%
<!-- | **Total:** | **13** | **16** | **81.25%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [internal/utils/misc.go](https://coveralls.io/builds/51466999/source?filename=internal%2Futils%2Fmisc.go#L185) | 6 | 34.42% |
<!-- | **Total:** | **6** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/51466999/badge)](https://coveralls.io/builds/51466999) |
| :-- | --: |
| Change from base [Build 2804634808](https://coveralls.io/builds/51453845): |  -0.003% |
| Covered Lines: | 1160 |
| Relevant Lines: | 5140 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/.gitignore b/.gitignore
index fce8fcdf..0845e293 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,7 +16,3 @@
 
 # IDE
 .vscode
-
-# Supabase
-**/supabase/.branches
-**/supabase/.temp
diff --git a/cmd/migration.go b/cmd/migration.go
index f6375360..56abf89d 100644
--- a/cmd/migration.go
+++ b/cmd/migration.go
@@ -1,6 +1,9 @@
 package cmd
 
 import (
+	"os"
+
+	"github.com/spf13/afero"
 	"github.com/spf13/cobra"
 	"github.com/supabase/cli/internal/migration/new"
 	"github.com/supabase/cli/internal/utils"
@@ -17,7 +20,7 @@ var (
 		Short: "Create an empty migration.",
 		Args:  cobra.ExactArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
-			return new.Run(args[0])
+			return new.Run(args[0], os.Stdin, afero.NewOsFs())
 		},
 	}
 )
diff --git a/internal/db/commit/migra.go b/internal/db/commit/migra.go
index 20570c7c..4f7ed775 100644
--- a/internal/db/commit/migra.go
+++ b/internal/db/commit/migra.go
@@ -27,9 +27,8 @@ import (
 )
 
 const (
-	migraId    = "supabase_migra_cli"
-	diffImage  = "djrobstep/migra:3.0.1621480950"
-	migrateDir = "supabase/migrations"
+	migraId   = "supabase_migra_cli"
+	diffImage = "djrobstep/migra:3.0.1621480950"
 )
 
 var (
@@ -101,7 +100,7 @@ func RunMigra(name, schema string, fsys afero.Fs) error {
 	}
 
 	filename := utils.GetCurrentTimestamp() + "_" + name + ".sql"
-	if err := afero.WriteFile(fsys, filepath.Join(migrateDir, filename), []byte(diff), 0644); err != nil {
+	if err := afero.WriteFile(fsys, filepath.Join(utils.MigrationsDir, filename), []byte(diff), 0644); err != nil {
 		return err
 	}
 
@@ -191,7 +190,7 @@ func applyMigrations(ctx context.Context, url string, fsys afero.Fs, options ...
 	}
 	defer conn.Close(ctx)
 	// Apply migrations
-	if migrations, err := afero.ReadDir(fsys, migrateDir); err == nil {
+	if migrations, err := afero.ReadDir(fsys, utils.MigrationsDir); err == nil {
 		for i, migration := range migrations {
 			// NOTE: To handle backward-compatibility. `<timestamp>_init.sql` as
 			// the first migration (prev versions of the CLI) is deprecated.
@@ -207,7 +206,7 @@ func applyMigrations(ctx context.Context, url string, fsys afero.Fs, options ...
 				}
 			}
 			fmt.Println("Applying migration " + utils.Bold(migration.Name()) + "...")
-			contents, err := afero.ReadFile(fsys, filepath.Join(migrateDir, migration.Name()))
+			contents, err := afero.ReadFile(fsys, filepath.Join(utils.MigrationsDir, migration.Name()))
 			if err != nil {
 				return err
 			}
diff --git a/internal/init/init.go b/internal/init/init.go
index 834312a5..b753e04d 100644
--- a/internal/init/init.go
+++ b/internal/init/init.go
@@ -1,7 +1,6 @@
 package init
 
 import (
-	"bytes"
 	_ "embed"
 	"errors"
 	"os"
@@ -19,17 +18,6 @@ var (
 )
 
 func Run(fsys afero.Fs) error {
-	if err := run(fsys); errors.Is(err, errAlreadyInitialized) {
-		return err
-	} else if err != nil {
-		_ = fsys.RemoveAll("supabase")
-		return err
-	}
-
-	return nil
-}
-
-func run(fsys afero.Fs) error {
 	// Sanity checks.
 	{
 		if _, err := fsys.Stat(utils.ConfigPath); err == nil {
@@ -39,46 +27,45 @@ func run(fsys afero.Fs) error {
 		}
 	}
 
-	if err := utils.MkdirIfNotExistFS(fsys, filepath.Dir(utils.ConfigPath)); err != nil {
-		return err
-	}
-
 	// 1. Write `config.toml`.
 	if err := utils.WriteConfig(fsys, false); err != nil {
 		return err
 	}
 
 	// 2. Append to `.gitignore`.
-	{
-		gitRoot, err := utils.GetGitRoot()
-		if err != nil {
-			return err
-		} else if gitRoot == nil {
-			// skip
-		} else {
-			gitignorePath := *gitRoot + "/.gitignore"
-			gitignore, err := afero.ReadFile(fsys, gitignorePath)
-			if errors.Is(err, os.ErrNotExist) {
-				if err := afero.WriteFile(fsys, gitignorePath, initGitignore, 0644); err != nil {
-					return err
-				}
-			} else if err != nil {
-				return err
-			} else if bytes.Contains(gitignore, initGitignore) {
-				// skip
-			} else {
-				f, err := fsys.OpenFile(gitignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
-				if err != nil {
-					return err
-				}
-				if _, err := f.Write(append([]byte("\n"), initGitignore...)); err != nil {
-					return err
-				}
-				if err := f.Close(); err != nil {
-					return err
-				}
-			}
-		}
+	if gitRoot, _ := utils.GetGitRoot(fsys); gitRoot == nil {
+		// User not using git
+		return nil
+	}
+
+	ignorePath := filepath.Join(filepath.Dir(utils.ConfigPath), ".gitignore")
+	if err := updateGitIgnore(ignorePath, fsys); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func updateGitIgnore(ignorePath string, fsys afero.Fs) error {
+	var contents []byte
+
+	if contained, err := afero.FileContainsBytes(fsys, ignorePath, initGitignore); contained {
+		return nil
+	} else if err == nil {
+		// Add a line break when appending
+		contents = append(contents, '\n')
+	} else if !errors.Is(err, os.ErrNotExist) {
+		return err
+	}
+
+	f, err := fsys.OpenFile(ignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	if _, err := f.Write(append(contents, initGitignore...)); err != nil {
+		return err
 	}
 
 	return nil
diff --git a/internal/init/templates/init_gitignore b/internal/init/templates/init_gitignore
index 031e285c..773c7c3e 100644
--- a/internal/init/templates/init_gitignore
+++ b/internal/init/templates/init_gitignore
@@ -1,3 +1,3 @@
 # Supabase
-**/supabase/.branches
-**/supabase/.temp
+.branches
+.temp
diff --git a/internal/migration/new/new.go b/internal/migration/new/new.go
index aeb23447..8efa3dc9 100644
--- a/internal/migration/new/new.go
+++ b/internal/migration/new/new.go
@@ -2,25 +2,35 @@ package new
 
 import (
 	"fmt"
+	"io"
 	"os"
+	"path/filepath"
 
+	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/utils"
 )
 
-func Run(migrationName string) error {
-	if err := utils.AssertSupabaseCliIsSetUp(); err != nil {
+func Run(migrationName string, stdin afero.File, fsys afero.Fs) error {
+	if err := utils.MkdirIfNotExistFS(fsys, utils.MigrationsDir); err != nil {
 		return err
 	}
 
-	if err := utils.MkdirIfNotExist("supabase/migrations"); err != nil {
+	name := utils.GetCurrentTimestamp() + "_" + migrationName + ".sql"
+	path := filepath.Join(utils.MigrationsDir, name)
+	f, err := fsys.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+	if err != nil {
 		return err
 	}
+	defer f.Close()
 
-	timestamp := utils.GetCurrentTimestamp()
-	if err := os.WriteFile("supabase/migrations/"+timestamp+"_"+migrationName+".sql", []byte{}, 0644); err != nil {
+	if fi, err := stdin.Stat(); err != nil {
 		return err
+	} else if fi.Size() > 0 {
+		if _, err := io.Copy(f, stdin); err != nil {
+			return err
+		}
 	}
 
-	fmt.Println("Created new migration at " + utils.Bold("supabase/migrations/"+timestamp+"_"+migrationName+".sql") + ".")
+	fmt.Println("Created new migration at " + utils.Bold(path) + ".")
 	return nil
 }
diff --git a/internal/utils/config.go b/internal/utils/config.go
index 6bb658b5..ac97b845 100644
--- a/internal/utils/config.go
+++ b/internal/utils/config.go
@@ -409,6 +409,10 @@ func WriteConfig(fsys afero.Fs, test bool) error {
 		return err
 	}
 
+	if err := MkdirIfNotExistFS(fsys, filepath.Dir(ConfigPath)); err != nil {
+		return err
+	}
+
 	if err := afero.WriteFile(fsys, ConfigPath, initConfigBuf.Bytes(), 0644); err != nil {
 		return err
 	}
diff --git a/internal/utils/misc.go b/internal/utils/misc.go
index c185da9c..6f7a2356 100644
--- a/internal/utils/misc.go
+++ b/internal/utils/misc.go
@@ -54,6 +54,7 @@ DO 'BEGIN WHILE (SELECT COUNT(*) FROM pg_replication_slots) > 0 LOOP END LOOP; E
 	ProjectRefPath = "supabase/.temp/project-ref"
 	RemoteDbPath   = "supabase/.temp/remote-db-url"
 	CurrBranchPath = "supabase/.branches/_current_branch"
+	MigrationsDir  = "supabase/migrations"
 )
 
 var (
@@ -138,14 +139,14 @@ func AssertSupabaseDbIsRunning() error {
 	return nil
 }
 
-func GetGitRoot() (*string, error) {
+func GetGitRoot(fsys afero.Fs) (*string, error) {
 	origWd, err := os.Getwd()
 	if err != nil {
 		return nil, err
 	}
 
 	for {
-		_, err := os.ReadDir(".git")
+		_, err := afero.ReadDir(fsys, ".git")
 
 		if err == nil {
 			gitRoot, err := os.Getwd()


Test patch:
diff --git a/internal/db/commit/migra_test.go b/internal/db/commit/migra_test.go
index 1bb45f42..6c6da7d7 100644
--- a/internal/db/commit/migra_test.go
+++ b/internal/db/commit/migra_test.go
@@ -10,6 +10,7 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/supabase/cli/internal/testing/pgtest"
+	"github.com/supabase/cli/internal/utils"
 )
 
 func TestApplyMigrations(t *testing.T) {
@@ -20,8 +21,8 @@ func TestApplyMigrations(t *testing.T) {
 		fsys := afero.NewMemMapFs()
 		// Setup initial migration
 		migrations := map[string]string{
-			filepath.Join(migrateDir, "20220727064247_init.sql"): "create table test",
-			filepath.Join(migrateDir, "20220727064248_drop.sql"): "drop table test;\n-- ignore me",
+			filepath.Join(utils.MigrationsDir, "20220727064247_init.sql"): "create table test",
+			filepath.Join(utils.MigrationsDir, "20220727064248_drop.sql"): "drop table test;\n-- ignore me",
 		}
 		for path, query := range migrations {
 			require.NoError(t, afero.WriteFile(fsys, path, []byte(query), 0644))
@@ -52,7 +53,7 @@ func TestApplyMigrations(t *testing.T) {
 		fsys := afero.NewMemMapFs()
 		// Setup initial migration
 		name := "20211208000000_init.sql"
-		path := filepath.Join(migrateDir, name)
+		path := filepath.Join(utils.MigrationsDir, name)
 		query := "create table test"
 		require.NoError(t, afero.WriteFile(fsys, path, []byte(query), 0644))
 		// Setup mock postgres
@@ -75,7 +76,7 @@ func TestApplyMigrations(t *testing.T) {
 		fsys := afero.NewMemMapFs()
 		// Setup initial migration
 		name := "20220727064247_create_table.sql"
-		path := filepath.Join(migrateDir, name)
+		path := filepath.Join(utils.MigrationsDir, name)
 		query := "create table test"
 		require.NoError(t, afero.WriteFile(fsys, path, []byte(query), 0644))
 		// Setup mock postgres
diff --git a/internal/init/init_test.go b/internal/init/init_test.go
index 5a36fd91..1474d4cc 100644
--- a/internal/init/init_test.go
+++ b/internal/init/init_test.go
@@ -10,16 +10,11 @@ import (
 	"github.com/supabase/cli/internal/utils"
 )
 
-func gitIgnorePath(t *testing.T) string {
-	root, err := utils.GetGitRoot()
-	assert.NoError(t, err)
-	return filepath.Join(*root, ".gitignore")
-}
-
 func TestInitCommand(t *testing.T) {
 	t.Run("creates config file", func(t *testing.T) {
 		// Setup in-memory fs
 		fsys := &afero.MemMapFs{}
+		require.NoError(t, fsys.Mkdir(".git", 0755))
 		// Run test
 		assert.NoError(t, Run(fsys))
 		// Validate generated config.toml
@@ -27,9 +22,26 @@ func TestInitCommand(t *testing.T) {
 		assert.NoError(t, err)
 		assert.True(t, exists)
 		// Validate generated .gitignore
-		exists, err = afero.Exists(fsys, gitIgnorePath(t))
+		ignorePath := filepath.Join(filepath.Dir(utils.ConfigPath), ".gitignore")
+		exists, err = afero.Exists(fsys, ignorePath)
+		assert.NoError(t, err)
+		assert.True(t, exists)
+	})
+
+	t.Run("does not generate gitignore if no git", func(t *testing.T) {
+		// Setup read-only fs
+		fsys := &afero.MemMapFs{}
+		// Run test
+		assert.NoError(t, Run(fsys))
+		// Validate generated config.toml
+		exists, err := afero.Exists(fsys, utils.ConfigPath)
 		assert.NoError(t, err)
 		assert.True(t, exists)
+		// Validate generated .gitignore
+		ignorePath := filepath.Join(filepath.Dir(utils.ConfigPath), ".gitignore")
+		exists, err = afero.Exists(fsys, ignorePath)
+		assert.NoError(t, err)
+		assert.False(t, exists)
 	})
 
 	t.Run("throws error when config file exists", func(t *testing.T) {
@@ -41,26 +53,46 @@ func TestInitCommand(t *testing.T) {
 		assert.Error(t, Run(fsys))
 	})
 
-	t.Run("throws error on failure to create directory", func(t *testing.T) {
+	t.Run("throws error on failure to write config", func(t *testing.T) {
 		// Setup read-only fs
 		fsys := afero.NewReadOnlyFs(afero.NewMemMapFs())
 		// Run test
 		assert.Error(t, Run(fsys))
 	})
+}
+
+func TestUpdateGitIgnore(t *testing.T) {
+	const ignorePath = "/home/supabase/.gitignore"
 
 	t.Run("appends to git ignore", func(t *testing.T) {
 		// Setup in-memory fs
 		fsys := &afero.MemMapFs{}
-		path := gitIgnorePath(t)
-		_, err := fsys.Create(path)
+		_, err := fsys.Create(ignorePath)
 		require.NoError(t, err)
 		// Run test
-		assert.NoError(t, Run(fsys))
+		assert.NoError(t, updateGitIgnore(ignorePath, fsys))
+		// Validate file contents
+		content, err := afero.ReadFile(fsys, ignorePath)
+		assert.NoError(t, err)
+		assert.Equal(t, append([]byte("\n"), initGitignore...), content)
+	})
+
+	t.Run("noop if already ignored", func(t *testing.T) {
+		// Setup read-only fs
+		fsys := &afero.MemMapFs{}
+		require.NoError(t, afero.WriteFile(fsys, ignorePath, initGitignore, 0644))
+		// Run test
+		assert.NoError(t, updateGitIgnore(ignorePath, fsys))
 		// Validate file contents
-		content, err := afero.ReadFile(fsys, path)
+		content, err := afero.ReadFile(fsys, ignorePath)
 		assert.NoError(t, err)
-		assert.Equal(t, initGitignore, content[1:])
+		assert.Equal(t, initGitignore, content)
 	})
 
-	// TODO: test all error edge cases around git ignore
+	t.Run("throws error on failure to create", func(t *testing.T) {
+		// Setup read-only fs
+		fsys := afero.NewReadOnlyFs(afero.NewMemMapFs())
+		// Run test
+		assert.Error(t, updateGitIgnore(ignorePath, fsys))
+	})
 }
diff --git a/internal/migration/new/new_test.go b/internal/migration/new/new_test.go
new file mode 100644
index 00000000..8c3c9e53
--- /dev/null
+++ b/internal/migration/new/new_test.go
@@ -0,0 +1,59 @@
+package new
+
+import (
+	"path/filepath"
+	"regexp"
+	"testing"
+
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/supabase/cli/internal/utils"
+)
+
+func TestNewCommand(t *testing.T) {
+	t.Run("creates new migration file", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// Setup empty stdin
+		stdin, err := fsys.Create("/dev/stdin")
+		require.NoError(t, err)
+		// Run test
+		assert.NoError(t, Run("test_migrate", stdin, fsys))
+		// Validate output
+		files, err := afero.ReadDir(fsys, utils.MigrationsDir)
+		assert.NoError(t, err)
+		match, err := regexp.MatchString(`([0-9]{14})_test_migrate\.sql`, files[0].Name())
+		assert.NoError(t, err)
+		assert.True(t, match)
+	})
+
+	t.Run("creates new file with contents", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// Setup stdin
+		script := "create table pet;\ndrop table pet;\n"
+		require.NoError(t, afero.WriteFile(fsys, "/dev/stdin", []byte(script), 0644))
+		stdin, err := fsys.Open("/dev/stdin")
+		require.NoError(t, err)
+		// Run test
+		assert.NoError(t, Run("test_migrate", stdin, fsys))
+		// Validate output
+		files, err := afero.ReadDir(fsys, utils.MigrationsDir)
+		assert.NoError(t, err)
+		path := filepath.Join(utils.MigrationsDir, files[0].Name())
+		contents, err := afero.ReadFile(fsys, path)
+		assert.NoError(t, err)
+		assert.Equal(t, []byte(script), contents)
+	})
+
+	t.Run("throws error on failure to create directory", func(t *testing.T) {
+		// Setup read-only fs
+		fsys := afero.NewMemMapFs()
+		// Setup empty stdin
+		stdin, err := fsys.Create("/dev/stdin")
+		require.NoError(t, err)
+		// Run test
+		assert.Error(t, Run("test_migrate", stdin, afero.NewReadOnlyFs(fsys)))
+	})
+}