github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/dockerignore/ignore.go (about)

     1  package dockerignore
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"strings"
     8  
     9  	"github.com/moby/buildkit/frontend/dockerfile/dockerignore"
    10  
    11  	tiltDockerignore "github.com/tilt-dev/dockerignore"
    12  	"github.com/tilt-dev/tilt/internal/ospath"
    13  )
    14  
    15  type dockerPathMatcher struct {
    16  	repoRoot string
    17  	matcher  *tiltDockerignore.PatternMatcher
    18  }
    19  
    20  func (i dockerPathMatcher) Matches(f string) (bool, error) {
    21  	if !filepath.IsAbs(f) {
    22  		f = filepath.Join(i.repoRoot, f)
    23  	}
    24  	return i.matcher.Matches(f)
    25  }
    26  
    27  func (i dockerPathMatcher) MatchesEntireDir(f string) (bool, error) {
    28  	matches, err := i.Matches(f)
    29  	if !matches || err != nil {
    30  		return matches, err
    31  	}
    32  
    33  	// We match the dir, but we might exclude files underneath it.
    34  	if i.matcher.Exclusions() {
    35  		for _, pattern := range i.matcher.Patterns() {
    36  			if !pattern.Exclusion() {
    37  				continue
    38  			}
    39  
    40  			// An exclusion pattern handles the case where the user
    41  			// does something like:
    42  			//
    43  			// *
    44  			// !path/to/include
    45  			// !pkg/**/*.go
    46  			//
    47  			// where they ignore all files and select the files
    48  			// they want to exclude.
    49  			//
    50  			// Because MatchesEntireDir is an optimization, we err
    51  			// on the side of crawling too much.
    52  			patternString := pattern.String()
    53  
    54  			// Handle the case where the pattern matches a subfile of the
    55  			// current directory.
    56  			if ospath.IsChild(f, patternString) {
    57  				// Found an exclusion match -- we don't match this whole dir
    58  				return false, nil
    59  			}
    60  
    61  			// Handle the case where the pattern has a glob that matches
    62  			// arbitrary files, and the glob could match a subfile of
    63  			// the current directory.
    64  			prefix := definitePatternPrefix(patternString)
    65  			if prefix != patternString && ospath.IsChild(prefix, f) {
    66  				return false, nil
    67  			}
    68  		}
    69  		return true, nil
    70  	}
    71  	return true, nil
    72  }
    73  
    74  // Truncate the pattern to just the prefxi that's a concrete directory.
    75  func definitePatternPrefix(p string) string {
    76  	if !strings.Contains(p, "*") {
    77  		return p
    78  	}
    79  	parts := strings.Split(p, string(filepath.Separator))
    80  	for i, part := range parts {
    81  		if strings.Contains(part, "*") {
    82  			if i == 0 {
    83  				return "."
    84  			}
    85  			return strings.Join(parts[0:i], string(filepath.Separator))
    86  		}
    87  	}
    88  	return p
    89  }
    90  
    91  func NewDockerIgnoreTester(repoRoot string) (*dockerPathMatcher, error) {
    92  	absRoot, err := filepath.Abs(repoRoot)
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  
    97  	patterns, err := readDockerignorePatterns(absRoot)
    98  	if err != nil {
    99  		return nil, err
   100  	}
   101  
   102  	return NewDockerPatternMatcher(absRoot, patterns)
   103  }
   104  
   105  // Make all the patterns use absolute paths.
   106  func absPatterns(absRoot string, patterns []string) []string {
   107  	absPatterns := make([]string, 0, len(patterns))
   108  	for _, p := range patterns {
   109  		// The pattern parsing here is loosely adapted from fileutils' NewPatternMatcher
   110  		p = strings.TrimSpace(p)
   111  		if p == "" {
   112  			continue
   113  		}
   114  		p = filepath.Clean(p)
   115  
   116  		pPath := p
   117  		isExclusion := false
   118  		if p[0] == '!' {
   119  			pPath = p[1:]
   120  			isExclusion = true
   121  		}
   122  
   123  		if !filepath.IsAbs(pPath) {
   124  			pPath = filepath.Join(absRoot, pPath)
   125  		}
   126  		absPattern := pPath
   127  		if isExclusion {
   128  			absPattern = fmt.Sprintf("!%s", pPath)
   129  		}
   130  		absPatterns = append(absPatterns, absPattern)
   131  	}
   132  	return absPatterns
   133  }
   134  
   135  func NewDockerPatternMatcher(repoRoot string, patterns []string) (*dockerPathMatcher, error) {
   136  	absRoot, err := filepath.Abs(repoRoot)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  
   141  	pm, err := tiltDockerignore.NewPatternMatcher(absPatterns(absRoot, patterns))
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  
   146  	return &dockerPathMatcher{
   147  		repoRoot: absRoot,
   148  		matcher:  pm,
   149  	}, nil
   150  }
   151  
   152  func readDockerignorePatterns(repoRoot string) ([]string, error) {
   153  	var excludes []string
   154  
   155  	f, err := os.Open(filepath.Join(repoRoot, ".dockerignore"))
   156  	switch {
   157  	case os.IsNotExist(err):
   158  		return excludes, nil
   159  	case err != nil:
   160  		return nil, err
   161  	}
   162  	defer func() { _ = f.Close() }()
   163  
   164  	return dockerignore.ReadAll(f)
   165  }
   166  
   167  func DockerIgnoreTesterFromContents(repoRoot string, contents string) (*dockerPathMatcher, error) {
   168  	patterns, err := dockerignore.ReadAll(strings.NewReader(contents))
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  
   173  	return NewDockerPatternMatcher(repoRoot, patterns)
   174  }