package main import ( "errors" "fmt" "os" "regexp" "strings" "code.gitea.io/sdk/gitea" "github.com/cqroot/prompt" "github.com/cqroot/prompt/input" "github.com/cqroot/prompt/multichoose" "github.com/google/go-github/github" "golang.org/x/exp/slices" ) // getGithubUsername prompts the user for their Github username. // The username is returned as a string. func getGithubUsername() string { result, err := prompt.New().Ask("Github username").Input("johndoe69") checkErr(err) return result } // getGithubToken prompts the user for their Github token. // The token is returned as a string. func getGithubToken() string { result, err := prompt.New().Ask("Github personal access token"). Input("", input.WithEchoMode(input.EchoPassword)) checkErr(err) return result } // getGiteaHost prompts the user for the host of their Gitea instance. // The host is returned as a string. func getGiteaHost() string { result, err := prompt.New().Ask("Gitea host").Input("https://gitea.com") checkErr(err) return result } // getGiteaUser prompts the user for their Gitea username. This is where the // repositories will be migrated to. // The username is returned as a string. func getGiteaUser() string { result, err := prompt.New().Ask("Gitea username").Input("johndoe69") checkErr(err) return result } // getGiteaToken prompts the user for their Gitea application token. // The token is returned as a string. func getGiteaToken() string { result, err := prompt.New().Ask("Gitea application token"). Input("", input.WithEchoMode(input.EchoPassword)) checkErr(err) return result } // getRepos first prompts users to choose how they want to select repositories. // They will have the option of fetching a list from github and selecting from that, // entering a list of repositories manually, providing a path to a file containing // a list of repositories, or selecting filters which will be used to fetch a list // of repositories from github. // Once the user has selected how they want to select repositories, they will be // prompted to select repositories based on their chosen method. // The selected repositories are returned as a list of strings. func getRepos(client *github.Client, user string, organization bool) []string { result, err := prompt.New().Ask("How do you want to select repositories?"). Choose([]string{"Fetch from Github", "Enter manually", "File path", "Filters"}) checkErr(err) var repos []string switch result { case "Fetch from Github": repos = getReposFromGithub(client) case "Enter manually": repos = getReposManually() case "File path": repos = getReposFromFile() case "Filters": repos = getReposByType(client, user, organization) } should_continue, err := prompt.New().Ask(fmt.Sprintf("You have selected %d repositories. Continue?", len(repos))). Choose([]string{"Yes", "No"}) checkErr(err) if should_continue == "No" { os.Exit(0) } return repos } // getReposFromGithub first asks the user for the name of the organization, // or user, whose repositories they want to migrate. // It then fetches a list of repositories from github, and prompts the user // to select which repositories they want to migrate. // The selected repositories are returned as a list of strings. func getReposFromGithub(client *github.Client) []string { result, err := prompt.New().Ask("Select repositories from:"). Choose([]string{"Organization", "User"}) checkErr(err) var repo_names []string switch result { case "Organization": result, err := prompt.New().Ask("Organization name").Input("acme") checkErr(err) repos := fetchReposForUser(client, result, true) repo_names = append(repo_names, repos...) case "User": result, err := prompt.New().Ask("User name").Input("johndoe69") checkErr(err) repos := fetchReposForUser(client, result, false) repo_names = append(repo_names, repos...) } selected, err := prompt.New().Ask("Select repositories:"). MultiChoose(repo_names, multichoose.WithTheme(ThemeScroll)) checkErr(err) return selected } // getReposManually prompts the user to enter a list of repositories manually. // The list of repositories is returned as a list of strings. func getReposManually() []string { result, err := prompt.New().Ask("Enter repositories manually"). Input("acme/repo1, acme/repo2, johndoe69/repo3") checkErr(err) repos := regexp.MustCompile(`\s*,\s*`).Split(result, -1) for i, repo := range repos { repos[i] = regexp.MustCompile(`^https?://(www.)?github.com/`).ReplaceAllString(repo, "") } return repos } // getReposFromFile prompts the user to enter a path to a file containing a repository // on each line. It then reads the file and returns the list of repositories as a list // of strings. func getReposFromFile() []string { result, err := prompt.New().Ask("Enter path to file containing repositories (one on each line)"). Input("./repos.txt") checkErr(err) data, err := os.ReadFile(result) checkErr(err) // Convert the bytes into a string str := string(data) repos := regexp.MustCompile(`\s*\n\s*`).Split(str, -1) for i, repo := range repos { repos[i] = regexp.MustCompile(`^https?://(www.)?github.com/`).ReplaceAllString(repo, "") } return repos } // getReposByType prompts the user to select the types of repositories they want to migrate. // The selected repositories are returned as a list of strings. func getReposByType(client *github.Client, user string, organization bool) []string { types, err := prompt.New().Ask("Select visibility:"). MultiChoose([]string{"All", "Public", "Private", "Forks", "Sources", "Member"}) checkErr(err) repos := fetchReposByType(client, user, organization, types) return repos } // getRepoVisibility prompts the user to select the visibility of the migrated repositories. // The selected visibility is returned as a string. func getRepoVisibility() string { result, err := prompt.New().Ask("Select visibility:"). Choose([]string{"Maintain", "Public", "Private"}) checkErr(err) return result } // getShouldMirror prompts the user to select whether they want to mirror the // repositories, or migrate them as a one-time copy. // The user's selection is returned as a boolean. func getShouldMirror() bool { result, err := prompt.New().Ask("Mirror repositories"). Choose([]string{"Yes", "No"}) checkErr(err) if result == "Yes" { return true } return false } // getItemsToMigrate prompts the user to select which items they want to migrate. // The user's selection is returned as a list of strings. func getItemsToMigrate() []string { result, err := prompt.New().Ask("Select items to migrate:"). MultiChoose([]string{"Issues", "Wiki", "Labels", "Pull Requests", "Milestones", "Releases"}) checkErr(err) return result } // getPushMirrorToGithub prompts the user to select whether they want to push // changes to the mirror on Gitea to the original repository on Github. // The user's selection is returned as a boolean. func getPushMirrorToGithub() bool { result, err := prompt.New().Ask("Would you like to mirror pushes to github?"). Choose([]string{"Yes", "No"}) checkErr(err) if result == "Yes" { return true } return false } // getUseGithubIssueTracker prompts the user to select whether they want to use // the github issue tracker, or migrate issues. This option is only available if // issues are not being migrated. // The user's selection is returned as a boolean. func getUseGithubIssueTracker() bool { result, err := prompt.New().Ask("Use Github issue tracker"). Choose([]string{"Yes", "No"}) checkErr(err) if result == "Yes" { return true } return false } type MigrationOptions struct { github_client *github.Client gitea_client *gitea.Client github_user string github_token string gitea_user string repos []string visibility_map map[string]string visibility string should_mirror bool use_github_issue_tracker bool push_mirror_to_github bool items_to_migrate []string } // executeMigration executes the migration process. // The migration process is executed with the given options. // If an error occurs during the migration process, it is returned. func executeMigration(options *MigrationOptions) error { repos := options.repos gitea_client := options.gitea_client var errs error for _, repo := range repos { fmt.Printf("Migrating %s...\n", repo) repo_address := fmt.Sprintf("https://github.com/%s", repo) repo_parts := strings.Split(repo, "/") repo_name := repo_parts[1] visibility := "public" if options.visibility == "Maintain" { visibility = options.visibility_map[repo] } new_repo, _, err := gitea_client.MigrateRepo(gitea.MigrateRepoOption{ RepoOwner: options.gitea_user, RepoName: repo_name, CloneAddr: repo_address, Service: gitea.GitServiceGithub, AuthUsername: options.github_user, AuthToken: options.github_token, Mirror: options.should_mirror, Private: visibility == "private", Wiki: slices.Contains(options.items_to_migrate, "Wiki"), Milestones: slices.Contains(options.items_to_migrate, "Milestones"), Labels: slices.Contains(options.items_to_migrate, "Labels"), PullRequests: slices.Contains(options.items_to_migrate, "Pull Requests"), Releases: slices.Contains(options.items_to_migrate, "Releases"), Issues: slices.Contains(options.items_to_migrate, "Issues"), }) if err != nil { errs = errors.Join(errs, err) continue } if options.use_github_issue_tracker { hasIssues := true _, _, err := gitea_client.EditRepo(new_repo.Owner.UserName, new_repo.Name, gitea.EditRepoOption{ HasIssues: &hasIssues, ExternalTracker: &gitea.ExternalTracker{ ExternalTrackerURL: repo_address + "/issues", ExternalTrackerFormat: "https://github.com/{user}/{repo}/issues/{index}", ExternalTrackerStyle: "numeric", }, }) if err != nil { errs = errors.Join(errs, err) continue } } } return errs } // Github to Gitea migration tool. // This tool will migrate selected repositories from Github to Gitea, // based on user input. This process is entirely interactive. // The tool will ask for the following: // - Github username // - Github password/token // - Gitea username // - Gitea application token // - Repositories to migrate, or filters to select repositories to migrate // - Whether repos should be mirrored, or migrated as a one-time copy // - Whether to migrate issues, use the github issue tracker, or neither // - How to set visibility of the migrated repos // - Whether to migrate wiki pages, labels, pull requests, milestones, and releases func main() { fmt.Println("Welcome to the Github to Gitea migration tool!") githubUsername := getGithubUsername() githubToken := getGithubToken() githubClient := authenticateGithub(githubUsername, githubToken) giteaHost := getGiteaHost() giteaUser := getGiteaUser() giteaToken := getGiteaToken() giteaClient := authenticateGitea(giteaHost, giteaUser, giteaToken) repos := getRepos(githubClient, "watzon", false) repo_visibility := getRepoVisibility() var visibility_map map[string]string if repo_visibility == "Maintain" { visibility_map = buildRepoVisibilityMap(githubClient, repos) } should_mirror := getShouldMirror() to_migrate := getItemsToMigrate() push_mirror_to_github := false // TODO: Waiting on https://gitea.com/gitea/go-sdk/issues/635 // if !should_mirror { // push_mirror_to_github = getPushMirrorToGithub() // } use_github_issue_tracker := false if !slices.Contains(to_migrate, "Issues") { use_github_issue_tracker = getUseGithubIssueTracker() } err := executeMigration(&MigrationOptions{ github_client: githubClient, gitea_client: giteaClient, github_user: githubUsername, github_token: githubToken, gitea_user: giteaUser, repos: repos, visibility_map: visibility_map, visibility: repo_visibility, should_mirror: should_mirror, use_github_issue_tracker: use_github_issue_tracker, items_to_migrate: to_migrate, push_mirror_to_github: push_mirror_to_github, }) if err != nil { fmt.Println("Error", err) } else { fmt.Println("Migration completed successfully!") } }