mirror of
https://codeberg.org/forgejo/forgejo
synced 2024-11-22 18:04:24 +01:00
4a2959b3ec
Also add a test for GogsDownloaderFactory.New() to make sure that the URL of the source repository is parsed correctly. When the source gogs instance is hosted at a subpath like `https://git.example.com/gogs/<username>/<reponame>` the migration fails. This PR fixes that. Co-authored-by: hecker <tomas.hecker@gmail.com> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3572 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Co-authored-by: varp0n <tom@gkstn.de> Co-committed-by: varp0n <tom@gkstn.de>
331 lines
8.6 KiB
Go
331 lines
8.6 KiB
Go
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package migrations
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.gitea.io/gitea/modules/log"
|
|
base "code.gitea.io/gitea/modules/migration"
|
|
"code.gitea.io/gitea/modules/proxy"
|
|
"code.gitea.io/gitea/modules/structs"
|
|
|
|
"github.com/gogs/go-gogs-client"
|
|
)
|
|
|
|
var (
|
|
_ base.Downloader = &GogsDownloader{}
|
|
_ base.DownloaderFactory = &GogsDownloaderFactory{}
|
|
)
|
|
|
|
func init() {
|
|
RegisterDownloaderFactory(&GogsDownloaderFactory{})
|
|
}
|
|
|
|
// GogsDownloaderFactory defines a gogs downloader factory
|
|
type GogsDownloaderFactory struct{}
|
|
|
|
// New returns a Downloader related to this factory according MigrateOptions
|
|
func (f *GogsDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) {
|
|
u, err := url.Parse(opts.CloneAddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
repoNameSpace := strings.TrimSuffix(u.Path, ".git")
|
|
repoNameSpace = strings.Trim(repoNameSpace, "/")
|
|
|
|
fields := strings.Split(repoNameSpace, "/")
|
|
numFields := len(fields)
|
|
if numFields < 2 {
|
|
return nil, fmt.Errorf("invalid path: %s", repoNameSpace)
|
|
}
|
|
|
|
repoOwner := fields[numFields-2]
|
|
repoName := fields[numFields-1]
|
|
|
|
u.Path = ""
|
|
u = u.JoinPath(fields[:numFields-2]...)
|
|
baseURL := u.String()
|
|
|
|
log.Trace("Create gogs downloader. BaseURL: %s RepoOwner: %s RepoName: %s", baseURL, repoOwner, repoName)
|
|
return NewGogsDownloader(ctx, baseURL, opts.AuthUsername, opts.AuthPassword, opts.AuthToken, repoOwner, repoName), nil
|
|
}
|
|
|
|
// GitServiceType returns the type of git service
|
|
func (f *GogsDownloaderFactory) GitServiceType() structs.GitServiceType {
|
|
return structs.GogsService
|
|
}
|
|
|
|
// GogsDownloader implements a Downloader interface to get repository information
|
|
// from gogs via API
|
|
type GogsDownloader struct {
|
|
base.NullDownloader
|
|
ctx context.Context
|
|
client *gogs.Client
|
|
baseURL string
|
|
repoOwner string
|
|
repoName string
|
|
userName string
|
|
password string
|
|
openIssuesFinished bool
|
|
openIssuesPages int
|
|
transport http.RoundTripper
|
|
}
|
|
|
|
// String implements Stringer
|
|
func (g *GogsDownloader) String() string {
|
|
return fmt.Sprintf("migration from gogs server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
|
|
}
|
|
|
|
func (g *GogsDownloader) LogString() string {
|
|
if g == nil {
|
|
return "<GogsDownloader nil>"
|
|
}
|
|
return fmt.Sprintf("<GogsDownloader %s %s/%s>", g.baseURL, g.repoOwner, g.repoName)
|
|
}
|
|
|
|
// SetContext set context
|
|
func (g *GogsDownloader) SetContext(ctx context.Context) {
|
|
g.ctx = ctx
|
|
}
|
|
|
|
// NewGogsDownloader creates a gogs Downloader via gogs API
|
|
func NewGogsDownloader(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GogsDownloader {
|
|
downloader := GogsDownloader{
|
|
ctx: ctx,
|
|
baseURL: baseURL,
|
|
userName: userName,
|
|
password: password,
|
|
repoOwner: repoOwner,
|
|
repoName: repoName,
|
|
}
|
|
|
|
var client *gogs.Client
|
|
if len(token) != 0 {
|
|
client = gogs.NewClient(baseURL, token)
|
|
downloader.userName = token
|
|
} else {
|
|
transport := NewMigrationHTTPTransport()
|
|
transport.Proxy = func(req *http.Request) (*url.URL, error) {
|
|
req.SetBasicAuth(userName, password)
|
|
return proxy.Proxy()(req)
|
|
}
|
|
downloader.transport = transport
|
|
|
|
client = gogs.NewClient(baseURL, "")
|
|
client.SetHTTPClient(&http.Client{
|
|
Transport: &downloader,
|
|
})
|
|
}
|
|
|
|
downloader.client = client
|
|
return &downloader
|
|
}
|
|
|
|
// RoundTrip wraps the provided request within this downloader's context and passes it to our internal http.Transport.
|
|
// This implements http.RoundTripper and makes the gogs client requests cancellable even though it is not cancellable itself
|
|
func (g *GogsDownloader) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
return g.transport.RoundTrip(req.WithContext(g.ctx))
|
|
}
|
|
|
|
// GetRepoInfo returns a repository information
|
|
func (g *GogsDownloader) GetRepoInfo() (*base.Repository, error) {
|
|
gr, err := g.client.GetRepo(g.repoOwner, g.repoName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// convert gogs repo to stand Repo
|
|
return &base.Repository{
|
|
Owner: g.repoOwner,
|
|
Name: g.repoName,
|
|
IsPrivate: gr.Private,
|
|
Description: gr.Description,
|
|
CloneURL: gr.CloneURL,
|
|
OriginalURL: gr.HTMLURL,
|
|
DefaultBranch: gr.DefaultBranch,
|
|
}, nil
|
|
}
|
|
|
|
// GetMilestones returns milestones
|
|
func (g *GogsDownloader) GetMilestones() ([]*base.Milestone, error) {
|
|
perPage := 100
|
|
milestones := make([]*base.Milestone, 0, perPage)
|
|
|
|
ms, err := g.client.ListRepoMilestones(g.repoOwner, g.repoName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, m := range ms {
|
|
milestones = append(milestones, &base.Milestone{
|
|
Title: m.Title,
|
|
Description: m.Description,
|
|
Deadline: m.Deadline,
|
|
State: string(m.State),
|
|
Closed: m.Closed,
|
|
})
|
|
}
|
|
|
|
return milestones, nil
|
|
}
|
|
|
|
// GetLabels returns labels
|
|
func (g *GogsDownloader) GetLabels() ([]*base.Label, error) {
|
|
perPage := 100
|
|
labels := make([]*base.Label, 0, perPage)
|
|
ls, err := g.client.ListRepoLabels(g.repoOwner, g.repoName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, label := range ls {
|
|
labels = append(labels, convertGogsLabel(label))
|
|
}
|
|
|
|
return labels, nil
|
|
}
|
|
|
|
// GetIssues returns issues according start and limit, perPage is not supported
|
|
func (g *GogsDownloader) GetIssues(page, _ int) ([]*base.Issue, bool, error) {
|
|
var state string
|
|
if g.openIssuesFinished {
|
|
state = string(gogs.STATE_CLOSED)
|
|
page -= g.openIssuesPages
|
|
} else {
|
|
state = string(gogs.STATE_OPEN)
|
|
g.openIssuesPages = page
|
|
}
|
|
|
|
issues, isEnd, err := g.getIssues(page, state)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
if isEnd {
|
|
if g.openIssuesFinished {
|
|
return issues, true, nil
|
|
}
|
|
g.openIssuesFinished = true
|
|
}
|
|
|
|
return issues, false, nil
|
|
}
|
|
|
|
func (g *GogsDownloader) getIssues(page int, state string) ([]*base.Issue, bool, error) {
|
|
allIssues := make([]*base.Issue, 0, 10)
|
|
|
|
issues, err := g.client.ListRepoIssues(g.repoOwner, g.repoName, gogs.ListIssueOption{
|
|
Page: page,
|
|
State: state,
|
|
})
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("error while listing repos: %w", err)
|
|
}
|
|
|
|
for _, issue := range issues {
|
|
if issue.PullRequest != nil {
|
|
continue
|
|
}
|
|
allIssues = append(allIssues, convertGogsIssue(issue))
|
|
}
|
|
|
|
return allIssues, len(issues) == 0, nil
|
|
}
|
|
|
|
// GetComments returns comments according issueNumber
|
|
func (g *GogsDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) {
|
|
allComments := make([]*base.Comment, 0, 100)
|
|
|
|
comments, err := g.client.ListIssueComments(g.repoOwner, g.repoName, commentable.GetForeignIndex())
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("error while listing repos: %w", err)
|
|
}
|
|
for _, comment := range comments {
|
|
if len(comment.Body) == 0 || comment.Poster == nil {
|
|
continue
|
|
}
|
|
allComments = append(allComments, &base.Comment{
|
|
IssueIndex: commentable.GetLocalIndex(),
|
|
Index: comment.ID,
|
|
PosterID: comment.Poster.ID,
|
|
PosterName: comment.Poster.Login,
|
|
PosterEmail: comment.Poster.Email,
|
|
Content: comment.Body,
|
|
Created: comment.Created,
|
|
Updated: comment.Updated,
|
|
})
|
|
}
|
|
|
|
return allComments, true, nil
|
|
}
|
|
|
|
// GetTopics return repository topics
|
|
func (g *GogsDownloader) GetTopics() ([]string, error) {
|
|
return []string{}, nil
|
|
}
|
|
|
|
// FormatCloneURL add authentication into remote URLs
|
|
func (g *GogsDownloader) FormatCloneURL(opts MigrateOptions, remoteAddr string) (string, error) {
|
|
if len(opts.AuthToken) > 0 || len(opts.AuthUsername) > 0 {
|
|
u, err := url.Parse(remoteAddr)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(opts.AuthToken) != 0 {
|
|
u.User = url.UserPassword(opts.AuthToken, "")
|
|
} else {
|
|
u.User = url.UserPassword(opts.AuthUsername, opts.AuthPassword)
|
|
}
|
|
return u.String(), nil
|
|
}
|
|
return remoteAddr, nil
|
|
}
|
|
|
|
func convertGogsIssue(issue *gogs.Issue) *base.Issue {
|
|
var milestone string
|
|
if issue.Milestone != nil {
|
|
milestone = issue.Milestone.Title
|
|
}
|
|
labels := make([]*base.Label, 0, len(issue.Labels))
|
|
for _, l := range issue.Labels {
|
|
labels = append(labels, convertGogsLabel(l))
|
|
}
|
|
|
|
var closed *time.Time
|
|
if issue.State == gogs.STATE_CLOSED {
|
|
// gogs client haven't provide closed, so we use updated instead
|
|
closed = &issue.Updated
|
|
}
|
|
|
|
return &base.Issue{
|
|
Title: issue.Title,
|
|
Number: issue.Index,
|
|
PosterID: issue.Poster.ID,
|
|
PosterName: issue.Poster.Login,
|
|
PosterEmail: issue.Poster.Email,
|
|
Content: issue.Body,
|
|
Milestone: milestone,
|
|
State: string(issue.State),
|
|
Created: issue.Created,
|
|
Updated: issue.Updated,
|
|
Labels: labels,
|
|
Closed: closed,
|
|
ForeignIndex: issue.Index,
|
|
}
|
|
}
|
|
|
|
func convertGogsLabel(label *gogs.Label) *base.Label {
|
|
return &base.Label{
|
|
Name: label.Name,
|
|
Color: label.Color,
|
|
}
|
|
}
|