forked from gitea/gitea
		
	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 | ||||
| 	case "gogs": | ||||
| 		return structs.GogsService | ||||
| 	case "onedev": | ||||
| 		return structs.OneDevService | ||||
| 	default: | ||||
| 		return structs.PlainGitService | ||||
| 	} | ||||
|  | ||||
| @ -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) | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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:"-"` | ||||
| } | ||||
|  | ||||
| @ -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"} | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
| 	}) | ||||
| 
 | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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{ | ||||
| 		{ | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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) | ||||
| 				} | ||||
|  | ||||
| @ -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{ | ||||
| 		{ | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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{ | ||||
| 		{ | ||||
|  | ||||
| @ -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), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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{ | ||||
|  | ||||
| @ -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...) | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										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 { | ||||
| 		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) { | ||||
|  | ||||
| @ -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, | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
							
								
								
									
										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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 KN4CK3R
						KN4CK3R