GitHub Repository mirroring for Gitea
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

256 lines
6.7 KiB

package main
import (
"context"
"fmt"
"log"
"os"
"sort"
"time"
"code.gitea.io/sdk/gitea"
"github.com/google/go-github/github"
"github.com/spf13/viper"
"golang.org/x/oauth2"
)
// Sortable Gitea repository list
type GTRepositoryList []*gitea.Repository
func (list GTRepositoryList) Len() int {
return len(list)
}
func (list GTRepositoryList) Less(i, j int) bool {
return list[i].Name < list[j].Name
}
func (list GTRepositoryList) Swap(i, j int) {
list[i], list[j] = list[j], list[i]
}
// Sortable Github repository list
type GHRepositoryList []*github.Repository
func (list GHRepositoryList) Len() int {
return len(list)
}
func (list GHRepositoryList) Less(i, j int) bool {
return *list[i].Name < *list[j].Name
}
func (list GHRepositoryList) Swap(i, j int) {
list[i], list[j] = list[j], list[i]
}
func initConfig() {
viper.SetConfigName("mirror-github") // name of config file (without extension)
viper.AddConfigPath("/etc/")
viper.AddConfigPath("$HOME/.mirror-github")
viper.AddConfigPath(".") // optionally look for config in the working directory
err := viper.ReadInConfig()
if err != nil {
panic(fmt.Errorf("Cannot read config file: %s\n", err))
}
for _, config := range []string{"GitHub.PersonalToken", "Gitea.ServerURL", "Gitea.PersonalToken"} {
if viper.GetString(config) == "" {
panic(fmt.Sprintf("key %s is missing from configuration file", config))
}
}
}
func initLogFile() {
logFile := viper.GetString("LogFile")
if logFile != "" {
logHandle, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
panic(fmt.Errorf("Cannot open log file '%s': %s\n", logFile, err))
}
log.SetOutput(logHandle)
}
}
func initGitHubClient() (*github.Client, *github.User) {
ghToken := viper.GetString("GitHub.PersonalToken")
ctx := context.Background()
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: ghToken},
)
ghClient := github.NewClient(oauth2.NewClient(ctx, ts))
ghUserInfo, _, err := ghClient.Users.Get(ctx, "")
if err != nil {
log.Fatal(err)
}
return ghClient, ghUserInfo
}
func listGitHubRepositories(ghClient *github.Client) GHRepositoryList {
var allRepos []*github.Repository
opt := &github.RepositoryListOptions{
ListOptions: github.ListOptions{PerPage: 100},
}
for {
repos, resp, err := ghClient.Repositories.List(context.Background(), "", opt)
if err != nil {
if _, ok := err.(*github.RateLimitError); ok {
log.Printf("Reached the GitHub API rate limiting. Sleeping for a while...\n")
time.Sleep(60 * time.Second)
continue
} else {
log.Fatal(err)
}
}
allRepos = append(allRepos, repos...)
if resp.NextPage == 0 {
break
}
opt.Page = resp.NextPage
}
return allRepos
}
func filterGitHubRepositories(ghRepos GHRepositoryList, ghUsername string) GHRepositoryList {
var filteredRepos []*github.Repository
for _, repo := range ghRepos {
// Mirror only repositories that the user owns and has not forked from someone else
if !*repo.Fork && *repo.Owner.Login == ghUsername {
filteredRepos = append(filteredRepos, repo)
}
}
return filteredRepos
}
func initGiteaClient() (*gitea.Client, *gitea.User) {
gtClient := gitea.NewClient(viper.GetString("Gitea.ServerURL"), viper.GetString("Gitea.PersonalToken"))
gtUserInfo, err := gtClient.GetMyUserInfo()
if err != nil {
log.Fatal(err)
}
return gtClient, gtUserInfo
}
func listGiteaRepositories(gtClient *gitea.Client) GTRepositoryList {
var allRepos GTRepositoryList
opt := gitea.ListReposOptions{
ListOptions: gitea.ListOptions{PageSize: 50},
}
var page int = 1
for {
opt.Page = page
repos, err := gtClient.ListMyRepos(opt)
if err != nil {
log.Fatal(err)
}
allRepos = append(allRepos, repos...)
if len(repos) == 0 || len(repos) != opt.ListOptions.PageSize {
// End of repository list
break
}
page = page + 1
}
return allRepos
}
func computeRepositoriesToMigrate(gh GHRepositoryList, gt GTRepositoryList) GHRepositoryList {
var toMigrate GHRepositoryList = make(GHRepositoryList, 0)
if len(gh) == 0 {
return gh
}
if len(gt) == 0 {
return gh
}
var ghi int = 0
var gti int = 0
for {
if ghi >= len(gh) { // No more GitHub repos to process...
if gti >= len(gt) { // and no more Gitea repos to process !
break
}
// On Gitea but not anymore on GitHub. We could remove the repo from gitea
// but to be safe, keep it there.
log.Printf("Gitea Repository %s is missing from GitHub, leaving it there...\n", gt[gti].Name)
gti = gti + 1
continue
}
if gti >= len(gt) { // No more Gitea repos to process
toMigrate = append(toMigrate, gh[ghi:len(gh)]...)
break
}
if *gh[ghi].Name == gt[gti].Name {
ghi = ghi + 1
gti = gti + 1
} else if *gh[ghi].Name < gt[gti].Name {
// On GitHub but not yet on Gitea. Migrate it.
toMigrate = append(toMigrate, gh[ghi])
ghi = ghi + 1
} else {
// On Gitea but not anymore on GitHub. We could remove the repo from gitea
// but to be safe, keep it there.
log.Printf("Gitea Repository %s is missing from GitHub, leaving it there...\n", gt[gti].Name)
gti = gti + 1
}
}
return toMigrate
}
func migrate(ghRepo *github.Repository, gtClient *gitea.Client, gtUser *gitea.User) {
migrationOptions := gitea.MigrateRepoOption{
CloneAddr: *ghRepo.CloneURL,
AuthUsername: *ghRepo.Owner.Login,
AuthPassword: viper.GetString("GitHub.PersonalToken"),
UID: int(gtUser.ID),
RepoName: *ghRepo.Name,
Mirror: true,
Private: *ghRepo.Private,
Description: *ghRepo.Description,
}
_, err := gtClient.MigrateRepo(migrationOptions)
if err != nil {
log.Fatal(fmt.Sprintf("MigrateRepo: %s: %s", ghRepo.Name, err))
}
}
func main() {
initConfig()
initLogFile()
// List Gitea repositories
gtClient, gtUserInfo := initGiteaClient()
log.Printf("Connected to Gitea as %s.\n", gtUserInfo.UserName)
gtRepos := listGiteaRepositories(gtClient)
log.Printf("Found %d Gitea repositories\n", len(gtRepos))
// List GitHub repositories
ghClient, ghUserInfo := initGitHubClient()
log.Printf("Connected to GitHub as %s.\n", *ghUserInfo.Login)
ghRepos := listGitHubRepositories(ghClient)
log.Printf("Found %d GitHub repositories.\n", len(ghRepos))
ghRepos = filterGitHubRepositories(ghRepos, *ghUserInfo.Login)
log.Printf("There are %d repositories left after filtering.\n", len(ghRepos))
// Sort all lists by repository name so that computeRepositoriesToMigrate
// can perform a diff
sort.Sort(gtRepos)
sort.Sort(ghRepos)
toMigrate := computeRepositoriesToMigrate(ghRepos, gtRepos)
log.Printf("There are %d repositories to migrate from GitHub to Gitea.\n", len(toMigrate))
// Migrate each repository
for _, repo := range toMigrate {
fmt.Printf("Migrating %s...\n", *repo.Name)
migrate(repo, gtClient, gtUserInfo)
}
}