From aef9154ca059674df65aca3ad36105843d692665 Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Fri, 23 Aug 2024 23:10:21 +0000 Subject: [PATCH] Add support for azdignore in package --- .devcontainer/devcontainer.json | 10 +- .vscode/cspell.global.yaml | 1 + cli/azd/internal/repository/initializer.go | 92 +------------- cli/azd/pkg/azdignore/azdignore.go | 120 ++++++++++++++++++ cli/azd/pkg/project/project_utils.go | 51 +++++++- cli/azd/pkg/rzip/rzip.go | 83 ++++++++++++ cli/azd/test/functional/init_test.go | 39 +----- cli/azd/test/functional/package_test.go | 58 +++++++++ .../testdata/samples/azdignore/.azdignore | 1 + .../testdata/samples/azdignore/azure.yaml | 9 ++ .../samples/azdignore/src/service1/.azdignore | 1 + .../azdignore/src/service1/testfile.py | 1 + .../src/service1/tests/testfile1.txt | 1 + .../testsignoredfromroot/testfile1.txt | 1 + cli/azd/test/gitcli/cli.go | 49 +++++++ 15 files changed, 383 insertions(+), 134 deletions(-) create mode 100644 cli/azd/pkg/azdignore/azdignore.go create mode 100644 cli/azd/test/functional/testdata/samples/azdignore/.azdignore create mode 100644 cli/azd/test/functional/testdata/samples/azdignore/azure.yaml create mode 100644 cli/azd/test/functional/testdata/samples/azdignore/src/service1/.azdignore create mode 100644 cli/azd/test/functional/testdata/samples/azdignore/src/service1/testfile.py create mode 100644 cli/azd/test/functional/testdata/samples/azdignore/src/service1/tests/testfile1.txt create mode 100644 cli/azd/test/functional/testdata/samples/azdignore/src/service1/testsignoredfromroot/testfile1.txt create mode 100644 cli/azd/test/gitcli/cli.go diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index dd6b3b6a2c7..7fbda3fb635 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,7 +5,7 @@ "ghcr.io/devcontainers/features/go:1": { "version": "1.21" }, - "ghcr.io/guiyomh/features/golangci-lint:0":{}, + "ghcr.io/guiyomh/features/golangci-lint:0": {}, "ghcr.io/devcontainers/features/docker-in-docker:2.11.0": { "version": "latest", "moby": true @@ -19,7 +19,6 @@ "ghcr.io/devcontainers/features/git:1": {}, "ghcr.io/devcontainers/features/github-cli:1": {}, "ghcr.io/devcontainers/features/terraform:1": {} - }, "customizations": { "vscode": { @@ -34,5 +33,10 @@ ] } }, - "postCreateCommand": "go install gotest.tools/gotestsum@latest" + "postCreateCommand": "go install gotest.tools/gotestsum@latest", + "runArgs": [ + "--cap-add=SYS_PTRACE", + "--security-opt", + "seccomp=unconfined" + ] } \ No newline at end of file diff --git a/.vscode/cspell.global.yaml b/.vscode/cspell.global.yaml index 106c213fa7b..d4ccb073b7e 100644 --- a/.vscode/cspell.global.yaml +++ b/.vscode/cspell.global.yaml @@ -73,6 +73,7 @@ ignoreWords: - fdfp - fics - Frontdoor + - gitcli - golobby - graphsdk - hndl diff --git a/cli/azd/internal/repository/initializer.go b/cli/azd/internal/repository/initializer.go index 77a390feacd..5c1de61b94d 100644 --- a/cli/azd/internal/repository/initializer.go +++ b/cli/azd/internal/repository/initializer.go @@ -12,6 +12,7 @@ import ( "path/filepath" "strings" + "github.com/azure/azure-dev/cli/azd/pkg/azdignore" "github.com/azure/azure-dev/cli/azd/pkg/environment" "github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext" "github.com/azure/azure-dev/cli/azd/pkg/input" @@ -24,7 +25,6 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet" "github.com/azure/azure-dev/cli/azd/pkg/tools/git" "github.com/azure/azure-dev/cli/azd/resources" - "github.com/denormal/go-gitignore" "github.com/otiai10/copy" ) @@ -86,12 +86,12 @@ func (i *Initializer) Initialize( return err } - ignoreMatcher, err := readIgnoreFile(staging) + ignoreMatcher, err := azdignore.ReadIgnoreFiles(staging) if err != nil && !os.IsNotExist(err) { return fmt.Errorf("reading .azdignore file: %w", err) } - err = removeIgnoredFiles(staging, ignoreMatcher) + err = azdignore.RemoveIgnoredFiles(staging, ignoreMatcher) if err != nil { return fmt.Errorf("removing ignored files: %w", err) } @@ -139,92 +139,6 @@ func (i *Initializer) Initialize( return nil } -// readIgnoreFile reads the .azdignore file and returns a gitignore.GitIgnore structure -func readIgnoreFile(projectDir string) (gitignore.GitIgnore, error) { - ignoreFilePath := filepath.Join(projectDir, ".azdignore") - if _, err := os.Stat(ignoreFilePath); os.IsNotExist(err) { - return nil, nil // No .azdignore file, no need to ignore any files - } - - ignoreMatcher, err := gitignore.NewFromFile(ignoreFilePath) - if err != nil { - return nil, fmt.Errorf("error reading .azdignore file: %w", err) - } - - return ignoreMatcher, 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 -} - -// removeIgnoredFiles removes files and directories based on .azdignore rules using a pre-collected list of paths -func removeIgnoredFiles(staging string, ignoreMatcher gitignore.GitIgnore) error { - if ignoreMatcher == nil { - return nil // No .azdignore file, 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() - } - - match := ignoreMatcher.Relative(relativePath, isDir) - if match != nil && match.Ignore() { - 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 -} - func (i *Initializer) fetchCode( ctx context.Context, templateUrl string, diff --git a/cli/azd/pkg/azdignore/azdignore.go b/cli/azd/pkg/azdignore/azdignore.go new file mode 100644 index 00000000000..e718d57f7ea --- /dev/null +++ b/cli/azd/pkg/azdignore/azdignore.go @@ -0,0 +1,120 @@ +package azdignore + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/denormal/go-gitignore" +) + +// ReadIgnoreFiles reads all .azdignore files in the directory hierarchy, from the projectDir upwards, +// and returns a slice of gitignore.GitIgnore structures. +func ReadIgnoreFiles(projectDir string) ([]gitignore.GitIgnore, error) { + var ignoreMatchers []gitignore.GitIgnore + + // Traverse upwards from the projectDir to the root directory + currentDir := projectDir + for { + ignoreFilePath := filepath.Join(currentDir, ".azdignore") + if _, err := os.Stat(ignoreFilePath); !os.IsNotExist(err) { + ignoreMatcher, err := gitignore.NewFromFile(ignoreFilePath) + if err != nil { + return nil, fmt.Errorf("error reading .azdignore file at %s: %w", ignoreFilePath, err) + } + ignoreMatchers = append([]gitignore.GitIgnore{ignoreMatcher}, ignoreMatchers...) + } + + // Stop if we've reached the root directory + parentDir := filepath.Dir(currentDir) + if parentDir == currentDir { + break + } + currentDir = parentDir + } + + return ignoreMatchers, 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() { + return true + } + } + return false +} + +// RemoveIgnoredFiles removes files and directories based on .azdignore rules using a pre-collected list of paths. +func RemoveIgnoredFiles(staging string, ignoreMatchers []gitignore.GitIgnore) error { + if len(ignoreMatchers) == 0 { + return nil // No .azdignore 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 +} diff --git a/cli/azd/pkg/project/project_utils.go b/cli/azd/pkg/project/project_utils.go index fdce5438556..18b7ef9562f 100644 --- a/cli/azd/pkg/project/project_utils.go +++ b/cli/azd/pkg/project/project_utils.go @@ -9,6 +9,7 @@ import ( "path/filepath" "time" + "github.com/azure/azure-dev/cli/azd/pkg/azdignore" "github.com/azure/azure-dev/cli/azd/pkg/rzip" "github.com/otiai10/copy" ) @@ -23,15 +24,23 @@ func createDeployableZip(projectName string, appName string, path string) (strin return "", fmt.Errorf("failed when creating zip package to deploy %s: %w", appName, err) } - if err := rzip.CreateFromDirectory(path, zipFile); err != nil { - // if we fail here just do our best to close things out and cleanup + // Read and honor the .azdignore files + ignoreMatchers, err := azdignore.ReadIgnoreFiles(path) + if err != nil && !os.IsNotExist(err) { + return "", fmt.Errorf("reading .azdignore files: %w", err) + } + + // Create the zip file, excluding files that match the .azdignore rules + err = rzip.CreateFromDirectoryWithIgnore(path, zipFile, ignoreMatchers) + 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 } if err := zipFile.Close(); err != nil { - // may fail but, again, we'll do our best to cleanup here. + // May fail, but again, we'll do our best to cleanup here. os.Remove(zipFile.Name()) return "", err } @@ -48,12 +57,40 @@ type buildForZipOptions struct { excludeConditions []excludeDirEntryCondition } -// buildForZip is use by projects which build strategy is to only copy the source code into a folder which is later -// zipped for packaging. For example Python and Node framework languages. buildForZipOptions provides the specific -// details for each language which should not be ever copied. +// 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 .azdignore file + ignoreMatchers, err := azdignore.ReadIgnoreFiles(src) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("reading .azdignore files: %w", err) + } + + options.excludeConditions = append(options.excludeConditions, func(path string, file os.FileInfo) bool { + if len(ignoreMatchers) > 0 { + // Get the relative path from the source directory + relativePath, err := filepath.Rel(src, path) + if err != nil { + return false + } + + // Check if the relative path should be ignored + isDir := file.IsDir() + if azdignore.ShouldIgnore(relativePath, isDir, ignoreMatchers) { + return true + } + } + + // Always exclude .azdignore files + if filepath.Base(path) == ".azdignore" { + return true + } + + return false + }) - // these exclude conditions applies to all projects + // These exclude conditions apply to all projects options.excludeConditions = append(options.excludeConditions, globalExcludeAzdFolder) return copy.Copy(src, dst, copy.Options{ diff --git a/cli/azd/pkg/rzip/rzip.go b/cli/azd/pkg/rzip/rzip.go index 34f5191954b..84bd8b74ac5 100644 --- a/cli/azd/pkg/rzip/rzip.go +++ b/cli/azd/pkg/rzip/rzip.go @@ -10,6 +10,8 @@ import ( "os" "path/filepath" "strings" + + "github.com/denormal/go-gitignore" ) func CreateFromDirectory(source string, buf *os.File) error { @@ -56,3 +58,84 @@ func CreateFromDirectory(source string, buf *os.File) error { return w.Close() } + +// CreateFromDirectoryWithIgnore creates a zip archive from the contents of a directory, excluding files +// that match any of the provided ignore rules. +func CreateFromDirectoryWithIgnore(srcDir string, writer io.Writer, ignoreMatchers []gitignore.GitIgnore) error { + zipWriter := zip.NewWriter(writer) + defer zipWriter.Close() + + err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Get the relative path of the file to ensure the root directory isn't included in the zip file + relativePath, err := filepath.Rel(srcDir, path) + if err != nil { + return err + } + + // Skip the root directory itself + if relativePath == "." { + return nil + } + + // Check if the file should be ignored based on the ignore matchers + isDir := info.IsDir() + if shouldIgnore(relativePath, isDir, ignoreMatchers) { + if isDir { + // If a directory should be ignored, skip its contents as well + return filepath.SkipDir + } + // Otherwise, just skip the file + return nil + } + + // Add the file or directory to the zip archive + header, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + header.Name = relativePath + + // Ensure directories are properly handled + if isDir { + header.Name += "/" + } else { + header.Method = zip.Deflate + } + + writer, err := zipWriter.CreateHeader(header) + if err != nil { + return err + } + + if !isDir { + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + _, err = io.Copy(writer, file) + if err != nil { + return err + } + } + + return nil + }) + + return err +} + +// shouldIgnore determines whether a file or directory should be ignored based on the provided ignore matchers. +func shouldIgnore(relativePath string, isDir bool, ignoreMatchers []gitignore.GitIgnore) bool { + for _, matcher := range ignoreMatchers { + if match := matcher.Relative(relativePath, isDir); match != nil && match.Ignore() { + return true + } + } + return false +} diff --git a/cli/azd/test/functional/init_test.go b/cli/azd/test/functional/init_test.go index b7bfe8b3f0a..9ddf822156f 100644 --- a/cli/azd/test/functional/init_test.go +++ b/cli/azd/test/functional/init_test.go @@ -4,7 +4,6 @@ package cli_test import ( - "context" "os" "path/filepath" "regexp" @@ -15,6 +14,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/osutil" "github.com/azure/azure-dev/cli/azd/pkg/project" "github.com/azure/azure-dev/cli/azd/test/azdcli" + "github.com/azure/azure-dev/cli/azd/test/gitcli" "github.com/stretchr/testify/require" ) @@ -257,7 +257,7 @@ a?c.txt`, } // Create the template repository with the specified files - createGitRepo(t, ctx, templateDir, files) + gitcli.CreateGitRepo(t, ctx, templateDir, files) // Log the .azdignore content for debugging azdignoreContent, err := os.ReadFile(filepath.Join(templateDir, ".azdignore")) @@ -325,7 +325,7 @@ func Test_CLI_Init_With_No_Azdignore(t *testing.T) { } // Create the template repository with the specified files - createGitRepo(t, ctx, templateDir, files) + gitcli.CreateGitRepo(t, ctx, templateDir, files) projectDir := tempDirWithDiagnostics(t) @@ -375,7 +375,7 @@ func Test_CLI_Init_With_Empty_Azdignore(t *testing.T) { } // Create the template repository with the specified files - createGitRepo(t, ctx, templateDir, files) + gitcli.CreateGitRepo(t, ctx, templateDir, files) projectDir := tempDirWithDiagnostics(t) @@ -404,34 +404,3 @@ func Test_CLI_Init_With_Empty_Azdignore(t *testing.T) { require.FileExists(t, filepath.Join(projectDir, file)) } } - -func createGitRepo(t *testing.T, ctx context.Context, dir string, files map[string]string) { - for path, content := range files { - fullPath := filepath.Join(dir, path) - err := os.MkdirAll(filepath.Dir(fullPath), osutil.PermissionDirectory) - require.NoError(t, err) - err = os.WriteFile(fullPath, []byte(content), osutil.PermissionFile) - require.NoError(t, err) - } - - cmdRun := exec.NewCommandRunner(nil) - - _, err := cmdRun.Run(ctx, exec.NewRunArgs("git", "-C", dir, "init")) - require.NoError(t, err) - - // Set up git user configuration - _, err = cmdRun.Run(ctx, exec.NewRunArgs("git", "-C", dir, "config", "user.email", "test@example.com")) - require.NoError(t, err) - _, err = cmdRun.Run(ctx, exec.NewRunArgs("git", "-C", dir, "config", "user.name", "Test User")) - require.NoError(t, err) - - _, err = cmdRun.Run(ctx, exec.NewRunArgs("git", "-C", dir, "add", ".")) - require.NoError(t, err) - _, err = cmdRun.Run(ctx, exec.NewRunArgs("git", "-C", dir, "commit", "-m", "Initial commit")) - require.NoError(t, err) - - t.Cleanup(func() { - err := os.RemoveAll(dir) - require.NoError(t, err) - }) -} diff --git a/cli/azd/test/functional/package_test.go b/cli/azd/test/functional/package_test.go index acb22b3596e..aa733b96bd3 100644 --- a/cli/azd/test/functional/package_test.go +++ b/cli/azd/test/functional/package_test.go @@ -1,9 +1,11 @@ package cli_test import ( + "archive/zip" "fmt" "os" "path/filepath" + "strings" "testing" "github.com/azure/azure-dev/cli/azd/pkg/osutil" @@ -160,3 +162,59 @@ func Test_CLI_Package(t *testing.T) { require.NoError(t, err) require.Contains(t, packageResult.Stdout, fmt.Sprintf("Package Output: %s", os.TempDir())) } + +func Test_CLI_Package_Azdignore(t *testing.T) { + // running this test in parallel is ok as it uses a t.TempDir() + t.Parallel() + ctx, cancel := newTestContext(t) + defer cancel() + + // Create a temporary directory for the project + dir := tempDirWithDiagnostics(t) + + // Set up the CLI with the appropriate working directory and environment variables + cli := azdcli.NewCLI(t) + cli.WorkingDirectory = dir + cli.Env = append(os.Environ(), "AZURE_LOCATION=eastus2") + + // Copy the sample project to the app directory + err := copySample(dir, "azdignore") + require.NoError(t, err, "failed expanding sample") + + // Run the init command to initialize the project + _, err = cli.RunCommandWithStdIn( + ctx, + "Use code in the current directory\n"+ + "Confirm and continue initializing my app\n"+ + "appdb\n"+ + "TESTENV\n", + "init", + ) + require.NoError(t, err) + + // Verify that the expected files were created during initialization + require.FileExists(t, filepath.Join(dir, "azure.yaml")) + + // Run the package command and specify an output path + _, err = cli.RunCommand(ctx, "package", "--output-path", "./dist") + require.NoError(t, err) + + // Verify that the package was created and the output directory exists + distPath := filepath.Join(dir, "dist") + files, err := os.ReadDir(distPath) + require.NoError(t, err) + require.Len(t, files, 1) + + // Verify that the 'tests' folder is not included in the packaged output + zipFilePath := filepath.Join(distPath, files[0].Name()) + zipReader, err := zip.OpenReader(zipFilePath) + require.NoError(t, err) + defer zipReader.Close() + + for _, file := range zipReader.File { + // Check if the file is in the "tests/" directory or the "testsignoredfromroot/" directory + if strings.HasPrefix(file.Name, "tests/") || strings.HasPrefix(file.Name, "testsignoredfromroot/") { + t.Errorf("file or folder '%s' should not be included in the package", file.Name) + } + } +} diff --git a/cli/azd/test/functional/testdata/samples/azdignore/.azdignore b/cli/azd/test/functional/testdata/samples/azdignore/.azdignore new file mode 100644 index 00000000000..27f9e20258d --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/azdignore/.azdignore @@ -0,0 +1 @@ +/testsignoredfromroot/ \ No newline at end of file diff --git a/cli/azd/test/functional/testdata/samples/azdignore/azure.yaml b/cli/azd/test/functional/testdata/samples/azdignore/azure.yaml new file mode 100644 index 00000000000..b2276a47f21 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/azdignore/azure.yaml @@ -0,0 +1,9 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: azdignoretest +services: + service1: + project: src/service1 + language: python + host: appservice + \ No newline at end of file diff --git a/cli/azd/test/functional/testdata/samples/azdignore/src/service1/.azdignore b/cli/azd/test/functional/testdata/samples/azdignore/src/service1/.azdignore new file mode 100644 index 00000000000..c1334d313ed --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/azdignore/src/service1/.azdignore @@ -0,0 +1 @@ +/tests/ \ No newline at end of file diff --git a/cli/azd/test/functional/testdata/samples/azdignore/src/service1/testfile.py b/cli/azd/test/functional/testdata/samples/azdignore/src/service1/testfile.py new file mode 100644 index 00000000000..0aba17d1083 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/azdignore/src/service1/testfile.py @@ -0,0 +1 @@ +import os \ No newline at end of file diff --git a/cli/azd/test/functional/testdata/samples/azdignore/src/service1/tests/testfile1.txt b/cli/azd/test/functional/testdata/samples/azdignore/src/service1/tests/testfile1.txt new file mode 100644 index 00000000000..c7e316005e7 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/azdignore/src/service1/tests/testfile1.txt @@ -0,0 +1 @@ +This is a placeholder test file \ No newline at end of file diff --git a/cli/azd/test/functional/testdata/samples/azdignore/src/service1/testsignoredfromroot/testfile1.txt b/cli/azd/test/functional/testdata/samples/azdignore/src/service1/testsignoredfromroot/testfile1.txt new file mode 100644 index 00000000000..c7e316005e7 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/azdignore/src/service1/testsignoredfromroot/testfile1.txt @@ -0,0 +1 @@ +This is a placeholder test file \ No newline at end of file diff --git a/cli/azd/test/gitcli/cli.go b/cli/azd/test/gitcli/cli.go new file mode 100644 index 00000000000..fab9343531c --- /dev/null +++ b/cli/azd/test/gitcli/cli.go @@ -0,0 +1,49 @@ +package gitcli + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/azure/azure-dev/cli/azd/pkg/exec" + "github.com/azure/azure-dev/cli/azd/pkg/osutil" + "github.com/stretchr/testify/require" +) + +// CreateGitRepo initializes a Git repository with the provided files. +// It sets up the initial commit and configures the user identity for the repository. +func CreateGitRepo(t *testing.T, ctx context.Context, dir string, files map[string]string) { + // Create directories and write files + for path, content := range files { + fullPath := filepath.Join(dir, path) + err := os.MkdirAll(filepath.Dir(fullPath), osutil.PermissionDirectory) + require.NoError(t, err) + err = os.WriteFile(fullPath, []byte(content), osutil.PermissionFile) + require.NoError(t, err) + } + + cmdRun := exec.NewCommandRunner(nil) + + // Initialize a Git repository + _, err := cmdRun.Run(ctx, exec.NewRunArgs("git", "-C", dir, "init")) + require.NoError(t, err) + + // Set up Git user configuration + _, err = cmdRun.Run(ctx, exec.NewRunArgs("git", "-C", dir, "config", "user.email", "test@example.com")) + require.NoError(t, err) + _, err = cmdRun.Run(ctx, exec.NewRunArgs("git", "-C", dir, "config", "user.name", "Test User")) + require.NoError(t, err) + + // Add files and create the initial commit + _, err = cmdRun.Run(ctx, exec.NewRunArgs("git", "-C", dir, "add", ".")) + require.NoError(t, err) + _, err = cmdRun.Run(ctx, exec.NewRunArgs("git", "-C", dir, "commit", "-m", "Initial commit")) + require.NoError(t, err) + + // Cleanup: remove the directory after the test completes + t.Cleanup(func() { + err := os.RemoveAll(dir) + require.NoError(t, err) + }) +}