diff --git a/models/issue_comment.go b/models/issue_comment.go index 85e2dbf06f..56cdc8c629 100644 --- a/models/issue_comment.go +++ b/models/issue_comment.go @@ -618,6 +618,20 @@ func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content stri }) } +// CreateCodeComment creates a plain code comment at the specified line / path +func CreateCodeComment(doer *User, repo *Repository, issue *Issue, commitSHA, content, treePath string, line int64) (*Comment, error) { + return CreateComment(&CreateCommentOptions{ + Type: CommentTypeCode, + Doer: doer, + Repo: repo, + Issue: issue, + Content: content, + LineNum: line, + TreePath: treePath, + CommitSHA: commitSHA, + }) +} + // CreateRefComment creates a commit reference comment to issue. func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commitSHA string) error { if len(commitSHA) == 0 { diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index aa9dd13107..fef6374bc0 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -180,6 +180,8 @@ var migrations = []Migration{ NewMigration("add last used passcode column for TOTP", addLastUsedPasscodeTOTP), // v63 -> v64 NewMigration("add language column for user setting", addLanguageSetting), + // v64 -> v65 + NewMigration("add review", addReview), } // Migrate database to current version diff --git a/models/migrations/v64.go b/models/migrations/v64.go new file mode 100644 index 0000000000..4924b41ceb --- /dev/null +++ b/models/migrations/v64.go @@ -0,0 +1,31 @@ +// Copyright 2018 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" + + "code.gitea.io/gitea/modules/util" + + "github.com/go-xorm/xorm" +) + +func addReview(x *xorm.Engine) error { + // Review see models/review.go + type Review struct { + ID int64 `xorm:"pk autoincr"` + Type string + ReviewerID int64 `xorm:"index"` + IssueID int64 `xorm:"index"` + Content string + CreatedUnix util.TimeStamp `xorm:"INDEX created"` + UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` + } + + if err := x.Sync2(new(Review)); err != nil { + return fmt.Errorf("Sync2: %v", err) + } + return nil +} diff --git a/models/models.go b/models/models.go index 549d2eadc9..b9fcb56efb 100644 --- a/models/models.go +++ b/models/models.go @@ -119,6 +119,7 @@ func init() { new(RepoIndexerStatus), new(LFSLock), new(Reaction), + new(Review), ) gonicNames := []string{"SSL", "UID"} diff --git a/models/review.go b/models/review.go index 640f020dea..14580c078f 100644 --- a/models/review.go +++ b/models/review.go @@ -16,13 +16,14 @@ const ( ReviewTypeComment // ReviewTypeReject gives feedback blocking merge ReviewTypeReject + // ReviewTypePending is a review which is not published yet + ReviewTypePending ) // Review represents collection of code comments giving feedback for a PR type Review struct { ID int64 `xorm:"pk autoincr"` Type ReviewType - Pending bool Reviewer *User `xorm:"-"` ReviewerID int64 `xorm:"index"` Issue *Issue `xorm:"-"` @@ -86,3 +87,34 @@ func getReviewByID(e Engine, id int64) (*Review, error) { func GetReviewByID(id int64) (*Review, error) { return getReviewByID(x, id) } + +func getPendingReviewByReviewerID(e Engine, reviewer *User, issue *Issue) (review *Review, err error) { + var exists bool + if exists, err = e.Table("review").Where("reviewer_id = ? and issue_id = ? and type = ?", reviewer.ID, issue.ID, ReviewTypePending). + Get(review); !exists && err == nil { + return nil, nil + } + return +} + +// GetPendingReviewByReviewer returns the latest pending review of reviewer at PR issue +func GetPendingReviewByReviewer(reviewer *User, issue *Issue) (*Review, error) { + return getPendingReviewByReviewerID(x, reviewer, issue) +} + +func createPendingReview(e Engine, reviewer *User, issue *Issue) (*Review, error) { + review := &Review{ + Type: ReviewTypePending, + Issue: issue, + IssueID: issue.ID, + Reviewer: reviewer, + ReviewerID: reviewer.ID, + } + _, err := e.Insert(review) + return review, err +} + +// CreatePendingReview creates an empty pending review +func CreatePendingReview(reviewer *User, issue *Issue) (*Review, error) { + return createPendingReview(x, reviewer, issue) +} diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go index 4aa9ce5f58..d76772568c 100644 --- a/modules/auth/repo_form.go +++ b/modules/auth/repo_form.go @@ -356,6 +356,21 @@ func (f *MergePullRequestForm) Validate(ctx *macaron.Context, errs binding.Error return validate(errs, ctx.Data, f, ctx.Locale) } +// CodeCommentForm form for adding code comments for PRs +type CodeCommentForm struct { + Content string `binding:"Required"` + Side string `binding:"Required;In(previous,proposed)"` + Line int64 + TreePath string `form:"path" binding:"Required"` + CommitSHA string `form:"commit_id" binding:"Required"` + IsReview bool `form:"is_review" binding:"Required"` +} + +// Validate validates the fields +func (f *CodeCommentForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { + return validate(errs, ctx.Data, f, ctx.Locale) +} + // __________ .__ // \______ \ ____ | | ____ _____ ______ ____ // | _// __ \| | _/ __ \\__ \ / ___// __ \ diff --git a/public/js/index.js b/public/js/index.js index afe95b24ae..6045d3276d 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -781,7 +781,6 @@ function initPullRequestReview() { commentCloud.find('.tab.segment').each(function(i, item) { $(item).attr('data-tab', $(item).attr('data-tab') + id); }); - initCommentPreviewTab(commentCloud.find('.form')); } commentCloud.find('textarea').focus(); diff --git a/routers/repo/pull_review.go b/routers/repo/pull_review.go new file mode 100644 index 0000000000..8709e5ea1e --- /dev/null +++ b/routers/repo/pull_review.go @@ -0,0 +1,84 @@ +// Copyright 2018 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 repo + +import ( + "fmt" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/notification" +) + +// CreateCodeComment will create a code comment including an pending review if required +func CreateCodeComment(ctx *context.Context, form auth.CodeCommentForm) { + issue := GetActionIssue(ctx) + + if !issue.IsPull { + return + } + if ctx.Written() { + return + } + + if ctx.HasError() { + ctx.Flash.Error(ctx.Data["ErrorMsg"].(string)) + ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index)) + return + } + var comment *models.Comment + defer func() { + if comment != nil { + ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files#%s", ctx.Repo.RepoLink, issue.Index, comment.HashTag())) + } else { + ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index)) + } + }() + signedLine := form.Line + if form.Side == "previous" { + signedLine *= -1 + } + //FIXME check if line and treepath exist + var err error + comment, err = models.CreateCodeComment( + ctx.User, + issue.Repo, + issue, + form.CommitSHA, + form.Content, + form.TreePath, + signedLine, + ) + if err != nil { + ctx.ServerError("CreateCodeComment", err) + return + } + + if form.IsReview { + review, err := models.GetPendingReviewByReviewer(ctx.User, issue) + if err != nil { + ctx.ServerError("CreateCodeComment", err) + return + } + if review == nil { + if review, err = models.CreatePendingReview(ctx.User, issue); err != nil { + ctx.ServerError("CreateCodeComment", err) + return + } + } + comment.Review = review + comment.ReviewID = review.ID + if err = models.UpdateComment(comment); err != nil { + ctx.ServerError("CreateCodeComment", err) + return + } + } else { + notification.Service.NotifyIssue(issue, ctx.User.ID) + } + + log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID) +} diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 9618d25268..725eb437d1 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -633,9 +633,14 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get(".diff", repo.DownloadPullDiff) m.Get(".patch", repo.DownloadPullPatch) m.Get("/commits", context.RepoRef(), repo.ViewPullCommits) - m.Get("/files", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ViewPullFiles) m.Post("/merge", reqRepoWriter, bindIgnErr(auth.MergePullRequestForm{}), repo.MergePullRequest) m.Post("/cleanup", context.RepoRef(), repo.CleanUpPullRequest) + m.Group("/files", func() { + m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ViewPullFiles) + m.Group("/reviews", func() { + m.Post("/comments", bindIgnErr(auth.CodeCommentForm{}), repo.CreateCodeComment) + }) + }) }, repo.MustAllowPulls) m.Group("/raw", func() { diff --git a/templates/repo/diff/new_comment.tmpl b/templates/repo/diff/new_comment.tmpl index 4731ba29e7..f586c1567e 100644 --- a/templates/repo/diff/new_comment.tmpl +++ b/templates/repo/diff/new_comment.tmpl @@ -1,10 +1,11 @@
-
+ {{.CsrfTokenHtml}} - - - - + + + + + @@ -25,7 +26,7 @@
{{$.i18n.Tr "cancel"}}
{{$.i18n.Tr "repo.diff.comment.add_single_comment"}}
-
{{$.i18n.Tr "repo.diff.comment.add_review_comment"}}
+
{{$.i18n.Tr "repo.diff.comment.start_review"}}