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

+6 -1
View File
@@ -32,7 +32,11 @@
color: var(--color-text);
}
.ui.button.hover-opaque:hover {
.ui.ui.ui.buttons.hover-opaque {
background: var(--color-body)
}
.ui.ui.ui.button.hover-opaque:hover {
/* ".ui.button:hover" uses alpha channel to "highlight", it doesn't work when the background is not body: the inherited color is not right
for such case, use this opaque color */
background: var(--color-hover-opaque);
@@ -282,6 +286,7 @@
font-size: 0;
vertical-align: baseline;
margin: 0 0.25em 0 0;
border-radius: var(--border-radius);
}
/* btn is a plain button without any opinionated styling, it only uses flex for vertical alignment like ".ui.button" in base.css */
+21 -3
View File
@@ -23,11 +23,14 @@ type CommitListResult = {
const elRoot = useTemplateRef('elRoot') as Readonly<ShallowRef<HTMLDivElement>>;
const elExpandBtn = useTemplateRef('elExpandBtn') as Readonly<ShallowRef<HTMLButtonElement>>;
const elShowAllChanges = useTemplateRef('elShowAllChanges') as Readonly<ShallowRef<HTMLDivElement>>;
const elMenu = useTemplateRef('elMenu') as Readonly<ShallowRef<HTMLDivElement>>;
const elMount = document.querySelector('#diff-commit-select')!;
const queryParams = elMount.getAttribute('data-queryparams');
const issueLink = elMount.getAttribute('data-issuelink');
const mergeBase = elMount.getAttribute('data-merge-base');
const beforeCommit = elMount.getAttribute('data-before-commit');
const afterCommit = elMount.getAttribute('data-after-commit');
const uniqueIdMenu = generateElemId('diff-commit-selector-menu-');
const uniqueIdShowAll = generateElemId('diff-commit-selector-show-all-');
@@ -37,6 +40,11 @@ const locale = shallowRef<Record<string, string>>({filter_changes_by_commit: elM
const commits = ref<Array<Commit>>([]); // deep, the commit objects are mutated in place
const hoverActivated = shallowRef(false);
const lastReviewCommitSha = shallowRef<string | null>(null);
const activeRange = shallowRef<[number, number] | null>(null);
function isActive(idx: number) {
return activeRange.value !== null && activeRange.value[0] <= idx && idx <= activeRange.value[1];
}
const commitsSinceLastReview = computed(() => {
if (lastReviewCommitSha.value) {
@@ -140,6 +148,8 @@ async function toggleMenu() {
nextTick(() => {
if (menuVisible.value) {
focusElem(elShowAllChanges.value, elExpandBtn.value);
const elActive = elMenu.value.querySelector<HTMLElement>('.item.active');
if (elActive) elMenu.value.scrollTop = elActive.offsetTop - elMenu.value.clientHeight / 2; // scrollIntoView would also scroll the page
} else {
focusElem(elExpandBtn.value, elShowAllChanges.value);
}
@@ -160,6 +170,8 @@ async function fetchCommits() {
lastReviewCommitSha.value = null;
}
locale.value = {...locale.value, ...results.locale};
const end = commits.value.findIndex((x) => x.id === afterCommit);
if (end >= 0) activeRange.value = [commits.value.findIndex((x) => x.id === beforeCommit) + 1, end]; // starts at 0 when beforeCommit is outside the list
}
function showAllChanges() {
@@ -228,7 +240,7 @@ function commitClickedShift(commit: Commit) {
<svg-icon name="octicon-git-commit"/>
</button>
<!-- this dropdown is not managed by Fomantic UI, so it needs some classes like "transition" explicitly -->
<div class="left menu transition" :id="uniqueIdMenu" :class="{visible: menuVisible}" v-show="menuVisible" v-cloak :aria-expanded="menuVisible ? 'true': 'false'">
<div class="left menu transition" ref="elMenu" :id="uniqueIdMenu" :class="{visible: menuVisible}" v-show="menuVisible" v-cloak :aria-expanded="menuVisible ? 'true': 'false'">
<div class="loading-indicator is-loading" v-if="isLoading"/>
<div v-if="!isLoading" class="item" :id="uniqueIdShowAll" ref="elShowAllChanges" role="menuitem" @keydown.enter="showAllChanges()" @click="showAllChanges()">
<div class="gt-ellipsis">
@@ -257,7 +269,8 @@ function commitClickedShift(commit: Commit) {
<template v-for="(commit, idx) in commits" :key="commit.id">
<div
class="item" role="menuitem"
:class="{selected: commit.selected, hovered: commit.hovered}"
:class="{selected: commit.selected, hovered: commit.hovered, active: isActive(idx)}"
:aria-current="isActive(idx) || undefined"
:data-commit-idx="idx"
@keydown.enter.exact="commitClicked(commit.id)"
@keydown.enter.shift.exact="commitClickedShift(commit)"
@@ -279,7 +292,8 @@ function commitClickedShift(commit: Commit) {
</span>
</div>
</div>
<div class="tw-font-mono">
<div class="tw-font-mono flex-text-block">
<svg-icon name="octicon-check" :size="14" v-if="isActive(idx)"/>
{{ commit.short_sha }}
</div>
</div>
@@ -329,6 +343,10 @@ function commitClickedShift(commit: Commit) {
background-color: var(--color-accent);
}
.ui.dropdown.diff-commit-selector .menu > .item.active {
font-weight: var(--font-weight-medium);
}
.ui.dropdown.diff-commit-selector .menu .commit-list-summary {
max-width: min(380px, 96vw);
}