From 7e4522a48a6314220c439d065dfc283d697cc1f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Ho=C3=9F?= Date: Fri, 9 Sep 2022 13:45:26 +0200 Subject: [PATCH 1/4] rename source file for default_value modifier --- internal/modifiers/{attribute.go => default_value.go} | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) rename internal/modifiers/{attribute.go => default_value.go} (78%) diff --git a/internal/modifiers/attribute.go b/internal/modifiers/default_value.go similarity index 78% rename from internal/modifiers/attribute.go rename to internal/modifiers/default_value.go index 4fd3070..2de26ea 100644 --- a/internal/modifiers/attribute.go +++ b/internal/modifiers/default_value.go @@ -7,7 +7,6 @@ package modifiers import ( "context" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -22,7 +21,7 @@ type defaultValueAttributePlanModifier struct { defaultValue attr.Value } -func (d *defaultValueAttributePlanModifier) Description(ctx context.Context) string { +func (d *defaultValueAttributePlanModifier) Description(_ context.Context) string { return "If the config does not contain a value, a default will be set using defaultValue." } @@ -33,9 +32,9 @@ func (d *defaultValueAttributePlanModifier) MarkdownDescription(ctx context.Cont // Modify checks that the value of the attribute in the configuration and assigns the default value if // the value in the config is null. This is a destructive operation in that it will overwrite any value // present in the plan. -func (d *defaultValueAttributePlanModifier) Modify(ctx context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) { - // If the attribute configuration is not null, we are done here +func (d *defaultValueAttributePlanModifier) Modify(_ context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) { if !req.AttributeConfig.IsNull() { + // If the attribute configuration is not null, we are done here return } From 140cc7d66954fb5d3056d3bfcd6058283c3bfbb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Ho=C3=9F?= Date: Fri, 9 Sep 2022 13:48:37 +0200 Subject: [PATCH 2/4] enable verbose output for specific terratest runs --- project.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.mk b/project.mk index 0116751..7578417 100644 --- a/project.mk +++ b/project.mk @@ -61,7 +61,7 @@ terratests: out/terratests-run-sentinel ## run all terratest tests .PHONY: terratest terratest: out/terratest-lock-sentinel ## run specific terratest tests - go test -timeout=120s -parallel=4 -tags testing -run $(filter-out $@,$(MAKECMDGOALS)) ./terratest/tests + go test -v -timeout=120s -parallel=4 -tags testing -run $(filter-out $@,$(MAKECMDGOALS)) ./terratest/tests .PHONY: tests tests: out/tests-sentinel ## run the unit tests From 44c1b674209e3f73ef50b4250178965a714c8731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Ho=C3=9F?= Date: Fri, 9 Sep 2022 13:49:29 +0200 Subject: [PATCH 3/4] add more test utilities --- internal/testutils/files.go | 6 +++++- internal/testutils/repository.go | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/internal/testutils/files.go b/internal/testutils/files.go index d2d16f0..2a9b07c 100644 --- a/internal/testutils/files.go +++ b/internal/testutils/files.go @@ -22,7 +22,11 @@ func TemporaryDirectory(t *testing.T) string { } func WriteFile(t *testing.T, name string) { - err := os.WriteFile(name, []byte("hello world!"), 0644) + WriteFileContent(t, name, "hello world!") +} + +func WriteFileContent(t *testing.T, name string, content string) { + err := os.WriteFile(name, []byte(content), 0644) if err != nil { t.Fatal(err) } diff --git a/internal/testutils/repository.go b/internal/testutils/repository.go index ad82001..1d9bf73 100644 --- a/internal/testutils/repository.go +++ b/internal/testutils/repository.go @@ -43,10 +43,14 @@ func GetRepositoryHead(t *testing.T, repository *git.Repository) *plumbing.Refer } func WriteFileInWorktree(t *testing.T, worktree *git.Worktree, name string) { - filename := filepath.Join(worktree.Filesystem.Root(), name) + filename := FileInWorktree(worktree, name) WriteFile(t, filename) } +func FileInWorktree(worktree *git.Worktree, name string) string { + return filepath.Join(worktree.Filesystem.Root(), name) +} + func AddAndCommitNewFile(t *testing.T, worktree *git.Worktree, name string) { WriteFileInWorktree(t, worktree, name) GitAdd(t, worktree, name) From 0f87fdca01cfe7c06af4b79ae0bdc629d352e232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Ho=C3=9F?= Date: Fri, 9 Sep 2022 13:56:08 +0200 Subject: [PATCH 4/4] fix git_add resource handling wrt. deleted files Using worktree.AddWithOptions does not work as documented: 1) Setting All to true always adds all added/modified files, even if Path or Glob is specified 2) Setting All to true does not add deleted files even In order to work around this, worktree.Add is called instead, and we are iterating manually over the current worktree.Status to get all files that somehow changed (including the deleted ones). --- docs/resources/add.md | 32 +- examples/resources/git_add/resource.tf | 22 +- internal/provider/git_worktree.go | 27 +- internal/provider/resource_git_add.go | 168 +++++--- internal/provider/resource_git_add_test.go | 235 ++--------- internal/provider/resource_git_tag.go | 2 +- .../resources/git_add/multiple_files/main.tf | 24 ++ .../git_add/multiple_files/outputs.tf | 18 + .../git_add/multiple_files/variables.tf | 10 + .../resources/git_add/single_file/main.tf | 25 ++ .../resources/git_add/single_file/outputs.tf | 26 ++ .../git_add/single_file/variables.tf | 14 + terratest/tests/resource_git_add_test.go | 374 ++++++++++++++++++ 13 files changed, 673 insertions(+), 304 deletions(-) create mode 100644 terratest/resources/git_add/multiple_files/main.tf create mode 100644 terratest/resources/git_add/multiple_files/outputs.tf create mode 100644 terratest/resources/git_add/multiple_files/variables.tf create mode 100644 terratest/resources/git_add/single_file/main.tf create mode 100644 terratest/resources/git_add/single_file/outputs.tf create mode 100644 terratest/resources/git_add/single_file/variables.tf create mode 100644 terratest/tests/resource_git_add_test.go diff --git a/docs/resources/add.md b/docs/resources/add.md index b71cf79..4030e27 100644 --- a/docs/resources/add.md +++ b/docs/resources/add.md @@ -3,38 +3,42 @@ page_title: "git_add Resource - terraform-provider-git" subcategory: "" description: |- - Add file contents to the index using git add. + Add file contents to the index similar to git add. --- # git_add (Resource) -Add file contents to the index using `git add`. +Add file contents to the index similar to `git add`. ## Example Usage ```terraform # add single file -resource "git_add" "file" { +resource "git_add" "single_file" { directory = "/path/to/git/repository" - exact_path = "path/to/file/in/repository" + add_paths = ["path/to/file/in/repository"] } # add all files in directory and its subdirectory recursively -resource "git_add" "directory" { +resource "git_add" "single_directory" { directory = "/path/to/git/repository" - exact_path = "path/to/directory/in/repository" + add_paths = ["path/to/directory/in/repository"] } # add files matching pattern -resource "git_add" "glob" { +resource "git_add" "glob_pattern" { directory = "/path/to/git/repository" - glob_path = "path/*/in/repo*" + add_paths = ["path/*/in/repo*"] } -# add all modified files -resource "git_add" "all" { +# mix exact paths and glob patterns +resource "git_add" "glob_pattern" { directory = "/path/to/git/repository" - all = true + add_paths = [ + "path/*/in/repo*", + "another/path/to/file/here", + "this/could/be/a/directory", + ] } ``` @@ -47,12 +51,10 @@ resource "git_add" "all" { ### Optional -- `all` (Boolean) Update the index not only where the working tree has a file matching `exact_path` or `glob_path` but also where the index already has an entry. This adds, modifies, and removes index entries to match the working tree. If no paths are given, all files in the entire working tree are updated. Defaults to `true`. -- `exact_path` (String) The exact filepath to the file or directory to be added. Conflicts with `glob_path`. -- `glob_path` (String) The glob pattern of files or directories to be added. Conflicts with `exact_path`. +- `add_paths` (List of String) The paths to add to the Git index. Values can be exact paths or glob patterns. ### Read-Only -- `id` (String) The same value as the `directory` attribute. +- `id` (Number) The timestamp of the last addition in Unix nanoseconds. diff --git a/examples/resources/git_add/resource.tf b/examples/resources/git_add/resource.tf index a73dd9d..8a03055 100644 --- a/examples/resources/git_add/resource.tf +++ b/examples/resources/git_add/resource.tf @@ -1,23 +1,27 @@ # add single file -resource "git_add" "file" { +resource "git_add" "single_file" { directory = "/path/to/git/repository" - exact_path = "path/to/file/in/repository" + add_paths = ["path/to/file/in/repository"] } # add all files in directory and its subdirectory recursively -resource "git_add" "directory" { +resource "git_add" "single_directory" { directory = "/path/to/git/repository" - exact_path = "path/to/directory/in/repository" + add_paths = ["path/to/directory/in/repository"] } # add files matching pattern -resource "git_add" "glob" { +resource "git_add" "glob_pattern" { directory = "/path/to/git/repository" - glob_path = "path/*/in/repo*" + add_paths = ["path/*/in/repo*"] } -# add all modified files -resource "git_add" "all" { +# mix exact paths and glob patterns +resource "git_add" "glob_pattern" { directory = "/path/to/git/repository" - all = true + add_paths = [ + "path/*/in/repo*", + "another/path/to/file/here", + "this/could/be/a/directory", + ] } diff --git a/internal/provider/git_worktree.go b/internal/provider/git_worktree.go index a748e6e..9d64a8b 100644 --- a/internal/provider/git_worktree.go +++ b/internal/provider/git_worktree.go @@ -6,9 +6,11 @@ package provider import ( + "context" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-log/tflog" ) func getWorktree(repository *git.Repository, diag *diag.Diagnostics) (*git.Worktree, error) { @@ -25,26 +27,29 @@ func getWorktree(repository *git.Repository, diag *diag.Diagnostics) (*git.Workt return nil, err } -func addPaths(worktree *git.Worktree, options *git.AddOptions, diag *diag.Diagnostics) error { - err := worktree.AddWithOptions(options) +func createCommit(worktree *git.Worktree, message string, options *git.CommitOptions, diag *diag.Diagnostics) *plumbing.Hash { + hash, err := worktree.Commit(message, options) if err != nil { diag.AddError( - "Cannot add paths to worktree", - "The given paths cannot be added to the worktree because of: "+err.Error(), + "Cannot create commit", + "Could not create commit because of: "+err.Error(), ) - return err + return nil } - return nil + return &hash } -func createCommit(worktree *git.Worktree, message string, options *git.CommitOptions, diag *diag.Diagnostics) *plumbing.Hash { - hash, err := worktree.Commit(message, options) +func getStatus(ctx context.Context, worktree *git.Worktree, diag *diag.Diagnostics) git.Status { + status, err := worktree.Status() if err != nil { diag.AddError( - "Cannot create commit", - "Could not create commit because of: "+err.Error(), + "Cannot read status", + "Could not read status because of: "+err.Error(), ) return nil } - return &hash + tflog.Trace(ctx, "read status", map[string]interface{}{ + "status": status.String(), + }) + return status } diff --git a/internal/provider/resource_git_add.go b/internal/provider/resource_git_add.go index b8f0421..6efe5be 100644 --- a/internal/provider/resource_git_add.go +++ b/internal/provider/resource_git_add.go @@ -8,7 +8,6 @@ package provider import ( "context" "github.com/go-git/go-git/v5" - "github.com/hashicorp/terraform-plugin-framework-validators/schemavalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" @@ -17,7 +16,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/metio/terraform-provider-git/internal/modifiers" + "path/filepath" + "time" ) type resourceGitAddType struct{} @@ -28,15 +28,13 @@ type resourceGitAdd struct { type resourceGitAddSchema struct { Directory types.String `tfsdk:"directory"` - Id types.String `tfsdk:"id"` - All types.Bool `tfsdk:"all"` - ExactPath types.String `tfsdk:"exact_path"` - GlobPath types.String `tfsdk:"glob_path"` + Id types.Int64 `tfsdk:"id"` + Paths types.List `tfsdk:"add_paths"` } func (r *resourceGitAddType) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ - MarkdownDescription: "Add file contents to the index using `git add`.", + MarkdownDescription: "Add file contents to the index similar to `git add`.", Attributes: map[string]tfsdk.Attribute{ "directory": { Description: "The path to the local Git repository.", @@ -50,45 +48,17 @@ func (r *resourceGitAddType) GetSchema(_ context.Context) (tfsdk.Schema, diag.Di }, }, "id": { - MarkdownDescription: "The same value as the `directory` attribute.", - Type: types.StringType, - Computed: true, - }, - "all": { - MarkdownDescription: "Update the index not only where the working tree has a file matching `exact_path` or `glob_path` but also where the index already has an entry. This adds, modifies, and removes index entries to match the working tree. If no paths are given, all files in the entire working tree are updated. Defaults to `true`.", - Type: types.BoolType, - Computed: true, - Optional: true, - PlanModifiers: []tfsdk.AttributePlanModifier{ - modifiers.DefaultValue(types.Bool{Value: true}), - resource.RequiresReplace(), - }, - }, - "exact_path": { - Description: "The exact filepath to the file or directory to be added. Conflicts with `glob_path`.", - Type: types.StringType, + Description: "The timestamp of the last addition in Unix nanoseconds.", + Type: types.Int64Type, Computed: true, - Optional: true, - Validators: []tfsdk.AttributeValidator{ - schemavalidator.ConflictsWith(path.MatchRoot("glob_path")), - stringvalidator.LengthAtLeast(1), - }, - PlanModifiers: []tfsdk.AttributePlanModifier{ - resource.RequiresReplace(), - }, }, - "glob_path": { - MarkdownDescription: "The glob pattern of files or directories to be added. Conflicts with `exact_path`.", - Type: types.StringType, - Computed: true, - Optional: true, - Validators: []tfsdk.AttributeValidator{ - schemavalidator.ConflictsWith(path.MatchRoot("exact_path")), - stringvalidator.LengthAtLeast(1), - }, - PlanModifiers: []tfsdk.AttributePlanModifier{ - resource.RequiresReplace(), + "add_paths": { + Description: "The paths to add to the Git index. Values can be exact paths or glob patterns.", + Type: types.ListType{ + ElemType: types.StringType, }, + Computed: true, + Optional: true, }, }, }, nil @@ -129,31 +99,44 @@ func (r *resourceGitAdd) Create(ctx context.Context, req resource.CreateRequest, return } - // NOTE: It seems default values are not working? - if inputs.All.IsNull() { - inputs.All = types.Bool{Value: true} + status := getStatus(ctx, worktree, &resp.Diagnostics) + if status == nil { + return } - options := &git.AddOptions{ - All: inputs.All.Value, - } - if !inputs.ExactPath.IsNull() { - options.Path = inputs.ExactPath.Value - } else if !inputs.GlobPath.IsNull() { - options.Glob = inputs.GlobPath.Value + paths := make([]string, len(inputs.Paths.Elems)) + resp.Diagnostics.Append(inputs.Paths.ElementsAs(ctx, &paths, false)...) + if resp.Diagnostics.HasError() { + return } - err = addPaths(worktree, options, &resp.Diagnostics) - if err != nil { - return + for _, pattern := range paths { + for file, fileStatus := range status { + if fileStatus.Worktree != git.Unmodified { + match, errMatch := filepath.Match(pattern, file) + if errMatch != nil { + resp.Diagnostics.AddError( + "Cannot match file path", + "Could not match pattern ["+pattern+"] because of: "+errMatch.Error(), + ) + } + if match { + _, errAdd := worktree.Add(file) + if errAdd != nil { + resp.Diagnostics.AddError( + "Cannot add file", + "Could not add file ["+file+"] because of: "+errAdd.Error(), + ) + } + } + } + } } var state resourceGitAddSchema state.Directory = inputs.Directory - state.Id = inputs.Directory - state.All = inputs.All - state.ExactPath = inputs.ExactPath - state.GlobPath = inputs.GlobPath + state.Id = types.Int64{Value: time.Now().UnixNano()} + state.Paths = inputs.Paths diags = resp.State.Set(ctx, &state) resp.Diagnostics.Append(diags...) @@ -176,3 +159,68 @@ func (r *resourceGitAdd) Delete(ctx context.Context, _ resource.DeleteRequest, _ tflog.Debug(ctx, "Delete git_add") // NO-OP: Terraform removes the state automatically for us } + +func (r *resourceGitAdd) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + tflog.Debug(ctx, "ModifyPlan git_add") + + if req.State.Raw.IsNull() { + // if we're creating the resource, no need to modify it + return + } + + if req.Plan.Raw.IsNull() { + // if we're deleting the resource, no need to modify it + return + } + + var inputs resourceGitAddSchema + diags := req.Config.Get(ctx, &inputs) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + directory := inputs.Directory.Value + + repository := openRepository(ctx, directory, &resp.Diagnostics) + if repository == nil { + return + } + + worktree, err := getWorktree(repository, &resp.Diagnostics) + if err != nil || worktree == nil { + return + } + + status := getStatus(ctx, worktree, &resp.Diagnostics) + if status == nil { + return + } + + paths := make([]string, len(inputs.Paths.Elems)) + resp.Diagnostics.Append(inputs.Paths.ElementsAs(ctx, &paths, false)...) + if resp.Diagnostics.HasError() { + return + } + + for _, pattern := range paths { + for key, val := range status { + if val.Worktree != git.Unmodified { + match, errMatch := filepath.Match(pattern, key) + if errMatch != nil { + resp.Diagnostics.AddError( + "Cannot match file path", + "Could not match pattern ["+pattern+"] because of: "+errMatch.Error(), + ) + return + } + if match { + id := path.Root("id") + resp.Plan.SetAttribute(ctx, id, time.Now().UnixNano()) + resp.RequiresReplace = append(resp.RequiresReplace, id) + break + } + } + } + } +} diff --git a/internal/provider/resource_git_add_test.go b/internal/provider/resource_git_add_test.go index 9d2ca64..8ec9be1 100644 --- a/internal/provider/resource_git_add_test.go +++ b/internal/provider/resource_git_add_test.go @@ -29,81 +29,20 @@ func TestResourceGitAdd(t *testing.T) { Config: fmt.Sprintf(` resource "git_add" "test" { directory = "%s" - } - `, directory), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("git_add.test", "directory", directory), - resource.TestCheckResourceAttr("git_add.test", "id", directory), - resource.TestCheckResourceAttr("git_add.test", "all", "true"), - resource.TestCheckNoResourceAttr("git_add.test", "exact_path"), - resource.TestCheckNoResourceAttr("git_add.test", "glob_path"), - ), - }, - }, - }) -} - -func TestResourceGitAdd_All_Disabled(t *testing.T) { - t.Parallel() - directory, repository := testutils.CreateRepository(t) - defer os.RemoveAll(directory) - worktree := testutils.GetRepositoryWorktree(t, repository) - name := "some-file" - testutils.WriteFileInWorktree(t, worktree, name) - - resource.UnitTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutils.ProviderFactories(), - Steps: []resource.TestStep{ - { - Config: fmt.Sprintf(` - resource "git_add" "test" { - directory = "%s" - all = "false" - } - `, directory), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("git_add.test", "directory", directory), - resource.TestCheckResourceAttr("git_add.test", "id", directory), - resource.TestCheckResourceAttr("git_add.test", "all", "false"), - resource.TestCheckNoResourceAttr("git_add.test", "exact_path"), - resource.TestCheckNoResourceAttr("git_add.test", "glob_path"), - ), - }, - }, - }) -} - -func TestResourceGitAdd_ExactPath(t *testing.T) { - t.Parallel() - directory, repository := testutils.CreateRepository(t) - defer os.RemoveAll(directory) - worktree := testutils.GetRepositoryWorktree(t, repository) - name := "some-file" - testutils.WriteFileInWorktree(t, worktree, name) - - resource.UnitTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutils.ProviderFactories(), - Steps: []resource.TestStep{ - { - Config: fmt.Sprintf(` - resource "git_add" "test" { - directory = "%s" - exact_path = "%s" + add_paths = ["%s"] } `, directory, name), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("git_add.test", "directory", directory), - resource.TestCheckResourceAttr("git_add.test", "id", directory), - resource.TestCheckResourceAttr("git_add.test", "all", "true"), - resource.TestCheckResourceAttr("git_add.test", "exact_path", name), - resource.TestCheckNoResourceAttr("git_add.test", "glob_path"), + resource.TestCheckResourceAttrWith("git_add.test", "id", testutils.CheckMinLength(1)), + resource.TestCheckResourceAttr("git_add.test", "add_paths.0", name), ), }, }, }) } -func TestResourceGitAdd_GlobPath(t *testing.T) { +func TestResourceGitAdd_AddPaths_Multiple(t *testing.T) { t.Parallel() directory, repository := testutils.CreateRepository(t) defer os.RemoveAll(directory) @@ -118,22 +57,21 @@ func TestResourceGitAdd_GlobPath(t *testing.T) { Config: fmt.Sprintf(` resource "git_add" "test" { directory = "%s" - glob_path = "some*" + add_paths = ["%s", "other-file"] } - `, directory), + `, directory, name), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("git_add.test", "directory", directory), - resource.TestCheckResourceAttr("git_add.test", "id", directory), - resource.TestCheckResourceAttr("git_add.test", "all", "true"), - resource.TestCheckNoResourceAttr("git_add.test", "exact_path"), - resource.TestCheckResourceAttr("git_add.test", "glob_path", "some*"), + resource.TestCheckResourceAttrWith("git_add.test", "id", testutils.CheckMinLength(1)), + resource.TestCheckResourceAttr("git_add.test", "add_paths.0", name), + resource.TestCheckResourceAttr("git_add.test", "add_paths.1", "other-file"), ), }, }, }) } -func TestResourceGitAdd_ExactPath_NonExistingFile(t *testing.T) { +func TestResourceGitAdd_AddPaths_NonExistingFile(t *testing.T) { t.Parallel() directory, _ := testutils.CreateRepository(t) defer os.RemoveAll(directory) @@ -146,22 +84,20 @@ func TestResourceGitAdd_ExactPath_NonExistingFile(t *testing.T) { Config: fmt.Sprintf(` resource "git_add" "test" { directory = "%s" - exact_path = "%s" + add_paths = ["%s"] } `, directory, name), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("git_add.test", "directory", directory), - resource.TestCheckResourceAttr("git_add.test", "id", directory), - resource.TestCheckResourceAttr("git_add.test", "all", "true"), - resource.TestCheckResourceAttr("git_add.test", "exact_path", name), - resource.TestCheckNoResourceAttr("git_add.test", "glob_path"), + resource.TestCheckResourceAttrWith("git_add.test", "id", testutils.CheckMinLength(1)), + resource.TestCheckResourceAttr("git_add.test", "add_paths.0", name), ), }, }, }) } -func TestResourceGitAdd_ExactPath_Directory(t *testing.T) { +func TestResourceGitAdd_AddPaths_Directory(t *testing.T) { t.Parallel() directory, _ := testutils.CreateRepository(t) defer os.RemoveAll(directory) @@ -177,85 +113,19 @@ func TestResourceGitAdd_ExactPath_Directory(t *testing.T) { Config: fmt.Sprintf(` resource "git_add" "test" { directory = "%s" - exact_path = "%s" + add_paths = ["%s"] } `, directory, "nested-folder"), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("git_add.test", "directory", directory), - resource.TestCheckResourceAttr("git_add.test", "id", directory), - resource.TestCheckResourceAttr("git_add.test", "all", "true"), - resource.TestCheckResourceAttr("git_add.test", "exact_path", "nested-folder"), - resource.TestCheckNoResourceAttr("git_add.test", "glob_path"), + resource.TestCheckResourceAttrWith("git_add.test", "id", testutils.CheckMinLength(1)), + resource.TestCheckResourceAttr("git_add.test", "add_paths.0", "nested-folder"), ), }, }, }) } -func TestResourceGitAdd_ExactPath_GlobPath(t *testing.T) { - t.Parallel() - directory, _ := testutils.CreateRepository(t) - defer os.RemoveAll(directory) - - resource.UnitTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutils.ProviderFactories(), - Steps: []resource.TestStep{ - { - Config: fmt.Sprintf(` - resource "git_add" "test" { - directory = "%s" - exact_path = "some-file" - glob_path = "some*" - } - `, directory), - ExpectError: regexp.MustCompile(`Invalid Attribute Combination`), - }, - }, - }) -} - -func TestResourceGitAdd_ExactPath_EmptyString(t *testing.T) { - t.Parallel() - directory, _ := testutils.CreateRepository(t) - defer os.RemoveAll(directory) - - resource.UnitTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutils.ProviderFactories(), - Steps: []resource.TestStep{ - { - Config: fmt.Sprintf(` - resource "git_add" "test" { - directory = "%s" - exact_path = "" - } - `, directory), - ExpectError: regexp.MustCompile(`Invalid Attribute Value Length`), - }, - }, - }) -} - -func TestResourceGitAdd_GlobPath_EmptyString(t *testing.T) { - t.Parallel() - directory, _ := testutils.CreateRepository(t) - defer os.RemoveAll(directory) - - resource.UnitTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutils.ProviderFactories(), - Steps: []resource.TestStep{ - { - Config: fmt.Sprintf(` - resource "git_add" "test" { - directory = "%s" - glob_path = "" - } - `, directory), - ExpectError: regexp.MustCompile(`Invalid Attribute Value Length`), - }, - }, - }) -} - func TestResourceGitAdd_BareRepository(t *testing.T) { t.Parallel() directory := testutils.CreateBareRepository(t) @@ -269,7 +139,7 @@ func TestResourceGitAdd_BareRepository(t *testing.T) { Config: fmt.Sprintf(` resource "git_add" "test" { directory = "%s" - exact_path = "%s" + add_paths = ["%s"] } `, directory, name), ExpectError: regexp.MustCompile(`Cannot add file to bare repository`), @@ -289,7 +159,7 @@ func TestResourceGitAdd_Directory_Invalid(t *testing.T) { Config: fmt.Sprintf(` resource "git_add" "test" { directory = "/some/random/path" - exact_path = "%s" + add_paths = ["%s"] } `, name), ExpectError: regexp.MustCompile(`Cannot open repository`), @@ -308,7 +178,7 @@ func TestResourceGitAdd_Directory_Missing(t *testing.T) { { Config: fmt.Sprintf(` resource "git_add" "test" { - exact_path = "%s" + add_paths = ["%s"] } `, name), ExpectError: regexp.MustCompile(`Missing required argument`), @@ -317,7 +187,7 @@ func TestResourceGitAdd_Directory_Missing(t *testing.T) { }) } -func TestResourceGitAdd_ExactPath_Update(t *testing.T) { +func TestResourceGitAdd_AddPaths_Update(t *testing.T) { t.Parallel() directory, repository := testutils.CreateRepository(t) defer os.RemoveAll(directory) @@ -334,77 +204,26 @@ func TestResourceGitAdd_ExactPath_Update(t *testing.T) { Config: fmt.Sprintf(` resource "git_add" "test" { directory = "%s" - exact_path = "%s" + add_paths = ["%s"] } `, directory, name1), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("git_add.test", "directory", directory), - resource.TestCheckResourceAttr("git_add.test", "id", directory), - resource.TestCheckResourceAttr("git_add.test", "all", "true"), - resource.TestCheckResourceAttr("git_add.test", "exact_path", name1), - resource.TestCheckNoResourceAttr("git_add.test", "glob_path"), + resource.TestCheckResourceAttrWith("git_add.test", "id", testutils.CheckMinLength(1)), + resource.TestCheckResourceAttr("git_add.test", "add_paths.0", name1), ), }, { Config: fmt.Sprintf(` resource "git_add" "test" { directory = "%s" - exact_path = "%s" + add_paths = ["%s"] } `, directory, name2), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("git_add.test", "directory", directory), - resource.TestCheckResourceAttr("git_add.test", "id", directory), - resource.TestCheckResourceAttr("git_add.test", "all", "true"), - resource.TestCheckResourceAttr("git_add.test", "exact_path", name2), - resource.TestCheckNoResourceAttr("git_add.test", "glob_path"), - ), - }, - }, - }) -} - -func TestResourceGitAdd_GlobPath_Update(t *testing.T) { - t.Parallel() - directory, repository := testutils.CreateRepository(t) - defer os.RemoveAll(directory) - worktree := testutils.GetRepositoryWorktree(t, repository) - name1 := "some-file" - name2 := "other-file" - testutils.WriteFileInWorktree(t, worktree, name1) - testutils.WriteFileInWorktree(t, worktree, name2) - - resource.UnitTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutils.ProviderFactories(), - Steps: []resource.TestStep{ - { - Config: fmt.Sprintf(` - resource "git_add" "test" { - directory = "%s" - glob_path = "some*" - } - `, directory), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("git_add.test", "directory", directory), - resource.TestCheckResourceAttr("git_add.test", "id", directory), - resource.TestCheckResourceAttr("git_add.test", "all", "true"), - resource.TestCheckNoResourceAttr("git_add.test", "exact_path"), - resource.TestCheckResourceAttr("git_add.test", "glob_path", "some*"), - ), - }, - { - Config: fmt.Sprintf(` - resource "git_add" "test" { - directory = "%s" - glob_path = "other*" - } - `, directory), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("git_add.test", "directory", directory), - resource.TestCheckResourceAttr("git_add.test", "id", directory), - resource.TestCheckResourceAttr("git_add.test", "all", "true"), - resource.TestCheckNoResourceAttr("git_add.test", "exact_path"), - resource.TestCheckResourceAttr("git_add.test", "glob_path", "other*"), + resource.TestCheckResourceAttrWith("git_add.test", "id", testutils.CheckMinLength(1)), + resource.TestCheckResourceAttr("git_add.test", "add_paths.0", name2), ), }, }, diff --git a/internal/provider/resource_git_tag.go b/internal/provider/resource_git_tag.go index b1753b2..e50b1aa 100644 --- a/internal/provider/resource_git_tag.go +++ b/internal/provider/resource_git_tag.go @@ -204,7 +204,7 @@ func (r *resourceGitTag) Read(ctx context.Context, req resource.ReadRequest, res } } -func (r *resourceGitTag) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { +func (r *resourceGitTag) Update(ctx context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { tflog.Debug(ctx, "Update git_tag") // NO-OP: all attributes require replace, thus Delete and Create methods will be called } diff --git a/terratest/resources/git_add/multiple_files/main.tf b/terratest/resources/git_add/multiple_files/main.tf new file mode 100644 index 0000000..15039ba --- /dev/null +++ b/terratest/resources/git_add/multiple_files/main.tf @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: The terraform-provider-git Authors +# SPDX-License-Identifier: 0BSD + +terraform { + required_providers { + git = { + source = "localhost/metio/git" + version = "9999.99.99" + } + } +} + +provider "git" { + # Configuration options +} + +resource "git_add" "add" { + directory = var.directory + add_paths = var.add_paths +} + +data "git_statuses" "multiple_files" { + directory = git_add.add.directory +} diff --git a/terratest/resources/git_add/multiple_files/outputs.tf b/terratest/resources/git_add/multiple_files/outputs.tf new file mode 100644 index 0000000..103edf3 --- /dev/null +++ b/terratest/resources/git_add/multiple_files/outputs.tf @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: The terraform-provider-git Authors +# SPDX-License-Identifier: 0BSD + +output "directory" { + value = git_add.add.directory +} + +output "id" { + value = git_add.add.id +} + +output "add_paths" { + value = git_add.add.add_paths +} + +output "files" { + value = data.git_statuses.multiple_files.files +} diff --git a/terratest/resources/git_add/multiple_files/variables.tf b/terratest/resources/git_add/multiple_files/variables.tf new file mode 100644 index 0000000..cdf9b3f --- /dev/null +++ b/terratest/resources/git_add/multiple_files/variables.tf @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: The terraform-provider-git Authors +# SPDX-License-Identifier: 0BSD + +variable "directory" { + type = string +} + +variable "add_paths" { + type = list(string) +} diff --git a/terratest/resources/git_add/single_file/main.tf b/terratest/resources/git_add/single_file/main.tf new file mode 100644 index 0000000..9133212 --- /dev/null +++ b/terratest/resources/git_add/single_file/main.tf @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: The terraform-provider-git Authors +# SPDX-License-Identifier: 0BSD + +terraform { + required_providers { + git = { + source = "localhost/metio/git" + version = "9999.99.99" + } + } +} + +provider "git" { + # Configuration options +} + +resource "git_add" "add" { + directory = var.directory + add_paths = var.add_paths +} + +data "git_status" "single_file" { + directory = git_add.add.directory + file = var.file +} diff --git a/terratest/resources/git_add/single_file/outputs.tf b/terratest/resources/git_add/single_file/outputs.tf new file mode 100644 index 0000000..17daf8b --- /dev/null +++ b/terratest/resources/git_add/single_file/outputs.tf @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: The terraform-provider-git Authors +# SPDX-License-Identifier: 0BSD + +output "directory" { + value = git_add.add.directory +} + +output "id" { + value = git_add.add.id +} + +output "add_paths" { + value = git_add.add.add_paths +} + +output "file" { + value = data.git_status.single_file.file +} + +output "staging" { + value = data.git_status.single_file.staging +} + +output "worktree" { + value = data.git_status.single_file.worktree +} diff --git a/terratest/resources/git_add/single_file/variables.tf b/terratest/resources/git_add/single_file/variables.tf new file mode 100644 index 0000000..1136204 --- /dev/null +++ b/terratest/resources/git_add/single_file/variables.tf @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: The terraform-provider-git Authors +# SPDX-License-Identifier: 0BSD + +variable "directory" { + type = string +} + +variable "add_paths" { + type = list(string) +} + +variable "file" { + type = string +} diff --git a/terratest/tests/resource_git_add_test.go b/terratest/tests/resource_git_add_test.go new file mode 100644 index 0000000..d79f02b --- /dev/null +++ b/terratest/tests/resource_git_add_test.go @@ -0,0 +1,374 @@ +/* + * SPDX-FileCopyrightText: The terraform-provider-git Authors + * SPDX-License-Identifier: 0BSD + */ + +package provider_test + +import ( + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/metio/terraform-provider-git/internal/testutils" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +func TestResourceGitAdd_SingleFile_Exact_WriteOnce(t *testing.T) { + directory, repository := testutils.CreateRepository(t) + defer os.RemoveAll(directory) + worktree := testutils.GetRepositoryWorktree(t, repository) + filename := "some-file" + testutils.WriteFileInWorktree(t, worktree, filename) + + terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: "../resources/git_add/single_file", + Vars: map[string]interface{}{ + "directory": directory, + "add_paths": []string{filename}, + "file": filename, + }, + NoColor: true, + }) + + defer terraform.Destroy(t, terraformOptions) + terraform.InitAndApplyAndIdempotent(t, terraformOptions) + + actualStaging := terraform.Output(t, terraformOptions, "staging") + actualWorktree := terraform.Output(t, terraformOptions, "worktree") + + assert.Equal(t, "A", actualStaging, "actualStaging") + assert.Equal(t, " ", actualWorktree, "actualWorktree") +} + +func TestResourceGitAdd_SingleFile_Glob_WriteOnce(t *testing.T) { + directory, repository := testutils.CreateRepository(t) + defer os.RemoveAll(directory) + worktree := testutils.GetRepositoryWorktree(t, repository) + filename := "some-file" + testutils.WriteFileInWorktree(t, worktree, filename) + + terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: "../resources/git_add/single_file", + Vars: map[string]interface{}{ + "directory": directory, + "add_paths": []string{"some*"}, + "file": filename, + }, + NoColor: true, + }) + + defer terraform.Destroy(t, terraformOptions) + terraform.InitAndApplyAndIdempotent(t, terraformOptions) + + actualStaging := terraform.Output(t, terraformOptions, "staging") + actualWorktree := terraform.Output(t, terraformOptions, "worktree") + + assert.Equal(t, "A", actualStaging, "actualStaging") + assert.Equal(t, " ", actualWorktree, "actualWorktree") +} + +func TestResourceGitAdd_SingleFile_Exact_NoMatch(t *testing.T) { + directory, repository := testutils.CreateRepository(t) + defer os.RemoveAll(directory) + worktree := testutils.GetRepositoryWorktree(t, repository) + filename := "some-file" + testutils.WriteFileInWorktree(t, worktree, filename) + + terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: "../resources/git_add/single_file", + Vars: map[string]interface{}{ + "directory": directory, + "add_paths": []string{"other-file"}, + "file": filename, + }, + NoColor: true, + }) + + defer terraform.Destroy(t, terraformOptions) + terraform.InitAndApplyAndIdempotent(t, terraformOptions) + + actualStaging := terraform.Output(t, terraformOptions, "staging") + actualWorktree := terraform.Output(t, terraformOptions, "worktree") + + assert.Equal(t, "?", actualStaging, "actualStaging") + assert.Equal(t, "?", actualWorktree, "actualWorktree") +} + +func TestResourceGitAdd_SingleFile_Glob_NoMatch(t *testing.T) { + directory, repository := testutils.CreateRepository(t) + defer os.RemoveAll(directory) + worktree := testutils.GetRepositoryWorktree(t, repository) + filename := "some-file" + testutils.WriteFileInWorktree(t, worktree, filename) + + terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: "../resources/git_add/single_file", + Vars: map[string]interface{}{ + "directory": directory, + "add_paths": []string{"other*"}, + "file": filename, + }, + NoColor: true, + }) + + defer terraform.Destroy(t, terraformOptions) + terraform.InitAndApplyAndIdempotent(t, terraformOptions) + + actualStaging := terraform.Output(t, terraformOptions, "staging") + actualWorktree := terraform.Output(t, terraformOptions, "worktree") + + assert.Equal(t, "?", actualStaging, "actualStaging") + assert.Equal(t, "?", actualWorktree, "actualWorktree") +} + +func TestResourceGitAdd_SingleFile_Exact_WriteMultiple(t *testing.T) { + directory, repository := testutils.CreateRepository(t) + defer os.RemoveAll(directory) + worktree := testutils.GetRepositoryWorktree(t, repository) + filename := "some-file" + testutils.WriteFileInWorktree(t, worktree, filename) + + terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: "../resources/git_add/single_file", + Vars: map[string]interface{}{ + "directory": directory, + "add_paths": []string{filename}, + "file": filename, + }, + NoColor: true, + }) + + defer terraform.Destroy(t, terraformOptions) + terraform.InitAndApplyAndIdempotent(t, terraformOptions) + + actualStaging := terraform.Output(t, terraformOptions, "staging") + actualWorktree := terraform.Output(t, terraformOptions, "worktree") + + assert.Equal(t, "A", actualStaging, "actualStaging") + assert.Equal(t, " ", actualWorktree, "actualWorktree") + + // Modify file in order to trigger new git-add call + testutils.WriteFileContent(t, testutils.FileInWorktree(worktree, filename), "new content") + terraform.ApplyAndIdempotent(t, terraformOptions) + + actualStaging2 := terraform.Output(t, terraformOptions, "staging") + actualWorktree2 := terraform.Output(t, terraformOptions, "worktree") + + assert.Equal(t, "A", actualStaging2, "actualStaging2") + assert.Equal(t, " ", actualWorktree2, "actualWorktree2") +} + +func TestResourceGitAdd_SingleFile_Glob_WriteMultiple(t *testing.T) { + directory, repository := testutils.CreateRepository(t) + defer os.RemoveAll(directory) + worktree := testutils.GetRepositoryWorktree(t, repository) + filename := "some-file" + testutils.WriteFileInWorktree(t, worktree, filename) + + terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: "../resources/git_add/single_file", + Vars: map[string]interface{}{ + "directory": directory, + "add_paths": []string{"some*"}, + "file": filename, + }, + NoColor: true, + }) + + defer terraform.Destroy(t, terraformOptions) + terraform.InitAndApplyAndIdempotent(t, terraformOptions) + + actualStaging := terraform.Output(t, terraformOptions, "staging") + actualWorktree := terraform.Output(t, terraformOptions, "worktree") + + assert.Equal(t, "A", actualStaging, "actualStaging") + assert.Equal(t, " ", actualWorktree, "actualWorktree") + + // Modify file in order to trigger new git-add call + testutils.WriteFileContent(t, testutils.FileInWorktree(worktree, filename), "new content") + terraform.ApplyAndIdempotent(t, terraformOptions) + + actualStaging2 := terraform.Output(t, terraformOptions, "staging") + actualWorktree2 := terraform.Output(t, terraformOptions, "worktree") + + assert.Equal(t, "A", actualStaging2, "actualStaging2") + assert.Equal(t, " ", actualWorktree2, "actualWorktree2") +} + +func TestResourceGitAdd_SingleFile_Exact_WriteDelete(t *testing.T) { + directory, repository := testutils.CreateRepository(t) + defer os.RemoveAll(directory) + worktree := testutils.GetRepositoryWorktree(t, repository) + filename := "some-file" + testutils.WriteFileInWorktree(t, worktree, filename) + + terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: "../resources/git_add/single_file", + Vars: map[string]interface{}{ + "directory": directory, + "add_paths": []string{filename}, + "file": filename, + }, + NoColor: true, + }) + + defer terraform.Destroy(t, terraformOptions) + terraform.InitAndApplyAndIdempotent(t, terraformOptions) + + actualStaging := terraform.Output(t, terraformOptions, "staging") + actualWorktree := terraform.Output(t, terraformOptions, "worktree") + + assert.Equal(t, "A", actualStaging, "actualStaging") + assert.Equal(t, " ", actualWorktree, "actualWorktree") + + os.Remove(testutils.FileInWorktree(worktree, filename)) + terraform.ApplyAndIdempotent(t, terraformOptions) + + actualStaging2 := terraform.Output(t, terraformOptions, "staging") + actualWorktree2 := terraform.Output(t, terraformOptions, "worktree") + + assert.Equal(t, "?", actualStaging2, "actualStaging2") + assert.Equal(t, "?", actualWorktree2, "actualWorktree2") +} + +func TestResourceGitAdd_SingleFile_Glob_WriteDelete(t *testing.T) { + directory, repository := testutils.CreateRepository(t) + defer os.RemoveAll(directory) + worktree := testutils.GetRepositoryWorktree(t, repository) + filename := "some-file" + testutils.WriteFileInWorktree(t, worktree, filename) + + terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: "../resources/git_add/single_file", + Vars: map[string]interface{}{ + "directory": directory, + "add_paths": []string{"some*"}, + "file": filename, + }, + NoColor: true, + }) + + defer terraform.Destroy(t, terraformOptions) + terraform.InitAndApplyAndIdempotent(t, terraformOptions) + + actualStaging := terraform.Output(t, terraformOptions, "staging") + actualWorktree := terraform.Output(t, terraformOptions, "worktree") + + assert.Equal(t, "A", actualStaging, "actualStaging") + assert.Equal(t, " ", actualWorktree, "actualWorktree") + + os.Remove(testutils.FileInWorktree(worktree, filename)) + terraform.ApplyAndIdempotent(t, terraformOptions) + + actualStaging2 := terraform.Output(t, terraformOptions, "staging") + actualWorktree2 := terraform.Output(t, terraformOptions, "worktree") + + assert.Equal(t, "?", actualStaging2, "actualStaging2") + assert.Equal(t, "?", actualWorktree2, "actualWorktree2") +} + +func TestResourceGitAdd_SingleFile_Exact_DeleteCommitted(t *testing.T) { + directory, repository := testutils.CreateRepository(t) + defer os.RemoveAll(directory) + worktree := testutils.GetRepositoryWorktree(t, repository) + filename := "some-file" + testutils.AddAndCommitNewFile(t, worktree, filename) + + terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: "../resources/git_add/single_file", + Vars: map[string]interface{}{ + "directory": directory, + "add_paths": []string{filename}, + "file": filename, + }, + NoColor: true, + }) + + defer terraform.Destroy(t, terraformOptions) + terraform.InitAndApplyAndIdempotent(t, terraformOptions) + + actualStaging := terraform.Output(t, terraformOptions, "staging") + actualWorktree := terraform.Output(t, terraformOptions, "worktree") + + assert.Equal(t, "?", actualStaging, "actualStaging") + assert.Equal(t, "?", actualWorktree, "actualWorktree") + + os.Remove(testutils.FileInWorktree(worktree, filename)) + terraform.ApplyAndIdempotent(t, terraformOptions) + + actualStaging2 := terraform.Output(t, terraformOptions, "staging") + actualWorktree2 := terraform.Output(t, terraformOptions, "worktree") + + assert.Equal(t, "D", actualStaging2, "actualStaging2") + assert.Equal(t, " ", actualWorktree2, "actualWorktree2") +} + +func TestResourceGitAdd_SingleFile_Glob_DeleteCommitted(t *testing.T) { + directory, repository := testutils.CreateRepository(t) + defer os.RemoveAll(directory) + worktree := testutils.GetRepositoryWorktree(t, repository) + filename := "some-file" + testutils.AddAndCommitNewFile(t, worktree, filename) + + terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: "../resources/git_add/single_file", + Vars: map[string]interface{}{ + "directory": directory, + "add_paths": []string{"some*"}, + "file": filename, + }, + NoColor: true, + }) + + defer terraform.Destroy(t, terraformOptions) + terraform.InitAndApplyAndIdempotent(t, terraformOptions) + + actualStaging := terraform.Output(t, terraformOptions, "staging") + actualWorktree := terraform.Output(t, terraformOptions, "worktree") + + assert.Equal(t, "?", actualStaging, "actualStaging") + assert.Equal(t, "?", actualWorktree, "actualWorktree") + + os.Remove(testutils.FileInWorktree(worktree, filename)) + terraform.ApplyAndIdempotent(t, terraformOptions) + + actualStaging2 := terraform.Output(t, terraformOptions, "staging") + actualWorktree2 := terraform.Output(t, terraformOptions, "worktree") + + assert.Equal(t, "D", actualStaging2, "actualStaging2") + assert.Equal(t, " ", actualWorktree2, "actualWorktree2") +} + +func TestResourceGitAdd_MultipleFiles_WriteOnce(t *testing.T) { + directory, repository := testutils.CreateRepository(t) + defer os.RemoveAll(directory) + worktree := testutils.GetRepositoryWorktree(t, repository) + filename := "some-file" + testutils.WriteFileInWorktree(t, worktree, filename) + testutils.WriteFileInWorktree(t, worktree, "other-file") + + terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: "../resources/git_add/multiple_files", + Vars: map[string]interface{}{ + "directory": directory, + "add_paths": []string{filename}, + }, + NoColor: true, + }) + + defer terraform.Destroy(t, terraformOptions) + terraform.InitAndApplyAndIdempotent(t, terraformOptions) + + actualFiles := terraform.OutputMapOfObjects(t, terraformOptions, "files") + + assert.NotNil(t, actualFiles[filename], filename) + assert.NotNil(t, actualFiles["other-file"], filename) + + stagedFile := actualFiles[filename].(map[string]interface{}) + unStagedFile := actualFiles["other-file"].(map[string]interface{}) + + assert.Equal(t, "A", stagedFile["staging"], "stagedFile-staging") + assert.Equal(t, " ", stagedFile["worktree"], "stagedFile-worktree") + assert.Equal(t, "?", unStagedFile["staging"], "unStagedFile-staging") + assert.Equal(t, "?", unStagedFile["worktree"], "unStagedFile-worktree") +}