dockerignore.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  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) (*dockerPathMatcher, error) {
  57. repoRoot := build.Context
  58. absRoot, err := filepath.Abs(repoRoot)
  59. if err != nil {
  60. return nil, err
  61. }
  62. // first try Dockerfile-specific ignore-file
  63. f, err := os.Open(filepath.Join(repoRoot, build.Dockerfile+".dockerignore"))
  64. if os.IsNotExist(err) {
  65. // defaults to a global .dockerignore
  66. f, err = os.Open(filepath.Join(repoRoot, ".dockerignore"))
  67. if os.IsNotExist(err) {
  68. return NewDockerPatternMatcher(repoRoot, nil)
  69. }
  70. }
  71. if err != nil {
  72. return nil, err
  73. }
  74. defer func() { _ = f.Close() }()
  75. patterns, err := readDockerignorePatterns(f)
  76. if err != nil {
  77. return nil, err
  78. }
  79. return NewDockerPatternMatcher(absRoot, patterns)
  80. }
  81. // Make all the patterns use absolute paths.
  82. func absPatterns(absRoot string, patterns []string) []string {
  83. absPatterns := make([]string, 0, len(patterns))
  84. for _, p := range patterns {
  85. // The pattern parsing here is loosely adapted from fileutils' NewPatternMatcher
  86. p = strings.TrimSpace(p)
  87. if p == "" {
  88. continue
  89. }
  90. p = filepath.Clean(p)
  91. pPath := p
  92. isExclusion := false
  93. if p[0] == '!' {
  94. pPath = p[1:]
  95. isExclusion = true
  96. }
  97. if !filepath.IsAbs(pPath) {
  98. pPath = filepath.Join(absRoot, pPath)
  99. }
  100. absPattern := pPath
  101. if isExclusion {
  102. absPattern = fmt.Sprintf("!%s", pPath)
  103. }
  104. absPatterns = append(absPatterns, absPattern)
  105. }
  106. return absPatterns
  107. }
  108. func NewDockerPatternMatcher(repoRoot string, patterns []string) (*dockerPathMatcher, error) {
  109. absRoot, err := filepath.Abs(repoRoot)
  110. if err != nil {
  111. return nil, err
  112. }
  113. // Check if "*" is present in patterns
  114. hasAllPattern := slices.Contains(patterns, "*")
  115. if hasAllPattern {
  116. // Remove all non-exclusion patterns (those that don't start with '!')
  117. patterns = slices.DeleteFunc(patterns, func(p string) bool {
  118. return p != "" && p[0] != '!' // Only keep exclusion patterns
  119. })
  120. }
  121. pm, err := patternmatcher.New(absPatterns(absRoot, patterns))
  122. if err != nil {
  123. return nil, err
  124. }
  125. return &dockerPathMatcher{
  126. repoRoot: absRoot,
  127. matcher: pm,
  128. }, nil
  129. }
  130. func readDockerignorePatterns(r io.Reader) ([]string, error) {
  131. patterns, err := ignorefile.ReadAll(r)
  132. if err != nil {
  133. return nil, fmt.Errorf("error reading .dockerignore: %w", err)
  134. }
  135. return patterns, nil
  136. }
  137. func DockerIgnoreTesterFromContents(repoRoot string, contents string) (*dockerPathMatcher, error) {
  138. patterns, err := ignorefile.ReadAll(strings.NewReader(contents))
  139. if err != nil {
  140. return nil, fmt.Errorf("error reading .dockerignore: %w", err)
  141. }
  142. return NewDockerPatternMatcher(repoRoot, patterns)
  143. }