Skip to content

Commit

Permalink
[Feature] Repository File Data Source (integrations#896)
Browse files Browse the repository at this point in the history
* Feature github_repository_file data source

* I think this is how to update docs
  • Loading branch information
chriskuchin authored Sep 28, 2021
1 parent 4890a3e commit 94328a1
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 77 deletions.
119 changes: 119 additions & 0 deletions github/data_source_github_repository_file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package github

import (
"context"
"fmt"
"log"

"github.com/google/go-github/v38/github"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)

func dataSourceGithubRepositoryFile() *schema.Resource {
return &schema.Resource{
Read: dataSourceGithubRepositoryFileRead,
Schema: map[string]*schema.Schema{
"repository": {
Type: schema.TypeString,
Required: true,
Description: "The repository name",
},
"file": {
Type: schema.TypeString,
Required: true,
Description: "The file path to manage",
},
"branch": {
Type: schema.TypeString,
Optional: true,
Description: "The branch name, defaults to \"main\"",
Default: "main",
},
"content": {
Type: schema.TypeString,
Computed: true,
Description: "The file's content",
},
"commit_sha": {
Type: schema.TypeString,
Computed: true,
Description: "The SHA of the commit that modified the file",
},
"commit_message": {
Type: schema.TypeString,
Computed: true,
Description: "The commit message when creating or updating the file",
},
"commit_author": {
Type: schema.TypeString,
Computed: true,
Description: "The commit author name, defaults to the authenticated user's name",
},
"commit_email": {
Type: schema.TypeString,
Computed: true,
Description: "The commit author email address, defaults to the authenticated user's email address",
},
"sha": {
Type: schema.TypeString,
Computed: true,
Description: "The blob SHA of the file",
},
},
}
}

func dataSourceGithubRepositoryFileRead(d *schema.ResourceData, meta interface{}) error {

client := meta.(*Owner).v3client
owner := meta.(*Owner).name
ctx := context.WithValue(context.Background(), ctxId, d.Id())

repo := d.Get("repository").(string)
file := d.Get("file").(string)
branch := d.Get("branch").(string)
if err := checkRepositoryBranchExists(client, owner, repo, branch); err != nil {
return err
}

log.Printf("[DEBUG] Data Source reading repository file: %s/%s/%s, branch: %s", owner, repo, file, branch)
opts := &github.RepositoryContentGetOptions{Ref: branch}
fc, _, _, err := client.Repositories.GetContents(ctx, owner, repo, file, opts)
if err != nil {
return err
}

content, err := fc.GetContent()
if err != nil {
return err
}

d.SetId(fmt.Sprintf("%s/%s", repo, file))
d.Set("content", content)
d.Set("repository", repo)
d.Set("file", file)
d.Set("sha", fc.GetSHA())

log.Printf("[DEBUG] Data Source fetching commit info for repository file: %s/%s/%s", owner, repo, file)
var commit *github.RepositoryCommit

// Use the SHA to lookup the commit info if we know it, otherwise loop through commits
if sha, ok := d.GetOk("commit_sha"); ok {
log.Printf("[DEBUG] Using known commit SHA: %s", sha.(string))
commit, _, err = client.Repositories.GetCommit(ctx, owner, repo, sha.(string), nil)
} else {
log.Printf("[DEBUG] Commit SHA unknown for file: %s/%s/%s, looking for commit...", owner, repo, file)
commit, err = getFileCommit(client, owner, repo, file, branch)
log.Printf("[DEBUG] Found file: %s/%s/%s, in commit SHA: %s ", owner, repo, file, commit.GetSHA())
}
if err != nil {
return err
}

d.Set("commit_sha", commit.GetSHA())
d.Set("commit_author", commit.Commit.GetCommitter().GetName())
d.Set("commit_email", commit.Commit.GetCommitter().GetEmail())
d.Set("commit_message", commit.GetCommit().GetMessage())

return nil
}
90 changes: 90 additions & 0 deletions github/data_source_github_repository_file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package github

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
)

func TestAccGithubRepositoryFileDataSource(t *testing.T) {

randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)

