Skip to content

Commit

Permalink
Simplify exclusion logic and update tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jongio committed Sep 16, 2024
1 parent e64531c commit cb82f33
Show file tree
Hide file tree
Showing 33 changed files with 256 additions and 371 deletions.
2 changes: 2 additions & 0 deletions .vscode/cspell.global.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ ignoreWords:
- fdfp
- fics
- Frontdoor
- funcignore
- gitcli
- golobby
- graphsdk
Expand Down Expand Up @@ -162,6 +163,7 @@ ignoreWords:
- vwan
- wafrg
- westus
- webappignore
- Wans
- apim
- Retryable
Expand Down
123 changes: 19 additions & 104 deletions cli/azd/pkg/dotignore/dotignore.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,123 +4,38 @@ import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/denormal/go-gitignore"
)

// ReadIgnoreFiles reads all ignore files (default to ".zipignore") in the directory hierarchy,
// from the projectDir upwards, and returns a slice of gitignore.GitIgnore structures.
func ReadIgnoreFiles(projectDir string, ignoreFileName ...string) ([]gitignore.GitIgnore, error) {
var ignoreMatchers []gitignore.GitIgnore

// Set default ignore file name to ".zipignore" if none is provided
fileName := ".zipignore"
if len(ignoreFileName) > 0 && ignoreFileName[0] != "" {
fileName = ignoreFileName[0]
// ReadDotIgnoreFile reads the ignore file located at the root of the project directory.
// If the ignoreFileName is blank or the file is not found, it returns nil, nil.
func ReadDotIgnoreFile(projectDir string, ignoreFileName string) (gitignore.GitIgnore, error) {
// Return nil if the ignoreFileName is empty
if ignoreFileName == "" {
return nil, nil
}

// Traverse upwards from the projectDir to the root directory
currentDir := projectDir
for {
ignoreFilePath := filepath.Join(currentDir, fileName)
if _, err := os.Stat(ignoreFilePath); !os.IsNotExist(err) {
ignoreMatcher, err := gitignore.NewFromFile(ignoreFilePath)
if err != nil {
return nil, fmt.Errorf("error reading %s file at %s: %w", fileName, ignoreFilePath, err)
}
ignoreMatchers = append([]gitignore.GitIgnore{ignoreMatcher}, ignoreMatchers...)
}
ignoreFilePath := filepath.Join(projectDir, ignoreFileName)
if _, err := os.Stat(ignoreFilePath); os.IsNotExist(err) {
// Return nil if the ignore file does not exist
return nil, nil
}

// Stop if we've reached the root directory
parentDir := filepath.Dir(currentDir)
if parentDir == currentDir {
break
}
currentDir = parentDir
ignoreMatcher, err := gitignore.NewFromFile(ignoreFilePath)
if err != nil {
return nil, fmt.Errorf("error reading %s file at %s: %w", ignoreFileName, ignoreFilePath, err)
}

return ignoreMatchers, nil
return ignoreMatcher, nil
}

// ShouldIgnore checks if a file or directory should be ignored based on a slice of gitignore.GitIgnore structures.
func ShouldIgnore(path string, isDir bool, ignoreMatchers []gitignore.GitIgnore) bool {
for _, matcher := range ignoreMatchers {
match := matcher.Relative(path, isDir)
if match != nil && match.Ignore() {
// ShouldIgnore determines whether a file or directory should be ignored based on the provided ignore matcher.
func ShouldIgnore(relativePath string, isDir bool, ignoreMatcher gitignore.GitIgnore) bool {
if ignoreMatcher != nil {
if match := ignoreMatcher.Relative(relativePath, isDir); match != nil && match.Ignore() {
return true
}
}
return false
}

// RemoveIgnoredFiles removes files and directories based on ignore rules using a pre-collected list of paths.
func RemoveIgnoredFiles(staging string, ignoreMatchers []gitignore.GitIgnore) error {
if len(ignoreMatchers) == 0 {
return nil // No ignore files, no files to ignore
}

// Collect all file and directory paths
paths, err := CollectFilePaths(staging)
if err != nil {
return fmt.Errorf("collecting file paths: %w", err)
}

// Map to store directories that should be ignored, preventing their children from being processed
ignoredDirs := make(map[string]struct{})

// Iterate through collected paths and determine which to remove
for _, path := range paths {
relativePath, err := filepath.Rel(staging, path)
if err != nil {
return err
}

// Skip processing if the path is within an ignored directory
skip := false
for ignoredDir := range ignoredDirs {
if strings.HasPrefix(relativePath, ignoredDir) {
skip = true
break
}
}
if skip {
continue
}

isDir := false
info, err := os.Lstat(path)
if err == nil {
isDir = info.IsDir()
}

// Check if the file should be ignored
if ShouldIgnore(relativePath, isDir, ignoreMatchers) {
if isDir {
ignoredDirs[relativePath] = struct{}{}
if err := os.RemoveAll(path); err != nil {
return fmt.Errorf("removing directory %s: %w", path, err)
}
} else {
if err := os.Remove(path); err != nil {
return fmt.Errorf("removing file %s: %w", path, err)
}
}
}
}

return nil
}

// CollectFilePaths collects all file and directory paths under the given root directory.
func CollectFilePaths(root string) ([]string, error) {
var paths []string
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
paths = append(paths, path)
return nil
})
return paths, err
}
8 changes: 4 additions & 4 deletions cli/azd/pkg/project/framework_service_npm.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,10 @@ func (np *npmProject) Package(
packageSource,
packageDest,
buildForZipOptions{
excludeCallback: func(src string) ([]excludeDirEntryCondition, error) {
return []excludeDirEntryCondition{excludeNodeModules}, nil
},
}); err != nil {
excludeConditions: []excludeDirEntryCondition{excludeNodeModules},
},
serviceConfig,
); err != nil {
return nil, fmt.Errorf("packaging for %s: %w", serviceConfig.Name, err)
}

Expand Down
8 changes: 4 additions & 4 deletions cli/azd/pkg/project/framework_service_python.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,10 @@ func (pp *pythonProject) Package(
packageSource,
packageDest,
buildForZipOptions{
excludeCallback: func(src string) ([]excludeDirEntryCondition, error) {
return []excludeDirEntryCondition{excludeVirtualEnv, excludePyCache}, nil
},
}); err != nil {
excludeConditions: []excludeDirEntryCondition{excludeVirtualEnv, excludePyCache},
},
serviceConfig,
); err != nil {

return nil, fmt.Errorf("packaging for %s: %w", serviceConfig.Name, err)
}
Expand Down
89 changes: 33 additions & 56 deletions cli/azd/pkg/project/project_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,38 +14,30 @@ import (
"github.com/otiai10/copy"
)

// CreateDeployableZip creates a zip file of a folder, recursively.
// Returns the path to the created zip file or an error if it fails.
// createDeployableZip creates a zip file of a folder.
func createDeployableZip(projectName string, appName string, path string) (string, error) {
// TODO: should probably avoid picking up files that weren't meant to be deployed (ie, local .env files, etc..)
// Create the output zip file path
filePath := filepath.Join(os.TempDir(), fmt.Sprintf("%s-%s-azddeploy-%d.zip", projectName, appName, time.Now().Unix()))
zipFile, err := os.Create(filePath)
if err != nil {
return "", fmt.Errorf("failed when creating zip package to deploy %s: %w", appName, err)
return "", fmt.Errorf("failed to create zip file: %w", err)
}

// Read and honor the .dotignore files
ignoreMatchers, err := dotignore.ReadIgnoreFiles(path)
if err != nil && !os.IsNotExist(err) {
return "", fmt.Errorf("reading .dotignore files: %w", err)
}

// Create the zip file, excluding files that match the .dotignore rules
err = rzip.CreateFromDirectoryWithIgnore(path, zipFile, ignoreMatchers)
// Zip the directory without any exclusions (they've already been handled in buildForZip)
err = rzip.CreateFromDirectory(path, zipFile)
if err != nil {
// If we fail here, just do our best to close things out and cleanup
zipFile.Close()
os.Remove(zipFile.Name())
return "", err
}

// Close the zip file and return the path
if err := zipFile.Close(); err != nil {
// May fail, but again, we'll do our best to cleanup here.
os.Remove(zipFile.Name())
return "", err
}

return zipFile.Name(), nil
return filePath, nil
}

// excludeDirEntryCondition resolves when a file or directory should be considered or not as part of build, when build is a
Expand All @@ -55,60 +47,45 @@ type excludeDirEntryCondition func(path string, file os.FileInfo) bool
// buildForZipOptions provides a set of options for doing build for zip
type buildForZipOptions struct {
excludeConditions []excludeDirEntryCondition
excludeCallback func(src string) ([]excludeDirEntryCondition, error)
}

// buildForZip is used by projects whose build strategy is to only copy the source code into a folder, which is later
// zipped for packaging. buildForZipOptions provides the specific details for each language regarding which files should
// not be copied.
func buildForZip(src, dst string, options buildForZipOptions) error {
// Add a global exclude condition for the .zipignore file
ignoreMatchers, err := dotignore.ReadIgnoreFiles(src, ".zipignore")
// buildForZip is used by projects to prepare a directory for
// zipping, excluding files based on the ignore file and other conditions.
func buildForZip(src, dst string, options buildForZipOptions, serviceConfig *ServiceConfig) error {
// Lookup the appropriate ignore file name based on the service kind (Host)
ignoreFileName := GetIgnoreFileNameByKind(serviceConfig.Host)

// Read and honor the specified ignore file if it exists
ignoreMatcher, err := dotignore.ReadDotIgnoreFile(src, ignoreFileName)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("reading .zipignore files: %w", err)
return fmt.Errorf("reading %s file: %w", ignoreFileName, err)
}

// Determine if the .zipignore file exists at the root of the service
zipIgnoreExists := len(ignoreMatchers) > 0

// Conditionally exclude virtual environments, __pycache__, and node_modules only if .zipignore doesn't exist
if !zipIgnoreExists {
if options.excludeCallback != nil {
callbackExcludes, err := options.excludeCallback(src)
if err != nil {
return fmt.Errorf("applying exclude callback: %w", err)
}
options.excludeConditions = append(options.excludeConditions, callbackExcludes...)
}
}
// Temporary array to build exclude conditions dynamically
tempExcludeConditions := []excludeDirEntryCondition{}

options.excludeConditions = append(options.excludeConditions, func(path string, file os.FileInfo) bool {
// Check if the relative path should be ignored based on .zipignore rules
if len(ignoreMatchers) > 0 {
// If there's no .ignore file, add the provided excludeConditions
if ignoreMatcher == nil {
tempExcludeConditions = append(tempExcludeConditions, options.excludeConditions...)
} else {
// If there's a .ignore file, apply ignoreMatcher only
tempExcludeConditions = append(tempExcludeConditions, func(path string, file os.FileInfo) bool {
relativePath, err := filepath.Rel(src, path)
if err != nil {
return false
}
isDir := file.IsDir()
if dotignore.ShouldIgnore(relativePath, isDir, ignoreMatchers) {
if err == nil && dotignore.ShouldIgnore(relativePath, file.IsDir(), ignoreMatcher) {
return true
}
}

// Always exclude .zipignore files
if filepath.Base(path) == ".zipignore" {
return true
}

return false
})
return false
})
}

// These exclude conditions apply to all projects
options.excludeConditions = append(options.excludeConditions, globalExcludeAzdFolder)
// Always append the global exclusions (e.g., .azure folder)
tempExcludeConditions = append(tempExcludeConditions, globalExcludeAzdFolder)

// Copy the source directory to the destination, applying the final exclude conditions
return copy.Copy(src, dst, copy.Options{
Skip: func(srcInfo os.FileInfo, src, dest string) (bool, error) {
for _, checkExclude := range options.excludeConditions {
// Apply exclude conditions (either the default or the ignoreMatcher)
for _, checkExclude := range tempExcludeConditions {
if checkExclude(src, srcInfo) {
return true, nil
}
Expand Down
13 changes: 13 additions & 0 deletions cli/azd/pkg/project/service_target.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,19 @@ type ServiceTarget interface {
) ([]string, error)
}

// GetIgnoreFileNameByKind returns the appropriate ignore file name (e.g., .funcignore, .webappignore)
// based on the service target kind.
func GetIgnoreFileNameByKind(kind ServiceTargetKind) string {
switch kind {
case AzureFunctionTarget:
return ".funcignore"
case AppServiceTarget:
return ".webappignore"
default:
return ""
}
}

// NewServiceDeployResult is a helper function to create a new ServiceDeployResult
func NewServiceDeployResult(
relatedResourceId string,
Expand Down
Loading

0 comments on commit cb82f33

Please sign in to comment.