Repository: supabase/cli
Pull request #339 (2022-08-06 13:39:54)

feat: move gitignore to supabase directory
## What kind of change does this PR introduce?

feature

## What is the current behavior?

Root level gitignore is modified by `supabase init`. The CLI should be agnostic to what users store outside of the generated `supabase` directory.

## What is the new behavior?

- creates `supabase/.gitignore`
- do not remove `supabase` directory on failure as it may contain migration files
- use `afero.FileContainsBytes` instead of reading the entire gitignore file

## Additional context

Add any other context or screenshots.


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

* **28** of **33**   **(84.85%)**  changed or added relevant lines in **3** files are covered.
* **1** unchanged line in **1** file lost coverage.
* Overall coverage increased (+**0.07%**) to **22.64%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [internal/init/init.go](https://coveralls.io/builds/51467624/source?filename=internal%2Finit%2Finit.go#L43) | 23 | 28 | 82.14%
<!-- | **Total:** | **28** | **33** | **84.85%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [internal/init/init.go](https://coveralls.io/builds/51467624/source?filename=internal%2Finit%2Finit.go#L69) | 1 | 80.49% |
<!-- | **Total:** | **1** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/51467624/badge)](https://coveralls.io/builds/51467624) |
| :-- | --: |
| Change from base [Build 2804634808](https://coveralls.io/builds/51453845): |  0.07% |
| Covered Lines: | 1161 |
| Relevant Lines: | 5128 |

---
##### 💛  - [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/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/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..b144cb0b 100644
--- a/internal/utils/misc.go
+++ b/internal/utils/misc.go
@@ -138,14 +138,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/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))
+	})
 }