From f6067a8465e7762aea1561106cfee291409a0fd6 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 24 Jan 2020 01:28:15 +0800
Subject: [PATCH] Migrate reviews when migrating repository from github (#9463)

* fix typo

* Migrate reviews when migrating repository from github

* fix lint

* Added test and migration when external user login

* fix test

* fix commented state

* Some improvements

* fix bug when get pull request and ref original author on code comments

* Fix migrated line; Added comment for review

* Don't load all pull requests attributes

* Fix typo

* wrong change copy head

* fix tests

* fix reactions

* Fix test

* fix fmt

* fix review comment reactions
---
 models/external_login_user.go                 |   5 +-
 models/migrate.go                             |  27 +++-
 models/migrations/migrations.go               |   2 +
 models/migrations/v125.go                     |  23 ++++
 models/pull.go                                |  13 ++
 models/review.go                              |  58 ++++++++-
 modules/migrations/base/downloader.go         |   1 +
 modules/migrations/base/review.go             |  44 +++++++
 modules/migrations/base/uploader.go           |   1 +
 modules/migrations/git.go                     |   5 +
 modules/migrations/gitea.go                   | 120 ++++++++++++++++++
 modules/migrations/github.go                  | 114 ++++++++++++++++-
 modules/migrations/github_test.go             |  91 +++++++++++++
 modules/migrations/migrate.go                 |  29 ++++-
 services/gitdiff/gitdiff.go                   |  23 ++--
 services/gitdiff/gitdiff_test.go              |   8 ++
 templates/repo/diff/comments.tmpl             |  18 ++-
 .../repo/issue/view_content/comments.tmpl     |  17 ++-
 18 files changed, 567 insertions(+), 32 deletions(-)
 create mode 100644 models/migrations/v125.go
 create mode 100644 modules/migrations/base/review.go

diff --git a/models/external_login_user.go b/models/external_login_user.go
index 6585e49fef..ff153dfb86 100644
--- a/models/external_login_user.go
+++ b/models/external_login_user.go
@@ -181,5 +181,8 @@ func UpdateMigrationsByType(tp structs.GitServiceType, externalUserID string, us
 		return err
 	}
 
-	return UpdateReactionsMigrationsByType(tp, externalUserID, userID)
+	if err := UpdateReactionsMigrationsByType(tp, externalUserID, userID); err != nil {
+		return err
+	}
+	return UpdateReviewsMigrationsByType(tp, externalUserID, userID)
 }
diff --git a/models/migrate.go b/models/migrate.go
index fd28fd156f..28661527a6 100644
--- a/models/migrate.go
+++ b/models/migrate.go
@@ -4,7 +4,12 @@
 
 package models
 
-import "xorm.io/xorm"
+import (
+	"code.gitea.io/gitea/modules/structs"
+
+	"xorm.io/builder"
+	"xorm.io/xorm"
+)
 
 // InsertMilestones creates milestones of repository.
 func InsertMilestones(ms ...*Milestone) (err error) {
@@ -202,3 +207,23 @@ func InsertReleases(rels ...*Release) error {
 
 	return sess.Commit()
 }
+
+// UpdateReviewsMigrationsByType updates reviews' migrations information via given git service type and original id and poster id
+func UpdateReviewsMigrationsByType(tp structs.GitServiceType, originalAuthorID string, posterID int64) error {
+	_, err := x.Table("review").
+		Where(builder.In("issue_id",
+			builder.Select("issue.id").
+				From("issue").
+				InnerJoin("repository", "issue.repo_id = repository.id").
+				Where(builder.Eq{
+					"repository.original_service_type": tp,
+				}),
+		)).
+		And("review.original_author_id = ?", originalAuthorID).
+		Update(map[string]interface{}{
+			"poster_id":          posterID,
+			"original_author":    "",
+			"original_author_id": 0,
+		})
+	return err
+}
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 226368b7f3..286e809abb 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -304,6 +304,8 @@ var migrations = []Migration{
 	NewMigration("Add original informations for reactions", addReactionOriginals),
 	// v124 -> v125
 	NewMigration("Add columns to user and repository", addUserRepoMissingColumns),
+	// v125 -> v126
+	NewMigration("Add some columns on review for migration", addReviewMigrateInfo),
 }
 
 // Migrate database to current version
diff --git a/models/migrations/v125.go b/models/migrations/v125.go
new file mode 100644
index 0000000000..ac567f66b9
--- /dev/null
+++ b/models/migrations/v125.go
@@ -0,0 +1,23 @@
+// Copyright 2020 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 (
+	"fmt"
+
+	"xorm.io/xorm"
+)
+
+func addReviewMigrateInfo(x *xorm.Engine) error {
+	type Review struct {
+		OriginalAuthor   string
+		OriginalAuthorID int64
+	}
+
+	if err := x.Sync2(new(Review)); err != nil {
+		return fmt.Errorf("Sync2: %v", err)
+	}
+	return nil
+}
diff --git a/models/pull.go b/models/pull.go
index 3ef631852e..42d93bd541 100644
--- a/models/pull.go
+++ b/models/pull.go
@@ -655,6 +655,19 @@ func GetPullRequestByID(id int64) (*PullRequest, error) {
 	return getPullRequestByID(x, id)
 }
 
