feat(review): 本地模式无变更时自动回退到分支比较模式

当本地模式(--local)检测到无暂存区或未提交代码变更时,自动回退到分支比较模式(当前分支 vs 默认分支),避免直接退出。主要变更包括:

1. 将 isLocalMode 改为可变变量,支持动态回退
2. 新增 effectiveBaseRef 和 effectiveHeadRef 用于回退时的分支引用
3. 本地无变更时自动检测当前分支和默认分支进行比较
4. 重构分支比较逻辑,
This commit is contained in:
Lyda
2026-03-03 18:44:31 +08:00
parent b96d05a937
commit 820ff8d04f
2 changed files with 132 additions and 33 deletions

View File

@@ -25,6 +25,73 @@ spaceflow review -p 123 -f src/index.ts
# 仅刷新状态(不执行 LLM 审查)
spaceflow review -p 123 --flush
# 本地模式:审查未提交的代码
spaceflow review --local
# 仅审查暂存区代码
spaceflow review --local staged
```
## 运行模式
Review 命令根据运行环境和参数自动选择合适的审查模式:
### 模式优先级
```text
CI 模式 (--ci) → PR 模式 (-p) → 分支比较模式 (-b/--head) → 本地模式 (--local) → 自动检测
```
### 模式说明
| 模式 | 触发条件 | 数据来源 | 适用场景 |
|------|----------|----------|----------|
| **CI 模式** | `--ci` 或 CI 环境变量 | GitHub/Gitea API | CI/CD 流水线 |
| **PR 模式** | `-p <number>` | Git Provider API | 审查指定 PR |
| **分支比较** | `-b <base> --head <head>` | 本地 Git | 审查分支差异 |
| **本地模式** | `--local` 或自动启用 | 本地 Git | 开发时快速审查 |
### 本地模式
本地模式用于在开发过程中快速审查未提交的代码变更,无需创建 PR 或推送到远程。
**自动启用条件**(同时满足):
- 非 CI 环境
- 未指定 PR 编号
- 未指定 base/head 分支
**模式选项**
- `--local``--local uncommitted`:审查暂存区 + 工作区的所有变更(默认)
- `--local staged`:仅审查暂存区的变更
- `--no-local`:禁用本地模式,强制使用分支比较
**回退机制**
当本地模式检测到没有未提交的变更时会自动回退到分支比较模式比较当前分支与默认分支main/master的差异。
```text
本地模式启动
检测本地变更
有变更 → 审查本地代码
无变更 → 回退到分支比较模式 → 审查分支差异
```
**使用示例**
```bash
# 自动检测:有本地变更则审查本地,无变更则比较分支
spaceflow review
# 显式启用本地模式
spaceflow review --local
# 仅审查暂存区(适合 pre-commit 场景)
spaceflow review --local staged
# 禁用本地模式,强制比较分支
spaceflow review --no-local
```
## 审查流程
@@ -34,7 +101,7 @@ spaceflow review -p 123 --flush
### 1. 准备阶段
```text
上下文构建 → 规则加载 → 变更文件获取 → 文件内容获取
上下文构建 → 模式选择 → 规则加载 → 变更文件获取 → 文件内容获取
```
- **上下文构建**合并命令行参数、PR 标题参数、配置文件,确定 owner/repo/prNumber/llmMode 等
@@ -293,6 +360,8 @@ Review 命令会加载 `references` 配置中指定的审查规范文件,用
| `--verify-concurrency <n>` | | 修复验证并发数(默认 10 |
| `--flush` | | 仅刷新状态(同步 reactions、resolved 等并执行 LLM 最终验证) |
| `--show-all` | | 显示所有问题,不过滤非变更行 |
| `--local [mode]` | | 本地模式:`uncommitted`(默认)或 `staged` |
| `--no-local` | | 禁用本地模式,使用分支比较 |
| `--dry-run` | `-d` | 试运行,不实际提交评论 |
| `--concurrency <n>` | | LLM 审查并发数 |
| `--verbose` | `-v` | 详细日志(`-v` 级别 1`-vv` 级别 2 |

View File

@@ -437,8 +437,11 @@ export class ReviewService {
// 直接审查文件模式:指定了 -f 文件且 base=head
const isDirectFileMode = files && files.length > 0 && baseRef === headRef;
// 本地模式:审查未提交的代码
const isLocalMode = !!localMode;
// 本地模式:审查未提交的代码(可能回退到分支比较)
let isLocalMode = !!localMode;
// 用于回退时动态计算的 base/head
let effectiveBaseRef = baseRef;
let effectiveHeadRef = headRef;
if (shouldLog(verbose, 1)) {
console.log(`🔍 Review 启动`);
@@ -475,31 +478,49 @@ export class ReviewService {
localMode === "staged" ? this.gitSdk.getStagedFiles() : this.gitSdk.getUncommittedFiles();
if (localFiles.length === 0) {
console.log(` 没有${localMode === "staged" ? "暂存区" : "未提交"}的代码变更`);
return {
success: true,
description: "",
issues: [],
summary: [],
round: 1,
};
// 本地无变更,回退到分支比较模式
if (shouldLog(verbose, 1)) {
console.log(
` 没有${localMode === "staged" ? "暂存区" : "未提交"}的代码变更,回退到分支比较模式`,
);
}
isLocalMode = false;
effectiveHeadRef = this.gitSdk.getCurrentBranch() ?? "HEAD";
effectiveBaseRef = this.gitSdk.getDefaultBranch();
if (shouldLog(verbose, 1)) {
console.log(`📌 自动检测分支: base=${effectiveBaseRef}, head=${effectiveHeadRef}`);
}
// 同分支无法比较,提前返回
if (effectiveBaseRef === effectiveHeadRef) {
console.log(` 当前分支 ${effectiveHeadRef} 与默认分支相同,没有可审查的代码变更`);
return {
success: true,
description: "",
issues: [],
summary: [],
round: 1,
};
}
} else {
// 一次性获取所有 diff避免每个文件调用一次 git 命令
const localDiffs =
localMode === "staged" ? this.gitSdk.getStagedDiff() : this.gitSdk.getUncommittedDiff();
const diffMap = new Map(localDiffs.map((d) => [d.filename, d.patch]));
changedFiles = localFiles.map((f) => ({
filename: f.filename,
status: f.status as ChangedFile["status"],
patch: diffMap.get(f.filename),
}));
if (shouldLog(verbose, 1)) {
console.log(` Changed files: ${changedFiles.length}`);
}
}
}
// 一次性获取所有 diff避免每个文件调用一次 git 命令
const localDiffs =
localMode === "staged" ? this.gitSdk.getStagedDiff() : this.gitSdk.getUncommittedDiff();
const diffMap = new Map(localDiffs.map((d) => [d.filename, d.patch]));
changedFiles = localFiles.map((f) => ({
filename: f.filename,
status: f.status as ChangedFile["status"],
patch: diffMap.get(f.filename),
}));
if (shouldLog(verbose, 1)) {
console.log(` Changed files: ${changedFiles.length}`);
}
} else if (prNumber) {
// PR 模式、分支比较模式、或本地模式回退后的分支比较
if (prNumber) {
if (shouldLog(verbose, 1)) {
console.log(`📥 获取 PR #${prNumber} 信息 (owner: ${owner}, repo: ${repo})`);
}
@@ -511,25 +532,34 @@ export class ReviewService {
console.log(` Commits: ${commits.length}`);
console.log(` Changed files: ${changedFiles.length}`);
}
} else if (baseRef && headRef) {
} else if (effectiveBaseRef && effectiveHeadRef) {
// 如果指定了 -f 文件且 base=head无差异模式直接审查指定文件
if (files && files.length > 0 && baseRef === headRef) {
if (files && files.length > 0 && effectiveBaseRef === effectiveHeadRef) {
if (shouldLog(verbose, 1)) {
console.log(`📥 直接审查指定文件模式 (${files.length} 个文件)`);
}
changedFiles = files.map((f) => ({ filename: f, status: "modified" as const }));
} else {
} else if (changedFiles.length === 0) {
// 仅当 changedFiles 为空时才获取(避免与回退逻辑重复)
if (shouldLog(verbose, 1)) {
console.log(`📥 获取 ${baseRef}...${headRef} 的差异 (owner: ${owner}, repo: ${repo})`);
console.log(
`📥 获取 ${effectiveBaseRef}...${effectiveHeadRef} 的差异 (owner: ${owner}, repo: ${repo})`,
);
}
changedFiles = await this.getChangedFilesBetweenRefs(owner, repo, baseRef, headRef);
commits = await this.getCommitsBetweenRefs(baseRef, headRef);
changedFiles = await this.getChangedFilesBetweenRefs(
owner,
repo,
effectiveBaseRef,
effectiveHeadRef,
);
commits = await this.getCommitsBetweenRefs(effectiveBaseRef, effectiveHeadRef);
if (shouldLog(verbose, 1)) {
console.log(` Changed files: ${changedFiles.length}`);
console.log(` Commits: ${commits.length}`);
}
}
} else {
} else if (!isLocalMode) {
// 非本地模式且无有效的 base/head
if (shouldLog(verbose, 1)) {
console.log(`❌ 错误: 缺少 prNumber 或 baseRef/headRef`, { prNumber, baseRef, headRef });
}