1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241package cmd
import (
"bufio"
"fmt"
"log"
"os"
"strings"
"github/situ2001.com/gitea-bulk-migration/client"
"github/situ2001.com/gitea-bulk-migration/common"
"github.com/google/go-github/v71/github"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
)
var cliOpts = &common.CliOption{
MigrationCliOption: common.MigrationCliOption{
DuplicationStrategy: common.DuplicationStrategySkip,
DuplicationOnNonMirrorStrategy: common.DuplicationOnNonMirrorStrategySkip,
DeletedRepoStrategy: common.DeletedRepoStrategySkip,
},
}
var rootCmd = &cobra.Command{
Use: "gitea-bulk-migrate",
Short: "Bulk migrate repositories from GitHub to Gitea, as mirror.",
}
func init() {
// Common options
rootCmd.PersistentFlags().StringVar(&cliOpts.EnvFilePath, "env-file", ".env", "path to the env file where store GITEA_URL, GITEA_TOKEN, GITHUB_TOKEN")
rootCmd.PersistentFlags().StringVar(&cliOpts.HttpProxy, "http-proxy", "", "http proxy to access GitHub")
// Migration options
rootCmd.PersistentFlags().StringVar(&cliOpts.MigrationCliOption.GiteaOwner, "gitea-owner", "", "the owner name of the repository after migration, can be username or org name")
rootCmd.PersistentFlags().StringVar(&cliOpts.MigrationCliOption.TypeOfRepoBeingMigrated, "repo-type", "all", "the type of repository being migrated (all, owner, public, private, member)")
rootCmd.PersistentFlags().BoolVar(&cliOpts.MigrationCliOption.ShouldMigrateForkedRepo, "migrate-fork-repo", false, "whether migrate forked repos from GitHub user")
rootCmd.PersistentFlags().BoolVar(&cliOpts.MigrationCliOption.ShouldMigrateLFS, "migrate-lfs", false, "whether migrate the LFS of GitHub repo or not")
rootCmd.PersistentFlags().BoolVar(&cliOpts.MigrationCliOption.TriggerSyncForExistingMirrorRepo, "sync-mirror-repo", false, "Should sync the repo already exists in Gitea and GitHub, after migration")
rootCmd.PersistentFlags().Var(&cliOpts.MigrationCliOption.DuplicationStrategy, "on-duplication", "strategy to handle the repository that already exists in Gitea(as mirror) and GitHub. (skip, overwrite, abort)")
rootCmd.PersistentFlags().Var(&cliOpts.MigrationCliOption.DuplicationOnNonMirrorStrategy, "on-duplication-non-mirror", "strategy to handle the repository that already exists in Gitea(as non-mirror) and GitHub. (skip, overwrite, abort)")
rootCmd.PersistentFlags().Var(&cliOpts.MigrationCliOption.DeletedRepoStrategy, "on-deletion", "strategy to handle the repository that already exists in Gitea but not in GitHub. (skip, delete, abort)")
// load env variables from .env file
InitEnv(&InitEnvOptions{
EnvFile: cliOpts.EnvFilePath,
Proxy: cliOpts.HttpProxy,
})
rootCmd.MarkPersistentFlagRequired("gitea-owner")
EnsureEnvValues()
}
func Execute() {
rootCmd.Run = func(cmd *cobra.Command, args []string) {
envValues := GetEnvValues()
githubClient := client.NewGitHubClient(github.NewClientWithEnvProxy().WithAuthToken(envValues.GithubToken))
giteaClient, err := client.NewGiteaClient(envValues.GiteaUrl, envValues.GiteaToken, &cliOpts.MigrationCliOption)
if err != nil {
log.Fatalln("Error creating Gitea client:", err)
}
// Start fetching repos from Gitea
reposUnderGiteaOwner, err := giteaClient.ListUserReposAll(cliOpts.MigrationCliOption.GiteaOwner)
if err != nil {
log.Fatalln("Error getting Gitea repos:", err)
}
log.Println("Length of repos under Gitea owner:", len(reposUnderGiteaOwner))
repoStr := ""
for _, repo := range reposUnderGiteaOwner {
repoStr += repo.FullName + ", "
}
log.Println(repoStr)
// Start fetching repos from GitHub
reposUnderGithubOwner, err := githubClient.GetAllGitHubRepoByUsername(&cliOpts.MigrationCliOption)
if err != nil {
log.Fatalln("Error getting GitHub repos:", err)
}
log.Println("Length of repos under GitHub owner:", len(reposUnderGithubOwner))
repoStr = ""
for _, repo := range reposUnderGithubOwner {
repoStr += repo.GetFullName() + ", "
}
log.Println(repoStr)
// filtering repos
if !cliOpts.MigrationCliOption.ShouldMigrateForkedRepo {
// filter out forked repos
filteredRepos := make([]*github.Repository, 0)
for _, repo := range reposUnderGithubOwner {
if !repo.GetFork() {
filteredRepos = append(filteredRepos, repo)
}
}
reposUnderGithubOwner = filteredRepos
log.Println("Length of repos under GitHub owner after filtering forked repos:", len(reposUnderGithubOwner))
repoStr := ""
for _, repo := range reposUnderGithubOwner {
repoStr += repo.GetFullName() + ", "
}
log.Println(repoStr)
}
diffSet := common.CompareGitHubAndGitea(reposUnderGithubOwner, reposUnderGiteaOwner)
printDiffSet(&diffSet, &cliOpts.MigrationCliOption)
if !promptUserConfirmation("Do you want to proceed with the migration? (yes/No): ") {
log.Println("Migration aborted by the user.")
return
}
log.Println("Start migrating the Mirror Repos Not on GitHub but on Gitea")
for idx, giteaRepo := range diffSet.MirrorRepoNotExistOnGithub {
log.Printf("(%d/%d) Mirror repo %s is not on GitHub", idx+1, len(diffSet.MirrorRepoNotExistOnGithub), giteaRepo.CloneURL)
switch cliOpts.MigrationCliOption.DeletedRepoStrategy {
case common.DeletedRepoStrategySkip:
log.Println("Skip deleting the repo.")
case common.DeletedRepoStrategyAbort:
log.Fatalln("Abort migration because the repo already exists on Gitea but not on GitHub.")
case common.DeletedRepoStrategyDelete:
log.Println("Start deleting", giteaRepo.FullName, "on Gitea")
giteaClient.DeleteRepo(cliOpts.MigrationCliOption.GiteaOwner, giteaRepo.Name)
log.Println("Deleted repo on Gitea:", giteaRepo.FullName)
}
}
log.Println("Completed.")
log.Println("Start migrating the Repo name are same on both side, but is not mirror repo on Gitea")
for idx, repos := range diffSet.RepoExistBothSideByNameButNotMirrorRepoOnGitea {
log.Printf("(%d/%d) Has non-mirrored repo %s on Gitea, with same name as GitHub repo %s", idx+1, len(diffSet.RepoExistBothSideByNameButNotMirrorRepoOnGitea), repos.GiteaRepo.CloneURL, repos.GithubRepo.GetCloneURL())
switch cliOpts.MigrationCliOption.DuplicationOnNonMirrorStrategy {
case common.DuplicationOnNonMirrorStrategySkip:
log.Println("Skip deleting the repo.")
case common.DuplicationOnNonMirrorStrategyAbort:
log.Fatalln("Abort migration because the repo already exists on Gitea but not on GitHub.")
case common.DuplicationOnNonMirrorStrategyOverwrite:
log.Println("Start deleting", repos.GiteaRepo.FullName, "on Gitea")
giteaClient.DeleteRepo(cliOpts.MigrationCliOption.GiteaOwner, repos.GiteaRepo.Name)
log.Println("Deleted repo on Gitea:", repos.GiteaRepo.FullName)
log.Println("Start migrating to Gitea")
giteaClient.MirrorGithubRepository(repos.GithubRepo, cliOpts.MigrationCliOption.GiteaOwner, envValues.GithubToken)
log.Println("Migrated repo on Gitea:", repos.GithubRepo.GetFullName())
}
}
log.Println("Completed.")
log.Println("Start migrating the GitHub Repos Not Mirrored on Gitea")
for idx, repo := range diffSet.GithubRepoNotMirroredOnGitea {
log.Printf("(%d/%d) GitHub repo %s is not mirrored on Gitea", idx+1, len(diffSet.GithubRepoNotMirroredOnGitea), repo.GetCloneURL())
log.Println("Start migrating to Gitea")
giteaClient.MirrorGithubRepository(repo, cliOpts.MigrationCliOption.GiteaOwner, envValues.GithubToken)
log.Println("Migrated repo on Gitea:", repo.GetFullName())
}
log.Println("Start migrating the Repos Mirrored on Both Sides")
for idx, repos := range diffSet.RepoExistBothSideWithSameUrl {
log.Printf("(%d/%d) GitHub repo %s is already mirrored on Gitea repo %s", idx+1, len(diffSet.RepoExistBothSideWithSameUrl), repos.GithubRepo.GetCloneURL(), repos.GiteaRepo.CloneURL)
if cliOpts.MigrationCliOption.TriggerSyncForExistingMirrorRepo {
log.Println("Start triggering sync for repo on Gitea")
giteaClient.MirrorSync(repos.GiteaRepo.Owner.UserName, repos.GiteaRepo.Name)
log.Println("Triggered sync for repo on Gitea:", repos.GithubRepo.GetFullName())
}
}
log.Println("Completed.")
}
if err := rootCmd.Execute(); err != nil {
log.Fatalln("Error executing command:", err)
}
}
func printDiffSet(diffSet *common.MigrationDifferenceSet, migrationCliOption *common.MigrationCliOption) {
log.Println("DiffSet Summary:")
log.Println()
// Create a table for each category in diffSet
log.Printf("Strategy: %s\n", migrationCliOption.DeletedRepoStrategy)
printTable("Mirror Repos Not on GitHub but on Gitea", []string{"Index", "Repo Name", "Clone URL"}, func(table *tablewriter.Table) {
for idx, repo := range diffSet.MirrorRepoNotExistOnGithub {
table.Append([]string{fmt.Sprintf("%d", idx+1), repo.FullName, repo.CloneURL})
}
})
log.Printf("Strategy: %s\n", migrationCliOption.DuplicationOnNonMirrorStrategy)
printTable("Repo name are same on both side, but is not mirror repo on Gitea", []string{"Index", "Gitea Repo", "GitHub Repo"}, func(table *tablewriter.Table) {
for idx, repos := range diffSet.RepoExistBothSideByNameButNotMirrorRepoOnGitea {
table.Append([]string{fmt.Sprintf("%d", idx+1), repos.GiteaRepo.FullName, repos.GithubRepo.GetFullName()})
}
})
log.Printf("Will be migrated\n")
printTable("GitHub Repos Not Mirrored on Gitea", []string{"Index", "Repo Name", "Clone URL"}, func(table *tablewriter.Table) {
for idx, repo := range diffSet.GithubRepoNotMirroredOnGitea {
table.Append([]string{fmt.Sprintf("%d", idx+1), repo.GetFullName(), repo.GetCloneURL()})
}
})
log.Printf("Will trigger sync: %v\n", migrationCliOption.TriggerSyncForExistingMirrorRepo)
printTable("Repos Mirrored on Both Sides", []string{"Index", "GitHub Repo", "Gitea Repo"}, func(table *tablewriter.Table) {
for idx, repos := range diffSet.RepoExistBothSideWithSameUrl {
table.Append([]string{fmt.Sprintf("%d", idx+1), repos.GithubRepo.GetFullName(), repos.GiteaRepo.FullName})
}
})
}
func printTable(title string, headers []string, fillTable func(table *tablewriter.Table)) {
fmt.Println(title)
table := tablewriter.NewWriter(os.Stdout) // TODO can it be logged to file?
table.SetHeader(headers)
fillTable(table)
table.Render()
fmt.Println()
}
// Function to prompt the user for confirmation
func promptUserConfirmation(message string) bool {
reader := bufio.NewReader(os.Stdin)
fmt.Print(message)
response, err := reader.ReadString('\n')
if err != nil {
log.Println("Error reading input:", err)
return false
}
response = strings.TrimSpace(strings.ToLower(response))
return response == "yes"
}