381 lines
12 KiB
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!")
|
|
}
|
|
}
|