mirror of
https://github.com/Luzifer/github2gitea.git
synced 2024-11-09 15:50:01 +00:00
191 lines
5.7 KiB
Go
191 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/Luzifer/rconfig/v2"
|
|
"github.com/google/go-github/github"
|
|
"github.com/pkg/errors"
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
var (
|
|
cfg = struct {
|
|
DryRun bool `flag:"dry-run,n" default:"false" description:"Only report actions to be done, don't execute them"`
|
|
GiteaToken string `flag:"gitea-token" default:"" description:"Token to interact with Gitea instance" validate:"nonzero"`
|
|
GiteaURL string `flag:"gitea-url" default:"" description:"URL of the Gitea instance" validate:"nonzero"`
|
|
GithubToken string `flag:"github-token" default:"" description:"Github access token" validate:"nonzero"`
|
|
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
|
|
MigrateArchived bool `flag:"migrate-archived" default:"false" description:"Create migrations for archived repos"`
|
|
MigrateForks bool `flag:"migrate-forks" default:"false" description:"Create migrations for forked repos"`
|
|
MigratePrivate bool `flag:"migrate-private" default:"true" description:"Migrate private repos (the given Github Token will be entered as sync credential!)"`
|
|
NoMirror bool `flag:"no-mirror" default:"false" description:"Do not enable mirroring but instad do a one-time clone"`
|
|
SourceExpression string `flag:"source-expression" default:"" description:"Regular expression to match the full name of the source repo (i.e. '^Luzifer/.*$')" validate:"nonzero"`
|
|
TargetUser int64 `flag:"target-user" default:"0" description:"ID of the User / Organization in Gitea to assign the repo to" validate:"nonzero"`
|
|
TargetUserName string `flag:"target-user-name" default:"" description:"Username of the given ID (to check whether repo already exists)" validate:"nonzero"`
|
|
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
|
}{}
|
|
|
|
version = "dev"
|
|
)
|
|
|
|
func init() {
|
|
rconfig.AutoEnv(true)
|
|
if err := rconfig.ParseAndValidate(&cfg); err != nil {
|
|
log.Fatalf("Unable to parse commandline options: %s", err)
|
|
}
|
|
|
|
if cfg.VersionAndExit {
|
|
fmt.Printf("create-gitea-migration %s\n", version)
|
|
os.Exit(0)
|
|
}
|
|
|
|
if l, err := log.ParseLevel(cfg.LogLevel); err != nil {
|
|
log.WithError(err).Fatal("Unable to parse log level")
|
|
} else {
|
|
log.SetLevel(l)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
log.WithFields(log.Fields{
|
|
"dry-run": cfg.DryRun,
|
|
"source": cfg.SourceExpression,
|
|
"target-user": cfg.TargetUserName,
|
|
"version": version,
|
|
}).Info("create-gitea-migration started")
|
|
|
|
log.Info("Collecting source repos...")
|
|
repos, err := fetchGithubRepos()
|
|
if err != nil {
|
|
log.WithError(err).Fatal("Failed to fetch repos")
|
|
}
|
|
|
|
log.Info("Creating target repos...")
|
|
for _, r := range repos {
|
|
if err := giteaCreateMigration(r); err != nil {
|
|
log.WithError(err).Error("Unable to create mirror")
|
|
}
|
|
}
|
|
}
|
|
|
|
func fetchGithubRepos() ([]*github.Repository, error) {
|
|
ctx := context.Background()
|
|
|
|
ts := oauth2.StaticTokenSource(
|
|
&oauth2.Token{AccessToken: cfg.GithubToken},
|
|
)
|
|
tc := oauth2.NewClient(ctx, ts)
|
|
|
|
client := github.NewClient(tc)
|
|
|
|
opt := &github.RepositoryListOptions{
|
|
ListOptions: github.ListOptions{PerPage: 100},
|
|
}
|
|
|
|
sourceExpr := regexp.MustCompile(cfg.SourceExpression)
|
|
|
|
// get all pages of results
|
|
var allRepos []*github.Repository
|
|
for {
|
|
repos, resp, err := client.Repositories.List(ctx, "", opt)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "Unable to list repos")
|
|
}
|
|
|
|
for _, r := range repos {
|
|
if !sourceExpr.MatchString(*r.FullName) {
|
|
log.WithField("repo", *r.FullName).Debug("Skip: Name does not match")
|
|
continue
|
|
}
|
|
|
|
if !cfg.MigrateArchived && boolFromPtr(r.Archived) {
|
|
log.WithField("repo", *r.FullName).Debug("Skip: Archived")
|
|
continue
|
|
}
|
|
|
|
if !cfg.MigrateForks && boolFromPtr(r.Fork) {
|
|
log.WithField("repo", *r.FullName).Debug("Skip: Fork")
|
|
continue
|
|
}
|
|
|
|
allRepos = append(allRepos, r)
|
|
}
|
|
|
|
if resp.NextPage == 0 {
|
|
break
|
|
}
|
|
|
|
opt.Page = resp.NextPage
|
|
}
|
|
|
|
return allRepos, nil
|
|
}
|
|
|
|
func giteaCreateMigration(gr *github.Repository) error {
|
|
logger := log.WithFields(log.Fields{
|
|
"repo": strFromPtr(gr.Name),
|
|
"private": boolFromPtr(gr.Private),
|
|
})
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, giteaURL(strings.Join([]string{"api/v1/repos", cfg.TargetUserName, strFromPtr(gr.Name)}, "/")), nil)
|
|
req.Header.Set("Authorization", "token "+cfg.GiteaToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Unable to create repo in Gitea")
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
logger.Info("Repo already exists, no action required")
|
|
return nil
|
|
}
|
|
|
|
cmr := createMigrationRequestFromGithubRepo(gr)
|
|
|
|
body := new(bytes.Buffer)
|
|
if err := json.NewEncoder(body).Encode(cmr); err != nil {
|
|
return errors.Wrap(err, "Unable to marshal creation request")
|
|
}
|
|
|
|
if cfg.DryRun {
|
|
logger.Warn("Repo not found, will be created in real run (dry-run enabled)")
|
|
return nil
|
|
}
|
|
|
|
logger.Info("Repo not found, creating")
|
|
|
|
req, _ = http.NewRequest(http.MethodPost, giteaURL("api/v1/repos/migrate"), body)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "token "+cfg.GiteaToken)
|
|
|
|
resp, err = http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Unable to create repo in Gitea")
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusCreated {
|
|
body, _ := ioutil.ReadAll(resp.Body)
|
|
return errors.Errorf("Unable to create repo in Gitea: Status %d: %s", resp.StatusCode, body)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func giteaURL(path string) string {
|
|
return strings.Join([]string{
|
|
strings.TrimRight(cfg.GiteaURL, "/"),
|
|
strings.TrimLeft(path, "/"),
|
|
}, "/")
|
|
}
|