diff --git a/pkg/generators/git_generator.go b/pkg/generators/git_generator.go index c07c2b3c3..94e9afa0d 100644 --- a/pkg/generators/git_generator.go +++ b/pkg/generators/git_generator.go @@ -77,10 +77,21 @@ func (g *GitGenerator) Write(overwrite ...bool) error { } existingLines := make(map[string]struct{}) + commentedNormalized := make(map[string]struct{}) var unmanagedLines []string lines := strings.Split(string(content), "\n") for i, line := range lines { existingLines[line] = struct{}{} + + // Track normalized commented versions of Windsor entries + trimmed := strings.TrimLeft(line, " \t") + if strings.HasPrefix(trimmed, "#") { + norm := normalizeGitignoreComment(trimmed) + if norm != "" { + commentedNormalized[norm] = struct{}{} + } + } + if i == len(lines)-1 && line == "" { continue } @@ -88,11 +99,18 @@ func (g *GitGenerator) Write(overwrite ...bool) error { } for _, line := range gitIgnoreLines { - if _, exists := existingLines[line]; !exists { - if line == "# managed by windsor cli" { + if line == "# managed by windsor cli" { + if _, exists := existingLines[line]; !exists { unmanagedLines = append(unmanagedLines, "") + unmanagedLines = append(unmanagedLines, line) + } + continue + } + + if _, exists := existingLines[line]; !exists { + if _, commentedExists := commentedNormalized[line]; !commentedExists { + unmanagedLines = append(unmanagedLines, line) } - unmanagedLines = append(unmanagedLines, line) } } @@ -109,6 +127,23 @@ func (g *GitGenerator) Write(overwrite ...bool) error { return nil } +// ============================================================================= +// Helper Functions +// ============================================================================= + +// normalizeGitignoreComment normalizes a commented .gitignore line to its uncommented form. +// It removes all leading #, whitespace, and trailing whitespace. +func normalizeGitignoreComment(line string) string { + trimmed := strings.TrimLeft(line, " \t") + if !strings.HasPrefix(trimmed, "#") { + return "" + } + // Remove all leading # and whitespace after # + noHash := strings.TrimLeft(trimmed, "#") + noHash = strings.TrimLeft(noHash, " \t") + return strings.TrimSpace(noHash) +} + // ============================================================================= // Interface Compliance // ============================================================================= diff --git a/pkg/generators/git_generator_test.go b/pkg/generators/git_generator_test.go index a40e87a82..78f09d02b 100644 --- a/pkg/generators/git_generator_test.go +++ b/pkg/generators/git_generator_test.go @@ -5,6 +5,7 @@ import ( "io/fs" "os" "path/filepath" + "strings" "testing" ) @@ -235,4 +236,81 @@ func TestGitGenerator_Write(t *testing.T) { t.Errorf("expected error %s, got %s", expectedError, err.Error()) } }) + + t.Run("HandlesCommentedOutLines", func(t *testing.T) { + // Given a GitGenerator with mocks + generator, mocks := setup(t) + + // And GetProjectRoot is mocked to return a specific path + mocks.Shell.GetProjectRootFunc = func() (string, error) { + return filepath.Join("mock", "project", "root"), nil + } + + // And ReadFile is mocked to return content with various commented out Windsor entries + commentedContent := "existing content\n# .aws/\n # .aws/\n# .aws/\n## .aws/\n#\t.aws/\n# .aws/ \n#contexts/**/.terraform/\n# contexts/**/.terraform/ " + commentedContent = strings.ReplaceAll(commentedContent, "#\t.aws/", "#\t.aws/") + mocks.Shims.ReadFile = func(path string) ([]byte, error) { + expectedPath := filepath.Join("mock", "project", "root", ".gitignore") + if path == expectedPath { + return []byte(commentedContent), nil + } + return nil, fmt.Errorf("unexpected file read: %s", path) + } + + // And WriteFile is mocked to verify the content + var writtenContent []byte + mocks.Shims.WriteFile = func(path string, content []byte, _ fs.FileMode) error { + writtenContent = content + return nil + } + + // When Write is called + err := generator.Write() + + // Then no error should occur + if err != nil { + t.Errorf("expected no error, got %v", err) + } + + // And the content should preserve all commented lines and not add uncommented duplicates + actualContent := string(writtenContent) + commentVariants := []string{ + "# .aws/", + " # .aws/", + "# .aws/", + "## .aws/", + "#\t.aws/", + "# .aws/ ", + "#contexts/**/.terraform/", + "# contexts/**/.terraform/ ", + } + commentVariants[4] = "#\t.aws/" + for i, variant := range commentVariants { + if i == 4 { + variant = "#\t.aws/" + } + if !strings.Contains(actualContent, variant) { + t.Errorf("expected content to preserve commented variant: %q", variant) + } + } + + // Check that uncommented versions are NOT added when any commented version exists + lines := strings.Split(actualContent, "\n") + hasUncommentedAws := false + hasUncommentedTerraform := false + for _, line := range lines { + if strings.TrimSpace(line) == ".aws/" { + hasUncommentedAws = true + } + if strings.TrimSpace(line) == "contexts/**/.terraform/" { + hasUncommentedTerraform = true + } + } + if hasUncommentedAws { + t.Errorf("expected content to not add uncommented .aws/ when any commented version exists") + } + if hasUncommentedTerraform { + t.Errorf("expected content to not add uncommented contexts/**/.terraform/ when any commented version exists") + } + }) }