mirror of
https://github.com/go-gitea/gitea.git
synced 2026-11-02 13:44:16 +09:00
feat(pulls): support per-commit review (#39326)
Opening a single commit of a PR let you look, but not review. The Review button was disabled and there was no way to reach the next or previous commit. This finishes https://github.com/go-gitea/gitea/pull/37475, which only reached the head commit. - Reviews can be submitted from a single commit, a commit range stays disabled - A bar above a single-commit diff shows the commit, its position in the PR and prev/next buttons - The commit dropdown marks the commits on screen - Timeline commits open the PR's commit view, commits force-pushed out of the PR redirect to the repository commit page Assisted-by: Claude Code (Fable 5.1) Co-authored-by: bircni <bircni@icloud.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
12 files changed
+155
-35
No files matched your search
@@ -6,11 +6,13 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -676,6 +678,26 @@ func indexCommit(commits []*git.Commit, commitID string) *git.Commit {
|
||||
return nil
|
||||
}
|
||||
|
||||
// setSingleCommitNavContext exposes the reviewed commit and its neighbors.
|
||||
func setSingleCommitNavContext(ctx *context.Context, commits []*git.Commit, commit *git.Commit) {
|
||||
idx := slices.IndexFunc(commits, func(c *git.Commit) bool { return bytes.Equal(c.ID.RawValue(), commit.ID.RawValue()) })
|
||||
if idx < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["SingleCommit"] = commit
|
||||
ctx.Data["SingleCommitIndex"] = len(commits) - idx
|
||||
ctx.Data["SingleCommitCount"] = len(commits)
|
||||
|
||||
// The commit list is newest-first while a reviewer walks it oldest-first, so the indexes are mirrored.
|
||||
if idx+1 < len(commits) {
|
||||
ctx.Data["SingleCommitPrevID"] = commits[idx+1].ID.String()
|
||||
}
|
||||
if idx > 0 {
|
||||
ctx.Data["SingleCommitNextID"] = commits[idx-1].ID.String()
|
||||
}
|
||||
}
|
||||
|
||||
// ViewPullFiles render pull request changed files list page
|
||||
func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
|
||||
var err error
|
||||
@@ -703,11 +725,17 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
|
||||
}
|
||||
|
||||
headCommitID := prCompareInfo.HeadCommitID
|
||||
isSingleCommit := beforeCommitID == "" && afterCommitID != ""
|
||||
// FIXME: when afterCommitID==headCommitID, isSingleCommit and isShowAllCommits can be both true, which doesn't seem right
|
||||
isShowAllCommits := (beforeCommitID == "" || beforeCommitID == prCompareInfo.CompareBase) && (afterCommitID == "" || afterCommitID == headCommitID)
|
||||
// show all commits when:
|
||||
// * both before and after commit IDs are empty (default view)
|
||||
// * before commit ID is the PR's base commit and after commit ID is the PR's head commit (default view)
|
||||
// * there is only one commit in the PR and after commit ID is the PR's head commit (single-commit view)
|
||||
isShowAllCommits := (beforeCommitID == "" && afterCommitID == "") ||
|
||||
(beforeCommitID == prCompareInfo.CompareBase && afterCommitID == headCommitID) ||
|
||||
(beforeCommitID == "" && afterCommitID == headCommitID && len(prCompareInfo.Commits) == 1)
|
||||
isSingleCommit := beforeCommitID == "" && afterCommitID != "" && !isShowAllCommits
|
||||
|
||||
ctx.Data["IsShowingOnlySingleCommit"] = isSingleCommit
|
||||
// it's questionable why single commit review can be submitted, but the range commits review cannot be submitted
|
||||
ctx.Data["CanSubmitReview"] = isShowAllCommits || isSingleCommit
|
||||
ctx.Data["IsShowingAllCommits"] = isShowAllCommits
|
||||
|
||||
// "commits list" is half-open, half-closed: (base, head]
|
||||
@@ -723,6 +751,10 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if afterCommit == nil && isSingleCommit {
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/commit/" + afterCommitID) // timeline commits outlive force pushes
|
||||
return
|
||||
}
|
||||
if afterCommit == nil {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
@@ -756,6 +788,11 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
|
||||
ctx.Data["CompareInfo"] = prCompareInfo
|
||||
ctx.Data["AfterCommitID"] = afterCommitID
|
||||
ctx.Data["BeforeCommitID"] = beforeCommitID
|
||||
ctx.Data["ReviewCommitID"] = afterCommitID // the review happens on the "after commit" (the latest commit in the range)
|
||||
|
||||
if isSingleCommit {
|
||||
setSingleCommitNavContext(ctx, prCompareInfo.Commits, afterCommit)
|
||||
}
|
||||
|
||||
maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles
|
||||
files := ctx.FormStrings("files")
|
||||
|
||||
@@ -56,6 +56,7 @@ func RenderNewCodeCommentForm(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
ctx.Data["AfterCommitID"] = pullHeadCommitID
|
||||
ctx.Data["ReviewCommitID"] = pullHeadCommitID // it's questionable why it only needs the head commit ID but not correct "AfterCommitID" (single commit review)
|
||||
upload.AddUploadContext(ctx, "comment")
|
||||
ctx.HTML(http.StatusOK, tplNewComment)
|
||||
}
|
||||
@@ -209,6 +210,7 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori
|
||||
return
|
||||
}
|
||||
ctx.Data["AfterCommitID"] = pullHeadCommitID
|
||||
ctx.Data["ReviewCommitID"] = pullHeadCommitID // it's questionable why it only needs the head commit ID but not correct "AfterCommitID" (single commit review)
|
||||
ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
|
||||
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user