Pull Request 流程
本页说明从提交 PR 到合并的完整流程、PR 模板、代码审查标准、CI 检查项、合并策略与冲突解决方法。所有代码变更必须通过 PR 流程合并至主干。
提交前自查
提交 PR 前请逐项确认:
- 分支基于最新
main,已rebase解决冲突 -
cargo fmt --all -- --check通过 -
cargo clippy --all-targets -- -D warnings通过 -
cargo nextest run全部通过 -
cargo nextest run -p <my-crate>全部通过 - 新增功能附带测试,覆盖率不下降
- 公共 API 变更已更新文档与
CHANGELOG - 提交信息符合 Conventional Commits
- PR 标题与描述清晰,关联了 Issue
- 不包含密钥、证书、临时文件等敏感内容
- 不包含
.env、*.pem、*.key等敏感文件 -
cargo deny check通过(无禁用许可或重复依赖) - PR 行数控制在 800 行以内(大型改动请拆分)
PR 流程
完整流程图
开发完成 → 自查 → 推送 → 创建 PR → CLA/CI 检查 → Review → 修改 → 合并 → 删分支
↓ ↓
失败时 拒绝时
↓ ↓
修复 关闭/重新提交
步骤详解
1. 创建 PR
在 GitHub 网页上点击”Compare & pull request”,或在命令行:
# 推送分支
git push origin feat/powerflow-pv-limits
# 使用 GitHub CLI 创建 PR
gh pr create \
--title "feat(powerflow): 支持 PV 母线无功上下限" \
--body "Closes #123" \
--base main \
--head feat/powerflow-pv-limits
# 或使用浏览器打开
gh pr create --web
2. 填写 PR 描述
按 PR 模板填写完整信息,关联 Issue。详见 PR 模板。
3. 等待自动化检查
PR 创建后,以下 bot 会自动检查:
| Bot | 检查内容 | 失败处理 |
|---|---|---|
| CLAssistant | CLA 签署状态 | 点击签署链接 |
| GitHub Actions | fmt / clippy / test | 修复后推送 |
| Dependabot | 依赖安全性 | 升级依赖 |
| Codecov | 覆盖率 | 补充测试 |
| Welcome Bot | 首次贡献者欢迎 | 无需操作 |
4. 等待 Review
- Maintainer 会在 2 个工作日内指派 Reviewer
- Reviewer 会在 5 个工作日内给出首轮反馈
- 紧急 PR 可在 PR 描述中标注
[URGENT]并 @ 维护者
5. 响应 Review
- 针对每条评论回复或修改,避免无视
- 大改动建议使用
git push --force-with-lease而非 merge commit,保持历史线性 - 长期未响应的 PR(>30 天)将被关闭,可随时重开
6. 合并
通过 CI 与至少一位 Reviewer 批准后由 Maintainer 合并。合并后分支由 Maintainer 删除,关联 Issue 自动关闭。
PR 模板
PR 描述须按以下模板填写(仓库已配置 .github/PULL_REQUEST_TEMPLATE.md):
## 变更说明
(一段话概述本 PR 做了什么,以及为什么。如果关联了 Issue,简述背景。)
## 关联 Issue
Closes #123
Depends on: #120(如有依赖 PR)
## 变更类型
- [ ] 新功能(feat)
- [ ] Bug 修复(fix)
- [ ] 重构(refactor)
- [ ] 文档(docs)
- [ ] 测试(test)
- [ ] 性能优化(perf)
- [ ] 构建/CI(build/ci)
- [ ] 杂项(chore)
## 破坏性变更
- [ ] 否
- [ ] 是(请说明迁移路径)
如有破坏性变更,请提供迁移指南:
\`\`\`rust
// 旧 API(v0.46.0)
let bus = Bus::new(1, 1.06);
// 新 API(v0.47.0)
let bus = Bus::builder().id(1).voltage(1.06).build();
\`\`\`
## 验证方式
(描述如何复现并验证本次变更)
1. 启动 API 服务器:`cargo run -p eneros-api`
2. 调用接口:`curl -X POST localhost:8080/api/powerflow/solve -d @ieee14.json`
3. 预期输出:包含 `converged: true` 的 JSON 响应
## 检查清单
- [ ] `cargo fmt --all -- --check` 通过
- [ ] `cargo clippy --all-targets -- -D warnings` 通过
- [ ] `cargo nextest run --workspace` 通过
- [ ] 新增功能已附测试
- [ ] 公共 API 变更已更新 `cargo doc`
- [ ] 已更新 `CHANGELOG.md`(如涉及行为变更)
- [ ] 不包含敏感文件(`.env`、`*.pem`、`*.key`)
- [ ] 提交信息符合 Conventional Commits
## 截图 / 性能数据
(如涉及 UI 变更或性能优化,请附截图或基准数据)
完整 PR 描述示例
## 变更说明
本 PR 为 `eneros-powerflow` 的 `NewtonRaphsonSolver` 新增 PV 母线无功上下限支持。当 PV 节点的无功出力超出 `[q_min, q_max]` 范围时,求解器会自动将其转换为 PQ 节点并重新求解,符合 IEEE Std 1547-2018 的要求。
## 关联 Issue
Closes #123
## 变更类型
- [x] 新功能(feat)
- [ ] Bug 修复(fix)
- [ ] 重构(refactor)
- [ ] 文档(docs)
- [x] 测试(test)
- [ ] 性能优化(perf)
- [ ] 构建/CI(build/ci)
- [ ] 杂项(chore)
## 破坏性变更
- [x] 否
新增的 `q_min`、`q_max` 字段使用 `Option<f64>`,默认 `None` 保持向后兼容。
## 验证方式
1. 运行单元测试:
```bash
cargo nextest run -p eneros-powerflow pv_limits
```
2. 运行 IEEE 14-bus 案例验证收敛性:
```bash
cargo run -p enerosctl -- powerflow solve --case ieee14 --verbose
```
3. 验证无功越限场景:
```bash
cargo run -p enerosctl -- powerflow solve --case ieee14_pv_limit --verbose
```
预期:Bus 8 在第 3 次迭代后从 PV 转为 PQ,最终收敛。
## 检查清单
- [x] `cargo fmt --all -- --check` 通过
- [x] `cargo clippy --all-targets -- -D warnings` 通过
- [x] `cargo nextest run --workspace` 通过
- [x] 新增功能已附测试(5 个用例)
- [x] 公共 API 变更已更新 `cargo doc`
- [x] 已更新 `CHANGELOG.md`
- [x] 不包含敏感文件
- [x] 提交信息符合 Conventional Commits
## 性能数据
IEEE 14-bus 基准测试结果(10 次平均):
| 场景 | 修改前 | 修改后 | 变化 |
|------|--------|--------|------|
| 无 PV 越限 | 12.3 ms | 12.4 ms | +0.8% |
| PV 越限(重新求解) | - | 18.7 ms | 新增 |
性能变化在可接受范围内(< 10%)。
代码审查标准
Review 维度
Reviewer 会从以下五个维度审查 PR:
| 维度 | 关注点 | 严重问题示例 |
|---|---|---|
| 正确性 | 算法正确性、边界条件、并发安全 | 潮流计算不收敛时未返回错误 |
| 安全性 | 约束引擎、审计链、密钥处理 | 绕过 ConstraintEngine 校验 |
| 性能 | 热路径分配、阻塞异步 runtime | 在 async 函数中调用 thread::sleep |
| 可维护性 | 命名、文档、模块边界 | 公共 API 缺少文档注释 |
| 测试充分性 | 失败路径、外部依赖 | 仅测试成功路径,未覆盖错误分支 |
正确性审查
- 算法是否符合电力领域知识?
- 边界条件是否覆盖(空集合、单元素、最大值、最小值)?
- 浮点数比较是否使用容差而非
==? - 错误处理是否完整,是否吞掉错误?
// 错误:吞掉错误
let result = solve_powerflow(&topo).unwrap_or_default();
// 正确:传播错误
let result = solve_powerflow(&topo).map_err(|e| {
tracing::error!("潮流计算失败: {}", e);
e
})?;
安全性审查
- 是否绕过
ConstraintEngine校验? - 是否绕过
AuditChain审计? - 是否引入新的明文通道(如
println!输出敏感数据)? - 密钥、证书、token 是否在日志中暴露?
- 是否使用
unsafe代码?如有,是否充分论证?
性能审查
- 热路径是否引入不必要的内存分配?
- 是否在异步上下文中调用阻塞函数?
- 是否在循环中创建新的连接或锁?
- 数据结构选择是否合适(
VecvsHashMapvsBTreeMap)?
// 错误:循环中分配
for bus in &buses {
let result = format!("bus_{}", bus.id); // 每次分配 String
println!("{}", result);
}
// 正确:复用缓冲区
let mut buf = String::with_capacity(32);
for bus in &buses {
buf.clear();
use std::fmt::Write;
write!(&mut buf, "bus_{}", bus.id).unwrap();
println!("{}", buf);
}
可维护性审查
- 命名是否清晰表达意图?
- 公共 API 是否需要文档注释?
- 模块边界是否合理?
- 是否有重复代码可以抽取?
- 复杂逻辑是否有注释说明?
测试充分性审查
- 测试是否覆盖失败路径?
- 是否依赖外部环境(网络、时间、文件系统)?
- 测试是否可重复运行?
- 测试命名是否符合规范?
CI 检查项
必须通过的检查
以下 CI 检查必须全部通过才能合并:
| 检查项 | 工作流 | 内容 | 失败处理 |
|---|---|---|---|
| Rustfmt | ci.yml | cargo fmt --all -- --check | 运行 cargo fmt --all |
| Clippy | ci.yml | cargo clippy --all-targets -- -D warnings | 修复警告 |
| Test | ci.yml | cargo nextest run --workspace | 修复测试 |
| Doc Test | ci.yml | cargo test --doc --workspace | 修复文档示例 |
| Cargo Deny | ci.yml | cargo deny check | 检查许可与重复依赖 |
| CLA | CLAssistant | 已签署 CLA | 点击签署链接 |
| Coverage | coverage.yml | 覆盖率不下降 | 补充测试 |
可选检查
| 检查项 | 工作流 | 内容 | 处理 |
|---|---|---|---|
| Benchmark | benchmark.yml | 性能回归 | 警告但不阻塞 |
| Conformance | conformance.yml | 协议一致性 | 必须通过(涉及协议时) |
| Security Scan | security.yml | SAST + 依赖审计 | 必须通过(涉及安全时) |
CI 失败处理
CI 失败时,请按以下步骤处理:
- 点击失败的检查项查看日志
- 在本地复现失败:
# 复现 CI 环境
cargo +stable fmt --all -- --check
cargo +stable clippy --all-targets -- -D warnings
cargo +stable nextest run --workspace
- 修复后推送:
git add .
git commit --fixup <commit-sha>
git rebase -i --autosquash upstream/main
git push origin feat/my-feature --force-with-lease
- CI 自动重新运行
处理 flaky 测试
如测试在 CI 上间歇性失败:
- 检查是否依赖外部环境(网络、时间)
- 使用
--retries 3在本地复现 - 在 PR 中说明,由 Maintainer 判断是否阻塞
合并策略
合并方式选择
| PR 类型 | 提交数 | 策略 | 命令 |
|---|---|---|---|
| 单一提交的小改动 | 1 | Squash merge | GitHub 自动 Squash |
| 多次提交的复杂改动 | >1 | Rebase merge | GitHub 自动 Rebase |
| 发布分支回滚 | - | Revert commit | git revert <sha> |
| 紧急修复 | 1 | Squash merge + cherry-pick | 合并后 cherry-pick 至 release |
Squash Merge
适用于小改动。GitHub 会将所有提交合并为一个,使用 PR 标题作为 commit message:
feat(powerflow): 支持 PV 母线无功上下限 (#124)
Rebase Merge
适用于多次提交的复杂改动,保留每次提交的细节。要求每个 commit 独立可编译且符合 Conventional Commits。
Create Merge Commit
仅用于发布分支合并回 main,平时不使用。
合并后操作
- 合并后分支由 Maintainer 删除(个人 fork 的分支由贡献者自行删除)
- 关联 Issue 自动关闭
CHANGELOG.md自动生成条目(通过 Conventional Commits 解析)- CI 触发文档站重新部署(如涉及
eneros-web/目录)
冲突解决
预防冲突
- 经常同步主干:
git fetch upstream && git rebase upstream/main - 小步快走,缩短 PR 周期
- 与其他贡献者协调,避免同时修改同一文件
解决冲突
# 1. 同步主干
git fetch upstream
git rebase upstream/main
# 2. 出现冲突时,git 会列出冲突文件
# CONFLICT (content): Merge conflict in crates/eneros-powerflow/src/solver.rs
# 3. 打开冲突文件,手动解决
# <<<<<<< HEAD
# (你的修改)
# =======
# (main 上的修改)
# >>>>>>> upstream/main
# 4. 标记冲突已解决
git add crates/eneros-powerflow/src/solver.rs
# 5. 继续 rebase
git rebase --continue
# 6. 推送(强制)
git push origin feat/my-feature --force-with-lease
冲突解决原则
- 保留双方意图:理解两边的修改目的,合并而非覆盖
- 测试验证:解决冲突后必须重新运行测试
- 询问协作:如不确定,可在 PR 中 @原作者讨论
- 避免自动合并:不要使用
git merge -X ours/theirs自动解决
处理大规模冲突
如果冲突涉及大量文件:
# 使用 mergetool
git mergetool
# 或使用 VS Code 集成冲突解决器
# 在 VS Code 中打开冲突文件,使用 "Resolve Conflict" 按钮
完整 PR 示例
以下是一个完整 PR 的全流程示例:
1. 准备工作
# 同步主干
git checkout main
git pull upstream main
# 创建分支
git checkout -b feat/powerflow-pv-limits
2. 实现功能
修改 crates/eneros-powerflow/src/bus.rs:
use serde::{Deserialize, Serialize};
/// 母线类型
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BusType {
/// 平衡节点
Slack,
/// PQ 节点(负荷)
PQ,
/// PV 节点(发电机)
PV,
}
/// 母线
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bus {
pub id: u64,
pub bus_type: BusType,
pub voltage_magnitude: f64,
pub voltage_angle: f64,
pub p_generation: f64,
pub q_generation: f64,
pub p_load: f64,
pub q_load: f64,
/// PV 节点无功下限(None 表示无限制)
pub q_min: Option<f64>,
/// PV 节点无功上限(None 表示无限制)
pub q_max: Option<f64>,
}
impl Bus {
pub fn new_pv(id: u64, voltage: f64, p_gen: f64, q_min: f64, q_max: f64) -> Self {
Self {
id,
bus_type: BusType::PV,
voltage_magnitude: voltage,
voltage_angle: 0.0,
p_generation: p_gen,
q_generation: 0.0,
p_load: 0.0,
q_load: 0.0,
q_min: Some(q_min),
q_max: Some(q_max),
}
}
/// 检查无功是否越限
pub fn is_q_violated(&self) -> bool {
if let (Some(q_min), Some(q_max)) = (self.q_min, self.q_max) {
self.q_generation < q_min || self.q_generation > q_max
} else {
false
}
}
/// 转换为 PQ 节点
pub fn convert_to_pq(&mut self) {
self.bus_type = BusType::PQ;
// 将无功出力限制在边界
if let (Some(q_min), Some(q_max)) = (self.q_min, self.q_max) {
self.q_generation = self.q_generation.clamp(q_min, q_max);
}
}
}
3. 修改求解器
修改 crates/eneros-powerflow/src/solver.rs(关键片段):
impl NewtonRaphsonSolver {
pub fn solve(&self, topology: &Topology) -> Result<PowerflowResult> {
let mut buses = topology.buses.clone();
let mut converted_buses = Vec::new();
for iteration in 0..self.max_iterations {
let result = self.iterate_once(&mut buses)?;
if result.converged {
return Ok(PowerflowResult {
converged: true,
iterations: iteration + 1,
buses,
converted_pv_buses: converted_buses,
});
}
// 检查 PV 节点无功越限
for bus in &mut buses {
if bus.bus_type == BusType::PV && bus.is_q_violated() {
let bus_id = bus.id;
bus.convert_to_pq();
converted_buses.push(bus_id);
tracing::info!(
bus_id,
q = bus.q_generation,
"PV 节点无功越限,转为 PQ 节点"
);
}
}
if !converted_buses.is_empty() {
// 重新求解
continue;
}
}
Err(PowerflowError::NonConvergence {
iterations: self.max_iterations,
max_iterations: self.max_iterations,
})
}
}
4. 补充测试
创建 crates/eneros-powerflow/tests/pv_limits_test.rs:
use eneros_powerflow::{NewtonRaphsonSolver, PowerflowResult, PowerflowError};
use eneros_topology::Topology;
#[test]
fn test_pv_bus_within_limits_no_conversion() {
let topo = Topology::ieee_14bus_with_pv_limits(-5.0, 5.0);
let solver = NewtonRaphsonSolver::new(50, 1e-8);
let result: PowerflowResult = solver.solve(&topo).unwrap();
assert!(result.converged);
assert!(result.converted_pv_buses.is_empty(), "无功未越限,不应转换节点");
}
#[test]
fn test_pv_bus_q_exceeds_max_gets_converted() {
let topo = Topology::ieee_14bus_with_pv_limits(-5.0, 1.0); // 上限很低
let solver = NewtonRaphsonSolver::new(50, 1e-8);
let result: PowerflowResult = solver.solve(&topo).unwrap();
assert!(result.converged);
assert!(!result.converted_pv_buses.is_empty(), "应有 PV 节点被转换");
}
#[test]
fn test_pv_bus_q_below_min_gets_converted() {
let topo = Topology::ieee_14bus_with_pv_limits(10.0, 20.0); // 下限很高
let solver = NewtonRaphsonSolver::new(50, 1e-8);
let result: PowerflowResult = solver.solve(&topo).unwrap();
assert!(result.converged);
assert!(!result.converted_pv_buses.is_empty());
}
#[test]
fn test_pv_limits_none_no_conversion() {
let topo = Topology::ieee_14bus(); // 默认无限制
let solver = NewtonRaphsonSolver::new(50, 1e-8);
let result: PowerflowResult = solver.solve(&topo).unwrap();
assert!(result.converged);
assert!(result.converted_pv_buses.is_empty());
}
#[test]
fn test_pv_limits_non_convergence_returns_error() {
let topo = Topology::ieee_14bus_with_pv_limits(0.0, 0.0); // 极端限制
let solver = NewtonRaphsonSolver::new(1, 1e-8); // 只允许 1 次迭代
let result = solver.solve(&topo);
assert!(matches!(result, Err(PowerflowError::NonConvergence { .. })));
}
5. 更新 CHANGELOG
## [Unreleased]
### Added
- `eneros-powerflow`: PV 母线无功上下限支持,越限时自动转为 PQ 节点 (#124)
6. 本地验证
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo nextest run -p eneros-powerflow
cargo nextest run --workspace
cargo doc --no-deps -p eneros-powerflow
7. 提交并推送
git add crates/eneros-powerflow/src/bus.rs \
crates/eneros-powerflow/src/solver.rs \
crates/eneros-powerflow/tests/pv_limits_test.rs \
CHANGELOG.md
git commit -m "feat(powerflow): 支持 PV 母线无功上下限
- 在 Bus 中新增 q_min/q_max 字段,默认 None 保持向后兼容
- NewtonRaphsonSolver 在无功越限时自动将 PV 节点转为 PQ 节点
- 补充 5 个测试用例覆盖越限场景
Closes #123"
git push origin feat/powerflow-pv-limits
8. 创建 PR
使用 GitHub CLI 或网页:
gh pr create \
--title "feat(powerflow): 支持 PV 母线无功上下限" \
--body-file .github/PULL_REQUEST_TEMPLATE.md
按模板填写后提交。