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:
authored and GitHub committed 2026-10-30 20:18:13 +00:00
1 parent cbb563b080
commit fa41fc94d4
12 files changed
+155 -35

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{{if .Statuses}}
{{if and (eq (len .Statuses) 1) .Status.TargetURL}}
<a class="flex-text-inline tw-no-underline {{.AdditionalClasses}}" data-global-init="initCommitStatuses" href="{{.Status.TargetURL}}">
<a class="flex-text-inline silenced {{.AdditionalClasses}}" data-global-init="initCommitStatuses" href="{{.Status.TargetURL}}">
{{template "repo/icons/commit_status" .Status}}
</a>
{{else}}
+1 -1
View File
@@ -8,7 +8,7 @@
<span class="badge badge-commit">{{svg "octicon-git-commit"}}</span>
{{ctx.RenderUtils.AvatarStack $commit.AvatarStackData}}
{{$commitBaseLink := printf "%s/commit" $.comment.Issue.PullRequest.BaseRepo.Link}}
{{$commitBaseLink := printf "%s/commits" $.comment.Issue.Link}}{{/* "/owner/repo/pulls/{idx}/commits" */}}
{{$commitLink:= printf "%s/%s" $commitBaseLink (PathEscape $gitCommit.ID.String)}}
<span class="tw-flex-1 tw-font-mono gt-ellipsis" title="{{$gitCommit.MessageTitle}}">
+3 -4
View File
@@ -36,6 +36,7 @@
{{template "repo/diff/options_dropdown" .}}
{{if .PageIsPullFiles}}
<div id="diff-commit-select" data-global-init="initDiffCommitSelect" data-merge-base="{{$.CompareInfo.CompareBase}}" data-issuelink="{{$.Issue.Link}}"
{{if not $.IsShowingAllCommits}}data-before-commit="{{$.BeforeCommitID}}" data-after-commit="{{$.AfterCommitID}}"{{end}}
data-queryparams="?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated={{$.ShowOutdatedComments}}"
data-text-filter-changes-by-commit="{{ctx.Locale.Tr "repo.pulls.filter_changes_by_commit"}}">
{{/* the following will be replaced by vue component, but this avoids any loading artifacts till the vue component is initialized */}}
@@ -50,10 +51,8 @@
</div>
</div>
{{if not .DiffNotAvailable}}
{{if and .IsShowingOnlySingleCommit .PageIsPullFiles}}
<div class="ui info message">
<div>{{ctx.Locale.Tr "repo.pulls.showing_only_single_commit" (ShortSha .AfterCommitID)}} - <a href="{{$.Issue.Link}}/files?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated={{$.ShowOutdatedComments}}">{{ctx.Locale.Tr "repo.pulls.show_all_commits"}}</a></div>
</div>
{{if and .PageIsPullFiles .SingleCommit}}
{{template "repo/diff/single_commit_bar" .}}
{{else if and (not .IsShowingAllCommits) .PageIsPullFiles}}
<div class="ui info message">
<div>{{ctx.Locale.Tr "repo.pulls.showing_specified_commit_range" (ShortSha .BeforeCommitID) (ShortSha .AfterCommitID)}} - <a href="{{$.Issue.Link}}/files?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated={{$.ShowOutdatedComments}}">{{ctx.Locale.Tr "repo.pulls.show_all_commits"}}</a></div>
+1 -1
View File
@@ -4,7 +4,7 @@
{{if $canAddComment}}
<form class="ui form {{if $replyReviewComment}}tw-hidden comment-form{{end}}" action="{{$root.Issue.Link}}/files/reviews/comments" method="post">
<input type="hidden" name="origin" value="{{if $root.PageIsPullFiles}}diff{{else}}timeline{{end}}">
<input type="hidden" name="latest_commit_id" value="{{$root.AfterCommitID}}">
<input type="hidden" name="latest_commit_id" value="{{$root.ReviewCommitID}}">
<input type="hidden" name="side" value="{{$replyReviewComment.DiffSide}}">
<input type="hidden" name="line" value="{{$replyReviewComment.UnsignedLine}}">
<input type="hidden" name="path" value="{{$replyReviewComment.TreePath}}">
+5 -5
View File
@@ -1,8 +1,8 @@
{{$disabledReviewTooltip := "" -}}
{{if $.Repository.IsArchived -}}
{{$disabledReviewTooltip = ctx.Locale.Tr "repo.archive.pull.nocomment" -}}
{{else if not $.IsShowingAllCommits -}}
{{$disabledReviewTooltip = ctx.Locale.Tr "repo.pulls.review_only_possible_for_full_diff" -}}
{{else if not $.CanSubmitReview -}}
{{$disabledReviewTooltip = ctx.Locale.Tr "repo.pulls.review_not_possible_for_commit_range" -}}
{{end -}}
<div data-global-init="initPullReviewBox">
<div {{if $disabledReviewTooltip}}data-tooltip-content="{{$disabledReviewTooltip}}"{{end}}>
@@ -12,13 +12,13 @@
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
</button>
</div>
{{if $.IsShowingAllCommits}}
{{if $.CanSubmitReview}}
<div class="review-box-panel tippy-target">
<div class="ui segment">
<form class="ui form form-fetch-action" action="{{.Issue.Link}}/files/reviews/submit" method="post">
<input type="hidden" name="commit_id" value="{{.AfterCommitID}}">
<input type="hidden" name="commit_id" value="{{.ReviewCommitID}}">
<div class="field flex-text-block">
<div class="tw-flex-1">{{ctx.Locale.Tr "repo.diff.review.header"}}</div>
<b class="tw-flex-1">{{ctx.Locale.Tr "repo.diff.review.header"}}</b>
<a class="muted close-review-panel">{{svg "octicon-x" 16}}</a>
</div>
<div class="field">
@@ -0,0 +1,33 @@
{{$queryParams := QueryBuild "?" "style" (Iif $.IsSplitStyle "split" "unified") "whitespace" $.WhitespaceBehavior "show-outdated" (Iif $.ShowOutdatedComments "true" "false")}}
{{$commit := $.SingleCommit}}{{$commitID := $commit.ID.String}}
{{$prevID := $.SingleCommitPrevID}}{{$nextID := $.SingleCommitNextID}}
{{$repoLink := $.RepoLink}}
{{$prLink := $.Issue.Link}}
<div class="ui info message flex-left-right" data-commit-single-index="{{$.SingleCommitIndex}}" data-commit-total-count="{{$.SingleCommitCount}}">
<div class="flex-relaxed-list">
<div class="flex-text-block tw-flex-wrap">
{{svg "octicon-git-commit"}}
<a class="tw-font-mono muted" href="{{$repoLink}}/commit/{{PathEscape $commitID}}">{{ShortSha $commitID}}</a>
<strong class="gt-ellipsis">{{$commit.MessageTitle | ctx.RenderUtils.RenderEmoji}}</strong>
</div>
<div class="flex-text-block tw-flex-wrap tw-text-text-light-2">
<div class="flex-text-block">
{{ctx.AvatarUtils.AvatarByEmail $commit.Author.Email $commit.Author.Name 16}}
<span class="gt-ellipsis">{{$commit.Author.Name}}</span>
</div>
{{DateUtils.TimeSince $commit.Author.When}}
</div>
</div>
<div class="flex-text-block tw-flex-wrap">
<span>{{ctx.Locale.Tr "repo.pulls.commit_position" $.SingleCommitIndex $.SingleCommitCount}}</span>
<div class="ui compact icon buttons hover-opaque">
<a class="ui tiny basic button js-previous-commit-link {{if not $.SingleCommitPrevID}}disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.previous_commit"}}"
{{if $prevID}}href="{{$prLink}}/commits/{{PathEscape $prevID}}{{$queryParams}}"{{end}}
>{{svg "octicon-chevron-left"}}</a>
<a class="ui tiny basic button js-next-commit-link {{if not $nextID}}disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.next_commit"}}"
{{if $nextID}}href="{{$prLink}}/commits/{{PathEscape $nextID}}{{$queryParams}}"{{end}}
>{{svg "octicon-chevron-right"}}</a>
</div>
<a class="ui tiny basic button hover-opaque" href="{{$prLink}}/files{{$queryParams}}">{{ctx.Locale.Tr "repo.pulls.show_all_commits"}}</a>
</div>
</div>