273 lines
8.4 KiB
Go
273 lines
8.4 KiB
Go
/*
|
|
Copyright © 2025 Justin Hammond
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
THE SOFTWARE.
|
|
*/
|
|
package migrate
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo"
|
|
"github.com/google/go-github/v68/github"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
var (
|
|
httpclientTransport = http.DefaultClient
|
|
)
|
|
|
|
func handleForgejoError(client *forgejo.Client, Org string, Repo string, err error, initialsync bool) (error) {
|
|
if initialsync && viper.GetBool("delete-on-error") {
|
|
slog.Info("Deleting Failed Migration Repo on Forgejo", "Repo", Repo)
|
|
_, err1 := client.DeleteRepo(Org, Repo)
|
|
if err1 != nil {
|
|
slog.Error("Error deleting failed migration repo", "Error", err)
|
|
return err1;
|
|
}
|
|
}
|
|
if viper.GetBool("exit-on-error") {
|
|
slog.Error("Canceling Migration of remaining Repos due to error")
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getGitHubRepos(httpclient *http.Client) ([]*github.Repository, *github.User, error) {
|
|
ghclient := github.NewClient(httpclient).WithAuthToken(viper.GetString("gh-pat"))
|
|
url, err := url.Parse(viper.GetString("gh-url"))
|
|
if err != nil {
|
|
slog.Error("Error parsing GitHub URL", "Error", err)
|
|
return nil, nil, err;
|
|
}
|
|
ghclient.BaseURL = url
|
|
|
|
ghuser, _, err := ghclient.Users.Get(context.Background(), "")
|
|
if err != nil {
|
|
slog.Error("Error Authenticating to GitHub", "Error", err)
|
|
return nil, nil, err;
|
|
}
|
|
|
|
slog.Info("Github: Authenticated", "user", *ghuser.Login)
|
|
|
|
lopts := github.RepositoryListByAuthenticatedUserOptions{
|
|
Visibility: "all",
|
|
Affiliation: "owner",
|
|
ListOptions: github.ListOptions{PerPage: 100, Page: 1},
|
|
}
|
|
|
|
ignoreRepo := viper.GetStringSlice("ignore-repos")
|
|
includeRepo := viper.GetStringSlice("include-repos")
|
|
repoList := make([]*github.Repository, 0)
|
|
|
|
if len(includeRepo) > 0 {
|
|
for _, repo := range includeRepo {
|
|
reposplit := strings.Split(repo, "/")
|
|
if len(reposplit) != 2 {
|
|
slog.Error("Invalid Repo format", "Repo", repo)
|
|
continue;
|
|
}
|
|
repo, _, err := ghclient.Repositories.Get(context.Background(), reposplit[0], reposplit[1])
|
|
if err != nil {
|
|
slog.Error("Error getting repo", "Error", err)
|
|
continue;
|
|
}
|
|
repoList = append(repoList, repo)
|
|
}
|
|
} else {
|
|
for {
|
|
repos, res, err := ghclient.Repositories.ListByAuthenticatedUser(context.Background(), &lopts)
|
|
|
|
if (err != nil) {
|
|
slog.Error("Error getting repos", "Error", err)
|
|
return nil, nil, err;
|
|
}
|
|
out:
|
|
for _, repo := range repos {
|
|
for _, ignore := range ignoreRepo {
|
|
if strings.Compare(*repo.Name, ignore) == 0 {
|
|
slog.Info("Ignoring Repo", "Repo", *repo.Name)
|
|
continue out
|
|
}
|
|
}
|
|
slog.Debug("Adding Repo", "Repo", *repo.Name)
|
|
repoList = append(repoList, repo)
|
|
}
|
|
|
|
if res.NextPage != 0 {
|
|
lopts.ListOptions.Page = res.NextPage
|
|
//lopts.Page = res.NextPage
|
|
slog.Debug("Getting Next Page of Repos", "page", res.NextPage)
|
|
} else {
|
|
slog.Debug("No More Repos")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return repoList, ghuser, nil
|
|
}
|
|
|
|
func setupForgejo(httpclient *http.Client) (*forgejo.Client, *forgejo.User, error) {
|
|
fgclient, err := forgejo.NewClient(viper.GetString("fg-url"), forgejo.SetToken(viper.GetString("fg-pat")), forgejo.SetHTTPClient(httpclient))
|
|
if err != nil {
|
|
slog.Error("Error Authenticating to Forgejo", "Error", err)
|
|
return nil, nil, err;
|
|
}
|
|
|
|
fguser, _, err := fgclient.GetMyUserInfo();
|
|
if err != nil {
|
|
slog.Error("Error Getting Forgejo", "Error", err)
|
|
return nil, nil, err;
|
|
}
|
|
slog.Info("Forgejo Authenticated", "user", fguser.UserName)
|
|
|
|
/* make sure the target organization exists */
|
|
_, _, err = fgclient.GetOrg(viper.GetString("fg-org"))
|
|
if err != nil {
|
|
slog.Error("Target Organization Does Not Exist", "Error", err)
|
|
return nil, nil, err;
|
|
}
|
|
|
|
return fgclient, fguser, nil
|
|
|
|
}
|
|
|
|
|
|
func StartSync() error {
|
|
repos, ghuser, err := getGitHubRepos(httpclientTransport)
|
|
if err != nil {
|
|
slog.Error("Error getting GitHub Repos", "Error", err)
|
|
return err;
|
|
}
|
|
|
|
fgclient, _, err := setupForgejo(httpclientTransport)
|
|
if err != nil {
|
|
slog.Error("Error getting Forgejo Repos", "Error", err)
|
|
return err;
|
|
}
|
|
|
|
|
|
for _, repo := range repos {
|
|
|
|
var owner string
|
|
if repo.Organization != nil {
|
|
owner = *repo.Organization.Login
|
|
} else {
|
|
owner = *ghuser.Login
|
|
}
|
|
|
|
slog.Debug("Github Repo", "Org", owner, "Repo", *repo.Name)
|
|
fgrepo, _, err := fgclient.GetRepo(viper.GetString("fg-org"), *repo.Name)
|
|
if err != nil {
|
|
slog.Info("Repo not found at Forgejo. Creating", "repo", *repo.Name)
|
|
opts := forgejo.MigrateRepoOption{
|
|
RepoName: *repo.Name,
|
|
RepoOwner: viper.GetString("fg-org"),
|
|
CloneAddr: *repo.CloneURL,
|
|
Service: forgejo.GitServiceGithub,
|
|
AuthToken: viper.GetString("gh-pat"),
|
|
Private: *repo.Private,
|
|
Mirror: viper.GetBool("mirror"),
|
|
Wiki: viper.GetBool("enable-wiki"),
|
|
Issues: viper.GetBool("enable-issues"),
|
|
PullRequests: viper.GetBool("enable-prs"),
|
|
Releases: viper.GetBool("enable-releases"),
|
|
LFS: true,
|
|
}
|
|
if *repo.Archived {
|
|
opts.Mirror = false
|
|
}
|
|
if repo.Description != nil {
|
|
opts.Description = *repo.Description
|
|
}
|
|
if !viper.GetBool("dry-run") {
|
|
slog.Info("Migrating repo to Forgejo", "Org", viper.GetString("fg-org"), "Repo", opts.RepoName)
|
|
fgrepo, _, err = fgclient.MigrateRepo(opts)
|
|
if err != nil {
|
|
slog.Error("Error migrating repo", "Error", err)
|
|
if err := handleForgejoError(fgclient, viper.GetString("fg-org"), opts.RepoName, err, true); err != nil {
|
|
return err;
|
|
}
|
|
} else {
|
|
if !viper.GetBool("mirror") && *repo.Archived {
|
|
slog.Info("Archiving repo at Forgejo", "Repo", opts.RepoName)
|
|
fgrepo, _, err = fgclient.EditRepo(viper.GetString("fg-org"), opts.RepoName, forgejo.EditRepoOption{Archived: forgejo.OptionalBool(true)})
|
|
if err != nil {
|
|
slog.Error("Error archiving repo", "Error", err)
|
|
if err := handleForgejoError(fgclient, viper.GetString("fg-org"), opts.RepoName, err, true); err != nil {
|
|
return err;
|
|
}
|
|
}
|
|
}
|
|
slog.Info("Migrated repo", "Repo", fgrepo.Name)
|
|
|
|
}
|
|
} else {
|
|
slog.Info("Dry Run: Would have created repo at Forgejo", "Org", viper.GetString("fg-org"), "Repo", opts.RepoName)
|
|
}
|
|
} else {
|
|
slog.Debug("Repo found at Forgejo", "repo", *repo.Name)
|
|
}
|
|
if viper.GetBool("sync-metadata") {
|
|
fgupdate := forgejo.EditRepoOption{}
|
|
doupdate := false
|
|
if repo.Description != nil {
|
|
if strings.Compare(fgrepo.Description, *repo.Description) != 0 {
|
|
fgupdate.Description = forgejo.OptionalString(*repo.Description)
|
|
doupdate = true
|
|
}
|
|
}
|
|
if !fgrepo.Mirror && fgrepo.Archived != *repo.Archived {
|
|
fgupdate.Archived = forgejo.OptionalBool(*repo.Archived)
|
|
doupdate = true
|
|
}
|
|
if fgrepo.Private != *repo.Private {
|
|
fgupdate.Private = forgejo.OptionalBool(*repo.Private)
|
|
doupdate = true
|
|
}
|
|
if repo.Homepage != nil {
|
|
if fgrepo.Website != *repo.Homepage {
|
|
fgupdate.Website = forgejo.OptionalString(*repo.Homepage)
|
|
doupdate = true
|
|
}
|
|
}
|
|
if doupdate {
|
|
if !viper.GetBool("dry-run") {
|
|
slog.Info("Updating repo at Forgejo", "Org", viper.GetString("fg-org"), "Repo", *repo.Name)
|
|
_, _, err := fgclient.EditRepo(viper.GetString("fg-org"), *repo.Name, fgupdate)
|
|
if err != nil {
|
|
slog.Error("Error updating repo", "Error", err)
|
|
if err := handleForgejoError(fgclient, viper.GetString("fg-org"), *repo.Name, err, false); err != nil {
|
|
return err;
|
|
}
|
|
}
|
|
} else {
|
|
slog.Info("Dry Run: Would have updated repo at Forgejo", "Org", viper.GetString("fg-org"), "Repo", *repo.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
|
|
}
|