assert.EqualValues(t, "APPROVED", review.State)
assert.EqualValues(t, 3, review.CodeCommentsCount)
+ // test dismiss review
+ req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d/dismissals?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, review.ID, token), &api.DismissPullReviewOptions{
+ Message: "test",
+ })
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &review)
+ assert.EqualValues(t, 6, review.ID)
+ assert.EqualValues(t, true, review.Dismissed)
+
+ // test dismiss review
+ req = NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d/undismissals?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, review.ID, token))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &review)
+ assert.EqualValues(t, 6, review.ID)
+ assert.EqualValues(t, false, review.Dismissed)
+
// test DeletePullReview
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{
Body: "just a comment",
// Possible action types.
const (
- ActionCreateRepo ActionType = iota + 1 // 1
- ActionRenameRepo // 2
- ActionStarRepo // 3
- ActionWatchRepo // 4
- ActionCommitRepo // 5
- ActionCreateIssue // 6
- ActionCreatePullRequest // 7
- ActionTransferRepo // 8
- ActionPushTag // 9
- ActionCommentIssue // 10
- ActionMergePullRequest // 11
- ActionCloseIssue // 12
- ActionReopenIssue // 13
- ActionClosePullRequest // 14
- ActionReopenPullRequest // 15
- ActionDeleteTag // 16
- ActionDeleteBranch // 17
- ActionMirrorSyncPush // 18
- ActionMirrorSyncCreate // 19
- ActionMirrorSyncDelete // 20
- ActionApprovePullRequest // 21
- ActionRejectPullRequest // 22
- ActionCommentPull // 23
- ActionPublishRelease // 24
+ ActionCreateRepo ActionType = iota + 1 // 1
+ ActionRenameRepo // 2
+ ActionStarRepo // 3
+ ActionWatchRepo // 4
+ ActionCommitRepo // 5
+ ActionCreateIssue // 6
+ ActionCreatePullRequest // 7
+ ActionTransferRepo // 8
+ ActionPushTag // 9
+ ActionCommentIssue // 10
+ ActionMergePullRequest // 11
+ ActionCloseIssue // 12
+ ActionReopenIssue // 13
+ ActionClosePullRequest // 14
+ ActionReopenPullRequest // 15
+ ActionDeleteTag // 16
+ ActionDeleteBranch // 17
+ ActionMirrorSyncPush // 18
+ ActionMirrorSyncCreate // 19
+ ActionMirrorSyncDelete // 20
+ ActionApprovePullRequest // 21
+ ActionRejectPullRequest // 22
+ ActionCommentPull // 23
+ ActionPublishRelease // 24
+ ActionPullReviewDismissed // 25
)
// Action represents user operation type and other information to
// GetIssueInfos returns a list of issues associated with
// the action.
func (a *Action) GetIssueInfos() []string {
- return strings.SplitN(a.Content, "|", 2)
+ return strings.SplitN(a.Content, "|", 3)
}
// GetIssueTitle returns the title of first issue associated
func (protectBranch *ProtectedBranch) GetGrantedApprovalsCount(pr *PullRequest) int64 {
sess := x.Where("issue_id = ?", pr.IssueID).
And("type = ?", ReviewTypeApprove).
- And("official = ?", true)
+ And("official = ?", true).
+ And("dismissed = ?", false)
if protectBranch.DismissStaleApprovals {
sess = sess.And("stale = ?", false)
}
rejectExist, err := x.Where("issue_id = ?", pr.IssueID).
And("type = ?", ReviewTypeReject).
And("official = ?", true).
+ And("dismissed = ?", false).
Exist(new(Review))
if err != nil {
log.Error("MergeBlockedByRejectedReview: %v", err)
issue_id: 12
official: true
updated_unix: 1603196749
- created_unix: 1603196749
\ No newline at end of file
+ created_unix: 1603196749
CommentTypeProject
// 31 Project board changed
CommentTypeProjectBoard
+ // Dismiss Review
+ CommentTypeDismissReview
)
// CommentTag defines comment tag type
}
sess := e.In("issue_id", ids)
err := sess.Select("issue_id, type, count(id) as `count`").
- Where("official = ?", true).
+ Where("official = ? AND dismissed = ?", true, false).
GroupBy("issue_id, type").
OrderBy("issue_id").
Table("review").
NewMigration("Recreate user table to fix default values", recreateUserTableToFixDefaultValues),
// v169 -> v170
NewMigration("Update DeleteBranch comments to set the old_ref to the commit_sha", commentTypeDeleteBranchUseOldRef),
+ // v170 -> v171
+ NewMigration("Add Dismissed to Review table", addDismissedReviewColumn),
}
// GetCurrentDBVersion returns the current db version
--- /dev/null
+// 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 (
+ "fmt"
+
+ "xorm.io/xorm"
+)
+
+func addDismissedReviewColumn(x *xorm.Engine) error {
+ type Review struct {
+ Dismissed bool `xorm:"NOT NULL DEFAULT false"`
+ }
+
+ if err := x.Sync2(new(Review)); err != nil {
+ return fmt.Errorf("Sync2: %v", err)
+ }
+ return nil
+}
func (pr *PullRequest) getApprovalCounts(e Engine) ([]*ReviewCount, error) {
rCounts := make([]*ReviewCount, 0, 6)
sess := e.Where("issue_id = ?", pr.IssueID)
- return rCounts, sess.Select("issue_id, type, count(id) as `count`").Where("official = ?", true).GroupBy("issue_id, type").Table("review").Find(&rCounts)
+ return rCounts, sess.Select("issue_id, type, count(id) as `count`").Where("official = ? AND dismissed = ?", true, false).GroupBy("issue_id, type").Table("review").Find(&rCounts)
}
// GetApprovers returns the approvers of the pull request
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)"`
- Stale bool `xorm:"NOT NULL DEFAULT false"`
+ Official bool `xorm:"NOT NULL DEFAULT false"`
+ CommitID string `xorm:"VARCHAR(40)"`
+ Stale bool `xorm:"NOT NULL DEFAULT false"`
+ Dismissed bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
// Get latest review of each reviwer, sorted in order they were made
- if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND original_author_id = 0 GROUP BY issue_id, reviewer_id) ORDER BY review.updated_unix ASC",
- issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest).
+ if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND dismissed = ? AND original_author_id = 0 GROUP BY issue_id, reviewer_id) ORDER BY review.updated_unix ASC",
+ issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, false).
Find(&reviews); err != nil {
return nil, err
}
return
}
+// DismissReview change the dismiss status of a review
+func DismissReview(review *Review, isDismiss bool) (err error) {
+ if review.Dismissed == isDismiss || (review.Type != ReviewTypeApprove && review.Type != ReviewTypeReject) {
+ return nil
+ }
+
+ review.Dismissed = isDismiss
+
+ _, err = x.Cols("dismissed").Update(review)
+
+ return
+}
+
// InsertReviews inserts review and review comments
func InsertReviews(reviews []*Review) error {
sess := x.NewSession()
}
}
}
+
+func TestDismissReview(t *testing.T) {
+ review1 := AssertExistsAndLoadBean(t, &Review{ID: 9}).(*Review)
+ review2 := AssertExistsAndLoadBean(t, &Review{ID: 11}).(*Review)
+ assert.NoError(t, DismissReview(review1, true))
+ assert.NoError(t, DismissReview(review2, true))
+ assert.NoError(t, DismissReview(review2, true))
+ assert.NoError(t, DismissReview(review2, false))
+ assert.NoError(t, DismissReview(review2, false))
+}
CommitID: r.CommitID,
Stale: r.Stale,
Official: r.Official,
+ Dismissed: r.Dismissed,
CodeCommentsCount: r.GetCodeCommentsCount(),
Submitted: r.CreatedUnix.AsTime(),
HTMLURL: r.HTMLURL(),
len(strings.TrimSpace(f.Content)) == 0
}
+// DismissReviewForm for dismissing stale review by repo admin
+type DismissReviewForm struct {
+ ReviewID int64 `binding:"Required"`
+ Message string
+}
+
// __________ .__
// \______ \ ____ | | ____ _____ ______ ____
// | _// __ \| | _/ __ \\__ \ / ___// __ \
}
}
+func (*actionNotifier) NotifyPullRevieweDismiss(doer *models.User, review *models.Review, comment *models.Comment) {
+ reviewerName := review.Reviewer.Name
+ if len(review.OriginalAuthor) > 0 {
+ reviewerName = review.OriginalAuthor
+ }
+ if err := models.NotifyWatchers(&models.Action{
+ ActUserID: doer.ID,
+ ActUser: doer,
+ OpType: models.ActionPullReviewDismissed,
+ Content: fmt.Sprintf("%d|%s|%s", review.Issue.Index, reviewerName, comment.Content),
+ RepoID: review.Issue.Repo.ID,
+ Repo: review.Issue.Repo,
+ IsPrivate: review.Issue.Repo.IsPrivate,
+ CommentID: comment.ID,
+ Comment: comment,
+ }); err != nil {
+ log.Error("NotifyWatchers [%d]: %v", review.Issue.ID, err)
+ }
+}
+
func (a *actionNotifier) NotifyPushCommits(pusher *models.User, repo *models.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
data, err := json.Marshal(commits)
if err != nil {
NotifyPullRequestCodeComment(pr *models.PullRequest, comment *models.Comment, mentions []*models.User)
NotifyPullRequestChangeTargetBranch(doer *models.User, pr *models.PullRequest, oldBranch string)
NotifyPullRequestPushCommits(doer *models.User, pr *models.PullRequest, comment *models.Comment)
+ NotifyPullRevieweDismiss(doer *models.User, review *models.Review, comment *models.Comment)
NotifyCreateIssueComment(doer *models.User, repo *models.Repository,
issue *models.Issue, comment *models.Comment, mentions []*models.User)
func (*NullNotifier) NotifyPullRequestPushCommits(doer *models.User, pr *models.PullRequest, comment *models.Comment) {
}
+// NotifyPullRevieweDismiss notifies when a review was dismissed by repo admin
+func (*NullNotifier) NotifyPullRevieweDismiss(doer *models.User, review *models.Review, comment *models.Comment) {
+}
+
// NotifyUpdateComment places a place holder function
func (*NullNotifier) NotifyUpdateComment(doer *models.User, c *models.Comment, oldContent string) {
}
m.NotifyCreateIssueComment(doer, comment.Issue.Repo, comment.Issue, comment, nil)
}
+func (m *mailNotifier) NotifyPullRevieweDismiss(doer *models.User, review *models.Review, comment *models.Comment) {
+ if err := mailer.MailParticipantsComment(comment, models.ActionPullReviewDismissed, review.Issue, []*models.User{}); err != nil {
+ log.Error("MailParticipantsComment: %v", err)
+ }
+}
+
func (m *mailNotifier) NotifyNewRelease(rel *models.Release) {
if err := rel.LoadAttributes(); err != nil {
log.Error("NotifyNewRelease: %v", err)
}
}
+// NotifyPullRevieweDismiss notifies when a review was dismissed by repo admin
+func NotifyPullRevieweDismiss(doer *models.User, review *models.Review, comment *models.Comment) {
+ for _, notifier := range notifiers {
+ notifier.NotifyPullRevieweDismiss(doer, review, comment)
+ }
+}
+
// NotifyUpdateComment notifies update comment to notifiers
func NotifyUpdateComment(doer *models.User, c *models.Comment, oldContent string) {
for _, notifier := range notifiers {
_ = ns.issueQueue.Push(opts)
}
+func (ns *notificationService) NotifyPullRevieweDismiss(doer *models.User, review *models.Review, comment *models.Comment) {
+ var opts = issueNotificationOpts{
+ IssueID: review.IssueID,
+ NotificationAuthorID: doer.ID,
+ CommentID: comment.ID,
+ }
+ _ = ns.issueQueue.Push(opts)
+}
+
func (ns *notificationService) NotifyIssueChangeAssignee(doer *models.User, issue *models.Issue, assignee *models.User, removed bool, comment *models.Comment) {
if !removed {
var opts = issueNotificationOpts{
CommitID string `json:"commit_id"`
Stale bool `json:"stale"`
Official bool `json:"official"`
+ Dismissed bool `json:"dismissed"`
CodeCommentsCount int `json:"comments_count"`
// swagger:strfmt date-time
Submitted time.Time `json:"submitted_at"`
Body string `json:"body"`
}
+// DismissPullReviewOptions are options to dismiss a pull review
+type DismissPullReviewOptions struct {
+ Message string `json:"message"`
+}
+
// PullReviewRequestOptions are options to add or remove pull review requests
type PullReviewRequestOptions struct {
Reviewers []string `json:"reviewers"`
return "diff"
case models.ActionPublishRelease:
return "tag"
+ case models.ActionPullReviewDismissed:
+ return "x"
default:
return "question"
}
issues = Issues
milestones = Milestones
+ok = OK
cancel = Cancel
save = Save
add = Add
issues.is_stale = There have been changes to this PR since this review
issues.remove_request_review=Remove review request
issues.remove_request_review_block=Can't remove review request
+issues.dismiss_review = Dismiss Review
+issues.dismiss_review_warning = Are you sure you want to dismiss this review?
issues.sign_in_require_desc = <a href="%s">Sign in</a> to join this conversation.
issues.edit = Edit
issues.cancel = Cancel
issues.review.self.rejection = You cannot request changes on your own pull request.
issues.review.approve = "approved these changes %s"
issues.review.comment = "reviewed %s"
+issues.review.dismissed = "dismissed %s’s review %s"
+issues.review.dismissed_label = Dismissed
issues.review.left_comment = left a comment
issues.review.content.empty = You need to leave a comment indicating the requested change(s).
issues.review.reject = "requested changes %s"
approve_pull_request = `approved <a href="%s/pulls/%s">%s#%[2]s</a>`
reject_pull_request = `suggested changes for <a href="%s/pulls/%s">%s#%[2]s</a>`
publish_release = `released <a href="%s/releases/tag/%s"> "%[4]s" </a> at <a href="%[1]s">%[3]s</a>`
+review_dismissed = `dismissed review from <b>%[4]s</b> for <a href="%[1]s/pulls/%[2]s">%[3]s#%[2]s</a>`
+review_dismissed_reason = Reason:
create_branch = created branch <a href="%[1]s/src/branch/%[2]s">%[3]s</a> in <a href="%[1]s">%[4]s</a>
[tool]
Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview)
m.Combo("/comments").
Get(repo.GetPullReviewComments)
+ m.Post("/dismissals", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview)
+ m.Post("/undismissals", reqToken(), repo.UnDismissPullReview)
})
})
m.Combo("/requested_reviewers").
return
}
}
+
+// DismissPullReview dismiss a review for a pull request
+func DismissPullReview(ctx *context.APIContext) {
+ // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/dismissals repository repoDismissPullReview
+ // ---
+ // summary: Dismiss a review for a pull request
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: index
+ // in: path
+ // description: index of the pull request
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: id
+ // in: path
+ // description: id of the review
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: body
+ // in: body
+ // required: true
+ // schema:
+ // "$ref": "#/definitions/DismissPullReviewOptions"
+ // responses:
+ // "200":
+ // "$ref": "#/responses/PullReview"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "422":
+ // "$ref": "#/responses/validationError"
+ opts := web.GetForm(ctx).(*api.DismissPullReviewOptions)
+ dismissReview(ctx, opts.Message, true)
+}
+
+// UnDismissPullReview cancel to dismiss a review for a pull request
+func UnDismissPullReview(ctx *context.APIContext) {
+ // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/undismissals repository repoUnDismissPullReview
+ // ---
+ // summary: Cancel to dismiss a review for a pull request
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: index
+ // in: path
+ // description: index of the pull request
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: id
+ // in: path
+ // description: id of the review
+ // type: integer
+ // format: int64
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/PullReview"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "422":
+ // "$ref": "#/responses/validationError"
+ dismissReview(ctx, "", false)
+}
+
+func dismissReview(ctx *context.APIContext, msg string, isDismiss bool) {
+ if !ctx.Repo.IsAdmin() {
+ ctx.Error(http.StatusForbidden, "", "Must be repo admin")
+ return
+ }
+ review, pr, isWrong := prepareSingleReview(ctx)
+ if isWrong {
+ return
+ }
+
+ if review.Type != models.ReviewTypeApprove && review.Type != models.ReviewTypeReject {
+ ctx.Error(http.StatusForbidden, "", "not need to dismiss this review because it's type is not Approve or change request")
+ return
+ }
+
+ if pr.Issue.IsClosed {
+ ctx.Error(http.StatusForbidden, "", "not need to dismiss this review because this pr is closed")
+ return
+ }
+
+ _, err := pull_service.DismissReview(review.ID, msg, ctx.User, isDismiss)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "pull_service.DismissReview", err)
+ return
+ }
+
+ if review, err = models.GetReviewByID(review.ID); err != nil {
+ ctx.Error(http.StatusInternalServerError, "GetReviewByID", err)
+ return
+ }
+
+ // convert response
+ apiReview, err := convert.ToPullReview(review, ctx.User)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "convertToPullReview", err)
+ return
+ }
+ ctx.JSON(http.StatusOK, apiReview)
+}
// in:body
SubmitPullReviewOptions api.SubmitPullReviewOptions
+ // in:body
+ DismissPullReviewOptions api.DismissPullReviewOptions
+
// in:body
MigrateRepoOptions api.MigrateRepoOptions
return
}
}
- } else if comment.Type == models.CommentTypeCode || comment.Type == models.CommentTypeReview {
+ } else if comment.Type == models.CommentTypeCode || comment.Type == models.CommentTypeReview || comment.Type == models.CommentTypeDismissReview {
comment.RenderedContent = string(markdown.Render([]byte(comment.Content), ctx.Repo.RepoLink,
ctx.Repo.Repository.ComposeMetas()))
if err = comment.LoadReview(); err != nil && !models.IsErrReviewNotExist(err) {
ctx.Redirect(fmt.Sprintf("%s/pulls/%d#%s", ctx.Repo.RepoLink, issue.Index, comm.HashTag()))
}
+
+// DismissReview dismissing stale review by repo admin
+func DismissReview(ctx *context.Context) {
+ form := web.GetForm(ctx).(*auth.DismissReviewForm)
+ comm, err := pull_service.DismissReview(form.ReviewID, form.Message, ctx.User, true)
+ if err != nil {
+ ctx.ServerError("pull_service.DismissReview", err)
+ return
+ }
+
+ ctx.Redirect(fmt.Sprintf("%s/pulls/%d#%s", ctx.Repo.RepoLink, comm.Issue.Index, comm.HashTag()))
+}
m.Post("/projects", reqRepoIssuesOrPullsWriter, repo.UpdateIssueProject)
m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee)
m.Post("/request_review", reqRepoIssuesOrPullsReader, repo.UpdatePullReviewRequest)
+ m.Post("/dismiss_review", reqRepoAdmin, bindIgnErr(auth.DismissReviewForm{}), repo.DismissReview)
m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus)
m.Post("/resolve_conversation", reqRepoIssuesOrPullsReader, repo.UpdateResolveConversation)
m.Post("/attachments", repo.UploadIssueAttachment)
name = "reopen"
case models.ActionMergePullRequest:
name = "merge"
+ case models.ActionPullReviewDismissed:
+ name = "review_dismissed"
default:
switch commentType {
case models.CommentTypeReview:
return review, comm, nil
}
+
+// DismissReview dismissing stale review by repo admin
+func DismissReview(reviewID int64, message string, doer *models.User, isDismiss bool) (comment *models.Comment, err error) {
+ review, err := models.GetReviewByID(reviewID)
+ if err != nil {
+ return
+ }
+
+ if review.Type != models.ReviewTypeApprove && review.Type != models.ReviewTypeReject {
+ return nil, fmt.Errorf("not need to dismiss this review because it's type is not Approve or change request")
+ }
+
+ if err = models.DismissReview(review, isDismiss); err != nil {
+ return
+ }
+
+ if !isDismiss {
+ return nil, nil
+ }
+
+ // load data for notify
+ if err = review.LoadAttributes(); err != nil {
+ return
+ }
+ if err = review.Issue.LoadPullRequest(); err != nil {
+ return
+ }
+ if err = review.Issue.LoadAttributes(); err != nil {
+ return
+ }
+
+ comment, err = models.CreateComment(&models.CreateCommentOptions{
+ Doer: doer,
+ Content: message,
+ Type: models.CommentTypeDismissReview,
+ ReviewID: review.ID,
+ Issue: review.Issue,
+ Repo: review.Issue.Repo,
+ })
+ if err != nil {
+ return
+ }
+
+ comment.Review = review
+ comment.Poster = doer
+ comment.Issue = review.Issue
+
+ notification.NotifyPullRevieweDismiss(doer, review, comment)
+
+ return
+}
<b>@{{.Doer.Name}}</b> requested changes on this pull request.
{{else if eq .ActionName "review"}}
<b>@{{.Doer.Name}}</b> commented on this pull request.
+ {{else if eq .ActionName "review_dismissed"}}
+ <b>@{{.Doer.Name}}</b> dismissed last review from {{.Comment.Review.Reviewer.Name}} for this pull request.
{{end}}
{{- if eq .Body ""}}
18 = REMOVED_DEADLINE, 19 = ADD_DEPENDENCY, 20 = REMOVE_DEPENDENCY, 21 = CODE,
22 = REVIEW, 23 = ISSUE_LOCKED, 24 = ISSUE_UNLOCKED, 25 = TARGET_BRANCH_CHANGED,
26 = DELETE_TIME_MANUAL, 27 = REVIEW_REQUEST, 28 = MERGE_PULL_REQUEST,
- 29 = PULL_PUSH_EVENT, 30 = PROJECT_CHANGED, 31 = PROJECT_BOARD_CHANGED -->
+ 29 = PULL_PUSH_EVENT, 30 = PROJECT_CHANGED, 31 = PROJECT_BOARD_CHANGED
+ 32 = DISMISSED_REVIEW -->
{{if eq .Type 0}}
<div class="timeline-item comment" id="{{.HashTag}}">
{{if .OriginalAuthor }}
{{else}}
{{$.i18n.Tr "repo.issues.review.comment" $createdStr | Safe}}
{{end}}
+ {{if .Review.Dismissed}}
+ <div class="ui small label">{{$.i18n.Tr "repo.issues.review.dismissed_label"}}</div>
+ {{end}}
</span>
</div>
{{if .Content}}
</span>
</div>
{{end}}
+ {{else if eq .Type 32}}
+ <div class="timeline-item-group">
+ <div class="timeline-item event" id="{{.HashTag}}">
+ <a class="timeline-avatar"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>
+ <img src="{{.Poster.RelAvatarLink}}">
+ </a>
+ <span class="badge grey">{{svg "octicon-x" 16}}</span>
+ <span class="text grey">
+ <a class="author"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>{{.Poster.GetDisplayName}}</a>
+ {{$reviewerName := ""}}
+ {{if eq .Review.OriginalAuthor ""}}
+ {{$reviewerName = .Review.Reviewer.Name}}
+ {{else}}
+ {{$reviewerName = .Review.OriginalAuthor}}
+ {{end}}
+ {{$.i18n.Tr "repo.issues.review.dismissed" $reviewerName $createdStr | Safe}}
+ </span>
+ </div>
+ {{if .Content}}
+ <div class="timeline-item comment">
+ <div class="content">
+ <div class="ui top attached header arrow-top">
+ <span class="text grey">
+ {{$.i18n.Tr "action.review_dismissed_reason"}}
+ </span>
+ </div>
+ <div class="ui attached segment">
+ <div class="render-content markdown">
+ {{if .RenderedContent}}
+ {{.RenderedContent|Str2html}}
+ {{else}}
+ <span class="no-content">{{$.i18n.Tr "repo.issues.no_content"}}</span>
+ {{end}}
+ </div>
+ </div>
+ </div>
+ </div>
+ {{end}}
+ </div>
{{end}}
{{end}}
</div>
<div class="review-item-right">
{{if .Review.Stale}}
- <span class="ui poping up type-icon text grey" data-content="{{$.i18n.Tr "repo.issues.is_stale"}}">
- <i class="octicon icon fa-hourglass-end"></i>
- </span>
+ <span class="ui poping up type-icon text grey" data-content="{{$.i18n.Tr "repo.issues.is_stale"}}">
+ <i class="octicon icon fa-hourglass-end"></i>
+ </span>
+ {{end}}
+ {{if (and $.Permission.IsAdmin (or (eq .Review.Type 1) (eq .Review.Type 3)) (not $.Issue.IsClosed))}}
+ <a href="#" class="ui grey poping up icon dismiss-review-btn" data-review-id="dismiss-review-{{.Review.ID}}" data-content="{{$.i18n.Tr "repo.issues.dismiss_review"}}">
+ {{svg "octicon-x" 16}}
+ </a>
+ <div class="ui small modal" id="dismiss-review-modal">
+ <div class="header">
+ {{$.i18n.Tr "repo.issues.dismiss_review"}}
+ </div>
+ <div class="content">
+ <div class="ui warning message text left">
+ {{$.i18n.Tr "repo.issues.dismiss_review_warning"}}
+ </div>
+ <form class="ui form dismiss-review-form" id="dismiss-review-{{.Review.ID}}" action="{{$.RepoLink}}/issues/dismiss_review" method="post">
+ {{$.CsrfTokenHtml}}
+ <input type="hidden" name="review_id" value="{{.Review.ID}}">
+ <div class="field">
+ <label for="message">{{$.i18n.Tr "action.review_dismissed_reason"}}</label>
+ <input id="message" name="message">
+ </div>
+ <div class="text right actions">
+ <div class="ui cancel button">{{$.i18n.Tr "settings.cancel"}}</div>
+ <button class="ui red button" type="submit">{{$.i18n.Tr "ok"}}</button>
+ </div>
+ </form>
+ </div>
+ </div>
{{end}}
<span class="type-icon text {{if eq .Review.Type 1}}green
{{- else if eq .Review.Type 2}}grey
}
}
},
+ "/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/dismissals": {
+ "post": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "repository"
+ ],
+ "summary": "Dismiss a review for a pull request",
+ "operationId": "repoDismissPullReview",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "index of the pull request",
+ "name": "index",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "id of the review",
+ "name": "id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/DismissPullReviewOptions"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/PullReview"
+ },
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
+ "422": {
+ "$ref": "#/responses/validationError"
+ }
+ }
+ }
+ },
+ "/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/undismissals": {
+ "post": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "repository"
+ ],
+ "summary": "Cancel to dismiss a review for a pull request",
+ "operationId": "repoUnDismissPullReview",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "index of the pull request",
+ "name": "index",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "id of the review",
+ "name": "id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/PullReview"
+ },
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
+ "422": {
+ "$ref": "#/responses/validationError"
+ }
+ }
+ }
+ },
"/repos/{owner}/{repo}/pulls/{index}/update": {
"post": {
"produces": [
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
+ "DismissPullReviewOptions": {
+ "description": "DismissPullReviewOptions are options to dismiss a pull review",
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string",
+ "x-go-name": "Message"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
"EditAttachmentOptions": {
"description": "EditAttachmentOptions options for editing attachments",
"type": "object",
"type": "string",
"x-go-name": "CommitID"
},
+ "dismissed": {
+ "type": "boolean",
+ "x-go-name": "Dismissed"
+ },
"html_url": {
"type": "string",
"x-go-name": "HTMLURL"
{{ $branchLink := .GetBranch | EscapePound | Escape}}
{{ $linkText := .Content | RenderEmoji }}
{{$.i18n.Tr "action.publish_release" .GetRepoLink $branchLink .ShortRepoPath $linkText | Str2html}}
+ {{else if eq .GetOpType 25}}
+ {{ $index := index .GetIssueInfos 0}}
+ {{ $reviewer := index .GetIssueInfos 1}}
+ {{$.i18n.Tr "action.review_dismissed" .GetRepoLink $index .ShortRepoPath $reviewer | Str2html}}
{{end}}
</p>
{{if or (eq .GetOpType 5) (eq .GetOpType 18)}}
<p class="text light grey">{{index .GetIssueInfos 1}}</p>
{{else if or (eq .GetOpType 12) (eq .GetOpType 13) (eq .GetOpType 14) (eq .GetOpType 15)}}
<span class="text truncate issue title">{{.GetIssueTitle | RenderEmoji}}</span>
+ {{else if eq .GetOpType 25}}
+ <p class="text light grey">{{$.i18n.Tr "action.review_dismissed_reason"}}</p>
+ <p class="text light grey">{{index .GetIssueInfos 2 | RenderEmoji}}</p>
{{end}}
<p class="text italic light grey">{{TimeSince .GetCreate $.i18n.Lang}}</p>
</div>
return false;
});
+ $('.dismiss-review-btn').on('click', function (e) {
+ e.preventDefault();
+ const $this = $(this);
+ const $dismissReviewModal = $this.next();
+ $dismissReviewModal.modal('show');
+ });
+
$(document).on('click', (event) => {
const urlTarget = $(':target');
if (urlTarget.length === 0) return;