Add migrate from OneDev (#16356)
* Use context to simplify logic. * Added migration from OneDev. This PR adds [OneDev](https://code.onedev.io/) as migration source. Supported: - [x] Milestones - [x] Issues - [x] Pull Requests - [x] Comments - [x] Reviews - [x] Labels
This commit is contained in:
parent
2d1935acc7
commit
cee5f7c5e2
@ -33,6 +33,8 @@ func ToGitServiceType(value string) structs.GitServiceType {
|
|||||||
return structs.GitlabService
|
return structs.GitlabService
|
||||||
case "gogs":
|
case "gogs":
|
||||||
return structs.GogsService
|
return structs.GogsService
|
||||||
|
case "onedev":
|
||||||
|
return structs.OneDevService
|
||||||
default:
|
default:
|
||||||
return structs.PlainGitService
|
return structs.PlainGitService
|
||||||
}
|
}
|
||||||
|
@ -13,9 +13,9 @@ import (
|
|||||||
|
|
||||||
// GetCommentOptions represents an options for get comment
|
// GetCommentOptions represents an options for get comment
|
||||||
type GetCommentOptions struct {
|
type GetCommentOptions struct {
|
||||||
IssueNumber int64
|
Context IssueContext
|
||||||
Page int
|
Page int
|
||||||
PageSize int
|
PageSize int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Downloader downloads the site repo information
|
// Downloader downloads the site repo information
|
||||||
@ -30,7 +30,7 @@ type Downloader interface {
|
|||||||
GetComments(opts GetCommentOptions) ([]*Comment, bool, error)
|
GetComments(opts GetCommentOptions) ([]*Comment, bool, error)
|
||||||
SupportGetRepoComments() bool
|
SupportGetRepoComments() bool
|
||||||
GetPullRequests(page, perPage int) ([]*PullRequest, bool, error)
|
GetPullRequests(page, perPage int) ([]*PullRequest, bool, error)
|
||||||
GetReviews(pullRequestNumber int64) ([]*Review, error)
|
GetReviews(pullRequestContext IssueContext) ([]*Review, error)
|
||||||
FormatCloneURL(opts MigrateOptions, remoteAddr string) (string, error)
|
FormatCloneURL(opts MigrateOptions, remoteAddr string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,25 @@ package base
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
|
// IssueContext is used to map between local and foreign issue/PR ids.
|
||||||
|
type IssueContext interface {
|
||||||
|
LocalID() int64
|
||||||
|
ForeignID() int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// BasicIssueContext is a 1:1 mapping between local and foreign ids.
|
||||||
|
type BasicIssueContext int64
|
||||||
|
|
||||||
|
// LocalID gets the local id.
|
||||||
|
func (c BasicIssueContext) LocalID() int64 {
|
||||||
|
return int64(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForeignID gets the foreign id.
|
||||||
|
func (c BasicIssueContext) ForeignID() int64 {
|
||||||
|
return int64(c)
|
||||||
|
}
|
||||||
|
|
||||||
// Issue is a standard issue information
|
// Issue is a standard issue information
|
||||||
type Issue struct {
|
type Issue struct {
|
||||||
Number int64
|
Number int64
|
||||||
@ -25,4 +44,5 @@ type Issue struct {
|
|||||||
Labels []*Label
|
Labels []*Label
|
||||||
Reactions []*Reaction
|
Reactions []*Reaction
|
||||||
Assignees []string
|
Assignees []string
|
||||||
|
Context IssueContext `yaml:"-"`
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ func (n NullDownloader) GetIssues(page, perPage int) ([]*Issue, bool, error) {
|
|||||||
return nil, false, &ErrNotSupported{Entity: "Issues"}
|
return nil, false, &ErrNotSupported{Entity: "Issues"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetComments returns comments according issueNumber
|
// GetComments returns comments according the options
|
||||||
func (n NullDownloader) GetComments(GetCommentOptions) ([]*Comment, bool, error) {
|
func (n NullDownloader) GetComments(GetCommentOptions) ([]*Comment, bool, error) {
|
||||||
return nil, false, &ErrNotSupported{Entity: "Comments"}
|
return nil, false, &ErrNotSupported{Entity: "Comments"}
|
||||||
}
|
}
|
||||||
@ -61,7 +61,7 @@ func (n NullDownloader) GetPullRequests(page, perPage int) ([]*PullRequest, bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetReviews returns pull requests review
|
// GetReviews returns pull requests review
|
||||||
func (n NullDownloader) GetReviews(pullRequestNumber int64) ([]*Review, error) {
|
func (n NullDownloader) GetReviews(pullRequestContext IssueContext) ([]*Review, error) {
|
||||||
return nil, &ErrNotSupported{Entity: "Reviews"}
|
return nil, &ErrNotSupported{Entity: "Reviews"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,6 @@ import (
|
|||||||
// PullRequest defines a standard pull request information
|
// PullRequest defines a standard pull request information
|
||||||
type PullRequest struct {
|
type PullRequest struct {
|
||||||
Number int64
|
Number int64
|
||||||
OriginalNumber int64 `yaml:"original_number"`
|
|
||||||
Title string
|
Title string
|
||||||
PosterName string `yaml:"poster_name"`
|
PosterName string `yaml:"poster_name"`
|
||||||
PosterID int64 `yaml:"poster_id"`
|
PosterID int64 `yaml:"poster_id"`
|
||||||
@ -34,6 +33,7 @@ type PullRequest struct {
|
|||||||
Assignees []string
|
Assignees []string
|
||||||
IsLocked bool `yaml:"is_locked"`
|
IsLocked bool `yaml:"is_locked"`
|
||||||
Reactions []*Reaction
|
Reactions []*Reaction
|
||||||
|
Context IssueContext `yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsForkPullRequest returns true if the pull request from a forked repository but not the same repository
|
// IsForkPullRequest returns true if the pull request from a forked repository but not the same repository
|
||||||
|
@ -182,14 +182,14 @@ func (d *RetryDownloader) GetPullRequests(page, perPage int) ([]*PullRequest, bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetReviews returns pull requests reviews
|
// GetReviews returns pull requests reviews
|
||||||
func (d *RetryDownloader) GetReviews(pullRequestNumber int64) ([]*Review, error) {
|
func (d *RetryDownloader) GetReviews(pullRequestContext IssueContext) ([]*Review, error) {
|
||||||
var (
|
var (
|
||||||
reviews []*Review
|
reviews []*Review
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
err = d.retry(func() error {
|
err = d.retry(func() error {
|
||||||
reviews, err = d.Downloader.GetReviews(pullRequestNumber)
|
reviews, err = d.Downloader.GetReviews(pullRequestContext)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -444,6 +444,7 @@ func (g *GiteaDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, err
|
|||||||
Labels: labels,
|
Labels: labels,
|
||||||
Assignees: assignees,
|
Assignees: assignees,
|
||||||
IsLocked: issue.IsLocked,
|
IsLocked: issue.IsLocked,
|
||||||
|
Context: base.BasicIssueContext(issue.Index),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -466,26 +467,26 @@ func (g *GiteaDownloader) GetComments(opts base.GetCommentOptions) ([]*base.Comm
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
comments, _, err := g.client.ListIssueComments(g.repoOwner, g.repoName, opts.IssueNumber, gitea_sdk.ListIssueCommentOptions{ListOptions: gitea_sdk.ListOptions{
|
comments, _, err := g.client.ListIssueComments(g.repoOwner, g.repoName, opts.Context.ForeignID(), gitea_sdk.ListIssueCommentOptions{ListOptions: gitea_sdk.ListOptions{
|
||||||
// PageSize: g.maxPerPage,
|
// PageSize: g.maxPerPage,
|
||||||
// Page: i,
|
// Page: i,
|
||||||
}})
|
}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, fmt.Errorf("error while listing comments for issue #%d. Error: %v", opts.IssueNumber, err)
|
return nil, false, fmt.Errorf("error while listing comments for issue #%d. Error: %v", opts.Context.ForeignID(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, comment := range comments {
|
for _, comment := range comments {
|
||||||
reactions, err := g.getCommentReactions(comment.ID)
|
reactions, err := g.getCommentReactions(comment.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Unable to load comment reactions during migrating issue #%d for comment %d to %s/%s. Error: %v", opts.IssueNumber, comment.ID, g.repoOwner, g.repoName, err)
|
log.Warn("Unable to load comment reactions during migrating issue #%d for comment %d to %s/%s. Error: %v", opts.Context.ForeignID(), comment.ID, g.repoOwner, g.repoName, err)
|
||||||
if err2 := models.CreateRepositoryNotice(
|
if err2 := models.CreateRepositoryNotice(
|
||||||
fmt.Sprintf("Unable to load reactions during migrating issue #%d for comment %d to %s/%s. Error: %v", opts.IssueNumber, comment.ID, g.repoOwner, g.repoName, err)); err2 != nil {
|
fmt.Sprintf("Unable to load reactions during migrating issue #%d for comment %d to %s/%s. Error: %v", opts.Context.ForeignID(), comment.ID, g.repoOwner, g.repoName, err)); err2 != nil {
|
||||||
log.Error("create repository notice failed: ", err2)
|
log.Error("create repository notice failed: ", err2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
allComments = append(allComments, &base.Comment{
|
allComments = append(allComments, &base.Comment{
|
||||||
IssueIndex: opts.IssueNumber,
|
IssueIndex: opts.Context.LocalID(),
|
||||||
PosterID: comment.Poster.ID,
|
PosterID: comment.Poster.ID,
|
||||||
PosterName: comment.Poster.UserName,
|
PosterName: comment.Poster.UserName,
|
||||||
PosterEmail: comment.Poster.Email,
|
PosterEmail: comment.Poster.Email,
|
||||||
@ -615,6 +616,7 @@ func (g *GiteaDownloader) GetPullRequests(page, perPage int) ([]*base.PullReques
|
|||||||
RepoName: g.repoName,
|
RepoName: g.repoName,
|
||||||
OwnerName: g.repoOwner,
|
OwnerName: g.repoOwner,
|
||||||
},
|
},
|
||||||
|
Context: base.BasicIssueContext(pr.Index),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -626,7 +628,7 @@ func (g *GiteaDownloader) GetPullRequests(page, perPage int) ([]*base.PullReques
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetReviews returns pull requests review
|
// GetReviews returns pull requests review
|
||||||
func (g *GiteaDownloader) GetReviews(index int64) ([]*base.Review, error) {
|
func (g *GiteaDownloader) GetReviews(context base.IssueContext) ([]*base.Review, error) {
|
||||||
if err := g.client.CheckServerVersionConstraint(">=1.12"); err != nil {
|
if err := g.client.CheckServerVersionConstraint(">=1.12"); err != nil {
|
||||||
log.Info("GiteaDownloader: instance to old, skip GetReviews")
|
log.Info("GiteaDownloader: instance to old, skip GetReviews")
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -642,7 +644,7 @@ func (g *GiteaDownloader) GetReviews(index int64) ([]*base.Review, error) {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
prl, _, err := g.client.ListPullReviews(g.repoOwner, g.repoName, index, gitea_sdk.ListPullReviewsOptions{ListOptions: gitea_sdk.ListOptions{
|
prl, _, err := g.client.ListPullReviews(g.repoOwner, g.repoName, context.ForeignID(), gitea_sdk.ListPullReviewsOptions{ListOptions: gitea_sdk.ListOptions{
|
||||||
Page: i,
|
Page: i,
|
||||||
PageSize: g.maxPerPage,
|
PageSize: g.maxPerPage,
|
||||||
}})
|
}})
|
||||||
@ -652,7 +654,7 @@ func (g *GiteaDownloader) GetReviews(index int64) ([]*base.Review, error) {
|
|||||||
|
|
||||||
for _, pr := range prl {
|
for _, pr := range prl {
|
||||||
|
|
||||||
rcl, _, err := g.client.ListPullReviewComments(g.repoOwner, g.repoName, index, pr.ID)
|
rcl, _, err := g.client.ListPullReviewComments(g.repoOwner, g.repoName, context.ForeignID(), pr.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -678,7 +680,7 @@ func (g *GiteaDownloader) GetReviews(index int64) ([]*base.Review, error) {
|
|||||||
|
|
||||||
allReviews = append(allReviews, &base.Review{
|
allReviews = append(allReviews, &base.Review{
|
||||||
ID: pr.ID,
|
ID: pr.ID,
|
||||||
IssueIndex: index,
|
IssueIndex: context.LocalID(),
|
||||||
ReviewerID: pr.Reviewer.ID,
|
ReviewerID: pr.Reviewer.ID,
|
||||||
ReviewerName: pr.Reviewer.UserName,
|
ReviewerName: pr.Reviewer.UserName,
|
||||||
Official: pr.Official,
|
Official: pr.Official,
|
||||||
|
@ -199,7 +199,7 @@ func TestGiteaDownloadRepo(t *testing.T) {
|
|||||||
}, issues)
|
}, issues)
|
||||||
|
|
||||||
comments, _, err := downloader.GetComments(base.GetCommentOptions{
|
comments, _, err := downloader.GetComments(base.GetCommentOptions{
|
||||||
IssueNumber: 4,
|
Context: base.BasicIssueContext(4),
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assertCommentsEqual(t, []*base.Comment{
|
assertCommentsEqual(t, []*base.Comment{
|
||||||
@ -265,7 +265,7 @@ func TestGiteaDownloadRepo(t *testing.T) {
|
|||||||
PatchURL: "https://gitea.com/gitea/test_repo/pulls/12.patch",
|
PatchURL: "https://gitea.com/gitea/test_repo/pulls/12.patch",
|
||||||
}, prs[1])
|
}, prs[1])
|
||||||
|
|
||||||
reviews, err := downloader.GetReviews(7)
|
reviews, err := downloader.GetReviews(base.BasicIssueContext(7))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assertReviewsEqual(t, []*base.Review{
|
assertReviewsEqual(t, []*base.Review{
|
||||||
{
|
{
|
||||||
|
@ -609,6 +609,9 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR
|
|||||||
|
|
||||||
// download patch file
|
// download patch file
|
||||||
err := func() error {
|
err := func() error {
|
||||||
|
if pr.PatchURL == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
// pr.PatchURL maybe a local file
|
// pr.PatchURL maybe a local file
|
||||||
ret, err := uri.Open(pr.PatchURL)
|
ret, err := uri.Open(pr.PatchURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -428,6 +428,7 @@ func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool,
|
|||||||
Closed: issue.ClosedAt,
|
Closed: issue.ClosedAt,
|
||||||
IsLocked: issue.GetLocked(),
|
IsLocked: issue.GetLocked(),
|
||||||
Assignees: assignees,
|
Assignees: assignees,
|
||||||
|
Context: base.BasicIssueContext(*issue.Number),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -441,15 +442,15 @@ func (g *GithubDownloaderV3) SupportGetRepoComments() bool {
|
|||||||
|
|
||||||
// GetComments returns comments according issueNumber
|
// GetComments returns comments according issueNumber
|
||||||
func (g *GithubDownloaderV3) GetComments(opts base.GetCommentOptions) ([]*base.Comment, bool, error) {
|
func (g *GithubDownloaderV3) GetComments(opts base.GetCommentOptions) ([]*base.Comment, bool, error) {
|
||||||
if opts.IssueNumber > 0 {
|
if opts.Context != nil {
|
||||||
comments, err := g.getComments(opts.IssueNumber)
|
comments, err := g.getComments(opts.Context)
|
||||||
return comments, false, err
|
return comments, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return g.GetAllComments(opts.Page, opts.PageSize)
|
return g.GetAllComments(opts.Page, opts.PageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GithubDownloaderV3) getComments(issueNumber int64) ([]*base.Comment, error) {
|
func (g *GithubDownloaderV3) getComments(issueContext base.IssueContext) ([]*base.Comment, error) {
|
||||||
var (
|
var (
|
||||||
allComments = make([]*base.Comment, 0, g.maxPerPage)
|
allComments = make([]*base.Comment, 0, g.maxPerPage)
|
||||||
created = "created"
|
created = "created"
|
||||||
@ -464,7 +465,7 @@ func (g *GithubDownloaderV3) getComments(issueNumber int64) ([]*base.Comment, er
|
|||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
g.sleep()
|
g.sleep()
|
||||||
comments, resp, err := g.client.Issues.ListComments(g.ctx, g.repoOwner, g.repoName, int(issueNumber), opt)
|
comments, resp, err := g.client.Issues.ListComments(g.ctx, g.repoOwner, g.repoName, int(issueContext.ForeignID()), opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error while listing repos: %v", err)
|
return nil, fmt.Errorf("error while listing repos: %v", err)
|
||||||
}
|
}
|
||||||
@ -495,7 +496,7 @@ func (g *GithubDownloaderV3) getComments(issueNumber int64) ([]*base.Comment, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
allComments = append(allComments, &base.Comment{
|
allComments = append(allComments, &base.Comment{
|
||||||
IssueIndex: issueNumber,
|
IssueIndex: issueContext.LocalID(),
|
||||||
PosterID: comment.GetUser().GetID(),
|
PosterID: comment.GetUser().GetID(),
|
||||||
PosterName: comment.GetUser().GetLogin(),
|
PosterName: comment.GetUser().GetLogin(),
|
||||||
PosterEmail: comment.GetUser().GetEmail(),
|
PosterEmail: comment.GetUser().GetEmail(),
|
||||||
@ -661,6 +662,7 @@ func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullReq
|
|||||||
},
|
},
|
||||||
PatchURL: pr.GetPatchURL(),
|
PatchURL: pr.GetPatchURL(),
|
||||||
Reactions: reactions,
|
Reactions: reactions,
|
||||||
|
Context: base.BasicIssueContext(*pr.Number),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -724,28 +726,28 @@ func (g *GithubDownloaderV3) convertGithubReviewComments(cs []*github.PullReques
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetReviews returns pull requests review
|
// GetReviews returns pull requests review
|
||||||
func (g *GithubDownloaderV3) GetReviews(pullRequestNumber int64) ([]*base.Review, error) {
|
func (g *GithubDownloaderV3) GetReviews(context base.IssueContext) ([]*base.Review, error) {
|
||||||
var allReviews = make([]*base.Review, 0, g.maxPerPage)
|
var allReviews = make([]*base.Review, 0, g.maxPerPage)
|
||||||
opt := &github.ListOptions{
|
opt := &github.ListOptions{
|
||||||
PerPage: g.maxPerPage,
|
PerPage: g.maxPerPage,
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
g.sleep()
|
g.sleep()
|
||||||
reviews, resp, err := g.client.PullRequests.ListReviews(g.ctx, g.repoOwner, g.repoName, int(pullRequestNumber), opt)
|
reviews, resp, err := g.client.PullRequests.ListReviews(g.ctx, g.repoOwner, g.repoName, int(context.ForeignID()), opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error while listing repos: %v", err)
|
return nil, fmt.Errorf("error while listing repos: %v", err)
|
||||||
}
|
}
|
||||||
g.rate = &resp.Rate
|
g.rate = &resp.Rate
|
||||||
for _, review := range reviews {
|
for _, review := range reviews {
|
||||||
r := convertGithubReview(review)
|
r := convertGithubReview(review)
|
||||||
r.IssueIndex = pullRequestNumber
|
r.IssueIndex = context.LocalID()
|
||||||
// retrieve all review comments
|
// retrieve all review comments
|
||||||
opt2 := &github.ListOptions{
|
opt2 := &github.ListOptions{
|
||||||
PerPage: g.maxPerPage,
|
PerPage: g.maxPerPage,
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
g.sleep()
|
g.sleep()
|
||||||
reviewComments, resp, err := g.client.PullRequests.ListReviewComments(g.ctx, g.repoOwner, g.repoName, int(pullRequestNumber), review.GetID(), opt2)
|
reviewComments, resp, err := g.client.PullRequests.ListReviewComments(g.ctx, g.repoOwner, g.repoName, int(context.ForeignID()), review.GetID(), opt2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error while listing repos: %v", err)
|
return nil, fmt.Errorf("error while listing repos: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -216,7 +216,7 @@ func TestGitHubDownloadRepo(t *testing.T) {
|
|||||||
|
|
||||||
// downloader.GetComments()
|
// downloader.GetComments()
|
||||||
comments, _, err := downloader.GetComments(base.GetCommentOptions{
|
comments, _, err := downloader.GetComments(base.GetCommentOptions{
|
||||||
IssueNumber: 2,
|
Context: base.BasicIssueContext(2),
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assertCommentsEqual(t, []*base.Comment{
|
assertCommentsEqual(t, []*base.Comment{
|
||||||
@ -286,6 +286,7 @@ func TestGitHubDownloadRepo(t *testing.T) {
|
|||||||
Merged: true,
|
Merged: true,
|
||||||
MergedTime: timePtr(time.Date(2019, 11, 12, 21, 39, 27, 0, time.UTC)),
|
MergedTime: timePtr(time.Date(2019, 11, 12, 21, 39, 27, 0, time.UTC)),
|
||||||
MergeCommitSHA: "f32b0a9dfd09a60f616f29158f772cedd89942d2",
|
MergeCommitSHA: "f32b0a9dfd09a60f616f29158f772cedd89942d2",
|
||||||
|
Context: base.BasicIssueContext(3),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Number: 4,
|
Number: 4,
|
||||||
@ -332,10 +333,11 @@ func TestGitHubDownloadRepo(t *testing.T) {
|
|||||||
Content: "+1",
|
Content: "+1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Context: base.BasicIssueContext(4),
|
||||||
},
|
},
|
||||||
}, prs)
|
}, prs)
|
||||||
|
|
||||||
reviews, err := downloader.GetReviews(3)
|
reviews, err := downloader.GetReviews(base.BasicIssueContext(3))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assertReviewsEqual(t, []*base.Review{
|
assertReviewsEqual(t, []*base.Review{
|
||||||
{
|
{
|
||||||
@ -367,7 +369,7 @@ func TestGitHubDownloadRepo(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}, reviews)
|
}, reviews)
|
||||||
|
|
||||||
reviews, err = downloader.GetReviews(4)
|
reviews, err = downloader.GetReviews(base.BasicIssueContext(4))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assertReviewsEqual(t, []*base.Review{
|
assertReviewsEqual(t, []*base.Review{
|
||||||
{
|
{
|
||||||
|
@ -63,17 +63,14 @@ func (f *GitlabDownloaderFactory) GitServiceType() structs.GitServiceType {
|
|||||||
// from gitlab via go-gitlab
|
// from gitlab via go-gitlab
|
||||||
// - issueCount is incremented in GetIssues() to ensure PR and Issue numbers do not overlap,
|
// - issueCount is incremented in GetIssues() to ensure PR and Issue numbers do not overlap,
|
||||||
// because Gitlab has individual Issue and Pull Request numbers.
|
// because Gitlab has individual Issue and Pull Request numbers.
|
||||||
// - issueSeen, working alongside issueCount, is checked in GetComments() to see whether we
|
|
||||||
// need to fetch the Issue or PR comments, as Gitlab stores them separately.
|
|
||||||
type GitlabDownloader struct {
|
type GitlabDownloader struct {
|
||||||
base.NullDownloader
|
base.NullDownloader
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
client *gitlab.Client
|
client *gitlab.Client
|
||||||
repoID int
|
repoID int
|
||||||
repoName string
|
repoName string
|
||||||
issueCount int64
|
issueCount int64
|
||||||
fetchPRcomments bool
|
maxPerPage int
|
||||||
maxPerPage int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGitlabDownloader creates a gitlab Downloader via gitlab API
|
// NewGitlabDownloader creates a gitlab Downloader via gitlab API
|
||||||
@ -364,6 +361,20 @@ func (g *GitlabDownloader) GetReleases() ([]*base.Release, error) {
|
|||||||
return releases, nil
|
return releases, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type gitlabIssueContext struct {
|
||||||
|
foreignID int64
|
||||||
|
localID int64
|
||||||
|
IsMergeRequest bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c gitlabIssueContext) LocalID() int64 {
|
||||||
|
return c.localID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c gitlabIssueContext) ForeignID() int64 {
|
||||||
|
return c.foreignID
|
||||||
|
}
|
||||||
|
|
||||||
// GetIssues returns issues according start and limit
|
// GetIssues returns issues according start and limit
|
||||||
// Note: issue label description and colors are not supported by the go-gitlab library at this time
|
// Note: issue label description and colors are not supported by the go-gitlab library at this time
|
||||||
func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
|
func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
|
||||||
@ -433,6 +444,11 @@ func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er
|
|||||||
Closed: issue.ClosedAt,
|
Closed: issue.ClosedAt,
|
||||||
IsLocked: issue.DiscussionLocked,
|
IsLocked: issue.DiscussionLocked,
|
||||||
Updated: *issue.UpdatedAt,
|
Updated: *issue.UpdatedAt,
|
||||||
|
Context: gitlabIssueContext{
|
||||||
|
foreignID: int64(issue.IID),
|
||||||
|
localID: int64(issue.IID),
|
||||||
|
IsMergeRequest: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// increment issueCount, to be used in GetPullRequests()
|
// increment issueCount, to be used in GetPullRequests()
|
||||||
@ -445,27 +461,26 @@ func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er
|
|||||||
// GetComments returns comments according issueNumber
|
// GetComments returns comments according issueNumber
|
||||||
// TODO: figure out how to transfer comment reactions
|
// TODO: figure out how to transfer comment reactions
|
||||||
func (g *GitlabDownloader) GetComments(opts base.GetCommentOptions) ([]*base.Comment, bool, error) {
|
func (g *GitlabDownloader) GetComments(opts base.GetCommentOptions) ([]*base.Comment, bool, error) {
|
||||||
var issueNumber = opts.IssueNumber
|
context, ok := opts.Context.(gitlabIssueContext)
|
||||||
|
if !ok {
|
||||||
|
return nil, false, fmt.Errorf("unexpected context: %+v", opts.Context)
|
||||||
|
}
|
||||||
|
|
||||||
var allComments = make([]*base.Comment, 0, g.maxPerPage)
|
var allComments = make([]*base.Comment, 0, g.maxPerPage)
|
||||||
|
|
||||||
var page = 1
|
var page = 1
|
||||||
var realIssueNumber int64
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
var comments []*gitlab.Discussion
|
var comments []*gitlab.Discussion
|
||||||
var resp *gitlab.Response
|
var resp *gitlab.Response
|
||||||
var err error
|
var err error
|
||||||
// fetchPRcomments decides whether to fetch Issue or PR comments
|
if !context.IsMergeRequest {
|
||||||
if !g.fetchPRcomments {
|
comments, resp, err = g.client.Discussions.ListIssueDiscussions(g.repoID, int(context.ForeignID()), &gitlab.ListIssueDiscussionsOptions{
|
||||||
realIssueNumber = issueNumber
|
|
||||||
comments, resp, err = g.client.Discussions.ListIssueDiscussions(g.repoID, int(realIssueNumber), &gitlab.ListIssueDiscussionsOptions{
|
|
||||||
Page: page,
|
Page: page,
|
||||||
PerPage: g.maxPerPage,
|
PerPage: g.maxPerPage,
|
||||||
}, nil, gitlab.WithContext(g.ctx))
|
}, nil, gitlab.WithContext(g.ctx))
|
||||||
} else {
|
} else {
|
||||||
// If this is a PR, we need to figure out the Gitlab/original PR ID to be passed below
|
comments, resp, err = g.client.Discussions.ListMergeRequestDiscussions(g.repoID, int(context.ForeignID()), &gitlab.ListMergeRequestDiscussionsOptions{
|
||||||
realIssueNumber = issueNumber - g.issueCount
|
|
||||||
comments, resp, err = g.client.Discussions.ListMergeRequestDiscussions(g.repoID, int(realIssueNumber), &gitlab.ListMergeRequestDiscussionsOptions{
|
|
||||||
Page: page,
|
Page: page,
|
||||||
PerPage: g.maxPerPage,
|
PerPage: g.maxPerPage,
|
||||||
}, nil, gitlab.WithContext(g.ctx))
|
}, nil, gitlab.WithContext(g.ctx))
|
||||||
@ -479,7 +494,7 @@ func (g *GitlabDownloader) GetComments(opts base.GetCommentOptions) ([]*base.Com
|
|||||||
if !comment.IndividualNote {
|
if !comment.IndividualNote {
|
||||||
for _, note := range comment.Notes {
|
for _, note := range comment.Notes {
|
||||||
allComments = append(allComments, &base.Comment{
|
allComments = append(allComments, &base.Comment{
|
||||||
IssueIndex: realIssueNumber,
|
IssueIndex: context.LocalID(),
|
||||||
PosterID: int64(note.Author.ID),
|
PosterID: int64(note.Author.ID),
|
||||||
PosterName: note.Author.Username,
|
PosterName: note.Author.Username,
|
||||||
PosterEmail: note.Author.Email,
|
PosterEmail: note.Author.Email,
|
||||||
@ -490,7 +505,7 @@ func (g *GitlabDownloader) GetComments(opts base.GetCommentOptions) ([]*base.Com
|
|||||||
} else {
|
} else {
|
||||||
c := comment.Notes[0]
|
c := comment.Notes[0]
|
||||||
allComments = append(allComments, &base.Comment{
|
allComments = append(allComments, &base.Comment{
|
||||||
IssueIndex: realIssueNumber,
|
IssueIndex: context.LocalID(),
|
||||||
PosterID: int64(c.Author.ID),
|
PosterID: int64(c.Author.ID),
|
||||||
PosterName: c.Author.Username,
|
PosterName: c.Author.Username,
|
||||||
PosterEmail: c.Author.Email,
|
PosterEmail: c.Author.Email,
|
||||||
@ -521,9 +536,6 @@ func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set fetchPRcomments to true here, so PR comments are fetched instead of Issue comments
|
|
||||||
g.fetchPRcomments = true
|
|
||||||
|
|
||||||
var allPRs = make([]*base.PullRequest, 0, perPage)
|
var allPRs = make([]*base.PullRequest, 0, perPage)
|
||||||
|
|
||||||
prs, _, err := g.client.MergeRequests.ListProjectMergeRequests(g.repoID, opt, nil, gitlab.WithContext(g.ctx))
|
prs, _, err := g.client.MergeRequests.ListProjectMergeRequests(g.repoID, opt, nil, gitlab.WithContext(g.ctx))
|
||||||
@ -587,7 +599,6 @@ func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
|
|||||||
allPRs = append(allPRs, &base.PullRequest{
|
allPRs = append(allPRs, &base.PullRequest{
|
||||||
Title: pr.Title,
|
Title: pr.Title,
|
||||||
Number: newPRNumber,
|
Number: newPRNumber,
|
||||||
OriginalNumber: int64(pr.IID),
|
|
||||||
PosterName: pr.Author.Username,
|
PosterName: pr.Author.Username,
|
||||||
PosterID: int64(pr.Author.ID),
|
PosterID: int64(pr.Author.ID),
|
||||||
Content: pr.Description,
|
Content: pr.Description,
|
||||||
@ -615,6 +626,11 @@ func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
|
|||||||
OwnerName: pr.Author.Username,
|
OwnerName: pr.Author.Username,
|
||||||
},
|
},
|
||||||
PatchURL: pr.WebURL + ".patch",
|
PatchURL: pr.WebURL + ".patch",
|
||||||
|
Context: gitlabIssueContext{
|
||||||
|
foreignID: int64(pr.IID),
|
||||||
|
localID: newPRNumber,
|
||||||
|
IsMergeRequest: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -622,8 +638,8 @@ func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetReviews returns pull requests review
|
// GetReviews returns pull requests review
|
||||||
func (g *GitlabDownloader) GetReviews(pullRequestNumber int64) ([]*base.Review, error) {
|
func (g *GitlabDownloader) GetReviews(context base.IssueContext) ([]*base.Review, error) {
|
||||||
approvals, resp, err := g.client.MergeRequestApprovals.GetConfiguration(g.repoID, int(pullRequestNumber), gitlab.WithContext(g.ctx))
|
approvals, resp, err := g.client.MergeRequestApprovals.GetConfiguration(g.repoID, int(context.ForeignID()), gitlab.WithContext(g.ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil && resp.StatusCode == 404 {
|
if resp != nil && resp.StatusCode == 404 {
|
||||||
log.Error(fmt.Sprintf("GitlabDownloader: while migrating a error occurred: '%s'", err.Error()))
|
log.Error(fmt.Sprintf("GitlabDownloader: while migrating a error occurred: '%s'", err.Error()))
|
||||||
@ -635,6 +651,7 @@ func (g *GitlabDownloader) GetReviews(pullRequestNumber int64) ([]*base.Review,
|
|||||||
var reviews = make([]*base.Review, 0, len(approvals.ApprovedBy))
|
var reviews = make([]*base.Review, 0, len(approvals.ApprovedBy))
|
||||||
for _, user := range approvals.ApprovedBy {
|
for _, user := range approvals.ApprovedBy {
|
||||||
reviews = append(reviews, &base.Review{
|
reviews = append(reviews, &base.Review{
|
||||||
|
IssueIndex: context.LocalID(),
|
||||||
ReviewerID: int64(user.User.ID),
|
ReviewerID: int64(user.User.ID),
|
||||||
ReviewerName: user.User.Username,
|
ReviewerName: user.User.Username,
|
||||||
CreatedAt: *approvals.UpdatedAt,
|
CreatedAt: *approvals.UpdatedAt,
|
||||||
|
@ -210,7 +210,11 @@ func TestGitlabDownloadRepo(t *testing.T) {
|
|||||||
}, issues)
|
}, issues)
|
||||||
|
|
||||||
comments, _, err := downloader.GetComments(base.GetCommentOptions{
|
comments, _, err := downloader.GetComments(base.GetCommentOptions{
|
||||||
IssueNumber: 2,
|
Context: gitlabIssueContext{
|
||||||
|
foreignID: 2,
|
||||||
|
localID: 2,
|
||||||
|
IsMergeRequest: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assertCommentsEqual(t, []*base.Comment{
|
assertCommentsEqual(t, []*base.Comment{
|
||||||
@ -252,15 +256,14 @@ func TestGitlabDownloadRepo(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assertPullRequestsEqual(t, []*base.PullRequest{
|
assertPullRequestsEqual(t, []*base.PullRequest{
|
||||||
{
|
{
|
||||||
Number: 4,
|
Number: 4,
|
||||||
OriginalNumber: 2,
|
Title: "Test branch",
|
||||||
Title: "Test branch",
|
Content: "do not merge this PR",
|
||||||
Content: "do not merge this PR",
|
Milestone: "1.0.0",
|
||||||
Milestone: "1.0.0",
|
PosterID: 1241334,
|
||||||
PosterID: 1241334,
|
PosterName: "lafriks",
|
||||||
PosterName: "lafriks",
|
State: "opened",
|
||||||
State: "opened",
|
Created: time.Date(2019, 11, 28, 15, 56, 54, 104000000, time.UTC),
|
||||||
Created: time.Date(2019, 11, 28, 15, 56, 54, 104000000, time.UTC),
|
|
||||||
Labels: []*base.Label{
|
Labels: []*base.Label{
|
||||||
{
|
{
|
||||||
Name: "bug",
|
Name: "bug",
|
||||||
@ -293,10 +296,15 @@ func TestGitlabDownloadRepo(t *testing.T) {
|
|||||||
Merged: false,
|
Merged: false,
|
||||||
MergedTime: nil,
|
MergedTime: nil,
|
||||||
MergeCommitSHA: "",
|
MergeCommitSHA: "",
|
||||||
|
Context: gitlabIssueContext{
|
||||||
|
foreignID: 2,
|
||||||
|
localID: 4,
|
||||||
|
IsMergeRequest: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, prs)
|
}, prs)
|
||||||
|
|
||||||
rvs, err := downloader.GetReviews(1)
|
rvs, err := downloader.GetReviews(base.BasicIssueContext(1))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assertReviewsEqual(t, []*base.Review{
|
assertReviewsEqual(t, []*base.Review{
|
||||||
{
|
{
|
||||||
@ -313,7 +321,7 @@ func TestGitlabDownloadRepo(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}, rvs)
|
}, rvs)
|
||||||
|
|
||||||
rvs, err = downloader.GetReviews(2)
|
rvs, err = downloader.GetReviews(base.BasicIssueContext(2))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assertReviewsEqual(t, []*base.Review{
|
assertReviewsEqual(t, []*base.Review{
|
||||||
{
|
{
|
||||||
|
@ -228,10 +228,9 @@ func (g *GogsDownloader) getIssues(page int, state string) ([]*base.Issue, bool,
|
|||||||
|
|
||||||
// GetComments returns comments according issueNumber
|
// GetComments returns comments according issueNumber
|
||||||
func (g *GogsDownloader) GetComments(opts base.GetCommentOptions) ([]*base.Comment, bool, error) {
|
func (g *GogsDownloader) GetComments(opts base.GetCommentOptions) ([]*base.Comment, bool, error) {
|
||||||
var issueNumber = opts.IssueNumber
|
|
||||||
var allComments = make([]*base.Comment, 0, 100)
|
var allComments = make([]*base.Comment, 0, 100)
|
||||||
|
|
||||||
comments, err := g.client.ListIssueComments(g.repoOwner, g.repoName, issueNumber)
|
comments, err := g.client.ListIssueComments(g.repoOwner, g.repoName, opts.Context.ForeignID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, fmt.Errorf("error while listing repos: %v", err)
|
return nil, false, fmt.Errorf("error while listing repos: %v", err)
|
||||||
}
|
}
|
||||||
@ -240,7 +239,7 @@ func (g *GogsDownloader) GetComments(opts base.GetCommentOptions) ([]*base.Comme
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
allComments = append(allComments, &base.Comment{
|
allComments = append(allComments, &base.Comment{
|
||||||
IssueIndex: issueNumber,
|
IssueIndex: opts.Context.LocalID(),
|
||||||
PosterID: comment.Poster.ID,
|
PosterID: comment.Poster.ID,
|
||||||
PosterName: comment.Poster.Login,
|
PosterName: comment.Poster.Login,
|
||||||
PosterEmail: comment.Poster.Email,
|
PosterEmail: comment.Poster.Email,
|
||||||
@ -304,6 +303,7 @@ func convertGogsIssue(issue *gogs.Issue) *base.Issue {
|
|||||||
Updated: issue.Updated,
|
Updated: issue.Updated,
|
||||||
Labels: labels,
|
Labels: labels,
|
||||||
Closed: closed,
|
Closed: closed,
|
||||||
|
Context: base.BasicIssueContext(issue.Index),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,7 +112,7 @@ func TestGogsDownloadRepo(t *testing.T) {
|
|||||||
|
|
||||||
// downloader.GetComments()
|
// downloader.GetComments()
|
||||||
comments, _, err := downloader.GetComments(base.GetCommentOptions{
|
comments, _, err := downloader.GetComments(base.GetCommentOptions{
|
||||||
IssueNumber: 1,
|
Context: base.BasicIssueContext(1),
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assertCommentsEqual(t, []*base.Comment{
|
assertCommentsEqual(t, []*base.Comment{
|
||||||
|
@ -318,7 +318,7 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts
|
|||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
log.Trace("migrating issue %d's comments", issue.Number)
|
log.Trace("migrating issue %d's comments", issue.Number)
|
||||||
comments, _, err := downloader.GetComments(base.GetCommentOptions{
|
comments, _, err := downloader.GetComments(base.GetCommentOptions{
|
||||||
IssueNumber: issue.Number,
|
Context: issue.Context,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !base.IsErrNotSupported(err) {
|
if !base.IsErrNotSupported(err) {
|
||||||
@ -376,7 +376,7 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts
|
|||||||
for _, pr := range prs {
|
for _, pr := range prs {
|
||||||
log.Trace("migrating pull request %d's comments", pr.Number)
|
log.Trace("migrating pull request %d's comments", pr.Number)
|
||||||
comments, _, err := downloader.GetComments(base.GetCommentOptions{
|
comments, _, err := downloader.GetComments(base.GetCommentOptions{
|
||||||
IssueNumber: pr.Number,
|
Context: pr.Context,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !base.IsErrNotSupported(err) {
|
if !base.IsErrNotSupported(err) {
|
||||||
@ -404,14 +404,7 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts
|
|||||||
// migrate reviews
|
// migrate reviews
|
||||||
var allReviews = make([]*base.Review, 0, reviewBatchSize)
|
var allReviews = make([]*base.Review, 0, reviewBatchSize)
|
||||||
for _, pr := range prs {
|
for _, pr := range prs {
|
||||||
number := pr.Number
|
reviews, err := downloader.GetReviews(pr.Context)
|
||||||
|
|
||||||
// on gitlab migrations pull number change
|
|
||||||
if pr.OriginalNumber > 0 {
|
|
||||||
number = pr.OriginalNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
reviews, err := downloader.GetReviews(number)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !base.IsErrNotSupported(err) {
|
if !base.IsErrNotSupported(err) {
|
||||||
return err
|
return err
|
||||||
@ -419,11 +412,6 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts
|
|||||||
log.Warn("migrating reviews is not supported, ignored")
|
log.Warn("migrating reviews is not supported, ignored")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if pr.OriginalNumber > 0 {
|
|
||||||
for i := range reviews {
|
|
||||||
reviews[i].IssueIndex = pr.Number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allReviews = append(allReviews, reviews...)
|
allReviews = append(allReviews, reviews...)
|
||||||
|
|
||||||
|
619
modules/migrations/onedev.go
Normal file
619
modules/migrations/onedev.go
Normal file
@ -0,0 +1,619 @@
|
|||||||
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/json"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/migrations/base"
|
||||||
|
"code.gitea.io/gitea/modules/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ base.Downloader = &OneDevDownloader{}
|
||||||
|
_ base.DownloaderFactory = &OneDevDownloaderFactory{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterDownloaderFactory(&OneDevDownloaderFactory{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// OneDevDownloaderFactory defines a downloader factory
|
||||||
|
type OneDevDownloaderFactory struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a downloader related to this factory according MigrateOptions
|
||||||
|
func (f *OneDevDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) {
|
||||||
|
u, err := url.Parse(opts.CloneAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
repoName := ""
|
||||||
|
|
||||||
|
fields := strings.Split(strings.Trim(u.Path, "/"), "/")
|
||||||
|
if len(fields) == 2 && fields[0] == "projects" {
|
||||||
|
repoName = fields[1]
|
||||||
|
} else if len(fields) == 1 {
|
||||||
|
repoName = fields[0]
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("invalid path: %s", u.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Path = ""
|
||||||
|
u.Fragment = ""
|
||||||
|
|
||||||
|
log.Trace("Create onedev downloader. BaseURL: %v RepoName: %s", u, repoName)
|
||||||
|
|
||||||
|
return NewOneDevDownloader(ctx, u, opts.AuthUsername, opts.AuthPassword, repoName), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GitServiceType returns the type of git service
|
||||||
|
func (f *OneDevDownloaderFactory) GitServiceType() structs.GitServiceType {
|
||||||
|
return structs.OneDevService
|
||||||
|
}
|
||||||
|
|
||||||
|
type onedevUser struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OneDevDownloader implements a Downloader interface to get repository informations
|
||||||
|
// from OneDev
|
||||||
|
type OneDevDownloader struct {
|
||||||
|
base.NullDownloader
|
||||||
|
ctx context.Context
|
||||||
|
client *http.Client
|
||||||
|
baseURL *url.URL
|
||||||
|
repoName string
|
||||||
|
repoID int64
|
||||||
|
maxIssueIndex int64
|
||||||
|
userMap map[int64]*onedevUser
|
||||||
|
milestoneMap map[int64]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContext set context
|
||||||
|
func (d *OneDevDownloader) SetContext(ctx context.Context) {
|
||||||
|
d.ctx = ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOneDevDownloader creates a new downloader
|
||||||
|
func NewOneDevDownloader(ctx context.Context, baseURL *url.URL, username, password, repoName string) *OneDevDownloader {
|
||||||
|
var downloader = &OneDevDownloader{
|
||||||
|
ctx: ctx,
|
||||||
|
baseURL: baseURL,
|
||||||
|
repoName: repoName,
|
||||||
|
client: &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: func(req *http.Request) (*url.URL, error) {
|
||||||
|
if len(username) > 0 && len(password) > 0 {
|
||||||
|
req.SetBasicAuth(username, password)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
userMap: make(map[int64]*onedevUser),
|
||||||
|
milestoneMap: make(map[int64]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
return downloader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *OneDevDownloader) callAPI(endpoint string, parameter map[string]string, result interface{}) error {
|
||||||
|
u, err := d.baseURL.Parse(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if parameter != nil {
|
||||||
|
query := u.Query()
|
||||||
|
for k, v := range parameter {
|
||||||
|
query.Set(k, v)
|
||||||
|
}
|
||||||
|
u.RawQuery = query.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(d.ctx, "GET", u.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := d.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(resp.Body)
|
||||||
|
return decoder.Decode(&result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRepoInfo returns repository information
|
||||||
|
func (d *OneDevDownloader) GetRepoInfo() (*base.Repository, error) {
|
||||||
|
info := make([]struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}, 0, 1)
|
||||||
|
|
||||||
|
err := d.callAPI(
|
||||||
|
"/api/projects",
|
||||||
|
map[string]string{
|
||||||
|
"query": `"Name" is "` + d.repoName + `"`,
|
||||||
|
"offset": "0",
|
||||||
|
"count": "1",
|
||||||
|
},
|
||||||
|
&info,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(info) != 1 {
|
||||||
|
return nil, fmt.Errorf("Project %s not found", d.repoName)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.repoID = info[0].ID
|
||||||
|
|
||||||
|
cloneURL, err := d.baseURL.Parse(info[0].Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
originalURL, err := d.baseURL.Parse("/projects/" + info[0].Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &base.Repository{
|
||||||
|
Name: info[0].Name,
|
||||||
|
Description: info[0].Description,
|
||||||
|
CloneURL: cloneURL.String(),
|
||||||
|
OriginalURL: originalURL.String(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMilestones returns milestones
|
||||||
|
func (d *OneDevDownloader) GetMilestones() ([]*base.Milestone, error) {
|
||||||
|
rawMilestones := make([]struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
DueDate *time.Time `json:"dueDate"`
|
||||||
|
Closed bool `json:"closed"`
|
||||||
|
}, 0, 100)
|
||||||
|
|
||||||
|
endpoint := fmt.Sprintf("/api/projects/%d/milestones", d.repoID)
|
||||||
|
|
||||||
|
var milestones = make([]*base.Milestone, 0, 100)
|
||||||
|
offset := 0
|
||||||
|
for {
|
||||||
|
err := d.callAPI(
|
||||||
|
endpoint,
|
||||||
|
map[string]string{
|
||||||
|
"offset": strconv.Itoa(offset),
|
||||||
|
"count": "100",
|
||||||
|
},
|
||||||
|
&rawMilestones,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(rawMilestones) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
offset += 100
|
||||||
|
|
||||||
|
for _, milestone := range rawMilestones {
|
||||||
|
d.milestoneMap[milestone.ID] = milestone.Name
|
||||||
|
closed := milestone.DueDate
|
||||||
|
if !milestone.Closed {
|
||||||
|
closed = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
milestones = append(milestones, &base.Milestone{
|
||||||
|
Title: milestone.Name,
|
||||||
|
Description: milestone.Description,
|
||||||
|
Deadline: milestone.DueDate,
|
||||||
|
Closed: closed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return milestones, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLabels returns labels
|
||||||
|
func (d *OneDevDownloader) GetLabels() ([]*base.Label, error) {
|
||||||
|
return []*base.Label{
|
||||||
|
{
|
||||||
|
Name: "Bug",
|
||||||
|
Color: "f64e60",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Build Failure",
|
||||||
|
Color: "f64e60",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Discussion",
|
||||||
|
Color: "8950fc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Improvement",
|
||||||
|
Color: "1bc5bd",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "New Feature",
|
||||||
|
Color: "1bc5bd",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Support Request",
|
||||||
|
Color: "8950fc",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type onedevIssueContext struct {
|
||||||
|
foreignID int64
|
||||||
|
localID int64
|
||||||
|
IsPullRequest bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c onedevIssueContext) LocalID() int64 {
|
||||||
|
return c.localID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c onedevIssueContext) ForeignID() int64 {
|
||||||
|
return c.foreignID
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIssues returns issues
|
||||||
|
func (d *OneDevDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
|
||||||
|
rawIssues := make([]struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Number int64 `json:"number"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
MilestoneID int64 `json:"milestoneId"`
|
||||||
|
SubmitterID int64 `json:"submitterId"`
|
||||||
|
SubmitDate time.Time `json:"submitDate"`
|
||||||
|
}, 0, perPage)
|
||||||
|
|
||||||
|
err := d.callAPI(
|
||||||
|
"/api/issues",
|
||||||
|
map[string]string{
|
||||||
|
"query": `"Project" is "` + d.repoName + `"`,
|
||||||
|
"offset": strconv.Itoa((page - 1) * perPage),
|
||||||
|
"count": strconv.Itoa(perPage),
|
||||||
|
},
|
||||||
|
&rawIssues,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
issues := make([]*base.Issue, 0, len(rawIssues))
|
||||||
|
for _, issue := range rawIssues {
|
||||||
|
fields := make([]struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}, 0, 10)
|
||||||
|
err := d.callAPI(
|
||||||
|
fmt.Sprintf("/api/issues/%d/fields", issue.ID),
|
||||||
|
nil,
|
||||||
|
&fields,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var label *base.Label
|
||||||
|
for _, field := range fields {
|
||||||
|
if field.Name == "Type" {
|
||||||
|
label = &base.Label{Name: field.Value}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state := strings.ToLower(issue.State)
|
||||||
|
if state == "released" {
|
||||||
|
state = "closed"
|
||||||
|
}
|
||||||
|
poster := d.tryGetUser(issue.SubmitterID)
|
||||||
|
issues = append(issues, &base.Issue{
|
||||||
|
Title: issue.Title,
|
||||||
|
Number: issue.Number,
|
||||||
|
PosterName: poster.Name,
|
||||||
|
PosterEmail: poster.Email,
|
||||||
|
Content: issue.Description,
|
||||||
|
Milestone: d.milestoneMap[issue.MilestoneID],
|
||||||
|
State: state,
|
||||||
|
Created: issue.SubmitDate,
|
||||||
|
Updated: issue.SubmitDate,
|
||||||
|
Labels: []*base.Label{label},
|
||||||
|
Context: onedevIssueContext{
|
||||||
|
foreignID: issue.ID,
|
||||||
|
localID: issue.Number,
|
||||||
|
IsPullRequest: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if d.maxIssueIndex < issue.Number {
|
||||||
|
d.maxIssueIndex = issue.Number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return issues, len(issues) == 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetComments returns comments
|
||||||
|
func (d *OneDevDownloader) GetComments(opts base.GetCommentOptions) ([]*base.Comment, bool, error) {
|
||||||
|
context, ok := opts.Context.(onedevIssueContext)
|
||||||
|
if !ok {
|
||||||
|
return nil, false, fmt.Errorf("unexpected comment context: %+v", opts.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawComments := make([]struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
UserID int64 `json:"userId"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}, 0, 100)
|
||||||
|
|
||||||
|
var endpoint string
|
||||||
|
if context.IsPullRequest {
|
||||||
|
endpoint = fmt.Sprintf("/api/pull-requests/%d/comments", context.ForeignID())
|
||||||
|
} else {
|
||||||
|
endpoint = fmt.Sprintf("/api/issues/%d/comments", context.ForeignID())
|
||||||
|
}
|
||||||
|
|
||||||
|
err := d.callAPI(
|
||||||
|
endpoint,
|
||||||
|
nil,
|
||||||
|
&rawComments,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rawChanges := make([]struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
UserID int64 `json:"userId"`
|
||||||
|
Data map[string]interface{} `json:"data"`
|
||||||
|
}, 0, 100)
|
||||||
|
|
||||||
|
if context.IsPullRequest {
|
||||||
|
endpoint = fmt.Sprintf("/api/pull-requests/%d/changes", context.ForeignID())
|
||||||
|
} else {
|
||||||
|
endpoint = fmt.Sprintf("/api/issues/%d/changes", context.ForeignID())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.callAPI(
|
||||||
|
endpoint,
|
||||||
|
nil,
|
||||||
|
&rawChanges,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
comments := make([]*base.Comment, 0, len(rawComments)+len(rawChanges))
|
||||||
|
for _, comment := range rawComments {
|
||||||
|
if len(comment.Content) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
poster := d.tryGetUser(comment.UserID)
|
||||||
|
comments = append(comments, &base.Comment{
|
||||||
|
IssueIndex: context.LocalID(),
|
||||||
|
PosterID: poster.ID,
|
||||||
|
PosterName: poster.Name,
|
||||||
|
PosterEmail: poster.Email,
|
||||||
|
Content: comment.Content,
|
||||||
|
Created: comment.Date,
|
||||||
|
Updated: comment.Date,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, change := range rawChanges {
|
||||||
|
contentV, ok := change.Data["content"]
|
||||||
|
if !ok {
|
||||||
|
contentV, ok = change.Data["comment"]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content, ok := contentV.(string)
|
||||||
|
if !ok || len(content) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
poster := d.tryGetUser(change.UserID)
|
||||||
|
comments = append(comments, &base.Comment{
|
||||||
|
IssueIndex: context.LocalID(),
|
||||||
|
PosterID: poster.ID,
|
||||||
|
PosterName: poster.Name,
|
||||||
|
PosterEmail: poster.Email,
|
||||||
|
Content: content,
|
||||||
|
Created: change.Date,
|
||||||
|
Updated: change.Date,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return comments, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPullRequests returns pull requests
|
||||||
|
func (d *OneDevDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
|
||||||
|
rawPullRequests := make([]struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Number int64 `json:"number"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
SubmitterID int64 `json:"submitterId"`
|
||||||
|
SubmitDate time.Time `json:"submitDate"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
TargetBranch string `json:"targetBranch"`
|
||||||
|
SourceBranch string `json:"sourceBranch"`
|
||||||
|
BaseCommitHash string `json:"baseCommitHash"`
|
||||||
|
CloseInfo *struct {
|
||||||
|
Date *time.Time `json:"date"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
}, 0, perPage)
|
||||||
|
|
||||||
|
err := d.callAPI(
|
||||||
|
"/api/pull-requests",
|
||||||
|
map[string]string{
|
||||||
|
"query": `"Target Project" is "` + d.repoName + `"`,
|
||||||
|
"offset": strconv.Itoa((page - 1) * perPage),
|
||||||
|
"count": strconv.Itoa(perPage),
|
||||||
|
},
|
||||||
|
&rawPullRequests,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pullRequests := make([]*base.PullRequest, 0, len(rawPullRequests))
|
||||||
|
for _, pr := range rawPullRequests {
|
||||||
|
var mergePreview struct {
|
||||||
|
TargetHeadCommitHash string `json:"targetHeadCommitHash"`
|
||||||
|
HeadCommitHash string `json:"headCommitHash"`
|
||||||
|
MergeStrategy string `json:"mergeStrategy"`
|
||||||
|
MergeCommitHash string `json:"mergeCommitHash"`
|
||||||
|
}
|
||||||
|
err := d.callAPI(
|
||||||
|
fmt.Sprintf("/api/pull-requests/%d/merge-preview", pr.ID),
|
||||||
|
nil,
|
||||||
|
&mergePreview,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
state := "open"
|
||||||
|
merged := false
|
||||||
|
var closeTime *time.Time
|
||||||
|
var mergedTime *time.Time
|
||||||
|
if pr.CloseInfo != nil {
|
||||||
|
state = "closed"
|
||||||
|
closeTime = pr.CloseInfo.Date
|
||||||
|
if pr.CloseInfo.Status == "MERGED" { // "DISCARDED"
|
||||||
|
merged = true
|
||||||
|
mergedTime = pr.CloseInfo.Date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
poster := d.tryGetUser(pr.SubmitterID)
|
||||||
|
|
||||||
|
number := pr.Number + d.maxIssueIndex
|
||||||
|
pullRequests = append(pullRequests, &base.PullRequest{
|
||||||
|
Title: pr.Title,
|
||||||
|
Number: number,
|
||||||
|
PosterName: poster.Name,
|
||||||
|
PosterID: poster.ID,
|
||||||
|
Content: pr.Description,
|
||||||
|
State: state,
|
||||||
|
Created: pr.SubmitDate,
|
||||||
|
Updated: pr.SubmitDate,
|
||||||
|
Closed: closeTime,
|
||||||
|
Merged: merged,
|
||||||
|
MergedTime: mergedTime,
|
||||||
|
Head: base.PullRequestBranch{
|
||||||
|
Ref: pr.SourceBranch,
|
||||||
|
SHA: mergePreview.HeadCommitHash,
|
||||||
|
RepoName: d.repoName,
|
||||||
|
},
|
||||||
|
Base: base.PullRequestBranch{
|
||||||
|
Ref: pr.TargetBranch,
|
||||||
|
SHA: mergePreview.TargetHeadCommitHash,
|
||||||
|
RepoName: d.repoName,
|
||||||
|
},
|
||||||
|
Context: onedevIssueContext{
|
||||||
|
foreignID: pr.ID,
|
||||||
|
localID: number,
|
||||||
|
IsPullRequest: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return pullRequests, len(pullRequests) == 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReviews returns pull requests reviews
|
||||||
|
func (d *OneDevDownloader) GetReviews(context base.IssueContext) ([]*base.Review, error) {
|
||||||
|
rawReviews := make([]struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
UserID int64 `json:"userId"`
|
||||||
|
Result *struct {
|
||||||
|
Commit string `json:"commit"`
|
||||||
|
Approved bool `json:"approved"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
}
|
||||||
|
}, 0, 100)
|
||||||
|
|
||||||
|
err := d.callAPI(
|
||||||
|
fmt.Sprintf("/api/pull-requests/%d/reviews", context.ForeignID()),
|
||||||
|
nil,
|
||||||
|
&rawReviews,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var reviews = make([]*base.Review, 0, len(rawReviews))
|
||||||
|
for _, review := range rawReviews {
|
||||||
|
state := base.ReviewStatePending
|
||||||
|
content := ""
|
||||||
|
if review.Result != nil {
|
||||||
|
if len(review.Result.Comment) > 0 {
|
||||||
|
state = base.ReviewStateCommented
|
||||||
|
content = review.Result.Comment
|
||||||
|
}
|
||||||
|
if review.Result.Approved {
|
||||||
|
state = base.ReviewStateApproved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
poster := d.tryGetUser(review.UserID)
|
||||||
|
reviews = append(reviews, &base.Review{
|
||||||
|
IssueIndex: context.LocalID(),
|
||||||
|
ReviewerID: poster.ID,
|
||||||
|
ReviewerName: poster.Name,
|
||||||
|
Content: content,
|
||||||
|
State: state,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return reviews, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTopics return repository topics
|
||||||
|
func (d *OneDevDownloader) GetTopics() ([]string, error) {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *OneDevDownloader) tryGetUser(userID int64) *onedevUser {
|
||||||
|
user, ok := d.userMap[userID]
|
||||||
|
if !ok {
|
||||||
|
err := d.callAPI(
|
||||||
|
fmt.Sprintf("/api/users/%d", userID),
|
||||||
|
nil,
|
||||||
|
&user,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
user = &onedevUser{
|
||||||
|
Name: fmt.Sprintf("User %d", userID),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.userMap[userID] = user
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
169
modules/migrations/onedev_test.go
Normal file
169
modules/migrations/onedev_test.go
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/migrations/base"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOneDevDownloadRepo(t *testing.T) {
|
||||||
|
resp, err := http.Get("https://code.onedev.io/projects/go-gitea-test_repo")
|
||||||
|
if err != nil || resp.StatusCode != 200 {
|
||||||
|
t.Skipf("Can't access test repo, skipping %s", t.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
u, _ := url.Parse("https://code.onedev.io")
|
||||||
|
downloader := NewOneDevDownloader(context.Background(), u, "", "", "go-gitea-test_repo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(fmt.Sprintf("NewOneDevDownloader is nil: %v", err))
|
||||||
|
}
|
||||||
|
repo, err := downloader.GetRepoInfo()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, &base.Repository{
|
||||||
|
Name: "go-gitea-test_repo",
|
||||||
|
Owner: "",
|
||||||
|
Description: "Test repository for testing migration from OneDev to gitea",
|
||||||
|
CloneURL: "https://code.onedev.io/go-gitea-test_repo",
|
||||||
|
OriginalURL: "https://code.onedev.io/projects/go-gitea-test_repo",
|
||||||
|
}, repo)
|
||||||
|
|
||||||
|
milestones, err := downloader.GetMilestones()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, milestones, 2)
|
||||||
|
deadline := time.Unix(1620086400, 0)
|
||||||
|
assert.EqualValues(t, []*base.Milestone{
|
||||||
|
{
|
||||||
|
Title: "1.0.0",
|
||||||
|
Deadline: &deadline,
|
||||||
|
Closed: &deadline,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "1.1.0",
|
||||||
|
Description: "next things?",
|
||||||
|
},
|
||||||
|
}, milestones)
|
||||||
|
|
||||||
|
labels, err := downloader.GetLabels()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, labels, 6)
|
||||||
|
|
||||||
|
issues, isEnd, err := downloader.GetIssues(1, 2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, issues, 2)
|
||||||
|
assert.False(t, isEnd)
|
||||||
|
assert.EqualValues(t, []*base.Issue{
|
||||||
|
{
|
||||||
|
Number: 4,
|
||||||
|
Title: "Hi there",
|
||||||
|
Content: "an issue not assigned to a milestone",
|
||||||
|
PosterName: "User 336",
|
||||||
|
State: "open",
|
||||||
|
Created: time.Unix(1628549776, 734000000),
|
||||||
|
Updated: time.Unix(1628549776, 734000000),
|
||||||
|
Labels: []*base.Label{
|
||||||
|
{
|
||||||
|
Name: "Improvement",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Context: onedevIssueContext{
|
||||||
|
foreignID: 398,
|
||||||
|
localID: 4,
|
||||||
|
IsPullRequest: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Number: 3,
|
||||||
|
Title: "Add an awesome feature",
|
||||||
|
Content: "just another issue to test against",
|
||||||
|
PosterName: "User 336",
|
||||||
|
State: "open",
|
||||||
|
Milestone: "1.1.0",
|
||||||
|
Created: time.Unix(1628549749, 878000000),
|
||||||
|
Updated: time.Unix(1628549749, 878000000),
|
||||||
|
Labels: []*base.Label{
|
||||||
|
{
|
||||||
|
Name: "New Feature",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Context: onedevIssueContext{
|
||||||
|
foreignID: 397,
|
||||||
|
localID: 3,
|
||||||
|
IsPullRequest: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, issues)
|
||||||
|
|
||||||
|
comments, _, err := downloader.GetComments(base.GetCommentOptions{
|
||||||
|
Context: onedevIssueContext{
|
||||||
|
foreignID: 398,
|
||||||
|
localID: 4,
|
||||||
|
IsPullRequest: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, comments, 1)
|
||||||
|
assert.EqualValues(t, []*base.Comment{
|
||||||
|
{
|
||||||
|
IssueIndex: 4,
|
||||||
|
PosterName: "User 336",
|
||||||
|
Created: time.Unix(1628549791, 128000000),
|
||||||
|
Updated: time.Unix(1628549791, 128000000),
|
||||||
|
Content: "it has a comment\r\n\r\nEDIT: that got edited",
|
||||||
|
},
|
||||||
|
}, comments)
|
||||||
|
|
||||||
|
prs, _, err := downloader.GetPullRequests(1, 1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, prs, 1)
|
||||||
|
assert.EqualValues(t, []*base.PullRequest{
|
||||||
|
{
|
||||||
|
Number: 5,
|
||||||
|
Title: "Pull to add a new file",
|
||||||
|
Content: "just do some git stuff",
|
||||||
|
PosterName: "User 336",
|
||||||
|
State: "open",
|
||||||
|
Created: time.Unix(1628550076, 25000000),
|
||||||
|
Updated: time.Unix(1628550076, 25000000),
|
||||||
|
Head: base.PullRequestBranch{
|
||||||
|
Ref: "branch-for-a-pull",
|
||||||
|
SHA: "343deffe3526b9bc84e873743ff7f6e6d8b827c0",
|
||||||
|
RepoName: "go-gitea-test_repo",
|
||||||
|
},
|
||||||
|
Base: base.PullRequestBranch{
|
||||||
|
Ref: "master",
|
||||||
|
SHA: "f32b0a9dfd09a60f616f29158f772cedd89942d2",
|
||||||
|
RepoName: "go-gitea-test_repo",
|
||||||
|
},
|
||||||
|
Context: onedevIssueContext{
|
||||||
|
foreignID: 186,
|
||||||
|
localID: 5,
|
||||||
|
IsPullRequest: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, prs)
|
||||||
|
|
||||||
|
rvs, err := downloader.GetReviews(onedevIssueContext{
|
||||||
|
foreignID: 186,
|
||||||
|
localID: 5,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, rvs, 1)
|
||||||
|
assert.EqualValues(t, []*base.Review{
|
||||||
|
{
|
||||||
|
IssueIndex: 5,
|
||||||
|
ReviewerName: "User 317",
|
||||||
|
State: "PENDING",
|
||||||
|
},
|
||||||
|
}, rvs)
|
||||||
|
}
|
@ -208,13 +208,16 @@ func (r *RepositoryRestorer) GetIssues(page, perPage int) ([]*base.Issue, bool,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
for _, issue := range issues {
|
||||||
|
issue.Context = base.BasicIssueContext(issue.Number)
|
||||||
|
}
|
||||||
return issues, true, nil
|
return issues, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetComments returns comments according issueNumber
|
// GetComments returns comments according issueNumber
|
||||||
func (r *RepositoryRestorer) GetComments(opts base.GetCommentOptions) ([]*base.Comment, bool, error) {
|
func (r *RepositoryRestorer) GetComments(opts base.GetCommentOptions) ([]*base.Comment, bool, error) {
|
||||||
var comments = make([]*base.Comment, 0, 10)
|
var comments = make([]*base.Comment, 0, 10)
|
||||||
p := filepath.Join(r.commentDir(), fmt.Sprintf("%d.yml", opts.IssueNumber))
|
p := filepath.Join(r.commentDir(), fmt.Sprintf("%d.yml", opts.Context.ForeignID()))
|
||||||
_, err := os.Stat(p)
|
_, err := os.Stat(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@ -258,14 +261,15 @@ func (r *RepositoryRestorer) GetPullRequests(page, perPage int) ([]*base.PullReq
|
|||||||
}
|
}
|
||||||
for _, pr := range pulls {
|
for _, pr := range pulls {
|
||||||
pr.PatchURL = "file://" + filepath.Join(r.baseDir, pr.PatchURL)
|
pr.PatchURL = "file://" + filepath.Join(r.baseDir, pr.PatchURL)
|
||||||
|
pr.Context = base.BasicIssueContext(pr.Number)
|
||||||
}
|
}
|
||||||
return pulls, true, nil
|
return pulls, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetReviews returns pull requests review
|
// GetReviews returns pull requests review
|
||||||
func (r *RepositoryRestorer) GetReviews(pullRequestNumber int64) ([]*base.Review, error) {
|
func (r *RepositoryRestorer) GetReviews(context base.IssueContext) ([]*base.Review, error) {
|
||||||
var reviews = make([]*base.Review, 0, 10)
|
var reviews = make([]*base.Review, 0, 10)
|
||||||
p := filepath.Join(r.reviewDir(), fmt.Sprintf("%d.yml", pullRequestNumber))
|
p := filepath.Join(r.reviewDir(), fmt.Sprintf("%d.yml", context.ForeignID()))
|
||||||
_, err := os.Stat(p)
|
_, err := os.Stat(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
|
@ -248,6 +248,7 @@ const (
|
|||||||
GiteaService // 3 gitea service
|
GiteaService // 3 gitea service
|
||||||
GitlabService // 4 gitlab service
|
GitlabService // 4 gitlab service
|
||||||
GogsService // 5 gogs service
|
GogsService // 5 gogs service
|
||||||
|
OneDevService // 6 onedev service
|
||||||
)
|
)
|
||||||
|
|
||||||
// Name represents the service type's name
|
// Name represents the service type's name
|
||||||
@ -267,6 +268,8 @@ func (gt GitServiceType) Title() string {
|
|||||||
return "GitLab"
|
return "GitLab"
|
||||||
case GogsService:
|
case GogsService:
|
||||||
return "Gogs"
|
return "Gogs"
|
||||||
|
case OneDevService:
|
||||||
|
return "OneDev"
|
||||||
case PlainGitService:
|
case PlainGitService:
|
||||||
return "Git"
|
return "Git"
|
||||||
}
|
}
|
||||||
@ -322,5 +325,6 @@ var (
|
|||||||
GitlabService,
|
GitlabService,
|
||||||
GiteaService,
|
GiteaService,
|
||||||
GogsService,
|
GogsService,
|
||||||
|
OneDevService,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -904,6 +904,7 @@ migrate.git.description = Migrating or Mirroring git data from Git services
|
|||||||
migrate.gitlab.description = Migrating data from GitLab.com or Self-Hosted gitlab server.
|
migrate.gitlab.description = Migrating data from GitLab.com or Self-Hosted gitlab server.
|
||||||
migrate.gitea.description = Migrating data from Gitea.com or Self-Hosted Gitea server.
|
migrate.gitea.description = Migrating data from Gitea.com or Self-Hosted Gitea server.
|
||||||
migrate.gogs.description = Migrating data from notabug.org or other Self-Hosted Gogs server.
|
migrate.gogs.description = Migrating data from notabug.org or other Self-Hosted Gogs server.
|
||||||
|
migrate.onedev.description = Migrating data from code.onedev.io or Self-Hosted OneDev server.
|
||||||
migrate.migrating_git = Migrating Git Data
|
migrate.migrating_git = Migrating Git Data
|
||||||
migrate.migrating_topics = Migrating Topics
|
migrate.migrating_topics = Migrating Topics
|
||||||
migrate.migrating_milestones = Migrating Milestones
|
migrate.migrating_milestones = Migrating Milestones
|
||||||
|
1
public/img/svg/gitea-onedev.svg
Normal file
1
public/img/svg/gitea-onedev.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg version="1.0" viewBox="0 0 700 700" class="svg gitea-onedev" width="16" height="16" aria-hidden="true"><path d="M315.5 99.6c-29.5 4-55.8 12-81.2 24.8L223 130l-5.2-4c-14.9-11.3-37.6-14.9-55.8-9-19.1 6.3-35.1 22.2-41.1 41-2.7 8.3-3.6 22.9-1.9 31.2 1.5 8 5 16.5 9.1 22.5 3.1 4.7 3.1 4.8 1.4 7.8C106 260 95.1 294.4 92 337.7c-1.1 15.7-.1 40.2 2.1 53l1.1 6.5-4.9 4.4c-2.8 2.3-7.5 7.6-10.6 11.6-19.4 25.5-24.7 57.9-14.4 88.3 9.2 26.9 31.2 48.8 58.4 58.1 20.6 6.9 40.6 7 61.1.1l6.7-2.2 10.5 7.1c45.6 31 92 45.5 146 45.5 33 0 61.6-5.2 91-16.4 67.6-25.8 122.9-81.1 148.4-148.4l2.7-7.2 7.7-3.8c9.1-4.5 21.1-15.7 25.9-24.3 21.1-37.5-1-84.3-43.2-91.7-19.9-3.5-39.3 2.7-53.9 17.2-7.1 7.1-11.7 14.5-15.3 24.7-3.4 9.4-3.8 25.8-.9 35.3 2.8 9.5 8.5 19.3 15.3 26.4 7.2 7.6 7.2 6 0 20.5-8.9 18.1-20.3 34.1-35.2 49.5-34.6 35.7-78.2 56.3-128.3 60.3-42.8 3.4-89.3-8.9-125-33-1.1-.8-1-1.7.8-5.2 12.1-23.6 13.5-53.7 3.9-78-8.7-21.8-27.5-41.6-48.6-51.2-9-4.1-22.7-7.4-34-8.3l-9.1-.7-.8-9.6c-3.5-46.9 13.5-99.8 45.5-141.7 6.5-8.6 24.3-26.7 33.6-34.2 43.8-35.6 101.3-52.8 158.1-47.2 39.9 3.9 79 19.1 110.6 43 16.9 12.8 37.5 34.9 48.6 52l4.3 6.7-3.3 5.2c-2.9 4.7-3.3 6.3-3.6 13.4-.3 7.3-.1 8.6 2.5 13.6 3.2 6.1 10.2 12 16.3 13.9 22.8 6.8 43-16.9 32.6-38.2-3.1-6.4-9.3-12.2-14.7-13.8-2.5-.8-4.1-2.1-5.2-4.3-.9-1.7-3.2-5.8-5.1-9.2l-3.5-6 3.6-5c17.7-24.4 15.8-57.5-4.4-79.4-8-8.6-15.5-13.6-25.9-17.2-19.8-6.8-38.9-4.2-56.5 7.8l-7.8 5.3-15.3-7.4c-27.9-13.4-55-21.3-84-24.4-13.3-1.5-48.1-1.2-60.3.5z"/><path d="M271.8 271.1c-13.9 2.1-30.5 17.3-40.5 37.4-18.3 36.4-13.4 81.5 9.8 91.5 15.2 6.5 34.5-2.7 48.6-23.2 5.5-8 9.7-15.7 9-16.5-.3-.2-2 .3-3.8 1.2-2.4 1.3-5.1 1.6-10.5 1.3-6.1-.3-7.9-.8-11.6-3.4-8.9-6.2-12.4-19.1-7.9-29 2.4-5.2 9-10.8 14.7-12.4 9.1-2.6 20 1.4 25.2 9.2l2.7 4.2.3-12.4c.4-18.9-3.4-31.6-12.4-40.5-6.3-6.3-14.2-8.8-23.6-7.4zM420.5 271c-11.6 1.9-20.2 11.3-24.9 27-2.1 6.9-3.1 20-2.2 27.4l.8 5.7 2.1-3.2c10.2-15 31.6-14 39.9 2 6 11.5 1.5 25.1-10.4 31.2-5 2.5-15 2.6-20 .1l-3.6-1.9 1.4 3.3c6.1 14.5 20 30.1 32.3 36.1 5.7 2.8 14.4 4 20.4 2.9 5.2-1 12.1-6.1 16.1-11.9 18.1-26.4 8.1-79-20-105.8-10.8-10.2-21.6-14.6-31.9-12.9zM322.5 431.9c-16.1 1.6-23.5 6.1-23.5 14.3 0 11.4 13 21.1 34 25.4 10.2 2 31.2 1.5 40.5-1 13.5-3.7 23.8-10.3 27.6-17.7 4.9-9.7-.2-17.1-13.8-20-6.1-1.2-54.2-2-64.8-1z"/></svg>
|
After Width: | Height: | Size: 2.2 KiB |
117
templates/repo/migrate/onedev.tmpl
Normal file
117
templates/repo/migrate/onedev.tmpl
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
{{template "base/head" .}}
|
||||||
|
<div class="page-content repository new migrate">
|
||||||
|
<div class="ui middle very relaxed page grid">
|
||||||
|
<div class="column">
|
||||||
|
<form class="ui form" action="{{.Link}}" method="post">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<h3 class="ui top attached header">
|
||||||
|
{{.i18n.Tr "repo.migrate.migrate" .service.Title}}
|
||||||
|
<input id="service_type" type="hidden" name="service" value="{{.service}}">
|
||||||
|
</h3>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
{{template "base/alert" .}}
|
||||||
|
<div class="inline required field {{if .Err_CloneAddr}}error{{end}}">
|
||||||
|
<label for="clone_addr">{{.i18n.Tr "repo.migrate.clone_address"}}</label>
|
||||||
|
<input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required>
|
||||||
|
<span class="help">
|
||||||
|
{{.i18n.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{.i18n.Tr "repo.migrate.clone_local_path"}}{{end}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inline field {{if .Err_Auth}}error{{end}}">
|
||||||
|
<label for="auth_username">{{.i18n.Tr "username"}}</label>
|
||||||
|
<input id="auth_username" name="auth_username" value="{{.auth_username}}" {{if not .auth_username}}data-need-clear="true"{{end}}>
|
||||||
|
</div>
|
||||||
|
<input class="fake" type="password">
|
||||||
|
<div class="inline field {{if .Err_Auth}}error{{end}}">
|
||||||
|
<label for="auth_password">{{.i18n.Tr "password"}}</label>
|
||||||
|
<input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{template "repo/migrate/options" .}}
|
||||||
|
|
||||||
|
<div id="migrate_items">
|
||||||
|
<div class="inline field">
|
||||||
|
<label>{{.i18n.Tr "repo.migrate_items"}}</label>
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}>
|
||||||
|
<label>{{.i18n.Tr "repo.migrate_items_milestones" | Safe}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input name="labels" type="checkbox" {{if .labels}}checked{{end}}>
|
||||||
|
<label>{{.i18n.Tr "repo.migrate_items_labels" | Safe}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inline field">
|
||||||
|
<label></label>
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input name="issues" type="checkbox" {{if .issues}}checked{{end}}>
|
||||||
|
<label>{{.i18n.Tr "repo.migrate_items_issues" | Safe}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}>
|
||||||
|
<label>{{.i18n.Tr "repo.migrate_items_pullrequests" | Safe}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
|
||||||
|
<div class="inline required field {{if .Err_Owner}}error{{end}}">
|
||||||
|
<label>{{.i18n.Tr "repo.owner"}}</label>
|
||||||
|
<div class="ui selection owner dropdown">
|
||||||
|
<input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required>
|
||||||
|
<span class="text truncated-item-container" title="{{.ContextUser.Name}}">
|
||||||
|
{{avatar .ContextUser 28 "mini"}}
|
||||||
|
<span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span>
|
||||||
|
</span>
|
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
|
<div class="menu" title="{{.SignedUser.Name}}">
|
||||||
|
<div class="item truncated-item-container" data-value="{{.SignedUser.ID}}">
|
||||||
|
{{avatar .SignedUser 28 "mini"}}
|
||||||
|
<span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span>
|
||||||
|
</div>
|
||||||
|
{{range .Orgs}}
|
||||||
|
<div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}">
|
||||||
|
{{avatar . 28 "mini"}}
|
||||||
|
<span class="truncated-item-name">{{.ShortName 40}}</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inline required field {{if .Err_RepoName}}error{{end}}">
|
||||||
|
<label for="repo_name">{{.i18n.Tr "repo.repo_name"}}</label>
|
||||||
|
<input id="repo_name" name="repo_name" value="{{.repo_name}}" required>
|
||||||
|
</div>
|
||||||
|
<div class="inline field">
|
||||||
|
<label>{{.i18n.Tr "repo.visibility"}}</label>
|
||||||
|
<div class="ui checkbox">
|
||||||
|
{{if .IsForcedPrivate}}
|
||||||
|
<input name="private" type="checkbox" checked readonly>
|
||||||
|
<label>{{.i18n.Tr "repo.visibility_helper_forced" | Safe}}</label>
|
||||||
|
{{else}}
|
||||||
|
<input name="private" type="checkbox" {{if .private}}checked{{end}}>
|
||||||
|
<label>{{.i18n.Tr "repo.visibility_helper" | Safe}}</label>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inline field {{if .Err_Description}}error{{end}}">
|
||||||
|
<label for="description">{{.i18n.Tr "repo.repo_desc"}}</label>
|
||||||
|
<textarea id="description" name="description">{{.description}}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inline field">
|
||||||
|
<label></label>
|
||||||
|
<button class="ui green button">
|
||||||
|
{{.i18n.Tr "repo.migrate_repo"}}
|
||||||
|
</button>
|
||||||
|
<a class="ui button" href="{{AppSubUrl}}/">{{.i18n.Tr "cancel"}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{template "base/footer" .}}
|
42
web_src/svg/gitea-onedev.svg
Normal file
42
web_src/svg/gitea-onedev.svg
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
|
||||||
|
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M3155 6004 c-295 -40 -558 -120 -812 -248 l-113 -56 -52 40 c-149
|
||||||
|
113 -376 149 -558 90 -191 -63 -351 -222 -411 -410 -27 -83 -36 -229 -19 -312
|
||||||
|
15 -80 50 -165 91 -225 31 -47 31 -48 14 -78 -235 -405 -344 -749 -375 -1182
|
||||||
|
-11 -157 -1 -402 21 -530 l11 -65 -49 -44 c-28 -23 -75 -76 -106 -116 -194
|
||||||
|
-255 -247 -579 -144 -883 92 -269 312 -488 584 -581 206 -69 406 -70 611 -1
|
||||||
|
l67 22 105 -71 c456 -310 920 -455 1460 -455 330 0 616 52 910 164 676 258
|
||||||
|
1229 811 1484 1484 l27 72 77 38 c91 45 211 157 259 243 211 375 -10 843 -432
|
||||||
|
917 -199 35 -393 -27 -539 -172 -71 -71 -117 -145 -153 -247 -34 -94 -38 -258
|
||||||
|
-9 -353 28 -95 85 -193 153 -264 72 -76 72 -60 0 -205 -89 -181 -203 -341
|
||||||
|
-352 -495 -346 -357 -782 -563 -1283 -603 -428 -34 -893 89 -1250 330 -11 8
|
||||||
|
-10 17 8 52 121 236 135 537 39 780 -87 218 -275 416 -486 512 -90 41 -227 74
|
||||||
|
-340 83 l-91 7 -8 96 c-35 469 135 998 455 1417 65 86 243 267 336 342 438
|
||||||
|
356 1013 528 1581 472 399 -39 790 -191 1106 -430 169 -128 375 -349 486 -520
|
||||||
|
l43 -67 -33 -52 c-29 -47 -33 -63 -36 -134 -3 -73 -1 -86 25 -136 32 -61 102
|
||||||
|
-120 163 -139 228 -68 430 169 326 382 -31 64 -93 122 -147 138 -25 8 -41 21
|
||||||
|
-52 43 -9 17 -32 58 -51 92 l-35 60 36 50 c177 244 158 575 -44 794 -80 86
|
||||||
|
-155 136 -259 172 -198 68 -389 42 -565 -78 l-78 -53 -153 74 c-279 134 -550
|
||||||
|
213 -840 244 -133 15 -481 12 -603 -5z"/>
|
||||||
|
<path d="M2718 4289 c-139 -21 -305 -173 -405 -374 -183 -364 -134 -815 98
|
||||||
|
-915 152 -65 345 27 486 232 55 80 97 157 90 165 -3 2 -20 -3 -38 -12 -24 -13
|
||||||
|
-51 -16 -105 -13 -61 3 -79 8 -116 34 -89 62 -124 191 -79 290 24 52 90 108
|
||||||
|
147 124 91 26 200 -14 252 -92 l27 -42 3 124 c4 189 -34 316 -124 405 -63 63
|
||||||
|
-142 88 -236 74z"/>
|
||||||
|
<path d="M4205 4290 c-116 -19 -202 -113 -249 -270 -21 -69 -31 -200 -22 -274
|
||||||
|
l8 -57 21 32 c102 150 316 140 399 -20 60 -115 15 -251 -104 -312 -50 -25
|
||||||
|
-150 -26 -200 -1 l-36 19 14 -33 c61 -145 200 -301 323 -361 57 -28 144 -40
|
||||||
|
204 -29 52 10 121 61 161 119 181 264 81 790 -200 1058 -108 102 -216 146
|
||||||
|
-319 129z"/>
|
||||||
|
<path d="M3225 2681 c-161 -16 -235 -61 -235 -143 0 -114 130 -211 340 -254
|
||||||
|
102 -20 312 -15 405 10 135 37 238 103 276 177 49 97 -2 171 -138 200 -61 12
|
||||||
|
-542 20 -648 10z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
Loading…
Reference in New Issue
Block a user