diff --git a/github/resource_github_repository_file.go b/github/resource_github_repository_file.go index e30af03ceb..c6b82661c6 100644 --- a/github/resource_github_repository_file.go +++ b/github/resource_github_repository_file.go @@ -40,6 +40,7 @@ func resourceGithubRepositoryFile() *schema.Resource { d.SetId(fmt.Sprintf("%s/%s", repo, file)) d.Set("branch", branch) + d.Set("overwrite_on_create", false) return []*schema.ResourceData{d}, nil }, @@ -93,6 +94,12 @@ func resourceGithubRepositoryFile() *schema.Resource { Computed: true, Description: "The blob SHA of the file", }, + "overwrite_on_create": { + Type: schema.TypeBool, + Optional: true, + Description: "Enable overwriting existing files, defaults to \"false\"", + Default: false, + }, }, } } @@ -135,6 +142,7 @@ func resourceGithubRepositoryFileOptions(d *schema.ResourceData) (*github.Reposi } func resourceGithubRepositoryFileCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Owner).v3client owner := meta.(*Owner).name ctx := context.Background() @@ -157,21 +165,46 @@ func resourceGithubRepositoryFileCreate(d *schema.ResourceData, meta interface{} opts.Message = &m } + log.Printf("[DEBUG] Checking if overwriting a repository file: %s/%s/%s in branch: %s", owner, repo, file, branch) + checkOpt := github.RepositoryContentGetOptions{Ref: branch} + fileContent, _, resp, err := client.Repositories.GetContents(ctx, owner, repo, file, &checkOpt) + if err != nil { + if resp != nil { + if resp.StatusCode != 404 { + // 404 is a valid response if the file does not exist + return err + } + } else { + // Response should be non-nil + return err + } + } + + if fileContent != nil { + if d.Get("overwrite_on_create").(bool) { + // Overwrite existing file if requested by configuring the options for + // `client.Repositories.CreateFile` to match the existing file's SHA + opts.SHA = fileContent.SHA + } else { + // Error if overwriting a file is not requested + return fmt.Errorf("[ERROR] Refusing to overwrite existing file. Configure `overwrite_on_create` to `true` to override.") + } + } + + // Create a new or overwritten file log.Printf("[DEBUG] Creating repository file: %s/%s/%s in branch: %s", owner, repo, file, branch) - resp, _, err := client.Repositories.CreateFile(ctx, owner, repo, file, opts) + _, _, err = client.Repositories.CreateFile(ctx, owner, repo, file, opts) if err != nil { return err } - d.Set("commit_author", resp.GetCommitter().GetName()) - d.Set("commit_email", resp.GetCommitter().GetEmail()) - d.Set("commit_message", resp.GetMessage()) d.SetId(fmt.Sprintf("%s/%s", repo, file)) return resourceGithubRepositoryFileRead(d, meta) } func resourceGithubRepositoryFileRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Owner).v3client owner := meta.(*Owner).name ctx := context.WithValue(context.Background(), ctxId, d.Id()) @@ -203,10 +236,21 @@ func resourceGithubRepositoryFileRead(d *schema.ResourceData, meta interface{}) d.Set("file", file) d.Set("sha", fc.GetSHA()) + log.Printf("[DEBUG] Fetching commit info for repository file: %s/%s/%s", owner, repo, file) + commit, err := getFileCommit(client, owner, repo, file, branch) + if err != nil { + return err + } + + d.Set("commit_author", commit.GetCommit().GetCommitter().GetName()) + d.Set("commit_email", commit.GetCommit().GetCommitter().GetEmail()) + d.Set("commit_message", commit.GetCommit().GetMessage()) + return nil } func resourceGithubRepositoryFileUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Owner).v3client owner := meta.(*Owner).name ctx := context.Background() @@ -230,19 +274,16 @@ func resourceGithubRepositoryFileUpdate(d *schema.ResourceData, meta interface{} } log.Printf("[DEBUG] Updating content in repository file: %s/%s/%s", owner, repo, file) - resp, _, err := client.Repositories.CreateFile(ctx, owner, repo, file, opts) + _, _, err = client.Repositories.CreateFile(ctx, owner, repo, file, opts) if err != nil { return err } - d.Set("commit_author", resp.GetCommitter().GetName()) - d.Set("commit_email", resp.GetCommitter().GetEmail()) - d.Set("commit_message", resp.GetMessage()) - return resourceGithubRepositoryFileRead(d, meta) } func resourceGithubRepositoryFileDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Owner).v3client owner := meta.(*Owner).name ctx := context.Background() @@ -297,3 +338,48 @@ func checkRepositoryFileExists(client *github.Client, owner, repo, file, branch return nil } + +func getFileCommit(client *github.Client, owner, repo, file, branch string) (*github.RepositoryCommit, error) { + ctx := context.WithValue(context.Background(), ctxId, fmt.Sprintf("%s/%s", repo, file)) + opts := &github.CommitsListOptions{ + SHA: branch, + } + allCommits := []*github.RepositoryCommit{} + for { + commits, resp, err := client.Repositories.ListCommits(ctx, owner, repo, opts) + if err != nil { + return nil, err + } + + allCommits = append(allCommits, commits...) + + if resp.NextPage == 0 { + break + } + + opts.Page = resp.NextPage + } + + for _, c := range allCommits { + sha := c.GetSHA() + + // Skip merge commits + if strings.Contains(c.Commit.GetMessage(), "Merge branch") { + continue + } + + rc, _, err := client.Repositories.GetCommit(ctx, owner, repo, sha) + if err != nil { + return nil, err + } + + for _, f := range rc.Files { + if f.GetFilename() == file && f.GetStatus() != "removed" { + log.Printf("[DEBUG] Found file: %s in commit: %s", file, sha) + return rc, nil + } + } + } + + return nil, fmt.Errorf("Cannot find file %s in repo %s/%s", file, owner, repo) +} diff --git a/github/resource_github_repository_file_test.go b/github/resource_github_repository_file_test.go index 911e8781e6..3515d45f55 100644 --- a/github/resource_github_repository_file_test.go +++ b/github/resource_github_repository_file_test.go @@ -1,110 +1,47 @@ package github import ( - "strings" - "fmt" + "regexp" + "strings" "testing" + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/helper/resource" - "github.com/hashicorp/terraform/helper/acctest" ) func TestAccGithubRepositoryFile(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - t.Run("creates and updates file content without error", func(t *testing.T) { - - fileContent := "file_content_value" - updatedFileContent := "updated_file_content_value" + t.Run("creates and manages files", func(t *testing.T) { config := fmt.Sprintf(` - resource "github_repository" "test" { - name = "tf-acc-test-%s" - auto_init = true - } - - resource "github_repository_file" "test" { - repository = github_repository.test.id - file = "test" - content = "%s" - } - `, randomID, fileContent) - - checks := map[string]resource.TestCheckFunc{ - "before": resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "github_repository_file.test", "sha", - "deee258b7c807901aad79d01da020d993739160a", - ), - ), - "after": resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "github_repository_file.test", "sha", - "ec9aad0ba478cdd7349faabbeac2a64e5ce72ddb", - ), - ), - } - - testCase := func(t *testing.T, mode string) { - resource.Test(t, resource.TestCase{ - PreCheck: func() { skipUnlessMode(t, mode) }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - Config: config, - Check: checks["before"], - }, - { - Config: strings.Replace(config, - fileContent, - updatedFileContent, 1), - Check: checks["after"], - }, - }, - }) - } - - t.Run("with an anonymous account", func(t *testing.T) { - t.Skip("anonymous account not supported for this operation") - }) - - t.Run("with an individual account", func(t *testing.T) { - testCase(t, individual) - }) - t.Run("with an organization account", func(t *testing.T) { - testCase(t, organization) - }) - - }) - - t.Run("manages file content for a specified branch", func(t *testing.T) { - - config := fmt.Sprintf(` resource "github_repository" "test" { - name = "tf-acc-test-%s" + name = "tf-acc-test-%s" auto_init = true } - resource "github_branch" "test" { - repository = github_repository.test.id - branch = "tf-acc-test-%[1]s" - } - resource "github_repository_file" "test" { - repository = github_repository.test.id - branch = github_branch.test.branch - file = "test" - content = "test" + repository = github_repository.test.name + branch = "main" + file = "test" + content = "bar" + commit_message = "Managed by Terraform" + commit_author = "Terraform User" + commit_email = "terraform@example.com" } `, randomID) check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_repository_file.test", "content", + "bar", + ), resource.TestCheckResourceAttr( "github_repository_file.test", "sha", - "30d74d258442c7c65512eafab474568dd706c430", + "ba0e162e1c47469e3fe4b393a8bf8c569f302116", ), ) @@ -135,36 +72,32 @@ func TestAccGithubRepositoryFile(t *testing.T) { }) - t.Run("commits with custom message, author and e-mail", func(t *testing.T) { + t.Run("can be configured to overwrite files on create", func(t *testing.T) { config := fmt.Sprintf(` resource "github_repository" "test" { - name = "tf-acc-test-%s" - auto_init = true + name = "tf-acc-test-%s" + auto_init = true } resource "github_repository_file" "test" { - repository = github_repository.test.id - file = "test" - content = "test" - commit_message = "Managed by Terraform" - commit_author = "Terraform User" - commit_email = "terraform@example.com" + repository = github_repository.test.name + branch = "main" + file = "README.md" + content = "overwritten" + overwrite_on_create = false } + `, randomID) check := resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr( - "github_repository_file.test", "commit_message", - "Managed by Terraform", - ), - resource.TestCheckResourceAttr( - "github_repository_file.test", "commit_author", - "Terraform User", + "github_repository_file.test", "content", + "overwritten", ), resource.TestCheckResourceAttr( - "github_repository_file.test", "commit_email", - "terraform@example.com", + "github_repository_file.test", "sha", + "67c1a95c2d9bb138aefeaebb319cca82e531736b", ), ) @@ -174,8 +107,14 @@ func TestAccGithubRepositoryFile(t *testing.T) { Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: config, - Check: check, + Config: config, + ExpectError: regexp.MustCompile(`Refusing to overwrite existing file`), + }, + { + Config: strings.Replace(config, + "overwrite_on_create = false", + "overwrite_on_create = true", 1), + Check: check, }, }, }) diff --git a/website/docs/r/repository_file.html.markdown b/website/docs/r/repository_file.html.markdown index 8dbbc1bdbb..0e0a6f58e9 100644 --- a/website/docs/r/repository_file.html.markdown +++ b/website/docs/r/repository_file.html.markdown @@ -14,11 +14,23 @@ GitHub repository. ## Example Usage ```hcl -resource "github_repository_file" "gitignore" { - repository = "example" - file = ".gitignore" - content = "**/*.tfstate" + +resource "github_repository" "foo" { + name = "tf-acc-test-%s" + auto_init = true +} + +resource "github_repository_file" "foo" { + repository = github_repository.foo.name + branch = "main" + file = ".gitignore" + content = "**/*.tfstate" + commit_message = "Managed by Terraform" + commit_author = "Terraform User" + commit_email = "terraform@example.com" + overwrite_on_create = true } + ``` @@ -41,6 +53,7 @@ The following arguments are supported: * `commit_message` - (Optional) Commit message when adding or updating the managed file. +* `overwrite_on_create` - (Optional) Enable overwriting existing files ## Attributes Reference