summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Vink <mike1994vink@gmail.com>2023-02-05 19:13:23 +0100
committerMike Vink <mike1994vink@gmail.com>2023-02-28 10:14:28 +0100
commitafb357c9295f969765d7e4c76fbde2bb27224c15 (patch)
treeb9b0ab8b2a67e9cec46ad82a53f776e04d3de2bb
parent6e180aae16b3e04cedcf0b5dd33a779072954e63 (diff)
feat: resource_azuredevops_git_repository_branch
-rw-r--r--azuredevops/internal/acceptancetests/resource_git_repository_branch_test.go225
-rw-r--r--azuredevops/internal/acceptancetests/resource_git_repository_file_test.go3
-rw-r--r--azuredevops/internal/service/git/resource_git_repository_branch.go285
-rw-r--r--azuredevops/internal/service/git/resource_git_repository_branch_test.go293
-rw-r--r--azuredevops/internal/service/git/resource_git_repository_test.go6
-rw-r--r--azuredevops/internal/utils/tfhelper/tfhelper.go17
-rw-r--r--azuredevops/provider.go1
-rw-r--r--azuredevops/provider_test.go1
-rw-r--r--website/docs/r/git_repository_branch.html.markdown78
9 files changed, 901 insertions, 8 deletions
diff --git a/azuredevops/internal/acceptancetests/resource_git_repository_branch_test.go b/azuredevops/internal/acceptancetests/resource_git_repository_branch_test.go
new file mode 100644
index 00000000..4ca47695
--- /dev/null
+++ b/azuredevops/internal/acceptancetests/resource_git_repository_branch_test.go
@@ -0,0 +1,225 @@
+//go:build (all || core || resource_git_repository_branch) && !exclude_resource_git_repository_branch
+// +build all core resource_git_repository_branch
+// +build !exclude_resource_git_repository_branch
+
+package acceptancetests
+
+import (
+ "fmt"
+ "regexp"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+ "github.com/microsoft/azure-devops-go-api/azuredevops/v6/git"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/acceptancetests/testutils"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/client"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils/tfhelper"
+)
+
+// TestAccGitRepoBranch_CreateUpdateDelete verifies that a branch can
+// be added to a repository and that it can be replaced
+func TestAccGitRepoBranch_CreateAndUpdate(t *testing.T) {
+ var gotBranch git.GitBranchStats
+ var gotBranch2 git.GitBranchStats
+ var gotBranch3 git.GitBranchStats
+ projectName := testutils.GenerateResourceName()
+ gitRepoName := testutils.GenerateResourceName()
+ branchName := testutils.GenerateResourceName()
+ branchNameChanged := testutils.GenerateResourceName()
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testutils.PreCheck(t, nil) },
+ Providers: testutils.GetProviders(),
+ Steps: []resource.TestStep{
+ {
+ Config: hclGitRepoBranches(projectName, gitRepoName, "Uninitialized", branchName),
+ Check: resource.ComposeTestCheckFunc(
+ testAccGitRepoBranchExists("foo_orphan", &gotBranch),
+ testAccGitRepoBranchExists("foo_from_ref", &gotBranch2),
+ testAccGitRepoBranchExists("foo_from_sha", &gotBranch3),
+ testAccGitRepoBranchAttributes("foo_orphan", &gotBranch, &testAccGitRepoBranchExpectedAttributes{
+ Name: fmt.Sprintf("testbranch-%s", branchName),
+ }, &testAccGitRepoBranchExpectedStateAttrs{
+ source_ref: "",
+ source_sha: false,
+ is_default_branch: true,
+ ref: fmt.Sprintf("refs/heads/testbranch-%s", branchName),
+ sha: true,
+ }),
+ testAccGitRepoBranchAttributes("foo_from_ref", &gotBranch2, &testAccGitRepoBranchExpectedAttributes{
+ Name: fmt.Sprintf("testbranch2-%s", branchName),
+ }, &testAccGitRepoBranchExpectedStateAttrs{
+ source_ref: fmt.Sprintf("refs/heads/testbranch-%s", branchName),
+ source_sha: true,
+ ref: fmt.Sprintf("refs/heads/testbranch2-%s", branchName),
+ sha: true,
+ }),
+ testAccGitRepoBranchAttributes("foo_from_sha", &gotBranch3, &testAccGitRepoBranchExpectedAttributes{
+ Name: fmt.Sprintf("testbranch3-%s", branchName),
+ }, &testAccGitRepoBranchExpectedStateAttrs{
+ source_ref: "",
+ source_sha: true,
+ ref: fmt.Sprintf("refs/heads/testbranch3-%s", branchName),
+ sha: true,
+ }),
+ ),
+ },
+ // Test import branch created from ref
+ {
+ ResourceName: "azuredevops_git_repository_branch.foo_from_ref",
+ ImportState: true,
+ ImportStateVerify: true,
+ ImportStateVerifyIgnore: []string{"source_ref", "source_sha"},
+ },
+ // Test replace/update branch when name changes
+ {
+ Config: hclGitRepoBranches(projectName, gitRepoName, "Uninitialized", branchNameChanged),
+ Check: resource.ComposeTestCheckFunc(
+ testAccGitRepoBranchExists("foo_orphan", &gotBranch),
+ testAccGitRepoBranchExists("foo_from_ref", &gotBranch2),
+ testAccGitRepoBranchExists("foo_from_sha", &gotBranch3),
+ testAccGitRepoBranchAttributes("foo_orphan", &gotBranch, &testAccGitRepoBranchExpectedAttributes{
+ Name: fmt.Sprintf("testbranch-%s", branchNameChanged),
+ }, &testAccGitRepoBranchExpectedStateAttrs{
+ source_ref: "",
+ source_sha: false,
+ is_default_branch: true,
+ ref: fmt.Sprintf("refs/heads/testbranch-%s", branchNameChanged),
+ sha: true,
+ }),
+ testAccGitRepoBranchAttributes("foo_from_ref", &gotBranch2, &testAccGitRepoBranchExpectedAttributes{
+ Name: fmt.Sprintf("testbranch2-%s", branchNameChanged),
+ }, &testAccGitRepoBranchExpectedStateAttrs{
+ source_ref: fmt.Sprintf("refs/heads/testbranch-%s", branchNameChanged),
+ source_sha: true,
+ ref: fmt.Sprintf("refs/heads/testbranch2-%s", branchNameChanged),
+ sha: true,
+ }),
+ testAccGitRepoBranchAttributes("foo_from_sha", &gotBranch3, &testAccGitRepoBranchExpectedAttributes{
+ Name: fmt.Sprintf("testbranch3-%s", branchNameChanged),
+ }, &testAccGitRepoBranchExpectedStateAttrs{
+ source_ref: "",
+ source_sha: true,
+ ref: fmt.Sprintf("refs/heads/testbranch3-%s", branchNameChanged),
+ sha: true,
+ }),
+ ),
+ },
+ // Test invalid ref
+ {
+ Config: fmt.Sprintf(`
+%s
+
+resource "azuredevops_git_repository_branch" "foo_nonexistent_tag" {
+ repository_id = azuredevops_git_repository.repository.id
+ name = "testbranch2-non-existent-tag"
+ source_ref = "refs/tags/non-existent"
+}
+`, hclGitRepoBranches(projectName, gitRepoName, "Clean", branchNameChanged)),
+ ExpectError: regexp.MustCompile(`No refs found that match source_ref "refs/tags/non-existent"`),
+ },
+ },
+ },
+ )
+}
+
+func testAccGitRepoBranchAttributes(node string, branch *git.GitBranchStats, want *testAccGitRepoBranchExpectedAttributes, wantState *testAccGitRepoBranchExpectedStateAttrs) resource.TestCheckFunc {
+ return func(s *terraform.State) error {
+ if *branch.Name != want.Name {
+ return fmt.Errorf("Error got name %s, want %s", *branch.Name, want.Name)
+ }
+
+ rs, ok := s.RootModule().Resources[fmt.Sprintf("azuredevops_git_repository_branch.%s", node)]
+ if !ok {
+ return fmt.Errorf("Not found: %s", node)
+ }
+
+ sourceRef := rs.Primary.Attributes["source_ref"]
+ if wantState.source_ref != sourceRef {
+ return fmt.Errorf("azuredevops_git_repository_branch.%s.source_ref = %s, want %s", node, sourceRef, wantState.source_ref)
+ }
+
+ sourceSha := rs.Primary.Attributes["source_sha"]
+ if wantState.source_sha && sourceSha == "" {
+ return fmt.Errorf("azuredevops_git_repository_branch.%s.source_sha is not set", node)
+ }
+
+ isDefaultBranch := rs.Primary.Attributes["is_default_branch"]
+ if wantState.is_default_branch && isDefaultBranch != "true" {
+ return fmt.Errorf("azuredevops_git_repository_branch.%s.is_default_branch = %s, want %v", node, isDefaultBranch, wantState.is_default_branch)
+ }
+
+ ref := rs.Primary.Attributes["ref"]
+ if wantState.ref != ref {
+ return fmt.Errorf("azuredevops_git_repository_branch.%s.ref = %s, want %s", node, ref, wantState.ref)
+ }
+
+ sha := rs.Primary.Attributes["sha"]
+ if wantState.sha && sha == "" {
+ return fmt.Errorf("azuredevops_git_repository_branch.%s.ref = %s, want %s", node, ref, wantState.ref)
+ }
+
+ return nil
+ }
+}
+
+func testAccGitRepoBranchExists(node string, gotBranch *git.GitBranchStats) resource.TestCheckFunc {
+ return func(s *terraform.State) error {
+ rs, ok := s.RootModule().Resources[fmt.Sprintf("azuredevops_git_repository_branch.%s", node)]
+ if !ok {
+ return fmt.Errorf("Not found: %s", node)
+ }
+
+ repoID, branchName, err := tfhelper.ParseGitRepoBranchID(rs.Primary.ID)
+ if err != nil {
+ return fmt.Errorf("Error in parsing branch ID: %w", err)
+ }
+
+ clients := testutils.GetProvider().Meta().(*client.AggregatedClient)
+ branch, err := clients.GitReposClient.GetBranch(clients.Ctx, git.GetBranchArgs{
+ RepositoryId: &repoID,
+ Name: &branchName,
+ })
+ if err != nil {
+ return err
+ }
+ *gotBranch = *branch
+
+ return nil
+ }
+}
+
+func hclGitRepoBranches(projectName, gitRepoName, initType, branchName string) string {
+ gitRepoResource := testutils.HclGitRepoResource(projectName, gitRepoName, initType)
+ return fmt.Sprintf(`
+%[1]s
+
+resource "azuredevops_git_repository_branch" "foo_orphan" {
+ repository_id = azuredevops_git_repository.repository.id
+ name = "testbranch-%[2]s"
+}
+resource "azuredevops_git_repository_branch" "foo_from_ref" {
+ repository_id = azuredevops_git_repository.repository.id
+ name = "testbranch2-%[2]s"
+ source_ref = azuredevops_git_repository_branch.foo_orphan.ref
+}
+resource "azuredevops_git_repository_branch" "foo_from_sha" {
+ repository_id = azuredevops_git_repository.repository.id
+ name = "testbranch3-%[2]s"
+ source_sha = azuredevops_git_repository_branch.foo_orphan.sha
+}
+ `, gitRepoResource, branchName)
+}
+
+type testAccGitRepoBranchExpectedStateAttrs struct {
+ source_ref string
+ source_sha bool
+ is_default_branch bool
+ ref string
+ sha bool
+}
+
+type testAccGitRepoBranchExpectedAttributes struct {
+ Name string
+}
diff --git a/azuredevops/internal/acceptancetests/resource_git_repository_file_test.go b/azuredevops/internal/acceptancetests/resource_git_repository_file_test.go
index 6ebbe60e..00dded6b 100644
--- a/azuredevops/internal/acceptancetests/resource_git_repository_file_test.go
+++ b/azuredevops/internal/acceptancetests/resource_git_repository_file_test.go
@@ -8,9 +8,8 @@ import (
"bytes"
"context"
"fmt"
- "strings"
-
"regexp"
+ "strings"
"testing"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
diff --git a/azuredevops/internal/service/git/resource_git_repository_branch.go b/azuredevops/internal/service/git/resource_git_repository_branch.go
new file mode 100644
index 00000000..ef200e2f
--- /dev/null
+++ b/azuredevops/internal/service/git/resource_git_repository_branch.go
@@ -0,0 +1,285 @@
+package git
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
+ "github.com/microsoft/azure-devops-go-api/azuredevops/v6/git"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/client"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils/converter"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils/tfhelper"
+)
+
+// ResourceGitRepositoryBranch schema to manage the lifecycle of a git repository branch
+func ResourceGitRepositoryBranch() *schema.Resource {
+ return &schema.Resource{
+ CreateContext: resourceGitRepositoryBranchCreate,
+ ReadContext: resourceGitRepositoryBranchRead,
+ DeleteContext: resourceGitRepositoryBranchDelete,
+ Importer: &schema.ResourceImporter{
+ StateContext: resourceGitRepositoryBranchImport,
+ },
+ Schema: map[string]*schema.Schema{
+ "name": {
+ Type: schema.TypeString,
+ Required: true,
+ ForceNew: true,
+ ValidateFunc: validation.NoZeroValues,
+ Sensitive: false,
+ },
+ "repository_id": {
+ Type: schema.TypeString,
+ Required: true,
+ ForceNew: true,
+ ValidateFunc: validation.IsUUID,
+ },
+ "source_ref": {
+ Type: schema.TypeString,
+ Optional: true,
+ ForceNew: true,
+ ValidateFunc: validation.StringIsNotEmpty,
+ },
+ "source_sha": {
+ Type: schema.TypeString,
+ Optional: true,
+ ForceNew: true,
+ Computed: true,
+ ValidateFunc: validation.StringIsNotEmpty,
+ },
+ "is_default_branch": {
+ Type: schema.TypeBool,
+ Computed: true,
+ },
+ "ref": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "sha": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ },
+ }
+}
+
+func resourceGitRepositoryBranchCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+ clients := m.(*client.AggregatedClient)
+ repoId := d.Get("repository_id").(string)
+ branchName := d.Get("name").(string)
+ branchRef := withRefsHeadsPrefix(branchName)
+ sourceRef, hasSourceRef := d.GetOk("source_ref")
+ _, hasSourceSha := d.GetOk("source_sha")
+
+ // Initialise new orphan branch
+ if !hasSourceRef && !hasSourceSha {
+ args := branchCreatePushArgs(branchRef, repoId)
+
+ _, err := clients.GitReposClient.CreatePush(clients.Ctx, args)
+ if err != nil {
+ return diag.FromErr(fmt.Errorf("Error initialising new branch: %w", err))
+ }
+
+ d.SetId(fmt.Sprintf("%s:%s", repoId, branchName))
+
+ return resourceGitRepositoryBranchRead(ctx, d, m)
+ }
+
+ // Get sha from source ref which can be a branch or a tag
+ if !hasSourceSha {
+ // Azuredevops GetRefs api returns refs whose "prefix" matches Filter sorted from shortest to longest
+ // Top1 should return best match
+ sourceRefName := sourceRef.(string)
+ filter := strings.TrimPrefix(sourceRefName, "refs/")
+
+ gotRefs, err := clients.GitReposClient.GetRefs(clients.Ctx, git.GetRefsArgs{
+ RepositoryId: converter.String(repoId),
+ Filter: converter.String(filter),
+ Top: converter.Int(1),
+ PeelTags: converter.Bool(true),
+ })
+ if err != nil {
+ return diag.FromErr(fmt.Errorf("Error getting refs matching %q: %w", filter, err))
+ }
+
+ if len(gotRefs.Value) == 0 {
+ return diag.FromErr(fmt.Errorf("No refs found that match source_ref %q.", sourceRefName))
+ }
+
+ gotRef := gotRefs.Value[0]
+ if gotRef.Name == nil {
+ return diag.FromErr(fmt.Errorf("Got unexpected GetRefs response, a ref without a name was returned."))
+ }
+
+ // Check for complete match. Sometimes refs exist that match prefix with Ref, but do not match completely.
+ if *gotRef.Name != sourceRefName {
+ return diag.FromErr(fmt.Errorf("Ref %q not found, closest match is %q.", filter, *gotRef.Name))
+ }
+
+ // Check if ref was a tag and we need to use PeeledObjectId to get the commit id of the tag
+ var refObjectIdSha *string
+ if gotRef.PeeledObjectId != nil {
+ refObjectIdSha = gotRef.PeeledObjectId
+ } else if gotRef.ObjectId != nil {
+ refObjectIdSha = gotRef.ObjectId
+ } else {
+ return diag.FromErr(fmt.Errorf("GetRefs response doesn't have a valid commit id."))
+ }
+ d.Set("source_sha", *refObjectIdSha)
+ }
+ newObjectId := d.Get("source_sha").(string)
+
+ _, err := updateRefs(clients, git.UpdateRefsArgs{
+ RefUpdates: &[]git.GitRefUpdate{{
+ Name: &branchRef,
+ NewObjectId: &newObjectId,
+ OldObjectId: converter.String("0000000000000000000000000000000000000000"),
+ }},
+ RepositoryId: converter.String(repoId),
+ })
+ if err != nil {
+ return diag.FromErr(fmt.Errorf("Error creating branch %q: %w", branchName, err))
+ }
+
+ d.SetId(fmt.Sprintf("%s:%s", repoId, branchName))
+
+ return resourceGitRepositoryBranchRead(ctx, d, m)
+}
+
+func resourceGitRepositoryBranchRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+ clients := m.(*client.AggregatedClient)
+
+ repoId, name, err := tfhelper.ParseGitRepoBranchID(d.Id())
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ branchStats, err := clients.GitReposClient.GetBranch(clients.Ctx, git.GetBranchArgs{
+ RepositoryId: converter.String(repoId),
+ Name: converter.String(name),
+ })
+ if err != nil {
+ if utils.ResponseWasNotFound(err) {
+ d.SetId("")
+ return nil
+ }
+ return diag.FromErr(fmt.Errorf("Error reading branch %q: %w", name, err))
+ }
+
+ d.SetId(fmt.Sprintf("%s:%s", repoId, name))
+ d.Set("name", name)
+ d.Set("repository_id", repoId)
+ d.Set("is_default_branch", *branchStats.IsBaseVersion)
+ d.Set("ref", converter.String(withRefsHeadsPrefix(name)))
+ d.Set("sha", *branchStats.Commit.CommitId)
+
+ return nil
+}
+
+func resourceGitRepositoryBranchDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+ clients := m.(*client.AggregatedClient)
+
+ repoId, name, err := tfhelper.ParseGitRepoBranchID(d.Id())
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ branchStats, err := clients.GitReposClient.GetBranch(clients.Ctx, git.GetBranchArgs{
+ RepositoryId: converter.String(repoId),
+ Name: converter.String(name),
+ })
+ if err != nil {
+ return diag.FromErr(fmt.Errorf("Error getting latest commit of %q: %w", name, err))
+ }
+
+ _, err = updateRefs(clients, git.UpdateRefsArgs{
+ RefUpdates: &[]git.GitRefUpdate{{
+ Name: converter.String(withRefsHeadsPrefix(name)),
+ OldObjectId: branchStats.Commit.CommitId,
+ NewObjectId: converter.String("0000000000000000000000000000000000000000"),
+ }},
+ RepositoryId: converter.String(repoId),
+ })
+ if err != nil {
+ return diag.FromErr(fmt.Errorf("Error deleting branch %q: %w", name, err))
+ }
+
+ return nil
+}
+
+func resourceGitRepositoryBranchImport(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) {
+ _, branchName, err := tfhelper.ParseGitRepoBranchID(d.Id())
+ if err != nil {
+ return nil, err
+ }
+
+ diags := resourceGitRepositoryBranchRead(ctx, d, m)
+ if diags.HasError() {
+ return nil, fmt.Errorf(diags[0].Summary)
+ }
+
+ if d.Id() == "" {
+ return nil, fmt.Errorf("Branch %q not found", branchName)
+ }
+
+ return []*schema.ResourceData{d}, nil
+}
+
+func branchCreatePushArgs(name, repoId string) git.CreatePushArgs {
+ args := git.CreatePushArgs{
+ RepositoryId: converter.String(repoId),
+ Push: &git.GitPush{
+ RefUpdates: &[]git.GitRefUpdate{
+ {
+ Name: converter.String(name),
+ OldObjectId: converter.String("0000000000000000000000000000000000000000"),
+ },
+ },
+ Commits: &[]git.GitCommitRef{
+ {
+ Comment: converter.String("Initial commit."),
+ Changes: &[]interface{}{
+ git.Change{
+ ChangeType: &git.VersionControlChangeTypeValues.Add,
+ Item: git.GitItem{
+ Path: converter.String("/readme.md"),
+ },
+ NewContent: &git.ItemContent{
+ ContentType: &git.ItemContentTypeValues.RawText,
+ Content: converter.String("Branch initialized with azuredevops terraform provider"),
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+ return args
+}
+
+func updateRefs(clients *client.AggregatedClient, args git.UpdateRefsArgs) (*[]git.GitRefUpdateResult, error) {
+ updateRefResults, err := clients.GitReposClient.UpdateRefs(clients.Ctx, args)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, refUpdate := range *updateRefResults {
+ if !*refUpdate.Success {
+ return nil, fmt.Errorf("Error got invalid GitRefUpdate.UpdateStatus: %s", *refUpdate.UpdateStatus)
+ }
+ }
+
+ return updateRefResults, nil
+}
+
+func withRefsHeadsPrefix(branchName string) string {
+ prefix := "refs/heads/"
+ if strings.HasPrefix(branchName, prefix) {
+ return branchName
+ }
+ return prefix + branchName
+}
diff --git a/azuredevops/internal/service/git/resource_git_repository_branch_test.go b/azuredevops/internal/service/git/resource_git_repository_branch_test.go
new file mode 100644
index 00000000..51a90441
--- /dev/null
+++ b/azuredevops/internal/service/git/resource_git_repository_branch_test.go
@@ -0,0 +1,293 @@
+package git
+
+import (
+ "context"
+ "fmt"
+ "reflect"
+ "strings"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+ "github.com/microsoft/azure-devops-go-api/azuredevops/v6/git"
+ "github.com/microsoft/terraform-provider-azuredevops/azdosdkmocks"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/client"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils/converter"
+)
+
+func TestGitRepositoryBranch_Create(t *testing.T) {
+ type args struct {
+ ctx context.Context
+ d *schema.ResourceData
+ m interface{}
+ }
+ tests := []struct {
+ name string
+ args func(g *azdosdkmocks.MockGitClient) args
+ want diag.Diagnostics
+ }{
+ {
+ "When source_ref is not given, create push does not swallow error",
+ func(g *azdosdkmocks.MockGitClient) args {
+ clients := &client.AggregatedClient{
+ GitReposClient: g,
+ Ctx: context.Background(),
+ }
+ expectedArgs := branchCreatePushArgs(withRefsHeadsPrefix("a-branch"), "a-repo")
+ d := schema.TestResourceDataRaw(t, ResourceGitRepositoryBranch().Schema, nil)
+ d.Set("name", "a-branch")
+ d.Set("repository_id", "a-repo")
+
+ g.EXPECT().
+ CreatePush(clients.Ctx, expectedArgs).
+ Return(nil, fmt.Errorf("an-error"))
+ return args{
+ context.Background(),
+ d,
+ clients,
+ }
+ },
+ diag.FromErr(fmt.Errorf("Error initialising new branch: an-error")),
+ },
+ {
+ "When source_ref is given, refs update does not swallow error",
+ func(g *azdosdkmocks.MockGitClient) args {
+ clients := &client.AggregatedClient{
+ GitReposClient: g,
+ Ctx: context.Background(),
+ }
+ d := schema.TestResourceDataRaw(t, ResourceGitRepositoryBranch().Schema, nil)
+ source_ref := "refs/heads/another-branch"
+ commit := "a-commit"
+ branchName := "a-branch"
+ repoId := "a-repo"
+ d.Set("source_ref", source_ref)
+ d.Set("name", branchName)
+ d.Set("repository_id", repoId)
+
+ g.EXPECT().
+ GetRefs(clients.Ctx, git.GetRefsArgs{
+ RepositoryId: &repoId,
+ Filter: converter.String(strings.TrimPrefix(source_ref, "refs/")),
+ Top: converter.Int(1),
+ PeelTags: converter.Bool(true),
+ }).
+ Return(&git.GetRefsResponseValue{
+ Value: []git.GitRef{{
+ Name: &source_ref,
+ ObjectId: &commit,
+ }},
+ }, nil)
+
+ g.EXPECT().
+ UpdateRefs(clients.Ctx, git.UpdateRefsArgs{
+ RefUpdates: &[]git.GitRefUpdate{{
+ Name: converter.String(withRefsHeadsPrefix("a-branch")),
+ NewObjectId: &commit,
+ OldObjectId: converter.String("0000000000000000000000000000000000000000"),
+ }},
+ RepositoryId: converter.String("a-repo"),
+ }).
+ Return(nil, fmt.Errorf("an-error"))
+ return args{
+ context.Background(),
+ d,
+ clients,
+ }
+ },
+ diag.FromErr(fmt.Errorf("Error creating branch \"a-branch\": an-error")),
+ },
+ {
+ "When invalid RefUpdate UpdateStatus, throw error",
+ func(g *azdosdkmocks.MockGitClient) args {
+ clients := &client.AggregatedClient{
+ GitReposClient: g,
+ Ctx: context.Background(),
+ }
+ d := schema.TestResourceDataRaw(t, ResourceGitRepositoryBranch().Schema, nil)
+ source_ref := "refs/heads/another-branch"
+ commit := "a-commit"
+ branchName := "a-branch"
+ repoId := "a-repo"
+ d.Set("source_ref", source_ref)
+ d.Set("name", branchName)
+ d.Set("repository_id", repoId)
+
+ g.EXPECT().
+ GetRefs(clients.Ctx, git.GetRefsArgs{
+ RepositoryId: &repoId,
+ Filter: converter.String(strings.TrimPrefix(source_ref, "refs/")),
+ Top: converter.Int(1),
+ PeelTags: converter.Bool(true),
+ }).
+ Return(&git.GetRefsResponseValue{
+ Value: []git.GitRef{{
+ Name: &source_ref,
+ ObjectId: &commit,
+ }},
+ }, nil)
+
+ g.EXPECT().
+ UpdateRefs(clients.Ctx, git.UpdateRefsArgs{
+ RefUpdates: &[]git.GitRefUpdate{{
+ Name: converter.String(withRefsHeadsPrefix("a-branch")),
+ NewObjectId: &commit,
+ OldObjectId: converter.String("0000000000000000000000000000000000000000"),
+ }},
+ RepositoryId: converter.String("a-repo"),
+ }).
+ Return(&[]git.GitRefUpdateResult{{
+ Success: converter.Bool(false),
+ UpdateStatus: &git.GitRefUpdateStatusValues.InvalidRefName,
+ }}, nil)
+ return args{
+ context.Background(),
+ d,
+ clients,
+ }
+ },
+ diag.FromErr(fmt.Errorf("Error creating branch \"a-branch\": Error got invalid GitRefUpdate.UpdateStatus: invalidRefName")),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ gitClient := azdosdkmocks.NewMockGitClient(ctrl)
+ testArgs := tt.args(gitClient)
+
+ if got := resourceGitRepositoryBranchCreate(testArgs.ctx, testArgs.d, testArgs.m); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("resourceGitRepositoryBranchCreate() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestGitRepositoryBranch_Read(t *testing.T) {
+ type args struct {
+ ctx context.Context
+ d *schema.ResourceData
+ m interface{}
+ }
+ tests := []struct {
+ name string
+ args func(g *azdosdkmocks.MockGitClient) args
+ want diag.Diagnostics
+ }{
+ {
+ "Read does not swallow error.",
+ func(g *azdosdkmocks.MockGitClient) args {
+ clients := &client.AggregatedClient{
+ GitReposClient: g,
+ Ctx: context.Background(),
+ }
+
+ d := schema.TestResourceDataRaw(t, ResourceGitRepositoryBranch().Schema, nil)
+ d.Set("ref", "another-branch")
+ d.Set("name", "a-branch")
+ d.Set("repository_id", "a-repo")
+ d.SetId("a-repo:a-branch")
+
+ g.EXPECT().
+ GetBranch(clients.Ctx, git.GetBranchArgs{
+ RepositoryId: converter.String("a-repo"),
+ Name: converter.String("a-branch"),
+ }).
+ Return(nil, fmt.Errorf("an-error"))
+
+ return args{
+ ctx: context.Background(),
+ d: d,
+ m: clients,
+ }
+ },
+ diag.FromErr(fmt.Errorf("Error reading branch \"a-branch\": an-error")),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ gitClient := azdosdkmocks.NewMockGitClient(ctrl)
+ testArgs := tt.args(gitClient)
+
+ if got := resourceGitRepositoryBranchRead(testArgs.ctx, testArgs.d, testArgs.m); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("resourceGitRepositoryBranchCreate() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestGitRepositoryBranch_Delete(t *testing.T) {
+ type args struct {
+ ctx context.Context
+ d *schema.ResourceData
+ m interface{}
+ }
+ tests := []struct {
+ name string
+ args func(g *azdosdkmocks.MockGitClient) args
+ want diag.Diagnostics
+ }{
+ {
+ "Delete based on repositoryId:branchName does not swallow error.",
+ func(g *azdosdkmocks.MockGitClient) args {
+ clients := &client.AggregatedClient{
+ GitReposClient: g,
+ Ctx: context.Background(),
+ }
+
+ d := schema.TestResourceDataRaw(t, ResourceGitRepositoryBranch().Schema, nil)
+ d.Set("ref", "another-branch")
+ d.Set("name", "a-branch")
+ d.Set("repository_id", "a-repo")
+ d.SetId("a-repo:a-branch")
+
+ g.EXPECT().
+ GetBranch(clients.Ctx, git.GetBranchArgs{
+ RepositoryId: converter.String("a-repo"),
+ Name: converter.String("a-branch"),
+ }).
+ Return(&git.GitBranchStats{
+ Commit: &git.GitCommitRef{
+ CommitId: converter.String("a-commit"),
+ },
+ }, nil)
+
+ g.EXPECT().
+ UpdateRefs(clients.Ctx, git.UpdateRefsArgs{
+ RefUpdates: &[]git.GitRefUpdate{{
+ Name: converter.String(withRefsHeadsPrefix("a-branch")),
+ OldObjectId: converter.String("a-commit"),
+ NewObjectId: converter.String("0000000000000000000000000000000000000000"),
+ }},
+ RepositoryId: converter.String("a-repo"),
+ }).
+ Return(nil, fmt.Errorf("an-error"))
+
+ return args{
+ ctx: clients.Ctx,
+ d: d,
+ m: clients,
+ }
+ },
+ diag.FromErr(fmt.Errorf("Error deleting branch \"a-branch\": an-error")),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ gitClient := azdosdkmocks.NewMockGitClient(ctrl)
+ testArgs := tt.args(gitClient)
+
+ if got := resourceGitRepositoryBranchDelete(testArgs.ctx, testArgs.d, testArgs.m); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("resourceGitRepositoryBranchDelete() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/azuredevops/internal/service/git/resource_git_repository_test.go b/azuredevops/internal/service/git/resource_git_repository_test.go
index 07973cf4..e1f5bb75 100644
--- a/azuredevops/internal/service/git/resource_git_repository_test.go
+++ b/azuredevops/internal/service/git/resource_git_repository_test.go
@@ -21,8 +21,10 @@ import (
"github.com/stretchr/testify/require"
)
-var testRepoProjectID = uuid.New()
-var testRepoID = uuid.New()
+var (
+ testRepoProjectID = uuid.New()
+ testRepoID = uuid.New()
+)
// This definition matches the overall structure of what a configured git repository would
// look like. Note that the ID and Name attributes match -- this is the service-side behavior
diff --git a/azuredevops/internal/utils/tfhelper/tfhelper.go b/azuredevops/internal/utils/tfhelper/tfhelper.go
index c4c88646..f28a0ac5 100644
--- a/azuredevops/internal/utils/tfhelper/tfhelper.go
+++ b/azuredevops/internal/utils/tfhelper/tfhelper.go
@@ -110,6 +110,18 @@ func ParseProjectIDAndResourceID(d *schema.ResourceData) (string, int, error) {
return projectID, resourceID, err
}
+func ParseGitRepoBranchID(id string) (string, string, error) {
+ return parseTwoPartID(id, ":", "repositoryID:branchName")
+}
+
+func parseTwoPartID(id, sep, want string) (string, string, error) {
+ parts := strings.SplitN(id, sep, 2)
+ if len(parts) != 2 || strings.EqualFold(parts[0], "") || strings.EqualFold(parts[1], "") {
+ return "", "", fmt.Errorf("unexpected format of ID (%s), expected %s", id, want)
+ }
+ return parts[0], parts[1], nil
+}
+
// ParseImportedID parse the imported int Id from the terraform import
func ParseImportedID(id string) (string, int, error) {
parts := strings.SplitN(id, "/", 2)
@@ -175,7 +187,6 @@ func ImportProjectQualifiedResource() *schema.ResourceImporter {
return &schema.ResourceImporter{
State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
projectNameOrID, resourceID, err := ParseImportedName(d.Id())
-
if err != nil {
return nil, fmt.Errorf("error parsing the resource ID from the Terraform resource data: %v", err)
}
@@ -198,7 +209,6 @@ func ImportProjectQualifiedResourceInteger() *schema.ResourceImporter {
return &schema.ResourceImporter{
State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
projectNameOrID, resourceID, err := ParseImportedName(d.Id())
-
if err != nil {
return nil, fmt.Errorf("error parsing the resource ID from the Terraform resource data: %v", err)
}
@@ -226,7 +236,6 @@ func ImportProjectQualifiedResourceUUID() *schema.ResourceImporter {
return &schema.ResourceImporter{
State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
projectNameOrID, resourceID, err := ParseImportedUUID(d.Id())
-
if err != nil {
return nil, fmt.Errorf("error parsing the resource ID from the Terraform resource data: %v", err)
}
@@ -243,7 +252,7 @@ func ImportProjectQualifiedResourceUUID() *schema.ResourceImporter {
// Get real project ID
func GetRealProjectId(projectNameOrID string, meta interface{}) (string, error) {
- //If request params is project name, try get the project ID
+ // If request params is project name, try get the project ID
if _, err := uuid.ParseUUID(projectNameOrID); err != nil {
clients := meta.(*client.AggregatedClient)
project, err := clients.CoreClient.GetProject(clients.Ctx, core.GetProjectArgs{
diff --git a/azuredevops/provider.go b/azuredevops/provider.go
index 9aa3726b..fbd21e0c 100644
--- a/azuredevops/provider.go
+++ b/azuredevops/provider.go
@@ -68,6 +68,7 @@ func Provider() *schema.Provider {
"azuredevops_serviceendpoint_generic_git": serviceendpoint.ResourceServiceEndpointGenericGit(),
"azuredevops_serviceendpoint_externaltfs": serviceendpoint.ResourceServiceEndpointExternalTFS(),
"azuredevops_git_repository": git.ResourceGitRepository(),
+ "azuredevops_git_repository_branch": git.ResourceGitRepositoryBranch(),
"azuredevops_git_repository_file": git.ResourceGitRepositoryFile(),
"azuredevops_user_entitlement": memberentitlementmanagement.ResourceUserEntitlement(),
"azuredevops_group_membership": graph.ResourceGroupMembership(),
diff --git a/azuredevops/provider_test.go b/azuredevops/provider_test.go
index 92d264c7..b8774385 100644
--- a/azuredevops/provider_test.go
+++ b/azuredevops/provider_test.go
@@ -53,6 +53,7 @@ func TestProvider_HasChildResources(t *testing.T) {
"azuredevops_repository_policy_reserved_names",
"azuredevops_repository_policy_check_credentials",
"azuredevops_git_repository",
+ "azuredevops_git_repository_branch",
"azuredevops_git_repository_file",
"azuredevops_user_entitlement",
"azuredevops_group_membership",
diff --git a/website/docs/r/git_repository_branch.html.markdown b/website/docs/r/git_repository_branch.html.markdown
new file mode 100644
index 00000000..dbf51c59
--- /dev/null
+++ b/website/docs/r/git_repository_branch.html.markdown
@@ -0,0 +1,78 @@
+---
+layout: "azuredevops"
+page_title: "AzureDevops: azuredevops_git_repository_branch"
+description: |-
+ Manages a Git Repository Branch.
+---
+
+# azuredevops_git_repository_branch
+
+Manages a Git Repository Branch.
+
+## Example Usage
+
+```hcl
+resource "azuredevops_project" "example" {
+ name = "Example Project"
+ visibility = "private"
+ version_control = "Git"
+ work_item_template = "Agile"
+}
+
+resource "azuredevops_git_repository" "example" {
+ project_id = azuredevops_project.example.id
+ name = "Example Git Repository"
+ initialization {
+ init_type = "Uninitialized"
+ }
+}
+
+resource "azuredevops_git_repository_branch" "example_orphan" {
+ repository_id = azuredevops_git_repository.example.id
+ name = "master"
+}
+
+resource "azuredevops_git_repository_branch" "example_from_ref" {
+ repository_id = azuredevops_git_repository.example.id
+ name = "develop"
+ source_ref = azuredevops_git_repository_branch.example_orphan.ref
+}
+
+resource "azuredevops_git_repository_branch" "example_from_sha" {
+ repository_id = azuredevops_git_repository.example.id
+ name = "somebranch"
+ source_sha = azuredevops_git_repository_branch.example_orphan.sha
+}
+```
+
+## Arguments Reference
+
+The following arguments are supported:
+
+- `name` - (Required) The name of the branch (not prefixed with `refs/heads/`).
+
+- `repository_id` - (Required) The ID of the repository the branch is created against.
+
+- `source_ref` - (Optional) The ref the branch is created from. (prefixed with `refs/heads/` or `refs/tags/`)
+
+- `source_sha` - (Optional) The commit object id the branch is created from. Set to commit object id of `source_ref` if not given. Otherwise, `source_ref` is ignored.
+
+## Attributes Reference
+
+In addition to the Arguments listed above - the following Attributes are exported:
+
+- `id` - The ID of the Git Repository Branch.
+
+- `is_default_branch` - True if the branch is the default branch of the git repository.
+
+- `ref` - The branch reference in `refs/heads/<name>` format.
+
+- `sha` - The commit SHA1 object id of the branch tip.
+
+## Import
+
+Git Repository Branches can be imported using the `resource id`, e.g.
+
+```shell
+terraform import azuredevops_git_repository_branch.example 00000000-0000-0000-0000-000000000000:master
+```