t.Run("read 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.name
branch = "main"
file = "test"
content = "bar"
commit_message = "Managed by Terraform"
commit_author = "Terraform User"
commit_email = "terraform@example.com"
}
data "github_repository_file" "test" {
repository = github_repository.test.name
branch = "main"
file = github_repository_file.test.file
}
`, randomID)

check := resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(
"data.github_repository_file.test", "content",
"bar",
),
resource.TestCheckResourceAttr(
"data.github_repository_file.test", "sha",
"ba0e162e1c47469e3fe4b393a8bf8c569f302116",
),
resource.TestCheckResourceAttrSet(
"data.github_repository_file.test", "commit_author",
),
resource.TestCheckResourceAttrSet(
"data.github_repository_file.test", "commit_email",
),
resource.TestCheckResourceAttrSet(
"data.github_repository_file.test", "commit_message",
),
resource.TestCheckResourceAttrSet(
"data.github_repository_file.test", "commit_sha",
),
)

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: check,
},
},
})
}

t.Run("with an anonymous account", func(t *testing.T) {
testCase(t, anonymous)
})

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)
})

})
}
1 change: 1 addition & 0 deletions github/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ func Provider() terraform.ResourceProvider {
"github_release": dataSourceGithubRelease(),
"github_repositories": dataSourceGithubRepositories(),
"github_repository": dataSourceGithubRepository(),
"github_repository_file": dataSourceGithubRepositoryFile(),
"github_repository_milestone": dataSourceGithubRepositoryMilestone(),
"github_repository_pull_request": dataSourceGithubRepositoryPullRequest(),
"github_repository_pull_requests": dataSourceGithubRepositoryPullRequests(),
Expand Down
87 changes: 87 additions & 0 deletions github/repository_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package github

import (
"context"
"fmt"
"log"
"net/http"
"strings"

"github.com/google/go-github/v38/github"
)

// checkRepositoryBranchExists tests if a branch exists in a repository.
func checkRepositoryBranchExists(client *github.Client, owner, repo, branch string) error {
ctx := context.WithValue(context.Background(), ctxId, buildTwoPartID(repo, branch))
_, _, err := client.Repositories.GetBranch(ctx, owner, repo, branch, true)
if err != nil {
if ghErr, ok := err.(*github.ErrorResponse); ok {
if ghErr.Response.StatusCode == http.StatusNotFound {
return fmt.Errorf("Branch %s not found in repository %s/%s or repository is not readable", branch, owner, repo)
}
}
return err
}

return nil
}

// checkRepositoryFileExists tests if a file exists in a repository.
func checkRepositoryFileExists(client *github.Client, owner, repo, file, branch string) error {
ctx := context.WithValue(context.Background(), ctxId, fmt.Sprintf("%s/%s", repo, file))
fc, _, _, err := client.Repositories.GetContents(ctx, owner, repo, file, &github.RepositoryContentGetOptions{Ref: branch})
if err != nil {
return nil
}
if fc == nil {
return fmt.Errorf("File %s not a file in in repository %s/%s or repository is not readable", file, owner, repo)
}

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,
Path: file,
}
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, nil)
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)
}
77 changes: 0 additions & 77 deletions github/resource_github_repository_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package github
import (
"context"
"log"
"net/http"
"strings"

"fmt"
Expand Down Expand Up @@ -327,79 +326,3 @@ func resourceGithubRepositoryFileDelete(d *schema.ResourceData, meta interface{}

return nil
}

// checkRepositoryBranchExists tests if a branch exists in a repository.
func checkRepositoryBranchExists(client *github.Client, owner, repo, branch string) error {
ctx := context.WithValue(context.Background(), ctxId, buildTwoPartID(repo, branch))
_, _, err := client.Repositories.GetBranch(ctx, owner, repo, branch, true)
if err != nil {
if ghErr, ok := err.(*github.ErrorResponse); ok {
if ghErr.Response.StatusCode == http.StatusNotFound {
return fmt.Errorf("Branch %s not found in repository %s/%s or repository is not readable", branch, owner, repo)
}
}
return err
}

return nil
}

// checkRepositoryFileExists tests if a file exists in a repository.
func checkRepositoryFileExists(client *github.Client, owner, repo, file, branch string) error {
ctx := context.WithValue(context.Background(), ctxId, fmt.Sprintf("%s/%s", repo, file))
fc, _, _, err := client.Repositories.GetContents(ctx, owner, repo, file, &github.RepositoryContentGetOptions{Ref: branch})
if err != nil {
return nil
}
if fc == nil {
return fmt.Errorf("File %s not a file in in repository %s/%s or repository is not readable", file, owner, repo)
}

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,
Path: file,
}
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, nil)
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)
}
Loading

0 comments on commit 94328a1

Please sign in to comment.