Skip to content

Commit

Permalink
fix git_add resource handling wrt. deleted files
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
Sebastian Hoß authored and sebhoss committed Sep 9, 2022
1 parent c94c346 commit 3960cd9
Show file tree
Hide file tree
Showing 13 changed files with 673 additions and 304 deletions.
32 changes: 17 additions & 15 deletions docs/resources/add.md
Expand Up @@ -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",
]
}
```

Expand All @@ -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.


22 changes: 13 additions & 9 deletions 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",
]
}
27 changes: 16 additions & 11 deletions internal/provider/git_worktree.go
Expand Up @@ -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) {
Expand All @@ -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
}
168 changes: 108 additions & 60 deletions internal/provider/resource_git_add.go
Expand Up @@ -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"
Expand All @@ -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{}
Expand All @@ -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.",
Expand All @@ -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
Expand Down Expand Up @@ -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...)
Expand All @@ -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
}
}
}
}
}

0 comments on commit 3960cd9

Please sign in to comment.