GiteaMigrator/main.go

381 lines
12 KiB
Go

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