用 git merge-base 精准运行 PR 变更文件的 Ruff 与 Pytest

举报
yd_217513249 发表于 2026/11/10 08:43:18 2026/11/10
【摘要】 利用 git merge-base 找到 PR 与目标分支的合并基点,再用 git diff --name-only 过滤删除项,只对新增、修改和重命名的 Python 文件运行 Ruff,并按约定映射对应 pytest 用例;无变更时直接跳过,附 CI Shell 脚本与验证步骤。

用 git merge-base 精准运行 PR 变更文件的 Ruff 与 Pytest

问题场景

在 Pull Request 的 CI 中,如果每次都执行 ruff check . 和全量 pytest,代码量增长后反馈会变慢;如果只用 HEAD 与目标分支做两点 diff,又可能把目标分支后续提交带来的无关差异混入检查范围。更稳妥的方式是先找到当前分支与目标分支的合并基点,再用三点 diff 只取本 PR 的变更文件。

本文给出一个可放入 CI 的 Shell 脚本:只对变更的 Python 文件运行 Ruff,只为这些文件映射对应测试并运行 pytest;通过 --diff-filter=ACMR 过滤已删除文件;没有 Python 变更时直接跳过。

核心命令

先取合并基点:

git fetch --no-tags origin main
BASE=$(git merge-base origin/main HEAD)

再取本 PR 的 Python 变更文件。--diff-filter=ACMR 表示只保留新增、复制、修改和重命名,删除项不会进入后续命令:

git diff --name-only --diff-filter=ACMR "$BASE"...HEAD -- '*.py'

脚本中使用 -z 与 mapfile -d '',避免文件名含空格时被拆分。重命名文件会按新路径参与 Ruff 与测试映射。

CI Shell 脚本

将以下内容保存为 ci/pr-checks.sh,按项目实际目录调整 TEST_ROOT 与 src/、app/ 前缀处理:

#!/usr/bin/env bash
set -euo pipefail

TARGET_BRANCH="${TARGET_BRANCH:-main}"
TEST_ROOT="${TEST_ROOT:-tests}"
REQUIRE_TESTS="${REQUIRE_TESTS:-0}"

git fetch --no-tags origin "$TARGET_BRANCH"
BASE=$(git merge-base "origin/$TARGET_BRANCH" HEAD)

mapfile -d '' -t changed_py < <(
  git diff --name-only -z --diff-filter=ACMR "$BASE"...HEAD -- '*.py'
)

if ((${#changed_py[@]} == 0)); then
  echo 'No changed Python files; skip ruff and pytest.'
  exit 0
fi

echo 'Changed Python files:'
for f in "${changed_py[@]}"; do echo "  $f"; done

ruff check "${changed_py[@]}"

declare -A seen=()
tests=()

add_test() {
  local f="$1"
  [[ -f "$f" ]] || return 0
  [[ -n "${seen[$f]:-}" ]] && return 0
  seen["$f"]=1
  tests+=("$f")
}

for f in "${changed_py[@]}"; do
  rel="${f#./}"
  dir=$(dirname "$rel")
  stem=$(basename "$rel" .py)
  [[ "$dir" == '.' ]] && dir=''

  if [[ "$rel" == "$TEST_ROOT"/* && ( "$stem" == test_* || "$stem" == *_test ) ]]; then
    add_test "$rel"
    continue
  fi

  case "$dir" in
    src/*) dir="${dir#src/}" ;;
    app/*) dir="${dir#app/}" ;;
  esac

  add_test "$TEST_ROOT/${dir:+$dir/}test_${stem}.py"
  add_test "$TEST_ROOT/test_${stem}.py"
  add_test "$TEST_ROOT/${dir:+$dir/}${stem}_test.py"
done

if ((${#tests[@]} == 0)); then
  echo 'No matching test files.'
  [[ "$REQUIRE_TESTS" == '1' ]] && exit 1
  exit 0
fi

echo 'Selected tests:'
for t in "${tests[@]}"; do echo "  $t"; done

pytest -q "${tests[@]}"

测试路径映射

默认按常见约定映射,例如:

  • src/pkg/a.py 会尝试 tests/pkg/test_a.py、tests/test_a.py、tests/pkg/a_test.py。
  • tests/pkg/test_b.py 本身是变更文件时直接加入测试列表。
  • 已删除文件由 --diff-filter=ACMR 排除,不会触发 Ruff 或 pytest。
  • 可通过 REQUIRE_TESTS=1 让“源码有变更但没有映射到测试”的情况失败,适合强制补测试的仓库。

如果项目测试目录不是 tests,设置 TEST_ROOT;如果源码前缀不是 src/ 或 app/,按实际结构调整 case 分支。

运行方法

在 CI 或本地执行:

chmod +x ci/pr-checks.sh
TARGET_BRANCH=main bash ci/pr-checks.sh

CI 检出代码时需要保证目标分支可获取。浅克隆可能导致 git merge-base 找不到共同祖先,应使用足够的检出深度,或显式 fetch 目标分支。

验证步骤

  1. 从目标分支创建演示分支,修改一个源文件并删除一个旧 Python 文件。
  2. 提交后运行脚本,确认 Ruff 只检查变更文件,删除文件未出现,测试列表包含映射到的测试。
  3. 再创建一个不包含 Python 变更的提交,运行脚本,确认输出跳过信息并返回 0。
  4. 将文件重命名后运行,确认新路径参与检查,旧路径不参与。
  5. 在 CI 中先跑本脚本,夜间流水线再跑全量 pytest,兼顾快速反馈与完整覆盖。

边界与限制

  • git merge-base 依赖完整历史;浅克隆或目标分支缺失时需调整 fetch 策略。
  • 该方案只覆盖约定路径能映射到的测试,不等同于全量回归;跨模块集成测试仍应定期全量运行。
  • Ruff 配置需位于仓库可发现的位置;生成代码或第三方目录如不应检查,应在 Ruff 配置中排除。
  • 测试映射是启发式的,复杂项目应维护显式映射表或改用测试影响分析工具。
  • 文件名含空格已通过 NUL 分隔处理;含换行符的极端路径仍建议在 CI 中单独验证。
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。