跳到主内容

Pull Request 流程

贡献指南

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检查内容失败处理
CLAssistantCLA 签署状态点击签署链接
GitHub Actionsfmt / 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 代码?如有,是否充分论证?

性能审查

  • 热路径是否引入不必要的内存分配?
  • 是否在异步上下文中调用阻塞函数?
  • 是否在循环中创建新的连接或锁?
  • 数据结构选择是否合适(Vec vs HashMap vs BTreeMap)?
// 错误:循环中分配
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 检查必须全部通过才能合并:

检查项工作流内容失败处理
Rustfmtci.ymlcargo fmt --all -- --check运行 cargo fmt --all
Clippyci.ymlcargo clippy --all-targets -- -D warnings修复警告
Testci.ymlcargo nextest run --workspace修复测试
Doc Testci.ymlcargo test --doc --workspace修复文档示例
Cargo Denyci.ymlcargo deny check检查许可与重复依赖
CLACLAssistant已签署 CLA点击签署链接
Coveragecoverage.yml覆盖率不下降补充测试

可选检查

检查项工作流内容处理
Benchmarkbenchmark.yml性能回归警告但不阻塞
Conformanceconformance.yml协议一致性必须通过(涉及协议时)
Security Scansecurity.ymlSAST + 依赖审计必须通过(涉及安全时)

CI 失败处理

CI 失败时,请按以下步骤处理:

  1. 点击失败的检查项查看日志
  2. 在本地复现失败:
# 复现 CI 环境
cargo +stable fmt --all -- --check
cargo +stable clippy --all-targets -- -D warnings
cargo +stable nextest run --workspace
  1. 修复后推送:
git add .
git commit --fixup <commit-sha>
git rebase -i --autosquash upstream/main
git push origin feat/my-feature --force-with-lease
  1. CI 自动重新运行

处理 flaky 测试

如测试在 CI 上间歇性失败:

  • 检查是否依赖外部环境(网络、时间)
  • 使用 --retries 3 在本地复现
  • 在 PR 中说明,由 Maintainer 判断是否阻塞

合并策略

合并方式选择

PR 类型提交数策略命令
单一提交的小改动1Squash mergeGitHub 自动 Squash
多次提交的复杂改动>1Rebase mergeGitHub 自动 Rebase
发布分支回滚-Revert commitgit revert <sha>
紧急修复1Squash 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

冲突解决原则

  1. 保留双方意图:理解两边的修改目的,合并而非覆盖
  2. 测试验证:解决冲突后必须重新运行测试
  3. 询问协作:如不确定,可在 PR 中 @原作者讨论
  4. 避免自动合并:不要使用 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

按模板填写后提交。

相关文档