Skip to content

Commit 44983fb

Browse files
authored
Merge pull request #1996 from onflow/cf/dup-ignores
Prevent duplicate entries in `.gitignore` and `.cursorignore` during creation
2 parents ce59780 + 08b1344 commit 44983fb

3 files changed

Lines changed: 250 additions & 32 deletions

File tree

internal/super/setup.go

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -107,36 +107,12 @@ func create(
107107
return &setupResult{targetDir: targetDir}, nil
108108
}
109109

110-
func updateGitignore(targetDir string) error {
111-
gitignorePath := filepath.Join(targetDir, ".gitignore")
112-
f, err := os.OpenFile(gitignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
113-
if err != nil {
114-
return err
115-
}
116-
defer f.Close()
117-
118-
_, err = f.WriteString("\n# flow\nemulator-account.pkey\nimports\n.env\n")
119-
if err != nil {
120-
return err
121-
}
122-
123-
return nil
110+
func updateGitignore(targetDir string, readerWriter flowkit.ReaderWriter) error {
111+
return util.AddFlowEntriesToGitIgnore(targetDir, readerWriter)
124112
}
125113

126-
func updateCursorIgnore(targetDir string) error {
127-
cursorignorePath := filepath.Join(targetDir, ".cursorignore")
128-
f, err := os.OpenFile(cursorignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
129-
if err != nil {
130-
return err
131-
}
132-
defer f.Close()
133-
134-
_, err = f.WriteString("\n# flow\nemulator-account.pkey\n.env\n\n# Pay attention to imports directory\n!imports/**\n")
135-
if err != nil {
136-
return err
137-
}
138-
139-
return nil
114+
func updateCursorIgnore(targetDir string, readerWriter flowkit.ReaderWriter) error {
115+
return util.AddFlowEntriesToCursorIgnore(targetDir, readerWriter)
140116
}
141117

142118
func createConfigOnly(targetDir string, readerWriter flowkit.ReaderWriter) error {
@@ -157,12 +133,12 @@ func createConfigOnly(targetDir string, readerWriter flowkit.ReaderWriter) error
157133
return err
158134
}
159135

160-
err = updateGitignore(targetDir)
136+
err = updateGitignore(targetDir, readerWriter)
161137
if err != nil {
162138
return err
163139
}
164140

165-
err = updateCursorIgnore(targetDir)
141+
err = updateCursorIgnore(targetDir, readerWriter)
166142
if err != nil {
167143
return err
168144
}
@@ -293,12 +269,12 @@ func startInteractiveSetup(
293269
return "", err
294270
}
295271

296-
err = updateGitignore(tempDir)
272+
err = updateGitignore(tempDir, state.ReaderWriter())
297273
if err != nil {
298274
return "", err
299275
}
300276

301-
err = updateCursorIgnore(tempDir)
277+
err = updateCursorIgnore(tempDir, state.ReaderWriter())
302278
if err != nil {
303279
return "", err
304280
}

internal/util/util.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,17 @@ func Exit(code int, msg string) {
4545
os.Exit(code)
4646
}
4747

48+
// entryExists checks if an entry already exists in the content
49+
func entryExists(content, entry string) bool {
50+
lines := strings.Split(strings.TrimSpace(content), "\n")
51+
for _, line := range lines {
52+
if strings.TrimSpace(line) == strings.TrimSpace(entry) {
53+
return true
54+
}
55+
}
56+
return false
57+
}
58+
4859
// AddToGitIgnore adds a new line to the .gitignore if one doesn't exist it creates it.
4960
func AddToGitIgnore(filename string, loader flowkit.ReaderWriter) error {
5061
currentWd, err := os.Getwd()
@@ -64,6 +75,11 @@ func AddToGitIgnore(filename string, loader flowkit.ReaderWriter) error {
6475
gitIgnoreFiles = string(gitIgnoreFilesRaw)
6576
filePermissions = fileStat.Mode().Perm()
6677
}
78+
79+
if entryExists(gitIgnoreFiles, filename) {
80+
return nil // Entry already exists, no need to add
81+
}
82+
6783
return loader.WriteFile(
6884
gitIgnorePath,
6985
fmt.Appendf(nil, "%s\n%s", gitIgnoreFiles, filename),
@@ -90,13 +106,92 @@ func AddToCursorIgnore(filename string, loader flowkit.ReaderWriter) error {
90106
cursorIgnoreFiles = string(cursorIgnoreFilesRaw)
91107
filePermissions = fileStat.Mode().Perm()
92108
}
109+
110+
if entryExists(cursorIgnoreFiles, filename) {
111+
return nil // Entry already exists, no need to add
112+
}
113+
93114
return loader.WriteFile(
94115
cursorIgnorePath,
95116
fmt.Appendf(nil, "%s\n%s", cursorIgnoreFiles, filename),
96117
filePermissions,
97118
)
98119
}
99120

121+
// addEntriesToIgnoreFile is a helper function that adds entries to an ignore file without duplicates
122+
func addEntriesToIgnoreFile(filePath string, entries []string, loader flowkit.ReaderWriter) error {
123+
existingContent := ""
124+
filePermissions := os.FileMode(0644)
125+
126+
// Try to read existing content using the loader
127+
existingContentRaw, err := loader.ReadFile(filePath)
128+
if err == nil {
129+
existingContent = string(existingContentRaw)
130+
// Try to get file permissions, but don't fail if we can't
131+
if stat, err := os.Stat(filePath); err == nil {
132+
filePermissions = stat.Mode().Perm()
133+
}
134+
}
135+
136+
// Split existing content into lines
137+
existingLines := strings.Split(strings.TrimSpace(existingContent), "\n")
138+
existingSet := make(map[string]bool)
139+
for _, line := range existingLines {
140+
if strings.TrimSpace(line) != "" {
141+
existingSet[strings.TrimSpace(line)] = true
142+
}
143+
}
144+
145+
// Add new entries that don't already exist
146+
var newEntries []string
147+
for _, entry := range entries {
148+
if !existingSet[strings.TrimSpace(entry)] {
149+
newEntries = append(newEntries, entry)
150+
}
151+
}
152+
153+
if len(newEntries) == 0 {
154+
return nil // All entries already exist
155+
}
156+
157+
// Combine existing content with new entries
158+
content := existingContent
159+
if content != "" && !strings.HasSuffix(content, "\n") {
160+
content += "\n"
161+
}
162+
content += strings.Join(newEntries, "\n")
163+
164+
return loader.WriteFile(filePath, []byte(content), filePermissions)
165+
}
166+
167+
// AddFlowEntriesToGitIgnore adds the standard Flow entries to .gitignore without duplicates
168+
func AddFlowEntriesToGitIgnore(targetDir string, loader flowkit.ReaderWriter) error {
169+
flowEntries := []string{
170+
"# flow",
171+
"emulator-account.pkey",
172+
"imports",
173+
".env",
174+
}
175+
176+
gitIgnorePath := filepath.Join(targetDir, ".gitignore")
177+
return addEntriesToIgnoreFile(gitIgnorePath, flowEntries, loader)
178+
}
179+
180+
// AddFlowEntriesToCursorIgnore adds the standard Flow entries to .cursorignore without duplicates
181+
func AddFlowEntriesToCursorIgnore(targetDir string, loader flowkit.ReaderWriter) error {
182+
flowEntries := []string{
183+
"# flow",
184+
"emulator-account.pkey",
185+
".env",
186+
"",
187+
"# Pay attention to imports directory",
188+
"!imports/**",
189+
}
190+
191+
cursorIgnorePath := filepath.Join(targetDir, ".cursorignore")
192+
return addEntriesToIgnoreFile(cursorIgnorePath, flowEntries, loader)
193+
}
194+
100195
// GetAddressNetwork returns the chain ID for an address.
101196
func GetAddressNetwork(address flow.Address) (flow.ChainID, error) {
102197
networks := []flow.ChainID{

internal/util/util_test.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Flow CLI
3+
*
4+
* Copyright Flow Foundation
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package util
20+
21+
import (
22+
"strings"
23+
"testing"
24+
25+
"github.com/stretchr/testify/assert"
26+
"github.com/stretchr/testify/require"
27+
)
28+
29+
func TestAddFlowEntriesToGitIgnore_NoDuplicates(t *testing.T) {
30+
_, state, _ := TestMocks(t)
31+
32+
err := AddFlowEntriesToGitIgnore("", state.ReaderWriter())
33+
require.NoError(t, err, "Failed to add Flow entries to gitignore")
34+
35+
content, err := state.ReaderWriter().ReadFile(".gitignore")
36+
require.NoError(t, err, "Failed to read gitignore file")
37+
38+
expectedEntries := []string{"# flow", "emulator-account.pkey", "imports", ".env"}
39+
for _, entry := range expectedEntries {
40+
assert.Contains(t, string(content), entry, "Expected gitignore to contain %s", entry)
41+
}
42+
43+
err = AddFlowEntriesToGitIgnore("", state.ReaderWriter())
44+
require.NoError(t, err, "Failed to add Flow entries to gitignore again")
45+
46+
content, err = state.ReaderWriter().ReadFile(".gitignore")
47+
require.NoError(t, err, "Failed to read gitignore file again")
48+
49+
for _, entry := range expectedEntries {
50+
occurrences := strings.Count(string(content), entry)
51+
assert.Equal(t, 1, occurrences, "Expected 1 occurrence of %s, but found %d", entry, occurrences)
52+
}
53+
}
54+
55+
func TestAddFlowEntriesToCursorIgnore_NoDuplicates(t *testing.T) {
56+
_, state, _ := TestMocks(t)
57+
58+
err := AddFlowEntriesToCursorIgnore("", state.ReaderWriter())
59+
require.NoError(t, err, "Failed to add Flow entries to cursorignore")
60+
61+
content, err := state.ReaderWriter().ReadFile(".cursorignore")
62+
require.NoError(t, err, "Failed to read cursorignore file")
63+
64+
expectedEntries := []string{"# flow", "emulator-account.pkey", ".env", "# Pay attention to imports directory", "!imports/**"}
65+
for _, entry := range expectedEntries {
66+
assert.Contains(t, string(content), entry, "Expected cursorignore to contain %s", entry)
67+
}
68+
69+
err = AddFlowEntriesToCursorIgnore("", state.ReaderWriter())
70+
require.NoError(t, err, "Failed to add Flow entries to cursorignore again")
71+
72+
content, err = state.ReaderWriter().ReadFile(".cursorignore")
73+
require.NoError(t, err, "Failed to read cursorignore file again")
74+
75+
for _, entry := range expectedEntries {
76+
occurrences := strings.Count(string(content), entry)
77+
assert.Equal(t, 1, occurrences, "Expected 1 occurrence of %s, but found %d", entry, occurrences)
78+
}
79+
}
80+
81+
func TestAddFlowEntriesToGitIgnore_WithExistingContent(t *testing.T) {
82+
_, state, _ := TestMocks(t)
83+
84+
existingContent := "# existing content\nnode_modules/\n*.log\n"
85+
err := state.ReaderWriter().WriteFile(".gitignore", []byte(existingContent), 0644)
86+
require.NoError(t, err, "Failed to create existing .gitignore")
87+
88+
err = AddFlowEntriesToGitIgnore("", state.ReaderWriter())
89+
require.NoError(t, err, "Failed to add Flow entries to gitignore")
90+
91+
content, err := state.ReaderWriter().ReadFile(".gitignore")
92+
require.NoError(t, err, "Failed to read gitignore file")
93+
94+
assert.Contains(t, string(content), existingContent, "Expected existing content to be preserved")
95+
96+
flowEntries := []string{"# flow", "emulator-account.pkey", "imports", ".env"}
97+
for _, entry := range flowEntries {
98+
assert.Contains(t, string(content), entry, "Expected gitignore to contain %s", entry)
99+
}
100+
}
101+
102+
func TestAddFlowEntriesToCursorIgnore_WithExistingContent(t *testing.T) {
103+
_, state, _ := TestMocks(t)
104+
105+
existingContent := "# existing cursor ignore\n.vscode/\n.idea/\n"
106+
err := state.ReaderWriter().WriteFile(".cursorignore", []byte(existingContent), 0644)
107+
require.NoError(t, err, "Failed to create existing .cursorignore")
108+
109+
err = AddFlowEntriesToCursorIgnore("", state.ReaderWriter())
110+
require.NoError(t, err, "Failed to add Flow entries to cursorignore")
111+
112+
content, err := state.ReaderWriter().ReadFile(".cursorignore")
113+
require.NoError(t, err, "Failed to read cursorignore file")
114+
115+
assert.Contains(t, string(content), existingContent, "Expected existing content to be preserved")
116+
117+
flowEntries := []string{"# flow", "emulator-account.pkey", ".env", "# Pay attention to imports directory", "!imports/**"}
118+
for _, entry := range flowEntries {
119+
assert.Contains(t, string(content), entry, "Expected cursorignore to contain %s", entry)
120+
}
121+
}
122+
123+
func TestAddEntriesToIgnoreFile_HelperFunction(t *testing.T) {
124+
_, state, _ := TestMocks(t)
125+
126+
entries := []string{"# test", "test-file.txt", "another-file.log"}
127+
err := addEntriesToIgnoreFile("test-ignore.txt", entries, state.ReaderWriter())
128+
require.NoError(t, err, "Failed to add entries to ignore file")
129+
130+
content, err := state.ReaderWriter().ReadFile("test-ignore.txt")
131+
require.NoError(t, err, "Failed to read ignore file")
132+
133+
for _, entry := range entries {
134+
assert.Contains(t, string(content), entry, "Expected ignore file to contain %s", entry)
135+
}
136+
137+
err = addEntriesToIgnoreFile("test-ignore.txt", entries, state.ReaderWriter())
138+
require.NoError(t, err, "Failed to add entries to ignore file again")
139+
140+
content, err = state.ReaderWriter().ReadFile("test-ignore.txt")
141+
require.NoError(t, err, "Failed to read ignore file again")
142+
143+
for _, entry := range entries {
144+
occurrences := strings.Count(string(content), entry)
145+
assert.Equal(t, 1, occurrences, "Expected 1 occurrence of %s, but found %d", entry, occurrences)
146+
}
147+
}

0 commit comments

Comments
 (0)