+// GetPullRequestByIssueIDWithNoAttributes returns pull request with no attributes loaded by given issue ID.
+func GetPullRequestByIssueIDWithNoAttributes(issueID int64) (*PullRequest, error) {
+	var pr PullRequest
+	has, err := x.Where("issue_id = ?", issueID).Get(&pr)
+	if err != nil {
+		return nil, err
+	}
+	if !has {
+		return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""}
+	}
+	return &pr, nil
+}
+
 func getPullRequestByIssueID(e Engine, issueID int64) (*PullRequest, error) {
 	pr := &PullRequest{
 		IssueID: issueID,
diff --git a/models/review.go b/models/review.go
index 2838cfa316..ec28b41d35 100644
--- a/models/review.go
+++ b/models/review.go
@@ -45,13 +45,15 @@ func (rt ReviewType) Icon() string {
 
 // Review represents collection of code comments giving feedback for a PR
 type Review struct {
-	ID         int64 `xorm:"pk autoincr"`
-	Type       ReviewType
-	Reviewer   *User  `xorm:"-"`
-	ReviewerID int64  `xorm:"index"`
-	Issue      *Issue `xorm:"-"`
-	IssueID    int64  `xorm:"index"`
-	Content    string `xorm:"TEXT"`
+	ID               int64 `xorm:"pk autoincr"`
+	Type             ReviewType
+	Reviewer         *User `xorm:"-"`
+	ReviewerID       int64 `xorm:"index"`
+	OriginalAuthor   string
+	OriginalAuthorID int64
+	Issue            *Issue `xorm:"-"`
+	IssueID          int64  `xorm:"index"`
+	Content          string `xorm:"TEXT"`
 	// Official is a review made by an assigned approver (counts towards approval)
 	Official bool   `xorm:"NOT NULL DEFAULT false"`
 	CommitID string `xorm:"VARCHAR(40)"`
@@ -62,6 +64,8 @@ type Review struct {
 
 	// CodeComments are the initial code comments of the review
 	CodeComments CodeComments `xorm:"-"`
+
+	Comments []*Comment `xorm:"-"`
 }
 
 func (r *Review) loadCodeComments(e Engine) (err error) {
@@ -398,3 +402,43 @@ func MarkReviewsAsNotStale(issueID int64, commitID string) (err error) {
 
 	return
 }
+
+// InsertReviews inserts review and review comments
+func InsertReviews(reviews []*Review) error {
+	sess := x.NewSession()
+	defer sess.Close()
+
+	if err := sess.Begin(); err != nil {
+		return err
+	}
+
+	for _, review := range reviews {
+		if _, err := sess.NoAutoTime().Insert(review); err != nil {
+			return err
+		}
+
+		if _, err := sess.NoAutoTime().Insert(&Comment{
+			Type:             CommentTypeReview,
+			Content:          review.Content,
+			PosterID:         review.ReviewerID,
+			OriginalAuthor:   review.OriginalAuthor,
+			OriginalAuthorID: review.OriginalAuthorID,
+			IssueID:          review.IssueID,
+			ReviewID:         review.ID,
+			CreatedUnix:      review.CreatedUnix,
+			UpdatedUnix:      review.UpdatedUnix,
+		}); err != nil {
+			return err
+		}
+
+		for _, c := range review.Comments {
+			c.ReviewID = review.ID
+		}
+
+		if _, err := sess.NoAutoTime().Insert(review.Comments); err != nil {
+			return err
+		}
+	}
+
+	return sess.Commit()
+}
diff --git a/modules/migrations/base/downloader.go b/modules/migrations/base/downloader.go
index 87ade5c02e..5de8c9513b 100644
--- a/modules/migrations/base/downloader.go
+++ b/modules/migrations/base/downloader.go
@@ -23,6 +23,7 @@ type Downloader interface {
 	GetIssues(page, perPage int) ([]*Issue, bool, error)
 	GetComments(issueNumber int64) ([]*Comment, error)
 	GetPullRequests(page, perPage int) ([]*PullRequest, error)
+	GetReviews(pullRequestNumber int64) ([]*Review, error)
 }
 
 // DownloaderFactory defines an interface to match a downloader implementation and create a downloader
diff --git a/modules/migrations/base/review.go b/modules/migrations/base/review.go
new file mode 100644
index 0000000000..8051fed653
--- /dev/null
+++ b/modules/migrations/base/review.go
@@ -0,0 +1,44 @@
+// Copyright 2019 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 base
+
+import "time"
+
+// enumerate all review states
+const (
+	ReviewStatePending          = "PENDING"
+	ReviewStateApproved         = "APPROVED"
+	ReviewStateChangesRequested = "CHANGES_REQUESTED"
+	ReviewStateCommented        = "COMMENTED"
+)
+
+// Review is a standard review information
+type Review struct {
+	ID           int64
+	IssueIndex   int64
+	ReviewerID   int64
+	ReviewerName string
+	Official     bool
+	CommitID     string
+	Content      string
+	CreatedAt    time.Time
+	State        string // PENDING, APPROVED, REQUEST_CHANGES, or COMMENT
+	Comments     []*ReviewComment
+}
+
+// ReviewComment represents a review comment
+type ReviewComment struct {
+	ID        int64
+	InReplyTo int64
+	Content   string
+	TreePath  string
+	DiffHunk  string
+	Position  int
+	CommitID  string
+	PosterID  int64
+	Reactions []*Reaction
+	CreatedAt time.Time
+	UpdatedAt time.Time
+}
diff --git a/modules/migrations/base/uploader.go b/modules/migrations/base/uploader.go
index 257c7a2909..85ad60fe0e 100644
--- a/modules/migrations/base/uploader.go
+++ b/modules/migrations/base/uploader.go
@@ -17,6 +17,7 @@ type Uploader interface {
 	CreateIssues(issues ...*Issue) error
 	CreateComments(comments ...*Comment) error
 	CreatePullRequests(prs ...*PullRequest) error
+	CreateReviews(reviews ...*Review) error
 	Rollback() error
 	Close()
 }
diff --git a/modules/migrations/git.go b/modules/migrations/git.go
index f7b1e857e4..af345808b5 100644
--- a/modules/migrations/git.go
+++ b/modules/migrations/git.go
@@ -78,3 +78,8 @@ func (g *PlainGitDownloader) GetComments(issueNumber int64) ([]*base.Comment, er
 func (g *PlainGitDownloader) GetPullRequests(start, limit int) ([]*base.PullRequest, error) {
 	return nil, ErrNotSupported
 }
+
+// GetReviews returns reviews according issue number
+func (g *PlainGitDownloader) GetReviews(issueNumber int64) ([]*base.Review, error) {
+	return nil, ErrNotSupported
+}
diff --git a/modules/migrations/gitea.go b/modules/migrations/gitea.go
index 82664d0d1a..96d47dc527 100644
--- a/modules/migrations/gitea.go
+++ b/modules/migrations/gitea.go
@@ -6,6 +6,7 @@
 package migrations
 
 import (
+	"bytes"
 	"context"
 	"fmt"
 	"io"
@@ -27,6 +28,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
+	"code.gitea.io/gitea/services/gitdiff"
 
 	gouuid "github.com/satori/go.uuid"
 )
@@ -48,6 +50,7 @@ type GiteaLocalUploader struct {
 	gitRepo        *git.Repository
 	prHeadCache    map[string]struct{}
 	userMap        map[int64]int64 // external user id mapping to user id
+	prCache        map[int64]*models.PullRequest
 	gitServiceType structs.GitServiceType
 }
 
@@ -60,6 +63,7 @@ func NewGiteaLocalUploader(ctx context.Context, doer *models.User, repoOwner, re
 		repoName:    repoName,
 		prHeadCache: make(map[string]struct{}),
 		userMap:     make(map[int64]int64),
+		prCache:     make(map[int64]*models.PullRequest),
 	}
 }
 
@@ -706,6 +710,122 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR
 	return &pullRequest, nil
 }
 
+func convertReviewState(state string) models.ReviewType {
+	switch state {
+	case base.ReviewStatePending:
+		return models.ReviewTypePending
+	case base.ReviewStateApproved:
+		return models.ReviewTypeApprove
+	case base.ReviewStateChangesRequested:
+		return models.ReviewTypeReject
+	case base.ReviewStateCommented:
+		return models.ReviewTypeComment
+	default:
+		return models.ReviewTypePending
+	}
+}
+
+// CreateReviews create pull request reviews
+func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
+	var cms = make([]*models.Review, 0, len(reviews))
+	for _, review := range reviews {
+		var issueID int64
+		if issueIDStr, ok := g.issues.Load(review.IssueIndex); !ok {
+			issue, err := models.GetIssueByIndex(g.repo.ID, review.IssueIndex)
+			if err != nil {
+				return err
+			}
+			issueID = issue.ID
+			g.issues.Store(review.IssueIndex, issueID)
+		} else {
+			issueID = issueIDStr.(int64)
+		}
+
+		userid, ok := g.userMap[review.ReviewerID]
+		tp := g.gitServiceType.Name()
+		if !ok && tp != "" {
+			var err error
+			userid, err = models.GetUserIDByExternalUserID(tp, fmt.Sprintf("%v", review.ReviewerID))
+			if err != nil {
+				log.Error("GetUserIDByExternalUserID: %v", err)
+			}
+			if userid > 0 {
+				g.userMap[review.ReviewerID] = userid
+			}
+		}
+
+		var cm = models.Review{
+			Type:        convertReviewState(review.State),
+			IssueID:     issueID,
+			Content:     review.Content,
+			Official:    review.Official,
+			CreatedUnix: timeutil.TimeStamp(review.CreatedAt.Unix()),
+			UpdatedUnix: timeutil.TimeStamp(review.CreatedAt.Unix()),
+		}
+
+		if userid > 0 {
+			cm.ReviewerID = userid
+		} else {
+			cm.ReviewerID = g.doer.ID
+			cm.OriginalAuthor = review.ReviewerName
+			cm.OriginalAuthorID = review.ReviewerID
+		}
+
+		// get pr
+		pr, ok := g.prCache[issueID]
+		if !ok {
+			var err error
+			pr, err = models.GetPullRequestByIssueIDWithNoAttributes(issueID)
+			if err != nil {
+				return err
+			}
+			g.prCache[issueID] = pr
+		}
+
+		for _, comment := range review.Comments {
+			headCommitID, err := g.gitRepo.GetRefCommitID(pr.GetGitRefName())
+			if err != nil {
+				return fmt.Errorf("GetRefCommitID[%s]: %v", pr.GetGitRefName(), err)
+			}
+			patchBuf := new(bytes.Buffer)
+			if err := gitdiff.GetRawDiffForFile(g.gitRepo.Path, pr.MergeBase, headCommitID, gitdiff.RawDiffNormal, comment.TreePath, patchBuf); err != nil {
+				return fmt.Errorf("GetRawDiffForLine[%s, %s, %s, %s]: %v", g.gitRepo.Path, pr.MergeBase, headCommitID, comment.TreePath, err)
+			}
+
+			_, _, line, _ := gitdiff.ParseDiffHunkString(comment.DiffHunk)
+
+			patch := gitdiff.CutDiffAroundLine(patchBuf, int64((&models.Comment{Line: int64(line + comment.Position - 1)}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines)
+
+			var c = models.Comment{
+				Type:        models.CommentTypeCode,
+				PosterID:    comment.PosterID,
+				IssueID:     issueID,
+				Content:     comment.Content,
+				Line:        int64(line + comment.Position - 1),
+				TreePath:    comment.TreePath,
+				CommitSHA:   comment.CommitID,
+				Patch:       patch,
+				CreatedUnix: timeutil.TimeStamp(comment.CreatedAt.Unix()),
+				UpdatedUnix: timeutil.TimeStamp(comment.UpdatedAt.Unix()),
+			}
+
+			if userid > 0 {
+				c.PosterID = userid
+			} else {
+				c.PosterID = g.doer.ID
+				c.OriginalAuthor = review.ReviewerName
+				c.OriginalAuthorID = review.ReviewerID
+			}
+
+			cm.Comments = append(cm.Comments, &c)
+		}
+
+		cms = append(cms, &cm)
+	}
+
+	return models.InsertReviews(cms)
+}
+
 // Rollback when migrating failed, this will rollback all the changes.
 func (g *GiteaLocalUploader) Rollback() error {
 	if g.repo != nil && g.repo.ID > 0 {
diff --git a/modules/migrations/github.go b/modules/migrations/github.go
index a99b05e11c..488cd82f5b 100644
--- a/modules/migrations/github.go
+++ b/modules/migrations/github.go
@@ -418,10 +418,14 @@ func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool,
 
 // GetComments returns comments according issueNumber
 func (g *GithubDownloaderV3) GetComments(issueNumber int64) ([]*base.Comment, error) {
-	var allComments = make([]*base.Comment, 0, 100)
+	var (
+		allComments = make([]*base.Comment, 0, 100)
+		created     = "created"
+		asc         = "asc"
+	)
 	opt := &github.IssueListCommentsOptions{
-		Sort:      "created",
-		Direction: "asc",
+		Sort:      created,
+		Direction: asc,
 		ListOptions: github.ListOptions{
 			PerPage: 100,
 		},
@@ -614,3 +618,107 @@ func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullReq
 
 	return allPRs, nil
 }
+
+func convertGithubReview(r *github.PullRequestReview) *base.Review {
+	return &base.Review{
+		ID:           r.GetID(),
+		ReviewerID:   r.GetUser().GetID(),
+		ReviewerName: r.GetUser().GetLogin(),
+		CommitID:     r.GetCommitID(),
+		Content:      r.GetBody(),
+		CreatedAt:    r.GetSubmittedAt(),
+		State:        r.GetState(),
+	}
+}
+
+func (g *GithubDownloaderV3) convertGithubReviewComments(cs []*github.PullRequestComment) ([]*base.ReviewComment, error) {
+	var rcs = make([]*base.ReviewComment, 0, len(cs))
+	for _, c := range cs {
+		// get reactions
+		var reactions []*base.Reaction
+		for i := 1; ; i++ {
+			g.sleep()
+			res, resp, err := g.client.Reactions.ListPullRequestCommentReactions(g.ctx, g.repoOwner, g.repoName, c.GetID(), &github.ListOptions{
+				Page:    i,
+				PerPage: 100,
+			})
+			if err != nil {
+				return nil, err
+			}
+			g.rate = &resp.Rate
+			if len(res) == 0 {
+				break
+			}
+			for _, reaction := range res {
+				reactions = append(reactions, &base.Reaction{
+					UserID:   reaction.User.GetID(),
+					UserName: reaction.User.GetLogin(),
+					Content:  reaction.GetContent(),
+				})
+			}
+		}
+
+		rcs = append(rcs, &base.ReviewComment{
+			ID:        c.GetID(),
+			InReplyTo: c.GetInReplyTo(),
+			Content:   c.GetBody(),
+			TreePath:  c.GetPath(),
+			DiffHunk:  c.GetDiffHunk(),
+			Position:  c.GetPosition(),
+			CommitID:  c.GetCommitID(),
+			PosterID:  c.GetUser().GetID(),
+			Reactions: reactions,
+			CreatedAt: c.GetCreatedAt(),
+			UpdatedAt: c.GetUpdatedAt(),
+		})
+	}
+	return rcs, nil
+}
+
+// GetReviews returns pull requests review
+func (g *GithubDownloaderV3) GetReviews(pullRequestNumber int64) ([]*base.Review, error) {
+	var allReviews = make([]*base.Review, 0, 100)
+	opt := &github.ListOptions{
+		PerPage: 100,
+	}
+	for {
+		g.sleep()
+		reviews, resp, err := g.client.PullRequests.ListReviews(g.ctx, g.repoOwner, g.repoName, int(pullRequestNumber), 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
+			// retrieve all review comments
+			opt2 := &github.ListOptions{
+				PerPage: 100,
+			}
+			for {
+				g.sleep()
+				reviewComments, resp, err := g.client.PullRequests.ListReviewComments(g.ctx, g.repoOwner, g.repoName, int(pullRequestNumber), review.GetID(), opt2)
+				if err != nil {
+					return nil, fmt.Errorf("error while listing repos: %v", err)
+				}
+				g.rate = &resp.Rate
+
+				cs, err := g.convertGithubReviewComments(reviewComments)
+				if err != nil {
+					return nil, err
+				}
+				r.Comments = append(r.Comments, cs...)
+				if resp.NextPage == 0 {
+					break
+				}
+				opt2.Page = resp.NextPage
+			}
+			allReviews = append(allReviews, r)
+		}
+		if resp.NextPage == 0 {
+			break
+		}
+		opt.Page = resp.NextPage
+	}
+	return allReviews, nil
+}
diff --git a/modules/migrations/github_test.go b/modules/migrations/github_test.go
index 9023e2ac4d..814c771e8c 100644
--- a/modules/migrations/github_test.go
+++ b/modules/migrations/github_test.go
@@ -361,4 +361,95 @@ func TestGitHubDownloadRepo(t *testing.T) {
 			},
 		},
 	}, prs)
+
+	reviews, err := downloader.GetReviews(3)
+	assert.NoError(t, err)
+	assert.EqualValues(t, []*base.Review{
+		{
+			ID:           315859956,
+			IssueIndex:   3,
+			ReviewerID:   42128690,
+			ReviewerName: "jolheiser",
+			CommitID:     "076160cf0b039f13e5eff19619932d181269414b",
+			CreatedAt:    time.Date(2019, 11, 12, 21, 35, 24, 0, time.UTC),
+			State:        base.ReviewStateApproved,
+		},
+		{
+			ID:           315860062,
+			IssueIndex:   3,
+			ReviewerID:   1824502,
+			ReviewerName: "zeripath",
+			CommitID:     "076160cf0b039f13e5eff19619932d181269414b",
+			CreatedAt:    time.Date(2019, 11, 12, 21, 35, 36, 0, time.UTC),
+			State:        base.ReviewStateApproved,
+		},
+		{
+			ID:           315861440,
+			IssueIndex:   3,
+			ReviewerID:   165205,
+			ReviewerName: "lafriks",
+			CommitID:     "076160cf0b039f13e5eff19619932d181269414b",
+			CreatedAt:    time.Date(2019, 11, 12, 21, 38, 00, 0, time.UTC),
+			State:        base.ReviewStateApproved,
+		},
+	}, reviews)
+
+	reviews, err = downloader.GetReviews(4)
+	assert.NoError(t, err)
+	assert.EqualValues(t, []*base.Review{
+		{
+			ID:           338338740,
+			IssueIndex:   4,
+			ReviewerID:   81045,
+			ReviewerName: "lunny",
+			CommitID:     "2be9101c543658591222acbee3eb799edfc3853d",
+			CreatedAt:    time.Date(2020, 01, 04, 05, 33, 18, 0, time.UTC),
+			State:        base.ReviewStateApproved,
+			Comments: []*base.ReviewComment{
+				{
+					ID:        363017488,
+					Content:   "This is a good pull request.",
+					TreePath:  "README.md",
+					DiffHunk:  "@@ -1,2 +1,4 @@\n # test_repo\n Test repository for testing migration from github to gitea\n+",
+					Position:  3,
+					CommitID:  "2be9101c543658591222acbee3eb799edfc3853d",
+					PosterID:  81045,
+					CreatedAt: time.Date(2020, 01, 04, 05, 33, 06, 0, time.UTC),
+					UpdatedAt: time.Date(2020, 01, 04, 05, 33, 18, 0, time.UTC),
+				},
+			},
+		},
+		{
+			ID:           338339651,
+			IssueIndex:   4,
+			ReviewerID:   81045,
+			ReviewerName: "lunny",
+			CommitID:     "2be9101c543658591222acbee3eb799edfc3853d",
+			CreatedAt:    time.Date(2020, 01, 04, 06, 07, 06, 0, time.UTC),
+			State:        base.ReviewStateChangesRequested,
+			Content:      "Don't add more reviews",
+		},
+		{
+			ID:           338349019,
+			IssueIndex:   4,
+			ReviewerID:   81045,
+			ReviewerName: "lunny",
+			CommitID:     "2be9101c543658591222acbee3eb799edfc3853d",
+			CreatedAt:    time.Date(2020, 01, 04, 11, 21, 41, 0, time.UTC),
+			State:        base.ReviewStateCommented,
+			Comments: []*base.ReviewComment{
+				{
+					ID:        363029944,
+					Content:   "test a single comment.",
+					TreePath:  "LICENSE",
+					DiffHunk:  "@@ -19,3 +19,5 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n SOFTWARE.\n+",
+					Position:  4,
+					CommitID:  "2be9101c543658591222acbee3eb799edfc3853d",
+					PosterID:  81045,
+					CreatedAt: time.Date(2020, 01, 04, 11, 21, 41, 0, time.UTC),
+					UpdatedAt: time.Date(2020, 01, 04, 11, 21, 41, 0, time.UTC),
+				},
+			},
+		},
+	}, reviews)
 }
diff --git a/modules/migrations/migrate.go b/modules/migrations/migrate.go
index fb143f7e29..3b3e318f69 100644
--- a/modules/migrations/migrate.go
+++ b/modules/migrations/migrate.go
@@ -181,7 +181,10 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts
 		}
 	}
 
-	var commentBatchSize = uploader.MaxBatchInsertSize("comment")
+	var (
+		commentBatchSize = uploader.MaxBatchInsertSize("comment")
+		reviewBatchSize  = uploader.MaxBatchInsertSize("review")
+	)
 
 	if opts.Issues {
 		log.Trace("migrating issues and comments")
@@ -248,6 +251,7 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts
 				continue
 			}
 
+			// plain comments
 			var allComments = make([]*base.Comment, 0, commentBatchSize)
 			for _, pr := range prs {
 				comments, err := downloader.GetComments(pr.Number)
@@ -270,6 +274,29 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts
 				}
 			}
 
+			// migrate reviews
+			var allReviews = make([]*base.Review, 0, reviewBatchSize)
+			for _, pr := range prs {
+				reviews, err := downloader.GetReviews(pr.Number)
+				if err != nil {
+					return err
+				}
+
+				allReviews = append(allReviews, reviews...)
+
+				if len(allReviews) >= reviewBatchSize {
+					if err := uploader.CreateReviews(allReviews[:reviewBatchSize]...); err != nil {
+						return err
+					}
+					allReviews = allReviews[reviewBatchSize:]
+				}
+			}
+			if len(allReviews) > 0 {
+				if err := uploader.CreateReviews(allReviews...); err != nil {
+					return err
+				}
+			}
+
 			if len(prs) < prBatchSize {
 				break
 			}
diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go
index fc55c03595..6632f2d94e 100644
--- a/services/gitdiff/gitdiff.go
+++ b/services/gitdiff/gitdiff.go
@@ -149,14 +149,9 @@ func (d *DiffLine) GetExpandDirection() DiffLineExpandDirection {
 	return DiffLineExpandSingle
 }
 
-func getDiffLineSectionInfo(curFile *DiffFile, line string, lastLeftIdx, lastRightIdx int) *DiffLineSectionInfo {
-	var (
-		leftLine  int
-		leftHunk  int
-		rightLine int
-		righHunk  int
-	)
-	ss := strings.Split(line, "@@")
+// ParseDiffHunkString parse the diffhunk content and return
+func ParseDiffHunkString(diffhunk string) (leftLine, leftHunk, rightLine, righHunk int) {
+	ss := strings.Split(diffhunk, "@@")
 	ranges := strings.Split(ss[1][1:], " ")
 	leftRange := strings.Split(ranges[0], ",")
 	leftLine, _ = com.StrTo(leftRange[0][1:]).Int()
@@ -170,12 +165,18 @@ func getDiffLineSectionInfo(curFile *DiffFile, line string, lastLeftIdx, lastRig
 			righHunk, _ = com.StrTo(rightRange[1]).Int()
 		}
 	} else {
-		log.Warn("Parse line number failed: %v", line)
+		log.Warn("Parse line number failed: %v", diffhunk)
 		rightLine = leftLine
 		righHunk = leftHunk
 	}
+	return
+}
+
+func getDiffLineSectionInfo(treePath, line string, lastLeftIdx, lastRightIdx int) *DiffLineSectionInfo {
+	leftLine, leftHunk, rightLine, righHunk := ParseDiffHunkString(line)
+
 	return &DiffLineSectionInfo{
-		Path:          curFile.Name,
+		Path:          treePath,
 		LastLeftIdx:   lastLeftIdx,
 		LastRightIdx:  lastRightIdx,
 		LeftIdx:       leftLine,
@@ -651,7 +652,7 @@ func ParsePatch(maxLines, maxLineCharacters, maxFiles int, reader io.Reader) (*D
 		case line[0] == '@':
 			curSection = &DiffSection{}
 			curFile.Sections = append(curFile.Sections, curSection)
-			lineSectionInfo := getDiffLineSectionInfo(curFile, line, leftLine-1, rightLine-1)
+			lineSectionInfo := getDiffLineSectionInfo(curFile.Name, line, leftLine-1, rightLine-1)
 			diffLine := &DiffLine{
 				Type:        DiffLineSection,
 				Content:     line,
diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go
index ea25b38dff..58604d97c4 100644
--- a/services/gitdiff/gitdiff_test.go
+++ b/services/gitdiff/gitdiff_test.go
@@ -209,3 +209,11 @@ func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) {
 		}
 	}
 }
+
+func TestParseDiffHunkString(t *testing.T) {
+	leftLine, leftHunk, rightLine, rightHunk := ParseDiffHunkString("@@ -19,3 +19,5 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER")
+	assert.EqualValues(t, 19, leftLine)
+	assert.EqualValues(t, 3, leftHunk)
+	assert.EqualValues(t, 19, rightLine)
+	assert.EqualValues(t, 5, rightHunk)
+}
diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl
index 64080f990f..f5b8d8042d 100644
--- a/templates/repo/diff/comments.tmpl
+++ b/templates/repo/diff/comments.tmpl
@@ -2,12 +2,22 @@
 
 {{ $createdStr:= TimeSinceUnix .CreatedUnix $.root.Lang }}
 <div class="comment" id="{{.HashTag}}">
-	<a class="avatar" {{if gt .Poster.ID 0}}href="{{.Poster.HomeLink}}"{{end}}>
-		<img src="{{.Poster.RelAvatarLink}}">
-	</a>
+	{{if .OriginalAuthor }}
+		<span class="avatar"><img src="/img/avatar_default.png"></span>
+	{{else}}
+		<a class="avatar" {{if gt .Poster.ID 0}}href="{{.Poster.HomeLink}}"{{end}}>
+			<img src="{{.Poster.RelAvatarLink}}">
+		</a>
+	{{end}}
 	<div class="content">
 		<div class="ui top attached header">
-			<span class="text grey"><a {{if gt .Poster.ID 0}}href="{{.Poster.HomeLink}}"{{end}}>{{.Poster.GetDisplayName}}</a> {{$.root.i18n.Tr "repo.issues.commented_at" .HashTag $createdStr | Safe}}</span>
+			<span class="text grey">
+				{{if .OriginalAuthor }}
+					<span class="text black"><i class="fa {{MigrationIcon $.root.Repository.GetOriginalURLHostname}}" aria-hidden="true"></i> {{ .OriginalAuthor }}</span><span class="text grey"> {{$.root.i18n.Tr "repo.issues.commented_at" .HashTag $createdStr | Safe}}</span> <span class="text migrate">{{if $.root.Repository.OriginalURL}} ({{$.root.i18n.Tr "repo.migrated_from" $.root.Repository.OriginalURL $.root.Repository.GetOriginalURLHostname | Safe }}){{end}}</span>
+				{{else}}
+					<a {{if gt .Poster.ID 0}}href="{{.Poster.HomeLink}}"{{end}}>{{.Poster.GetDisplayName}}</a> {{$.root.i18n.Tr "repo.issues.commented_at" .HashTag $createdStr | Safe}}
+				{{end}}
+			</span>
 			<div class="ui right actions">
 			{{if and .Review}}
 				{{if eq .Review.Type 0}}
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index 40db434dec..be423e9d4c 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -319,10 +319,19 @@
 	{{else if eq .Type 22}}
 		<div class="event" id="{{.HashTag}}">
 			<span class="octicon octicon-{{.Review.Type.Icon}} issue-symbol"></span>
-			<a class="ui avatar image" href="{{.Poster.HomeLink}}">
-				<img src="{{.Poster.RelAvatarLink}}">
-			</a>
-			<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.GetDisplayName}}</a>
+			{{if .OriginalAuthor }}
+			{{else}}
+				<a class="avatar"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>
+					<img src="{{.Poster.RelAvatarLink}}">
+				</a>
+			{{end}}
+			<span class="text grey">
+				{{if .OriginalAuthor }}
+					<span class="text black"><i class="fa {{MigrationIcon $.Repository.GetOriginalURLHostname}}" aria-hidden="true"></i> {{ .OriginalAuthor }}</span><span class="text grey"> {{if $.Repository.OriginalURL}}</span><span class="text migrate">({{$.i18n.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname | Safe }}){{end}}</span>
+				{{else}}
+					<a{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>{{.Poster.GetDisplayName}}</a>
+				{{end}}
+			
 				{{if eq .Review.Type 1}}
 					{{$.i18n.Tr "repo.issues.review.approve" $createdStr | Safe}}
 				{{else if eq .Review.Type 2}}