Repository: docker/compose
Pull request #12239 (2024-10-27 06:26:48)

fix(dockerignore): Add wildcard support to dockerignore.go
**What I did**
Added wildcard support to `dockerignore::NewDockerPatternMatcher`, because if the .dockerignore contains a wildcard to ignore all files + lines with an exclusion, it ignores all files while it shouldn't ignore the excluded files

**Related issue**
https://github.com/docker/compose/issues/12236

PR to reproduce the bug https://github.com/docker/compose/pull/12238

**(not mandatory) A picture of a cute animal, if possible in relation to what you did**
![image](https://github.com/user-attachments/assets/4957f22b-959e-45ba-8cd4-9be56b2c2fc9)



Patch:
diff --git a/pkg/compose/push.go b/pkg/compose/push.go
index 86698194d..257bc5da1 100644
--- a/pkg/compose/push.go
+++ b/pkg/compose/push.go
@@ -63,7 +63,7 @@ func (s *composeService) push(ctx context.Context, project *types.Project, optio
 	for _, service := range project.Services {
 		if service.Build == nil || service.Image == "" {
 			if options.ImageMandatory && service.Image == "" {
-				return fmt.Errorf("%q attribut is mandatory to push an image for service %q", "service.image", service.Name)
+				return fmt.Errorf("%q attribute is mandatory to push an image for service %q", "service.image", service.Name)
 			}
 			w.Event(progress.Event{
 				ID:     service.Name,
diff --git a/pkg/watch/dockerignore.go b/pkg/watch/dockerignore.go
index aea77e807..16650999c 100644
--- a/pkg/watch/dockerignore.go
+++ b/pkg/watch/dockerignore.go
@@ -21,6 +21,7 @@ import (
 	"io"
 	"os"
 	"path/filepath"
+	"slices"
 	"strings"
 
 	"github.com/compose-spec/compose-go/v2/types"
@@ -128,6 +129,15 @@ func NewDockerPatternMatcher(repoRoot string, patterns []string) (*dockerPathMat
 		return nil, err
 	}
 
+	// Check if "*" is present in patterns
+	hasAllPattern := slices.Contains(patterns, "*")
+	if hasAllPattern {
+		// Remove all non-exclusion patterns (those that don't start with '!')
+		patterns = slices.DeleteFunc(patterns, func(p string) bool {
+			return p != "" && p[0] != '!' // Only keep exclusion patterns
+		})
+	}
+
 	pm, err := patternmatcher.New(absPatterns(absRoot, patterns))
 	if err != nil {
 		return nil, err


Test patch:
diff --git a/pkg/watch/dockerignore_test.go b/pkg/watch/dockerignore_test.go
new file mode 100644
index 000000000..6e88a8574
--- /dev/null
+++ b/pkg/watch/dockerignore_test.go
@@ -0,0 +1,108 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   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 watch
+
+import (
+	"testing"
+)
+
+func TestNewDockerPatternMatcher(t *testing.T) {
+	tests := []struct {
+		name         string
+		repoRoot     string
+		patterns     []string
+		expectedErr  bool
+		expectedRoot string
+		expectedPat  []string
+	}{
+		{
+			name:         "Basic patterns without wildcard",
+			repoRoot:     "/repo",
+			patterns:     []string{"dir1/", "file.txt"},
+			expectedErr:  false,
+			expectedRoot: "/repo",
+			expectedPat:  []string{"/repo/dir1", "/repo/file.txt"},
+		},
+		{
+			name:         "Patterns with exclusion",
+			repoRoot:     "/repo",
+			patterns:     []string{"dir1/", "!file.txt"},
+			expectedErr:  false,
+			expectedRoot: "/repo",
+			expectedPat:  []string{"/repo/dir1", "!/repo/file.txt"},
+		},
+		{
+			name:         "Wildcard with exclusion",
+			repoRoot:     "/repo",
+			patterns:     []string{"*", "!file.txt"},
+			expectedErr:  false,
+			expectedRoot: "/repo",
+			expectedPat:  []string{"!/repo/file.txt"},
+		},
+		{
+			name:         "No patterns",
+			repoRoot:     "/repo",
+			patterns:     []string{},
+			expectedErr:  false,
+			expectedRoot: "/repo",
+			expectedPat:  nil,
+		},
+		{
+			name:         "Only exclusion pattern",
+			repoRoot:     "/repo",
+			patterns:     []string{"!file.txt"},
+			expectedErr:  false,
+			expectedRoot: "/repo",
+			expectedPat:  []string{"!/repo/file.txt"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Call the function with the test data
+			matcher, err := NewDockerPatternMatcher(tt.repoRoot, tt.patterns)
+
+			// Check if we expect an error
+			if (err != nil) != tt.expectedErr {
+				t.Fatalf("expected error: %v, got: %v", tt.expectedErr, err)
+			}
+
+			// If no error is expected, check the output
+			if !tt.expectedErr {
+				if matcher.repoRoot != tt.expectedRoot {
+					t.Errorf("expected root: %v, got: %v", tt.expectedRoot, matcher.repoRoot)
+				}
+
+				// Compare patterns
+				actualPatterns := matcher.matcher.Patterns()
+				if len(actualPatterns) != len(tt.expectedPat) {
+					t.Errorf("expected patterns length: %v, got: %v", len(tt.expectedPat), len(actualPatterns))
+				}
+
+				for i, expectedPat := range tt.expectedPat {
+					actualPatternStr := actualPatterns[i].String()
+					if actualPatterns[i].Exclusion() {
+						actualPatternStr = "!" + actualPatternStr
+					}
+					if actualPatternStr != expectedPat {
+						t.Errorf("expected pattern: %v, got: %v", expectedPat, actualPatterns[i])
+					}
+				}
+			}
+		})
+	}
+}