diff options
24 files changed, 1093 insertions, 92 deletions
diff --git a/modules/convert/utils.go b/modules/convert/utils.go index 69de306689..a0463d7b10 100644 --- a/modules/convert/utils.go +++ b/modules/convert/utils.go @@ -33,6 +33,8 @@ func ToGitServiceType(value string) structs.GitServiceType { return structs.GitlabService case "gogs": return structs.GogsService + case "onedev": + return structs.OneDevService default: return structs.PlainGitService } diff --git a/modules/migrations/base/downloader.go b/modules/migrations/base/downloader.go index 71c8f3eaf9..3c581b8699 100644 --- a/modules/migrations/base/downloader.go +++ b/modules/migrations/base/downloader.go @@ -13,9 +13,9 @@ import ( // GetCommentOptions represents an options for get comment type GetCommentOptions struct { - IssueNumber int64 - Page int - PageSize int + Context IssueContext + Page int + PageSize int } // Downloader downloads the site repo information @@ -30,7 +30,7 @@ type Downloader interface { GetComments(opts GetCommentOptions) ([]*Comment, bool, error) SupportGetRepoComments() bool GetPullRequests(page, perPage int) ([]*PullRequest, bool, error) - GetReviews(pullRequestNumber int64) ([]*Review, error) + GetReviews(pullRequestContext IssueContext) ([]*Review, error) FormatCloneURL(opts MigrateOptions, remoteAddr string) (string, error) } diff --git a/modules/migrations/base/issue.go b/modules/migrations/base/issue.go index 8b1b461244..7addd1336a 100644 --- a/modules/migrations/base/issue.go +++ b/modules/migrations/base/issue.go @@ -7,6 +7,25 @@ package base 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 type Issue struct { Number int64 @@ -25,4 +44,5 @@ type Issue struct { Labels []*Label Reactions []*Reaction Assignees []string + Context IssueContext `yaml:"-"` } diff --git a/modules/migrations/base/null_downloader.go b/modules/migrations/base/null_downloader.go index 53a536709d..c64d0e2633 100644 --- a/modules/migrations/base/null_downloader.go +++ b/modules/migrations/base/null_downloader.go @@ -50,7 +50,7 @@ func (n NullDownloader) GetIssues(page, perPage int) ([]*Issue, bool, error) { 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) { return nil, false, &ErrNotSupported{Entity: "Comments"} } @@ -61,7 +61,7 @@ func (n NullDownloader) GetPullRequests(page, perPage int) ([]*PullRequest, bool } // 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"} } diff --git a/modules/migrations/base/pullrequest.go b/modules/migrations/base/pullrequest.go index 6411137d0a..84b302d18f 100644 --- a/modules/migrations/base/pullrequest.go +++ b/modules/migrations/base/pullrequest.go @@ -13,7 +13,6 @@ import ( // PullRequest defines a standard pull request information type PullRequest struct { Number int64 - OriginalNumber int64 `yaml:"original_number"` Title string PosterName string `yaml:"poster_name"` PosterID int64 `yaml:"poster_id"` @@ -34,6 +33,7 @@ type PullRequest struct { Assignees []string IsLocked bool `yaml:"is_locked"` Reactions []*Reaction + Context IssueContext `yaml:"-"` } // IsForkPullRequest returns true if the pull request from a forked repository but not the same repository diff --git a/modules/migrations/base/retry_downloader.go b/modules/migrations/base/retry_downloader.go index e6c80038f1..623bfc86b5 100644 --- a/modules/migrations/base/retry_downloader.go +++ b/modules/migrations/base/retry_downloader.go @@ -182,14 +182,14 @@ func (d *RetryDownloader) GetPullRequests(page, perPage int) ([]*PullRequest, bo } // GetReviews returns pull requests reviews -func (d *RetryDownloader) GetReviews(pullRequestNumber int64) ([]*Review, error) { +func (d *RetryDownloader) GetReviews(pullRequestContext IssueContext) ([]*Review, error) { var ( reviews []*Review err error ) err = d.retry(func() error { - reviews, err = d.Downloader.GetReviews(pullRequestNumber) + reviews, err = d.Downloader.GetReviews(pullRequestContext) return err }) diff --git a/modules/migrations/gitea_downloader.go b/modules/migrations/gitea_downloader.go index 23ede93a42..b947ee74a4 100644 --- a/modules/migrations/gitea_downloader.go +++ b/modules/migrations/gitea_downloader.go @@ -444,6 +444,7 @@ func (g *GiteaDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, err Labels: labels, Assignees: assignees, IsLocked: issue.IsLocked, + Context: base.BasicIssueContext(issue.Index), }) } @@ -466,26 +467,26 @@ func (g *GiteaDownloader) GetComments(opts base.GetCommentOptions) ([]*base.Comm 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, // Page: i, }}) 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 { reactions, err := g.getCommentReactions(comment.ID) 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( - 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) } } allComments = append(allComments, &base.Comment{ - IssueIndex: opts.IssueNumber, + IssueIndex: opts.Context.LocalID(), PosterID: comment.Poster.ID, PosterName: comment.Poster.UserName, PosterEmail: comment.Poster.Email, @@ -615,6 +616,7 @@ func (g *GiteaDownloader) GetPullRequests(page, perPage int) ([]*base.PullReques RepoName: g.repoName, 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 -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 { log.Info("GiteaDownloader: instance to old, skip GetReviews") return nil, nil @@ -642,7 +644,7 @@ func (g *GiteaDownloader) GetReviews(index int64) ([]*base.Review, error) { 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, PageSize: g.maxPerPage, }}) @@ -652,7 +654,7 @@ func (g *GiteaDownloader) GetReviews(index int64) ([]*base.Review, error) { 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 { return nil, err } @@ -678,7 +680,7 @@ func (g *GiteaDownloader) GetReviews(index int64) ([]*base.Review, error) { allReviews = append(allReviews, &base.Review{ ID: pr.ID, - IssueIndex: index, + IssueIndex: context.LocalID(), ReviewerID: pr.Reviewer.ID, ReviewerName: pr.Reviewer.UserName, Official: pr.Official, diff --git a/modules/migrations/gitea_downloader_test.go b/modules/migrations/gitea_downloader_test.go index 7ce8aa0e0b..71bdecaead 100644 --- a/modules/migrations/gitea_downloader_test.go +++ b/modules/migrations/gitea_downloader_test.go @@ -199,7 +199,7 @@ func TestGiteaDownloadRepo(t *testing.T) { }, issues) comments, _, err := downloader.GetComments(base.GetCommentOptions{ - IssueNumber: 4, + Context: base.BasicIssueContext(4), }) assert.NoError(t, err) assertCommentsEqual(t, []*base.Comment{ @@ -265,7 +265,7 @@ func TestGiteaDownloadRepo(t *testing.T) { PatchURL: "https://gitea.com/gitea/test_repo/pulls/12.patch", }, prs[1]) - reviews, err := downloader.GetReviews(7) + reviews, err := downloader.GetReviews(base.BasicIssueContext(7)) assert.NoError(t, err) assertReviewsEqual(t, []*base.Review{ { diff --git a/modules/migrations/gitea_uploader.go b/modules/migrations/gitea_uploader.go index 01fb9bda8a..c77ace797b 100644 --- a/modules/migrations/gitea_uploader.go +++ b/modules/migrations/gitea_uploader.go @@ -609,6 +609,9 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR // download patch file err := func() error { + if pr.PatchURL == "" { + return nil + } // pr.PatchURL maybe a local file ret, err := uri.Open(pr.PatchURL) if err != nil { diff --git a/modules/migrations/github.go b/modules/migrations/github.go index f6063b0661..54af10d116 100644 --- a/modules/migrations/github.go +++ b/modules/migrations/github.go @@ -428,6 +428,7 @@ func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool, Closed: issue.ClosedAt, IsLocked: issue.GetLocked(), Assignees: assignees, + Context: base.BasicIssueContext(*issue.Number), }) } @@ -441,15 +442,15 @@ func (g *GithubDownloaderV3) SupportGetRepoComments() bool { // GetComments returns comments according issueNumber func (g *GithubDownloaderV3) GetComments(opts base.GetCommentOptions) ([]*base.Comment, bool, error) { - if opts.IssueNumber > 0 { - comments, err := g.getComments(opts.IssueNumber) + if opts.Context != nil { + comments, err := g.getComments(opts.Context) return comments, false, err } 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 ( allComments = make([]*base.Comment, 0, g.maxPerPage) created = "created" @@ -464,7 +465,7 @@ func (g *GithubDownloaderV3) getComments(issueNumber int64) ([]*base.Comment, er } for { 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 { 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{ - IssueIndex: issueNumber, + IssueIndex: issueContext.LocalID(), PosterID: comment.GetUser().GetID(), PosterName: comment.GetUser().GetLogin(), PosterEmail: comment.GetUser().GetEmail(), @@ -661,6 +662,7 @@ func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullReq }, PatchURL: pr.GetPatchURL(), Reactions: reactions, + Context: base.BasicIssueContext(*pr.Number), }) } @@ -724,28 +726,28 @@ func (g *GithubDownloaderV3) convertGithubReviewComments(cs []*github.PullReques } // 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) opt := &github.ListOptions{ PerPage: g.maxPerPage, } for { 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 { return nil, fmt.Errorf("error while listing repos: %v", err) } g.rate = &resp.Rate for _, review := range reviews { r := convertGithubReview(review) - r.IssueIndex = pullRequestNumber + r.IssueIndex = context.LocalID() // retrieve all review comments opt2 := &github.ListOptions{ PerPage: g.maxPerPage, } for { 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 { return nil, fmt.Errorf("error while listing repos: %v", err) } diff --git a/modules/migrations/github_test.go b/modules/migrations/github_test.go index 16d79d200c..4a53f20a76 100644 --- a/modules/migrations/github_test.go +++ b/modules/migrations/github_test.go @@ -216,7 +216,7 @@ func TestGitHubDownloadRepo(t *testing.T) { // downloader.GetComments() comments, _, err := downloader.GetComments(base.GetCommentOptions{ - IssueNumber: 2, + Context: base.BasicIssueContext(2), }) assert.NoError(t, err) assertCommentsEqual(t, []*base.Comment{ @@ -286,6 +286,7 @@ func TestGitHubDownloadRepo(t *testing.T) { Merged: true, MergedTime: timePtr(time.Date(2019, 11, 12, 21, 39, 27, 0, time.UTC)), MergeCommitSHA: "f32b0a9dfd09a60f616f29158f772cedd89942d2", + Context: base.BasicIssueContext(3), }, { Number: 4, @@ -332,10 +333,11 @@ func TestGitHubDownloadRepo(t *testing.T) { Content: "+1", }, }, + Context: base.BasicIssueContext(4), }, }, prs) - reviews, err := downloader.GetReviews(3) + reviews, err := downloader.GetReviews(base.BasicIssueContext(3)) assert.NoError(t, err) assertReviewsEqual(t, []*base.Review{ { @@ -367,7 +369,7 @@ func TestGitHubDownloadRepo(t *testing.T) { }, }, reviews) - reviews, err = downloader.GetReviews(4) + reviews, err = downloader.GetReviews(base.BasicIssueContext(4)) assert.NoError(t, err) assertReviewsEqual(t, []*base.Review{ { diff --git a/modules/migrations/gitlab.go b/modules/migrations/gitlab.go index 28e9eac63c..d5bf2d2d76 100644 --- a/modules/migrations/gitlab.go +++ b/modules/migrations/gitlab.go @@ -63,17 +63,14 @@ func (f *GitlabDownloaderFactory) GitServiceType() structs.GitServiceType { // from gitlab via go-gitlab // - issueCount is incremented in GetIssues() to ensure PR and Issue numbers do not overlap, // 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 { base.NullDownloader - ctx context.Context - client *gitlab.Client - repoID int - repoName string - issueCount int64 - fetchPRcomments bool - maxPerPage int + ctx context.Context + client *gitlab.Client + repoID int + repoName string + issueCount int64 + maxPerPage int } // NewGitlabDownloader creates a gitlab Downloader via gitlab API @@ -364,6 +361,20 @@ func (g *GitlabDownloader) GetReleases() ([]*base.Release, error) { 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 // 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) { @@ -433,6 +444,11 @@ func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er Closed: issue.ClosedAt, IsLocked: issue.DiscussionLocked, Updated: *issue.UpdatedAt, + Context: gitlabIssueContext{ + foreignID: int64(issue.IID), + localID: int64(issue.IID), + IsMergeRequest: false, + }, }) // 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 // TODO: figure out how to transfer comment reactions 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 page = 1 - var realIssueNumber int64 for { var comments []*gitlab.Discussion var resp *gitlab.Response var err error - // fetchPRcomments decides whether to fetch Issue or PR comments - if !g.fetchPRcomments { - realIssueNumber = issueNumber - comments, resp, err = g.client.Discussions.ListIssueDiscussions(g.repoID, int(realIssueNumber), &gitlab.ListIssueDiscussionsOptions{ + if !context.IsMergeRequest { + comments, resp, err = g.client.Discussions.ListIssueDiscussions(g.repoID, int(context.ForeignID()), &gitlab.ListIssueDiscussionsOptions{ Page: page, PerPage: g.maxPerPage, }, nil, gitlab.WithContext(g.ctx)) } else { - // If this is a PR, we need to figure out the Gitlab/original PR ID to be passed below - realIssueNumber = issueNumber - g.issueCount - comments, resp, err = g.client.Discussions.ListMergeRequestDiscussions(g.repoID, int(realIssueNumber), &gitlab.ListMergeRequestDiscussionsOptions{ + comments, resp, err = g.client.Discussions.ListMergeRequestDiscussions(g.repoID, int(context.ForeignID()), &gitlab.ListMergeRequestDiscussionsOptions{ Page: page, PerPage: g.maxPerPage, }, nil, gitlab.WithContext(g.ctx)) @@ -479,7 +494,7 @@ func (g *GitlabDownloader) GetComments(opts base.GetCommentOptions) ([]*base.Com if !comment.IndividualNote { for _, note := range comment.Notes { allComments = append(allComments, &base.Comment{ - IssueIndex: realIssueNumber, + IssueIndex: context.LocalID(), PosterID: int64(note.Author.ID), PosterName: note.Author.Username, PosterEmail: note.Author.Email, @@ -490,7 +505,7 @@ func (g *GitlabDownloader) GetComments(opts base.GetCommentOptions) ([]*base.Com } else { c := comment.Notes[0] allComments = append(allComments, &base.Comment{ - IssueIndex: realIssueNumber, + IssueIndex: context.LocalID(), PosterID: int64(c.Author.ID), PosterName: c.Author.Username, 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) 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{ Title: pr.Title, Number: newPRNumber, - OriginalNumber: int64(pr.IID), PosterName: pr.Author.Username, PosterID: int64(pr.Author.ID), Content: pr.Description, @@ -615,6 +626,11 @@ func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque OwnerName: pr.Author.Username, }, 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 -func (g *GitlabDownloader) GetReviews(pullRequestNumber int64) ([]*base.Review, error) { - approvals, resp, err := g.client.MergeRequestApprovals.GetConfiguration(g.repoID, int(pullRequestNumber), gitlab.WithContext(g.ctx)) +func (g *GitlabDownloader) GetReviews(context base.IssueContext) ([]*base.Review, error) { + approvals, resp, err := g.client.MergeRequestApprovals.GetConfiguration(g.repoID, int(context.ForeignID()), gitlab.WithContext(g.ctx)) if err != nil { if resp != nil && resp.StatusCode == 404 { 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)) for _, user := range approvals.ApprovedBy { reviews = append(reviews, &base.Review{ + IssueIndex: context.LocalID(), ReviewerID: int64(user.User.ID), ReviewerName: user.User.Username, CreatedAt: *approvals.UpdatedAt, diff --git a/modules/migrations/gitlab_test.go b/modules/migrations/gitlab_test.go index 8fd915e0f5..c3ee8118c5 100644 --- a/modules/migrations/gitlab_test.go +++ b/modules/migrations/gitlab_test.go @@ -210,7 +210,11 @@ func TestGitlabDownloadRepo(t *testing.T) { }, issues) comments, _, err := downloader.GetComments(base.GetCommentOptions{ - IssueNumber: 2, + Context: gitlabIssueContext{ + foreignID: 2, + localID: 2, + IsMergeRequest: false, + }, }) assert.NoError(t, err) assertCommentsEqual(t, []*base.Comment{ @@ -252,15 +256,14 @@ func TestGitlabDownloadRepo(t *testing.T) { assert.NoError(t, err) assertPullRequestsEqual(t, []*base.PullRequest{ { - Number: 4, - OriginalNumber: 2, - Title: "Test branch", - Content: "do not merge this PR", - Milestone: "1.0.0", - PosterID: 1241334, - PosterName: "lafriks", - State: "opened", - Created: time.Date(2019, 11, 28, 15, 56, 54, 104000000, time.UTC), + Number: 4, + Title: "Test branch", + Content: "do not merge this PR", + Milestone: "1.0.0", + PosterID: 1241334, + PosterName: "lafriks", + State: "opened", + Created: time.Date(2019, 11, 28, 15, 56, 54, 104000000, time.UTC), Labels: []*base.Label{ { Name: "bug", @@ -293,10 +296,15 @@ func TestGitlabDownloadRepo(t *testing.T) { Merged: false, MergedTime: nil, MergeCommitSHA: "", + Context: gitlabIssueContext{ + foreignID: 2, + localID: 4, + IsMergeRequest: true, + }, }, }, prs) - rvs, err := downloader.GetReviews(1) + rvs, err := downloader.GetReviews(base.BasicIssueContext(1)) assert.NoError(t, err) assertReviewsEqual(t, []*base.Review{ { @@ -313,7 +321,7 @@ func TestGitlabDownloadRepo(t *testing.T) { }, }, rvs) - rvs, err = downloader.GetReviews(2) + rvs, err = downloader.GetReviews(base.BasicIssueContext(2)) assert.NoError(t, err) assertReviewsEqual(t, []*base.Review{ { diff --git a/modules/migrations/gogs.go b/modules/migrations/gogs.go index 2c7fa76146..06c944278b 100644 --- a/modules/migrations/gogs.go +++ b/modules/migrations/gogs.go @@ -228,10 +228,9 @@ func (g *GogsDownloader) getIssues(page int, state string) ([]*base.Issue, bool, // GetComments returns comments according issueNumber func (g *GogsDownloader) GetComments(opts base.GetCommentOptions) ([]*base.Comment, bool, error) { - var issueNumber = opts.IssueNumber 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 { 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 } allComments = append(allComments, &base.Comment{ - IssueIndex: issueNumber, + IssueIndex: opts.Context.LocalID(), PosterID: comment.Poster.ID, PosterName: comment.Poster.Login, PosterEmail: comment.Poster.Email, @@ -304,6 +303,7 @@ func convertGogsIssue(issue *gogs.Issue) *base.Issue { Updated: issue.Updated, Labels: labels, Closed: closed, + Context: base.BasicIssueContext(issue.Index), } } diff --git a/modules/migrations/gogs_test.go b/modules/migrations/gogs_test.go index e5bd634c55..8816fab44f 100644 --- a/modules/migrations/gogs_test.go +++ b/modules/migrations/gogs_test.go @@ -112,7 +112,7 @@ func TestGogsDownloadRepo(t *testing.T) { // downloader.GetComments() comments, _, err := downloader.GetComments(base.GetCommentOptions{ - IssueNumber: 1, + Context: base.BasicIssueContext(1), }) assert.NoError(t, err) assertCommentsEqual(t, []*base.Comment{ diff --git a/modules/migrations/migrate.go b/modules/migrations/migrate.go index 0a507d9c33..7d5aa9670b 100644 --- a/modules/migrations/migrate.go +++ b/modules/migrations/migrate.go @@ -318,7 +318,7 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts for _, issue := range issues { log.Trace("migrating issue %d's comments", issue.Number) comments, _, err := downloader.GetComments(base.GetCommentOptions{ - IssueNumber: issue.Number, + Context: issue.Context, }) if err != nil { if !base.IsErrNotSupported(err) { @@ -376,7 +376,7 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts for _, pr := range prs { log.Trace("migrating pull request %d's comments", pr.Number) comments, _, err := downloader.GetComments(base.GetCommentOptions{ - IssueNumber: pr.Number, + Context: pr.Context, }) if err != nil { if !base.IsErrNotSupported(err) { @@ -404,14 +404,7 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts // migrate reviews var allReviews = make([]*base.Review, 0, reviewBatchSize) for _, pr := range prs { - number := pr.Number - - // on gitlab migrations pull number change - if pr.OriginalNumber > 0 { - number = pr.OriginalNumber - } - - reviews, err := downloader.GetReviews(number) + reviews, err := downloader.GetReviews(pr.Context) if err != nil { if !base.IsErrNotSupported(err) { return err @@ -419,11 +412,6 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts log.Warn("migrating reviews is not supported, ignored") break } - if pr.OriginalNumber > 0 { - for i := range reviews { - reviews[i].IssueIndex = pr.Number - } - } allReviews = append(allReviews, reviews...) diff --git a/modules/migrations/onedev.go b/modules/migrations/onedev.go new file mode 100644 index 0000000000..e60265895f --- /dev/null +++ b/modules/migrations/onedev.go @@ -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 +} diff --git a/modules/migrations/onedev_test.go b/modules/migrations/onedev_test.go new file mode 100644 index 0000000000..48d56c3e22 --- /dev/null +++ b/modules/migrations/onedev_test.go @@ -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) +} diff --git a/modules/migrations/restore.go b/modules/migrations/restore.go index 6177f80cbb..6287d601c2 100644 --- a/modules/migrations/restore.go +++ b/modules/migrations/restore.go @@ -208,13 +208,16 @@ func (r *RepositoryRestorer) GetIssues(page, perPage int) ([]*base.Issue, bool, if err != nil { return nil, false, err } + for _, issue := range issues { + issue.Context = base.BasicIssueContext(issue.Number) + } return issues, true, nil } // GetComments returns comments according issueNumber func (r *RepositoryRestorer) GetComments(opts base.GetCommentOptions) ([]*base.Comment, bool, error) { 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) if err != nil { if os.IsNotExist(err) { @@ -258,14 +261,15 @@ func (r *RepositoryRestorer) GetPullRequests(page, perPage int) ([]*base.PullReq } for _, pr := range pulls { pr.PatchURL = "file://" + filepath.Join(r.baseDir, pr.PatchURL) + pr.Context = base.BasicIssueContext(pr.Number) } return pulls, true, nil } // 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) - 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) if err != nil { if os.IsNotExist(err) { diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 2089f4d69c..313a982f43 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -248,6 +248,7 @@ const ( GiteaService // 3 gitea service GitlabService // 4 gitlab service GogsService // 5 gogs service + OneDevService // 6 onedev service ) // Name represents the service type's name @@ -267,6 +268,8 @@ func (gt GitServiceType) Title() string { return "GitLab" case GogsService: return "Gogs" + case OneDevService: + return "OneDev" case PlainGitService: return "Git" } @@ -322,5 +325,6 @@ var ( GitlabService, GiteaService, GogsService, + OneDevService, } ) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 4715afcd3e..3eb3825776 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -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.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.onedev.description = Migrating data from code.onedev.io or Self-Hosted OneDev server. migrate.migrating_git = Migrating Git Data migrate.migrating_topics = Migrating Topics migrate.migrating_milestones = Migrating Milestones diff --git a/public/img/svg/gitea-onedev.svg b/public/img/svg/gitea-onedev.svg new file mode 100644 index 0000000000..1f0d1d8363 --- /dev/null +++ b/public/img/svg/gitea-onedev.svg @@ -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>
\ No newline at end of file diff --git a/templates/repo/migrate/onedev.tmpl b/templates/repo/migrate/onedev.tmpl new file mode 100644 index 0000000000..def366f9d8 --- /dev/null +++ b/templates/repo/migrate/onedev.tmpl @@ -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" .}} diff --git a/web_src/svg/gitea-onedev.svg b/web_src/svg/gitea-onedev.svg new file mode 100644 index 0000000000..490c22fc8e --- /dev/null +++ b/web_src/svg/gitea-onedev.svg @@ -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>
\ No newline at end of file |