diff --git a/internal/super/setup.go b/internal/super/setup.go index 15c5032be..9442314b6 100644 --- a/internal/super/setup.go +++ b/internal/super/setup.go @@ -107,36 +107,12 @@ func create( return &setupResult{targetDir: targetDir}, nil } -func updateGitignore(targetDir string) error { - gitignorePath := filepath.Join(targetDir, ".gitignore") - f, err := os.OpenFile(gitignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return err - } - defer f.Close() - - _, err = f.WriteString("\n# flow\nemulator-account.pkey\nimports\n.env\n") - if err != nil { - return err - } - - return nil +func updateGitignore(targetDir string, readerWriter flowkit.ReaderWriter) error { + return util.AddFlowEntriesToGitIgnore(targetDir, readerWriter) } -func updateCursorIgnore(targetDir string) error { - cursorignorePath := filepath.Join(targetDir, ".cursorignore") - f, err := os.OpenFile(cursorignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return err - } - defer f.Close() - - _, err = f.WriteString("\n# flow\nemulator-account.pkey\n.env\n\n# Pay attention to imports directory\n!imports/**\n") - if err != nil { - return err - } - - return nil +func updateCursorIgnore(targetDir string, readerWriter flowkit.ReaderWriter) error { + return util.AddFlowEntriesToCursorIgnore(targetDir, readerWriter) } func createConfigOnly(targetDir string, readerWriter flowkit.ReaderWriter) error { @@ -157,12 +133,12 @@ func createConfigOnly(targetDir string, readerWriter flowkit.ReaderWriter) error return err } - err = updateGitignore(targetDir) + err = updateGitignore(targetDir, readerWriter) if err != nil { return err } - err = updateCursorIgnore(targetDir) + err = updateCursorIgnore(targetDir, readerWriter) if err != nil { return err } @@ -293,12 +269,12 @@ func startInteractiveSetup( return "", err } - err = updateGitignore(tempDir) + err = updateGitignore(tempDir, state.ReaderWriter()) if err != nil { return "", err } - err = updateCursorIgnore(tempDir) + err = updateCursorIgnore(tempDir, state.ReaderWriter()) if err != nil { return "", err } diff --git a/internal/util/util.go b/internal/util/util.go index 5c284b451..ae39723ae 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -45,6 +45,17 @@ func Exit(code int, msg string) { os.Exit(code) } +// entryExists checks if an entry already exists in the content +func entryExists(content, entry string) bool { + lines := strings.Split(strings.TrimSpace(content), "\n") + for _, line := range lines { + if strings.TrimSpace(line) == strings.TrimSpace(entry) { + return true + } + } + return false +} + // AddToGitIgnore adds a new line to the .gitignore if one doesn't exist it creates it. func AddToGitIgnore(filename string, loader flowkit.ReaderWriter) error { currentWd, err := os.Getwd() @@ -64,6 +75,11 @@ func AddToGitIgnore(filename string, loader flowkit.ReaderWriter) error { gitIgnoreFiles = string(gitIgnoreFilesRaw) filePermissions = fileStat.Mode().Perm() } + + if entryExists(gitIgnoreFiles, filename) { + return nil // Entry already exists, no need to add + } + return loader.WriteFile( gitIgnorePath, fmt.Appendf(nil, "%s\n%s", gitIgnoreFiles, filename), @@ -90,6 +106,11 @@ func AddToCursorIgnore(filename string, loader flowkit.ReaderWriter) error { cursorIgnoreFiles = string(cursorIgnoreFilesRaw) filePermissions = fileStat.Mode().Perm() } + + if entryExists(cursorIgnoreFiles, filename) { + return nil // Entry already exists, no need to add + } + return loader.WriteFile( cursorIgnorePath, fmt.Appendf(nil, "%s\n%s", cursorIgnoreFiles, filename), @@ -97,6 +118,80 @@ func AddToCursorIgnore(filename string, loader flowkit.ReaderWriter) error { ) } +// addEntriesToIgnoreFile is a helper function that adds entries to an ignore file without duplicates +func addEntriesToIgnoreFile(filePath string, entries []string, loader flowkit.ReaderWriter) error { + existingContent := "" + filePermissions := os.FileMode(0644) + + // Try to read existing content using the loader + existingContentRaw, err := loader.ReadFile(filePath) + if err == nil { + existingContent = string(existingContentRaw) + // Try to get file permissions, but don't fail if we can't + if stat, err := os.Stat(filePath); err == nil { + filePermissions = stat.Mode().Perm() + } + } + + // Split existing content into lines + existingLines := strings.Split(strings.TrimSpace(existingContent), "\n") + existingSet := make(map[string]bool) + for _, line := range existingLines { + if strings.TrimSpace(line) != "" { + existingSet[strings.TrimSpace(line)] = true + } + } + + // Add new entries that don't already exist + var newEntries []string + for _, entry := range entries { + if !existingSet[strings.TrimSpace(entry)] { + newEntries = append(newEntries, entry) + } + } + + if len(newEntries) == 0 { + return nil // All entries already exist + } + + // Combine existing content with new entries + content := existingContent + if content != "" && !strings.HasSuffix(content, "\n") { + content += "\n" + } + content += strings.Join(newEntries, "\n") + + return loader.WriteFile(filePath, []byte(content), filePermissions) +} + +// AddFlowEntriesToGitIgnore adds the standard Flow entries to .gitignore without duplicates +func AddFlowEntriesToGitIgnore(targetDir string, loader flowkit.ReaderWriter) error { + flowEntries := []string{ + "# flow", + "emulator-account.pkey", + "imports", + ".env", + } + + gitIgnorePath := filepath.Join(targetDir, ".gitignore") + return addEntriesToIgnoreFile(gitIgnorePath, flowEntries, loader) +} + +// AddFlowEntriesToCursorIgnore adds the standard Flow entries to .cursorignore without duplicates +func AddFlowEntriesToCursorIgnore(targetDir string, loader flowkit.ReaderWriter) error { + flowEntries := []string{ + "# flow", + "emulator-account.pkey", + ".env", + "", + "# Pay attention to imports directory", + "!imports/**", + } + + cursorIgnorePath := filepath.Join(targetDir, ".cursorignore") + return addEntriesToIgnoreFile(cursorIgnorePath, flowEntries, loader) +} + // GetAddressNetwork returns the chain ID for an address. func GetAddressNetwork(address flow.Address) (flow.ChainID, error) { networks := []flow.ChainID{ diff --git a/internal/util/util_test.go b/internal/util/util_test.go new file mode 100644 index 000000000..acfadb92c --- /dev/null +++ b/internal/util/util_test.go @@ -0,0 +1,147 @@ +/* + * Flow CLI + * + * Copyright Flow Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package util + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAddFlowEntriesToGitIgnore_NoDuplicates(t *testing.T) { + _, state, _ := TestMocks(t) + + err := AddFlowEntriesToGitIgnore("", state.ReaderWriter()) + require.NoError(t, err, "Failed to add Flow entries to gitignore") + + content, err := state.ReaderWriter().ReadFile(".gitignore") + require.NoError(t, err, "Failed to read gitignore file") + + expectedEntries := []string{"# flow", "emulator-account.pkey", "imports", ".env"} + for _, entry := range expectedEntries { + assert.Contains(t, string(content), entry, "Expected gitignore to contain %s", entry) + } + + err = AddFlowEntriesToGitIgnore("", state.ReaderWriter()) + require.NoError(t, err, "Failed to add Flow entries to gitignore again") + + content, err = state.ReaderWriter().ReadFile(".gitignore") + require.NoError(t, err, "Failed to read gitignore file again") + + for _, entry := range expectedEntries { + occurrences := strings.Count(string(content), entry) + assert.Equal(t, 1, occurrences, "Expected 1 occurrence of %s, but found %d", entry, occurrences) + } +} + +func TestAddFlowEntriesToCursorIgnore_NoDuplicates(t *testing.T) { + _, state, _ := TestMocks(t) + + err := AddFlowEntriesToCursorIgnore("", state.ReaderWriter()) + require.NoError(t, err, "Failed to add Flow entries to cursorignore") + + content, err := state.ReaderWriter().ReadFile(".cursorignore") + require.NoError(t, err, "Failed to read cursorignore file") + + expectedEntries := []string{"# flow", "emulator-account.pkey", ".env", "# Pay attention to imports directory", "!imports/**"} + for _, entry := range expectedEntries { + assert.Contains(t, string(content), entry, "Expected cursorignore to contain %s", entry) + } + + err = AddFlowEntriesToCursorIgnore("", state.ReaderWriter()) + require.NoError(t, err, "Failed to add Flow entries to cursorignore again") + + content, err = state.ReaderWriter().ReadFile(".cursorignore") + require.NoError(t, err, "Failed to read cursorignore file again") + + for _, entry := range expectedEntries { + occurrences := strings.Count(string(content), entry) + assert.Equal(t, 1, occurrences, "Expected 1 occurrence of %s, but found %d", entry, occurrences) + } +} + +func TestAddFlowEntriesToGitIgnore_WithExistingContent(t *testing.T) { + _, state, _ := TestMocks(t) + + existingContent := "# existing content\nnode_modules/\n*.log\n" + err := state.ReaderWriter().WriteFile(".gitignore", []byte(existingContent), 0644) + require.NoError(t, err, "Failed to create existing .gitignore") + + err = AddFlowEntriesToGitIgnore("", state.ReaderWriter()) + require.NoError(t, err, "Failed to add Flow entries to gitignore") + + content, err := state.ReaderWriter().ReadFile(".gitignore") + require.NoError(t, err, "Failed to read gitignore file") + + assert.Contains(t, string(content), existingContent, "Expected existing content to be preserved") + + flowEntries := []string{"# flow", "emulator-account.pkey", "imports", ".env"} + for _, entry := range flowEntries { + assert.Contains(t, string(content), entry, "Expected gitignore to contain %s", entry) + } +} + +func TestAddFlowEntriesToCursorIgnore_WithExistingContent(t *testing.T) { + _, state, _ := TestMocks(t) + + existingContent := "# existing cursor ignore\n.vscode/\n.idea/\n" + err := state.ReaderWriter().WriteFile(".cursorignore", []byte(existingContent), 0644) + require.NoError(t, err, "Failed to create existing .cursorignore") + + err = AddFlowEntriesToCursorIgnore("", state.ReaderWriter()) + require.NoError(t, err, "Failed to add Flow entries to cursorignore") + + content, err := state.ReaderWriter().ReadFile(".cursorignore") + require.NoError(t, err, "Failed to read cursorignore file") + + assert.Contains(t, string(content), existingContent, "Expected existing content to be preserved") + + flowEntries := []string{"# flow", "emulator-account.pkey", ".env", "# Pay attention to imports directory", "!imports/**"} + for _, entry := range flowEntries { + assert.Contains(t, string(content), entry, "Expected cursorignore to contain %s", entry) + } +} + +func TestAddEntriesToIgnoreFile_HelperFunction(t *testing.T) { + _, state, _ := TestMocks(t) + + entries := []string{"# test", "test-file.txt", "another-file.log"} + err := addEntriesToIgnoreFile("test-ignore.txt", entries, state.ReaderWriter()) + require.NoError(t, err, "Failed to add entries to ignore file") + + content, err := state.ReaderWriter().ReadFile("test-ignore.txt") + require.NoError(t, err, "Failed to read ignore file") + + for _, entry := range entries { + assert.Contains(t, string(content), entry, "Expected ignore file to contain %s", entry) + } + + err = addEntriesToIgnoreFile("test-ignore.txt", entries, state.ReaderWriter()) + require.NoError(t, err, "Failed to add entries to ignore file again") + + content, err = state.ReaderWriter().ReadFile("test-ignore.txt") + require.NoError(t, err, "Failed to read ignore file again") + + for _, entry := range entries { + occurrences := strings.Count(string(content), entry) + assert.Equal(t, 1, occurrences, "Expected 1 occurrence of %s, but found %d", entry, occurrences) + } +}