Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 113 additions & 27 deletions github/resource_github_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,24 @@ func resourceGithubRepository() *schema.Resource {
ValidateDiagFunc: toDiagFunc(validation.StringInSlice([]string{"public", "private", "internal"}, false), "visibility"),
Description: "Can be 'public' or 'private'. If your organization is associated with an enterprise account using GitHub Enterprise Cloud or GitHub Enterprise Server 2.20+, visibility can also be 'internal'.",
},
"fork": {
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
Description: "Set to 'true' to fork an existing repository.",
},
"source_owner": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Description: "The owner of the source repository to fork from.",
},
"source_repo": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Description: "The name of the source repository to fork from.",
},
"security_and_analysis": {
Type: schema.TypeList,
Optional: true,
Expand Down Expand Up @@ -556,37 +574,90 @@ func resourceGithubRepositoryCreate(d *schema.ResourceData, meta interface{}) er
repoReq.Private = github.Bool(isPrivate)

if template, ok := d.GetOk("template"); ok {
templateConfigBlocks := template.([]interface{})

for _, templateConfigBlock := range templateConfigBlocks {
templateConfigMap, ok := templateConfigBlock.(map[string]interface{})
if !ok {
return errors.New("failed to unpack template configuration block")
templateConfigBlocks := template.([]interface{})

for _, templateConfigBlock := range templateConfigBlocks {
templateConfigMap, ok := templateConfigBlock.(map[string]interface{})
if !ok {
return errors.New("failed to unpack template configuration block")
}

templateRepo := templateConfigMap["repository"].(string)
templateRepoOwner := templateConfigMap["owner"].(string)
includeAllBranches := templateConfigMap["include_all_branches"].(bool)

templateRepoReq := github.TemplateRepoRequest{
Name: &repoName,
Owner: &owner,
Description: github.String(d.Get("description").(string)),
Private: github.Bool(isPrivate),
IncludeAllBranches: github.Bool(includeAllBranches),
}

repo, _, err := client.Repositories.CreateFromTemplate(ctx,
templateRepoOwner,
templateRepo,
&templateRepoReq,
)
if err != nil {
return err
}

d.SetId(*repo.Name)
}
} else if d.Get("fork").(bool) {
// Handle repository forking
sourceOwner := d.Get("source_owner").(string)
sourceRepo := d.Get("source_repo").(string)
requestedName := d.Get("name").(string)
owner := meta.(*Owner).name
log.Printf("[INFO] Creating fork of %s/%s in %s", sourceOwner, sourceRepo, owner)

if sourceOwner == "" || sourceRepo == "" {
return fmt.Errorf("source_owner and source_repo must be provided when forking a repository")
}

templateRepo := templateConfigMap["repository"].(string)
templateRepoOwner := templateConfigMap["owner"].(string)
includeAllBranches := templateConfigMap["include_all_branches"].(bool)

templateRepoReq := github.TemplateRepoRequest{
Name: &repoName,
Owner: &owner,
Description: github.String(d.Get("description").(string)),
Private: github.Bool(isPrivate),
IncludeAllBranches: github.Bool(includeAllBranches),

// Create the fork using the GitHub client library
opts := &github.RepositoryCreateForkOptions{
Name: requestedName,
}

repo, _, err := client.Repositories.CreateFromTemplate(ctx,
templateRepoOwner,
templateRepo,
&templateRepoReq,
)

if meta.(*Owner).IsOrganization {
opts.Organization = owner
}

fork, resp, err := client.Repositories.CreateFork(ctx, sourceOwner, sourceRepo, opts)

if err != nil {
return err
// Handle accepted error (202) which means the fork is being created asynchronously
if _, ok := err.(*github.AcceptedError); ok {
log.Printf("[INFO] Fork is being created asynchronously")
// Despite the 202 status, the API should still return preliminary fork information
if fork == nil {
return fmt.Errorf("fork information not available after accepted status")
}
log.Printf("[DEBUG] Fork name: %s", fork.GetName())
} else {
return fmt.Errorf("failed to create fork: %v", err)
}
} else if resp != nil {
log.Printf("[DEBUG] Fork response status: %d", resp.StatusCode)
}

d.SetId(*repo.Name)
}

if fork == nil {
return fmt.Errorf("fork creation failed - no repository returned")
}

log.Printf("[INFO] Fork created with name: %s", fork.GetName())
d.SetId(fork.GetName())
log.Printf("[DEBUG] Set resource ID to just the name: %s", d.Id())

d.Set("name", fork.GetName())
d.Set("full_name", fork.GetFullName()) // Add the full name for reference
d.Set("html_url", fork.GetHTMLURL())
d.Set("ssh_clone_url", fork.GetSSHURL())
d.Set("git_clone_url", fork.GetGitURL())
d.Set("http_clone_url", fork.GetCloneURL())
} else {
// Create without a repository template
var repo *github.Repository
Expand Down Expand Up @@ -705,6 +776,21 @@ func resourceGithubRepositoryRead(d *schema.ResourceData, meta interface{}) erro
}
}

// Set fork information if this is a fork
if repo.GetFork() {
d.Set("fork", true)

// If the repository has parent information, set the source details
if repo.Parent != nil {
d.Set("source_owner", repo.Parent.GetOwner().GetLogin())
d.Set("source_repo", repo.Parent.GetName())
}
} else {
d.Set("fork", false)
d.Set("source_owner", "")
d.Set("source_repo", "")
}

if repo.TemplateRepository != nil {
if err = d.Set("template", []interface{}{
map[string]interface{}{
Expand Down
152 changes: 152 additions & 0 deletions github/resource_github_repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1700,3 +1700,155 @@ func TestGithubRepositoryNameFailsValidationWithSpace(t *testing.T) {
t.Error(fmt.Errorf("unexpected name validation failure; expected=%s; action=%s", expectedFailure, actualFailure))
}
}

func TestAccGithubRepository_fork(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)

t.Run("forks a repository without error", func(t *testing.T) {
config := fmt.Sprintf(`
resource "github_repository" "forked" {
name = "terraform-provider-github-%s"
description = "Terraform acceptance test - forked repository %[1]s"
fork = true
source_owner = "integrations"
source_repo = "terraform-provider-github"
}
`, randomID)

check := resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(
"github_repository.forked", "fork",
"true",
),
resource.TestCheckResourceAttrSet(
"github_repository.forked", "html_url",
),
resource.TestCheckResourceAttrSet(
"github_repository.forked", "ssh_clone_url",
),
resource.TestCheckResourceAttrSet(
"github_repository.forked", "git_clone_url",
),
resource.TestCheckResourceAttrSet(
"github_repository.forked", "http_clone_url",
),
)

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 individual account", func(t *testing.T) {
testCase(t, individual)
})

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

t.Run("with an anonymous account", func(t *testing.T) {
t.Skip("anonymous account not supported for this operation")
})
})

t.Run("can update forked repository properties", func(t *testing.T) {
initialConfig := fmt.Sprintf(`
resource "github_repository" "forked_update" {
name = "terraform-provider-github-update-%s"
description = "Initial description for forked repo"
fork = true
source_owner = "integrations"
source_repo = "terraform-provider-github"
has_wiki = true
has_issues = false
}
`, randomID)

updatedConfig := fmt.Sprintf(`
resource "github_repository" "forked_update" {
name = "terraform-provider-github-update-%s"
description = "Updated description for forked repo"
fork = true
source_owner = "integrations"
source_repo = "terraform-provider-github"
has_wiki = false
has_issues = true
}
`, randomID)

checks := map[string]resource.TestCheckFunc{
"before": resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(
"github_repository.forked_update", "description",
"Initial description for forked repo",
),
resource.TestCheckResourceAttr(
"github_repository.forked_update", "has_wiki",
"true",
),
resource.TestCheckResourceAttr(
"github_repository.forked_update", "has_issues",
"false",
),
),
"after": resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(
"github_repository.forked_update", "description",
"Updated description for forked repo",
),
resource.TestCheckResourceAttr(
"github_repository.forked_update", "has_wiki",
"false",
),
resource.TestCheckResourceAttr(
"github_repository.forked_update", "has_issues",
"true",
),
),
}

testCase := func(t *testing.T, mode string) {
resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessMode(t, mode) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: initialConfig,
Check: checks["before"],
},
{
Config: updatedConfig,
Check: checks["after"],
},
{
ResourceName: "github_repository.forked_update",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{ "auto_init"},
},
},
})
}

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("with an anonymous account", func(t *testing.T) {
t.Skip("anonymous account not supported for this operation")
})
})
}