dockerignore.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package watch
  14. import (
  15. "fmt"
  16. "io"
  17. "os"
  18. "path/filepath"
  19. "slices"
  20. "strings"
  21. "github.com/compose-spec/compose-go/v2/types"
  22. "github.com/docker/compose/v2/internal/paths"
  23. "github.com/moby/patternmatcher"
  24. "github.com/moby/patternmatcher/ignorefile"
  25. )
  26. type dockerPathMatcher struct {
  27. repoRoot string
  28. matcher *patternmatcher.PatternMatcher
  29. }
  30. func (i dockerPathMatcher) Matches(f string) (bool, error) {
  31. if !filepath.IsAbs(f) {
  32. f = filepath.Join(i.repoRoot, f)
  33. }
  34. return i.matcher.MatchesOrParentMatches(f)
  35. }
  36. func (i dockerPathMatcher) MatchesEntireDir(f string) (bool, error) {
  37. matches, err := i.Matches(f)
  38. if !matches || err != nil {
  39. return matches, err
  40. }
  41. // We match the dir, but we might exclude files underneath it.
  42. if i.matcher.Exclusions() {
  43. for _, pattern := range i.matcher.Patterns() {
  44. if !pattern.Exclusion() {
  45. continue
  46. }
  47. if paths.IsChild(f, pattern.String()) {
  48. // Found an exclusion match -- we don't match this whole dir
  49. return false, nil
  50. }
  51. }
  52. return true, nil
  53. }
  54. return true, nil
  55. }
  56. func LoadDockerIgnore(build *types.BuildConfig) (PathMatcher, error) {
  57. if build == nil {
  58. return EmptyMatcher{}, nil
  59. }
  60. repoRoot := build.Context
  61. absRoot, err := filepath.Abs(repoRoot)
  62. if err != nil {
  63. return nil, err
  64. }
  65. // first try Dockerfile-specific ignore-file
  66. f, err := os.Open(filepath.Join(repoRoot, build.Dockerfile+".dockerignore"))
  67. if os.IsNotExist(err) {
  68. // defaults to a global .dockerignore
  69. f, err = os.Open(filepath.Join(repoRoot, ".dockerignore"))
  70. if os.IsNotExist(err) {
  71. return NewDockerPatternMatcher(repoRoot, nil)
  72. }
  73. }
  74. if err != nil {
  75. return nil, err
  76. }
  77. defer func() { _ = f.Close() }()
  78. patterns, err := readDockerignorePatterns(f)
  79. if err != nil {
  80. return nil, err
  81. }
  82. return NewDockerPatternMatcher(absRoot, patterns)
  83. }
  84. // Make all the patterns use absolute paths.
  85. func absPatterns(absRoot string, patterns []string) []string {
  86. absPatterns := make([]string, 0, len(patterns))
  87. for _, p := range patterns {
  88. // The pattern parsing here is loosely adapted from fileutils' NewPatternMatcher
  89. p = strings.TrimSpace(p)
  90. if p == "" {
  91. continue
  92. }
  93. p = filepath.Clean(p)
  94. pPath := p
  95. isExclusion := false
  96. if p[0] == '!' {
  97. pPath = p[1:]
  98. isExclusion = true
  99. }
  100. if !filepath.IsAbs(pPath) {
  101. pPath = filepath.Join(absRoot, pPath)
  102. }
  103. absPattern := pPath
  104. if isExclusion {
  105. absPattern = fmt.Sprintf("!%s", pPath)
  106. }
  107. absPatterns = append(absPatterns, absPattern)
  108. }
  109. return absPatterns
  110. }
  111. func NewDockerPatternMatcher(repoRoot string, patterns []string) (*dockerPathMatcher, error) {
  112. absRoot, err := filepath.Abs(repoRoot)
  113. if err != nil {
  114. return nil, err
  115. }
  116. // Check if "*" is present in patterns
  117. hasAllPattern := slices.Contains(patterns, "*")
  118. if hasAllPattern {
  119. // Remove all non-exclusion patterns (those that don't start with '!')
  120. patterns = slices.DeleteFunc(patterns, func(p string) bool {
  121. return p != "" && p[0] != '!' // Only keep exclusion patterns
  122. })
  123. }
  124. pm, err := patternmatcher.New(absPatterns(absRoot, patterns))
  125. if err != nil {
  126. return nil, err
  127. }
  128. return &dockerPathMatcher{
  129. repoRoot: absRoot,
  130. matcher: pm,
  131. }, nil
  132. }
  133. func readDockerignorePatterns(r io.Reader) ([]string, error) {
  134. patterns, err := ignorefile.ReadAll(r)
  135. if err != nil {
  136. return nil, fmt.Errorf("error reading .dockerignore: %w", err)
  137. }
  138. return patterns, nil
  139. }
  140. func DockerIgnoreTesterFromContents(repoRoot string, contents string) (*dockerPathMatcher, error) {
  141. patterns, err := ignorefile.ReadAll(strings.NewReader(contents))
  142. if err != nil {
  143. return nil, fmt.Errorf("error reading .dockerignore: %w", err)
  144. }
  145. return NewDockerPatternMatcher(repoRoot, patterns)
  146. }