Skip to content
Draft
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
41 changes: 26 additions & 15 deletions backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,29 +63,40 @@ func cloneNewRepo(repoDir string, repo *Repository, bare bool) ([]byte, error) {
log.Printf("Cloning %s\n", repo.Name)
log.Printf("%#v\n", repo)

if repo.Private && ignorePrivate != nil && *ignorePrivate {
if shouldSkipPrivateRepo(repo) {
log.Printf("Skipping %s as it is a private repo.\n", repo.Name)
return nil, nil
}

cloneURL := repo.CloneURL
if useHTTPSClone != nil && *useHTTPSClone {
// Add username and token to the clone URL
// https://gitlab.com/amitsaha/testproject1 => https://amitsaha:[email protected]/amitsaha/testproject1
u, err := url.Parse(repo.CloneURL)
if err != nil {
log.Fatalf("Invalid clone URL: %v\n", err)
}
cloneURL = u.Scheme + "://" + gitHostUsername + ":" + gitHostToken + "@" + u.Host + u.Path
cloneURL := buildAuthenticatedCloneURL(repo.CloneURL)
cmd := buildGitCloneCommand(cloneURL, repoDir, bare)
return cmd.CombinedOutput()
}

// shouldSkipPrivateRepo checks if a private repo should be skipped
func shouldSkipPrivateRepo(repo *Repository) bool {
return repo.Private && ignorePrivate != nil && *ignorePrivate
}

// buildAuthenticatedCloneURL adds authentication to the clone URL if using HTTPS
func buildAuthenticatedCloneURL(originalURL string) string {
if useHTTPSClone == nil || !*useHTTPSClone {
return originalURL
}

var cmd *exec.Cmd
u, err := url.Parse(originalURL)
if err != nil {
log.Fatalf("Invalid clone URL: %v\n", err)
}
return u.Scheme + "://" + gitHostUsername + ":" + gitHostToken + "@" + u.Host + u.Path
}

// buildGitCloneCommand creates the appropriate git clone command
func buildGitCloneCommand(cloneURL, repoDir string, bare bool) *exec.Cmd {
if bare {
cmd = execCommand(gitCommand, "clone", "--mirror", cloneURL, repoDir)
} else {
cmd = execCommand(gitCommand, "clone", cloneURL, repoDir)
return execCommand(gitCommand, "clone", "--mirror", cloneURL, repoDir)
}
return cmd.CombinedOutput()
return execCommand(gitCommand, "clone", cloneURL, repoDir)
}

// setupBackupDir determines and creates the backup directory path
Expand Down
56 changes: 39 additions & 17 deletions bitbucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,38 +18,60 @@ func getBitbucketRepositories(
log.Fatalf("Couldn't acquire a client to talk to %s", service)
}

var repositories []*Repository

resp, err := client.(*bitbucket.Client).Workspaces.List()
workspaces, err := client.(*bitbucket.Client).Workspaces.List()
if err != nil {
return nil, err
}

for _, workspace := range resp.Workspaces {
options := &bitbucket.RepositoriesOptions{Owner: workspace.Slug}
return fetchBitbucketRepositoriesFromWorkspaces(client.(*bitbucket.Client), workspaces.Workspaces)
}

// fetchBitbucketRepositoriesFromWorkspaces retrieves repositories from all workspaces
func fetchBitbucketRepositoriesFromWorkspaces(client *bitbucket.Client, workspaces []bitbucket.Workspace) ([]*Repository, error) {
var repositories []*Repository

resp, err := client.(*bitbucket.Client).Repositories.ListForAccount(options)
for _, workspace := range workspaces {
workspaceRepos, err := fetchBitbucketWorkspaceRepositories(client, workspace.Slug)
if err != nil {
return nil, err
}
repositories = append(repositories, workspaceRepos...)
}

for _, repo := range resp.Items {
namespace := strings.Split(repo.Full_name, "/")[0]
return repositories, nil
}

httpsURL, sshURL := extractBitbucketCloneURLs(repo.Links)
cloneURL := getCloneURL(httpsURL, sshURL)
// fetchBitbucketWorkspaceRepositories retrieves all repositories from a single workspace
func fetchBitbucketWorkspaceRepositories(client *bitbucket.Client, workspaceSlug string) ([]*Repository, error) {
options := &bitbucket.RepositoriesOptions{Owner: workspaceSlug}

resp, err := client.Repositories.ListForAccount(options)
if err != nil {
return nil, err
}

repositories = append(repositories, &Repository{
CloneURL: cloneURL,
Name: repo.Slug,
Namespace: namespace,
Private: repo.Is_private,
})
}
var repositories []*Repository
for _, repo := range resp.Items {
repositories = append(repositories, buildBitbucketRepository(repo))
}

return repositories, nil
}

// buildBitbucketRepository converts a Bitbucket repository to our Repository type
func buildBitbucketRepository(repo bitbucket.Repository) *Repository {
namespace := strings.Split(repo.Full_name, "/")[0]
httpsURL, sshURL := extractBitbucketCloneURLs(repo.Links)
cloneURL := getCloneURL(httpsURL, sshURL)

return &Repository{
CloneURL: cloneURL,
Name: repo.Slug,
Namespace: namespace,
Private: repo.Is_private,
}
}

func extractBitbucketCloneURLs(links map[string]interface{}) (httpsURL, sshURL string) {
linkmaps, ok := links["clone"].([]interface{})
if !ok {
Expand Down
83 changes: 40 additions & 43 deletions github.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,46 +19,30 @@ func getGithubRepositories(
log.Fatalf("Couldn't acquire a client to talk to %s", service)
}

var repositories []*Repository

ctx := context.Background()

if githubRepoType == "starred" {
return getGithubStarredRepositories(ctx, client.(*github.Client), ignoreFork)
}

options := github.RepositoryListOptions{Type: githubRepoType}
return getGithubUserRepositories(ctx, client.(*github.Client), githubRepoType, githubNamespaceWhitelist, ignoreFork)
}

// getGithubUserRepositories retrieves user repositories (not starred) from GitHub
func getGithubUserRepositories(ctx context.Context, client *github.Client, repoType string, namespaceWhitelist []string, ignoreFork bool) ([]*Repository, error) {
var repositories []*Repository
options := github.RepositoryListOptions{Type: repoType}

for {
repos, resp, err := client.(*github.Client).Repositories.List(ctx, "", &options)
repos, resp, err := client.Repositories.List(ctx, "", &options)
if err != nil {
return nil, err
}
for _, repo := range repos {
if *repo.Fork && ignoreFork {
if shouldSkipGithubRepo(repo, namespaceWhitelist, ignoreFork) {
continue
}
namespace := strings.Split(*repo.FullName, "/")[0]

if len(githubNamespaceWhitelist) > 0 && !contains(githubNamespaceWhitelist, namespace) {
continue
}

var httpsCloneURL, sshCloneURL string
if repo.CloneURL != nil {
httpsCloneURL = *repo.CloneURL
}
if repo.SSHURL != nil {
sshCloneURL = *repo.SSHURL
}

cloneURL := getCloneURL(httpsCloneURL, sshCloneURL)
repositories = append(repositories, &Repository{
CloneURL: cloneURL,
Name: *repo.Name,
Namespace: namespace,
Private: *repo.Private,
})
repositories = append(repositories, buildGithubRepository(repo))
}
if resp.NextPage == 0 {
break
Expand All @@ -68,6 +52,35 @@ func getGithubRepositories(
return repositories, nil
}

// shouldSkipGithubRepo determines if a repository should be skipped based on filters
func shouldSkipGithubRepo(repo *github.Repository, namespaceWhitelist []string, ignoreFork bool) bool {
if *repo.Fork && ignoreFork {
return true
}
namespace := strings.Split(*repo.FullName, "/")[0]
return len(namespaceWhitelist) > 0 && !contains(namespaceWhitelist, namespace)
}

// buildGithubRepository converts a GitHub repository to our Repository type
func buildGithubRepository(repo *github.Repository) *Repository {
namespace := strings.Split(*repo.FullName, "/")[0]

var httpsCloneURL, sshCloneURL string
if repo.CloneURL != nil {
httpsCloneURL = *repo.CloneURL
}
if repo.SSHURL != nil {
sshCloneURL = *repo.SSHURL
}

return &Repository{
CloneURL: getCloneURL(httpsCloneURL, sshCloneURL),
Name: *repo.Name,
Namespace: namespace,
Private: *repo.Private,
}
}

func getGithubStarredRepositories(ctx context.Context, client *github.Client, ignoreFork bool) ([]*Repository, error) {
var repositories []*Repository
options := github.ActivityListStarredOptions{}
Expand All @@ -81,23 +94,7 @@ func getGithubStarredRepositories(ctx context.Context, client *github.Client, ig
if *star.Repository.Fork && ignoreFork {
continue
}
namespace := strings.Split(*star.Repository.FullName, "/")[0]

var httpsCloneURL, sshCloneURL string
if star.Repository.CloneURL != nil {
httpsCloneURL = *star.Repository.CloneURL
}
if star.Repository.SSHURL != nil {
sshCloneURL = *star.Repository.SSHURL
}

cloneURL := getCloneURL(httpsCloneURL, sshCloneURL)
repositories = append(repositories, &Repository{
CloneURL: cloneURL,
Name: *star.Repository.Name,
Namespace: namespace,
Private: *star.Repository.Private,
})
repositories = append(repositories, buildGithubRepository(star.Repository))
}
if resp.NextPage == 0 {
break
Expand Down
72 changes: 46 additions & 26 deletions github_create_user_migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import (
"context"
"log"
"time"

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

// defaultMigrationPollingInterval is the default time to wait between migration status checks
const defaultMigrationPollingInterval = 60 * time.Second

func handleGithubCreateUserMigration(client interface{}, c *appConfig) {
repos, err := getRepositories(
client,
Expand All @@ -21,6 +26,12 @@ func handleGithubCreateUserMigration(client interface{}, c *appConfig) {
log.Fatalf("Error getting list of repositories: %v", err)
}

createUserMigration(client, c, repos)
processOrganizationMigrations(client, c)
}

// createUserMigration creates and optionally downloads a user migration
func createUserMigration(client interface{}, c *appConfig, repos []*Repository) {
log.Printf("Creating a user migration for %d repos", len(repos))
m, err := createGithubUserMigration(
context.Background(),
Expand All @@ -33,47 +44,56 @@ func handleGithubCreateUserMigration(client interface{}, c *appConfig) {
}

if c.githubWaitForMigrationComplete {
migrationStatePollingDuration := 60 * time.Second
err = downloadGithubUserMigrationData(
context.Background(),
client, c.backupDir,
m.ID,
migrationStatePollingDuration,
defaultMigrationPollingInterval,
)
if err != nil {
log.Fatalf("Error querying/downloading migration: %v", err)
}
}
}

// processOrganizationMigrations creates migrations for all user-owned organizations
func processOrganizationMigrations(client interface{}, c *appConfig) {
orgs, err := getGithubUserOwnedOrgs(context.Background(), client)
if err != nil {
log.Fatal("Error getting user organizations", err)
}

for _, o := range orgs {
orgRepos, err := getGithubOrgRepositories(context.Background(), client, o)
if err != nil {
log.Fatal("Error getting org repos", err)
}
if len(orgRepos) == 0 {
log.Printf("No repos found in %s", *o.Login)
continue
}
log.Printf("Creating a org migration (%s) for %d repos", *o.Login, len(orgRepos))
oMigration, err := createGithubOrgMigration(context.Background(), client, *o.Login, orgRepos)
if err != nil {
log.Fatalf("Error creating migration: %v", err)
}
if c.githubWaitForMigrationComplete {
migrationStatePollingDuration := 60 * time.Second
downloadGithubOrgMigrationData(
context.Background(),
client,
*o.Login,
c.backupDir,
oMigration.ID,
migrationStatePollingDuration,
)
}
createOrganizationMigration(client, c, o)
}
}

// createOrganizationMigration creates and optionally downloads a migration for a single organization
func createOrganizationMigration(client interface{}, c *appConfig, org *github.Organization) {
orgRepos, err := getGithubOrgRepositories(context.Background(), client, org)
if err != nil {
log.Fatal("Error getting org repos", err)
}

if len(orgRepos) == 0 {
log.Printf("No repos found in %s", *org.Login)
return
}

log.Printf("Creating a org migration (%s) for %d repos", *org.Login, len(orgRepos))
oMigration, err := createGithubOrgMigration(context.Background(), client, *org.Login, orgRepos)
if err != nil {
log.Fatalf("Error creating migration: %v", err)
}

if c.githubWaitForMigrationComplete {
downloadGithubOrgMigrationData(
context.Background(),
client,
*org.Login,
c.backupDir,
oMigration.ID,
defaultMigrationPollingInterval,
)
}
}
Loading
Loading