feat: 重构被审查分支的选择逻辑

push 审查
  check_branches 现在只表示「要做 push 审查的分支」,不再兼作 PR 目标分支:
  - 勾选具体分支:这些分支的 push 会审查,通过后晋级到管理分支
  - 新增「任意分支」(*):所有分支的 push 都审查
  - 留空:只审 PR,不审 push

PR 审查
  始终覆盖「勾选的分支 + 管理分支」作为合并目标。
  管理分支是最终目标,不需要出现在检查分支里也能被保护。

管理分支从候选中移除
  分支选择器不再列出管理分支——它是终点,不需要再检查自己。

安全边界
  「任意分支」只扩大审查范围,不会把任意分支自动合并进管理分支;
  晋级仍要求分支被显式勾选(promotesToManaged)。

同时修正一处语义错误
  早先 branchMatches 把空列表当成「审所有分支」,导致新建仓库在用户
  还没选分支时就审查全部 push。现在空列表明确表示「只审 PR」。

测试:分支匹配矩阵重写,覆盖 push/PR/晋级/保护目标四类判定,共 20 项通过。
This commit is contained in:
2026-09-21 17:12:57 +08:00
parent 7cd2c85888
commit 216bb51c92
6 changed files with 209 additions and 67 deletions
+50 -15
View File
@@ -197,10 +197,14 @@ function shortUrl(url) {
function branchTags(list, managed) {
const names = String(list || "").split(",").map((s) => s.trim()).filter(Boolean);
if (!names.length) return '<span class="tag muted">—</span>';
return names.map((n) => n === managed
? `<span class="tag ok">${esc(n)}</span>`
: `<span class="tag muted">${esc(n)}</span>`).join(" ");
if (names.includes("*")) {
const extra = names.filter((n) => n !== "*");
return `<span class="tag warn">任意分支</span>` +
(extra.length ? ` ${extra.map((n) => `<span class="tag muted">${esc(n)}</span>`).join(" ")}` : "");
}
const own = names.filter((n) => n !== managed);
if (!own.length) return '<span class="tag muted">仅 PR</span>';
return own.map((n) => `<span class="tag muted">${esc(n)}</span>`).join(" ");
}
function scopeLabel(scope) {
@@ -251,30 +255,59 @@ const modal = document.getElementById("modal");
const form = document.getElementById("repo-form");
const branchList = document.getElementById("branch-list");
function selectedBranches() {
const ANY_BRANCH = "*";
function allSelected() {
return String(form.elements.check_branches.value || "")
.split(",").map((s) => s.trim()).filter(Boolean);
}
function setSelectedBranches(list) {
form.elements.check_branches.value = list.join(",");
/** Branch names only; the "任意分支" token is not a branch. */
function selectedBranches() {
return allSelected().filter((b) => b !== ANY_BRANCH);
}
function reviewsAnyBranch() {
return allSelected().includes(ANY_BRANCH);
}
function setSelectedBranches(list, { any = reviewsAnyBranch() } = {}) {
const names = list.filter((b) => b && b !== ANY_BRANCH);
form.elements.check_branches.value = (any ? [ANY_BRANCH, ...names] : names).join(",");
}
function renderBranchList() {
const chosen = new Set(selectedBranches());
const managed = form.elements.managed_branch.value.trim();
const any = reviewsAnyBranch();
if (!state.branches.length) {
branchList.innerHTML = '<span class="hint">点「加载分支」获取远端分支列表。</span>';
return;
}
branchList.innerHTML = state.branches.map((b) => {
const on = chosen.has(b);
const label = b === managed ? `${b}(管理)` : b;
return `<button type="button" class="${on ? "on" : ""}" data-branch="${esc(b)}">${esc(label)}</button>`;
}).join("");
// The managed branch is the final target and is always reviewed for pull
// requests, so it is not offered as a checked branch.
const selectable = state.branches.filter((b) => b !== managed);
const parts = [
`<button type="button" class="${any ? "on" : ""}" data-any="1">任意分支</button>`,
];
if (selectable.length === 0) {
parts.push('<span class="hint">除管理分支外没有其它分支。</span>');
} else {
parts.push(...selectable.map((b) => {
const on = chosen.has(b);
return `<button type="button" class="${on ? "on" : ""}" data-branch="${esc(b)}">${esc(b)}</button>`;
}));
}
branchList.innerHTML = parts.join("");
}
branchList.addEventListener("click", (ev) => {
const anyBtn = ev.target.closest("button[data-any]");
if (anyBtn) {
setSelectedBranches(selectedBranches(), { any: !reviewsAnyBranch() });
renderBranchList();
return;
}
const btn = ev.target.closest("button[data-branch]");
if (!btn) return;
const name = btn.dataset.branch;
@@ -299,7 +332,7 @@ async function openRepo(id) {
if (!repo) {
if (field === "enabled" || field === "create_issue") input.checked = true;
if (field === "managed_branch") input.value = "main";
if (field === "check_branches") input.value = "main";
if (field === "check_branches") input.value = "";
continue;
}
if (input.type === "checkbox") input.checked = Boolean(repo[field]);
@@ -334,7 +367,9 @@ async function loadBranches({ silent = false } = {}) {
: await api(`/repos/${id}/discover`, { method: "POST" });
state.branches = info.branches || [];
if (!form.elements.managed_branch.value) form.elements.managed_branch.value = info.default_branch || "main";
if (!form.elements.check_branches.value) form.elements.check_branches.value = state.branches.join(",");
// Existing repositories keep their saved selection; a fresh one starts
// with no explicit branches checked (only the managed branch is reviewed).
if (form.elements.check_branches.value === "") setSelectedBranches([], { any: false });
renderBranchList();
hint.className = "hint ok";
hint.textContent =
@@ -496,7 +531,7 @@ document.getElementById("import-list").addEventListener("click", async (ev) => {
body: {
repo_url: remote.clone_url,
managed_branch: remote.default_branch || "main",
check_branches: remote.default_branch || "main",
check_branches: "",
},
});
// Discover immediately so the branch list is populated on first open.
+5 -4
View File
@@ -185,15 +185,16 @@
<button type="button" id="branch-create">克隆创建</button>
</span>
</label>
<p class="hint">所有检查通过后只合并到这一个分支;它也会出现在下面的检查分支里。</p>
<label>检查分支(可多选)
<p class="hint">最终目标分支。它是 PR 审查的默认合并目标,但不需要出现在下面的检查分支里。</p>
<label>检查分支(push 审查;可多选)
<span class="inline">
<input name="check_branches" placeholder="main,release/1.0" />
<input name="check_branches" placeholder="留空表示只审查 PR;* 表示任意分支" />
<button type="button" id="branch-reload">加载分支</button>
</span>
</label>
<div id="branch-list" class="branch-list"></div>
<p class="hint">填「被保护的目标分支」。任何合并进这些分支的 PR 都会审查(功能分支不用列出来);向这些分支推送也会审查。只有指向管理分支的 PR 才会自动合并。</p>
<p class="hint">选「任意分支」则所有分支的 push 都审查,也可只勾选指定分支。勾选的分支 push 通过后会自动晋级到管理分支;「任意分支」只扩大审查范围,不会自动合并任意分支。</p>
<p class="hint">PR 审查始终覆盖「勾选的分支 + 管理分支」作为合并目标,与管理分支是否列在这里无关。</p>
</fieldset>
<fieldset>