mirror of
https://github.com/Lydanne/spaceflow.git
synced 2026-03-09 18:52:24 +08:00
feat(review): 本地模式无变更时自动回退到分支比较模式
当本地模式(--local)检测到无暂存区或未提交代码变更时,自动回退到分支比较模式(当前分支 vs 默认分支),避免直接退出。主要变更包括: 1. 将 isLocalMode 改为可变变量,支持动态回退 2. 新增 effectiveBaseRef 和 effectiveHeadRef 用于回退时的分支引用 3. 本地无变更时自动检测当前分支和默认分支进行比较 4. 重构分支比较逻辑,
This commit is contained in:
@@ -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) |
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user