From 3960cd991b9b04636c5f1b406d3a3ad7c51a530e 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] 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") +}