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