从 PR 到架构动画:用开源工具把合并请求变成可视化架构图

发布时间:2026/8/31 16:50:24
从 PR 到架构动画:用开源工具把合并请求变成可视化架构图 从 PR 到架构动画我用开源工具把每次合并请求变成可视化架构图在日常开发中我们经常遇到一个问题代码仓库越来越大每次 Pull RequestPR改动的模块越来越多评审者很难在几分钟内快速理解这次变更波及了哪些服务、哪些组件、哪些数据流。即便有架构图静态图也往往跟不上代码演进的速度。前段时间我在调研可视化方案时看到不少开源项目尝试“把每次 PR 变成动画架构图”这个思路很新颖也很实用。本文就来完整拆解这套方案它解决什么问题、整体架构怎么设计、核心代码怎么写、如何接入 CI 实现自动化以及我在实际落地中遇到的高频坑点。需要先澄清一个容易混淆的点这里的 PR 是 Pull Request也就是代码合并请求不是 Adobe Premiere Pro 的缩写。如果你搜索“pr下载”“pr安装包”搜到的是视频剪辑软件那和本文主题无关。本文讨论的是 Git 协作工作流中的 Pull Request以及如何通过动画架构图让代码评审更直观。1. 背景与核心概念1.1 PRPull Request是什么Pull Request 是 Git 协作开发中非常重要的一环。开发者把本地分支推送到远程仓库后通过 PR 向维护者发出合并请求请求将当前分支的变更合并到目标分支通常是 main 或 develop。评审者可以在 PR 中查看代码差异、发表评论、要求修改通过后合并。一个标准的 PR 包含源分支与目标分支变更文件列表每次文件的增删改内容提交记录评审讨论记录。PR 最大的价值不是“合并代码”这个动作而是“变更发生后的审查过程”。但实际审查中一个改动可能涉及十几个文件、多个服务模块评审者需要先看目录结构、再逐个看文件、最后自己在脑内拼出影响面这个过程效率不高。1.2 架构图在代码评审中的作用架构图是表达系统结构的工具它能展示模块划分、服务依赖、数据流向、组件关系。常见的架构图形式包括分层架构图展示 Controller、Service、DAO 等层次微服务依赖图展示各服务之间的调用关系数据流图展示数据从入口到存储的流转路径领域模型图展示业务实体之间的关系。这些图在项目启动阶段通常画得比较完整但代码持续迭代后架构图容易和实际代码脱节。这是因为架构图往往是静态文档不会随着 PR 自动更新。1.3 动画架构图解决的痛点把 PR 变成动画架构图的思路是根据 PR 的变更文件集合生成一张架构图并用动画突出显示本次变更涉及的节点、边和调用关系。和静态架构图相比动画架构图有几个明显优势直观展示变更影响面哪些模块被改动哪些依赖被新增或删除一眼就能看出来动态演进过程通过帧序列展示每个文件的变更先后顺序能看出开发者的实现思路适合嵌入式评论生成 GIF 或短视频后可以直接贴在 PR 评论中团队沟通成本大幅降低自动化程度高只要 CI 触发全流程无需人工干预。当然这不是要完全替代人工评审而是给评审提供一个“第一眼概览”让评审者更快定位重点。2. 整体架构与核心设计2.1 整体工作流程这个开源方案的整体流程可以拆成下面几步监听 PR 事件获取 PR 的变更文件列表解析变更文件的代码结构提取包名、类名、函数名、依赖关系将源码依赖关系映射为架构图节点和边按文件提交顺序生成多帧静态架构图将多帧图片合成为动画 GIF将 GIF 上传到 PR 评论或作为 CI 产物展示。用一个 ASCII 简图来表示Git PR 事件 ↓ 获取变更文件列表git diff --name-only ↓ 解析代码依赖结构AST / 正则 / 静态分析 ↓ 生成架构图节点和边Graphviz / PlantUML ↓ 按提交顺序渲染多帧图片 ↓ 合并为动画 GIF ↓ 上传到 PR 评论 / CI Artifacts2.2 核心模块拆解整个工具可以拆成四个模块变更采集模块负责和 Git 仓库交互获取 PR 的变更文件、提交历史、目标分支信息依赖解析模块负责读取源码文件分析类、方法、模块之间的依赖生成节点和边的结构化数据架构图渲染模块把结构化数据渲染成图片并按照时间顺序输出多帧动画合成与发布模块把多帧图片合成为 GIF调用 GitHub API 或 GitLab API 发布到 PR 评论区。这四个模块建议解耦前一个模块的输出是后一个模块的输入中间用 JSON 或 YAML 过渡方便调试和替换实现。2.3 技术选型分析这部分需要根据目标仓库的语言来决定没有银弹。我这里给出几种常用组合用途可选方案说明Git 操作Git CLI、GitPythonCLI 最稳定GitPython 方便跨平台依赖解析Tree-sitter、AST、正则精确度要求高用 AST快速原型用正则架构图渲染Graphviz、PlantUML、D2Graphviz 灵活支持批量输出帧图片合成Pillow、imageio、ffmpegGIF 场景用 imageio 最省事CI 平台GitHub Actions、GitLab CI示例以 GitHub Actions 为主PR 评论 APIPyGithub、Octokit认证用 Token注意最小权限以 Python 为例后续代码示例都会围绕这个技术栈展开。如果你更熟悉 Java 或 TypeScript完全可以用同样的流程移植核心思路是一致的。3. 环境准备与版本说明3.1 开发环境本文示例使用的环境如下版本需要根据你的项目实际情况调整操作系统Ubuntu 22.04其他系统类似Python3.10 及以上Git2.39 及以上Graphviz确保dot命令可用GitHub CLI可选用于本地调试 PR 信息CI 平台GitHub Actions。在本地开发时你需要准备一个测试仓库至少包含两个分支和一个 PR。我建议先拿一个小型项目试手比如只有一个 Controller、两个 Service、一个 Repository 的 Java 或 Python 项目。3.2 项目结构我建议把工具做成一个独立项目目录结构如下pr-arch-animator/ ├── pyproject.toml ├── README.md ├── src/ │ └── pr_arch_animator/ │ ├── __init__.py │ ├── cli.py │ ├── config.py │ ├── collector.py │ ├── parser.py │ ├── renderer.py │ ├── animator.py │ └── publisher.py ├── scripts/ │ └── run.sh └── tests/ └── test_collector.py这个结构把功能拆成模块方便后续扩展。3.3 依赖安装创建虚拟环境并安装依赖python3 -m venv .venv source .venv/bin/activate pip install --upgrade pip pip install gitpython pygithub graphviz imageio pillow这里解释几个核心依赖的用途gitpython在 Python 中操作 Git 仓库获取 diff 和提交信息pygithub调用 GitHub REST API获取 PR 详情、发布评论graphvizPython 调用 Graphviz 的接口生成有向图imageio读取多张 PNG 并写入 GIFpillow做图片尺寸归一化避免帧大小不一致导致动画抖动。4. 核心代码实现4.1 获取 PR 变更集第一步是拿到 PR 改了哪些文件。这里有两种方式如果工具运行在 CI 环境中可以通过环境变量拿到 PR 信息如果本地调试可以用 GitPython 直接对比分支。参考代码如下需要放在src/pr_arch_animator/collector.pyfrom pathlib import Path from typing import List from git import Repo class ChangeCollector: 采集 PR 的变更文件列表和提交顺序。 def __init__(self, repo_path: str .): self.repo_path repo_path self.repo Repo(repo_path) def get_changed_files( self, base_branch: str main, head_branch: str feature/demo, ) - List[str]: 对比两个分支返回变更文件路径列表。 # 用法说明确保本地已经 fetch 远程分支 # 返回的结果中M 表示修改A 表示新增D 表示删除 diff_index self.repo.git.diff( base_branch, head_branch, --name-status, ) changed_files [] for line in diff_index.splitlines(): parts line.split(\t) if len(parts) 2: continue # 只保留源码文件跳过测试和文档 file_path parts[1] if self._is_source_file(file_path): changed_files.append(file_path) return changed_files staticmethod def _is_source_file(file_path: str) - bool: 过滤需要关注的源码文件。 if file_path.startswith(test/): return False if file_path.startswith(docs/): return False if file_path.endswith(.md): return False if file_path.endswith(.py): return True if file_path.endswith(.java): return True return False def get_commit_sequence(self, base_branch: str, head_branch: str) - List[str]: 按时间顺序获取两个分支之间的提交列表。 commits self.repo.git.log( f{base_branch}..{head_branch}, --reverse, --format%h %s, ) return commits.splitlines()这段代码实现了两个功能get_changed_files使用git diff --name-status获取变更文件只保留源码文件get_commit_sequence使用git log --reverse获取从基础分支到特性分支的提交顺序。需要注意git diff输出格式在不同版本之间有细微差异建议先跑命令确认输出格式再决定字符串解析方式。4.2 解析代码依赖关系拿到文件列表后需要从源码中提取节点和依赖边。对 Python 项目可以使用ast模块解析import语句对 Java 项目可以解析package、import和类名。这里给出一个简化版 Python 解析器放在src/pr_arch_animator/parser.pyimport ast from pathlib import Path from typing import Dict, List, Set class DependencyParser: 从源码文件中提取模块依赖关系。 def __init__(self, repo_path: str): self.repo_path Path(repo_path) def parse_file(self, file_path: str) - Dict[str, Set[str]]: 解析单个 Python 文件返回节点和依赖集合。 abs_path self.repo_path / file_path if not abs_path.exists(): return {} source abs_path.read_text(encodingutf-8) tree ast.parse(source) current_module file_path.replace(/, .).rsplit(., 1)[0] dependencies: Set[str] set() for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: dependencies.add(alias.name) elif isinstance(node, ast.ImportFrom): if node.module: dependencies.add(node.module) return {current_module: dependencies} def parse_files(self, files: List[str]) - Dict[str, Set[str]]: 批量解析文件合并为全局依赖图。 graph: Dict[str, Set[str]] {} for file_path in files: result self.parse_file(file_path) for module, deps in result.items(): graph.setdefault(module, set()).update(deps) return graph这个解析器只做了最基本的 import 提取没有处理相对导入 vs 绝对导入条件导入动态导入类级别的依赖类 A 调用了类 B但没有直接 import B。真实项目中我建议使用 Tree-sitter 或 Semgrep 这类更强大的静态分析工具这里是为了让读者先理解流程。对 Java 项目思路类似需要解析 package 和 import 语句。4.3 生成架构图静态帧有了依赖图下一步用 Graphviz 渲染图片。为了生成动画我们需要按提交顺序一帧一帧地渲染第 1 帧只展示第一次提交涉及的模块第 2 帧叠加第二次提交的模块直到所有提交都展示完毕。渲染器代码放在src/pr_arch_animator/renderer.pyfrom graphviz import Digraph class ArchitectureRenderer: 将依赖图渲染为多帧 PNG 图片。 def __init__(self, output_dir: str frames): self.output_dir output_dir import os os.makedirs(output_dir, exist_okTrue) def render_frame( self, graph_data: dict, frame_index: int, highlight_nodes: set, ) - str: 渲染一帧图片highlight_nodes 标记新增节点。 dot Digraph(commentPR Architecture) dot.attr(rankdirLR) dot.attr(node, shapebox) for module in graph_data: style filled if module in highlight_nodes else solid color #ff9900 if module in highlight_nodes else #cccccc dot.node(module, module, stylestyle, fillcolorcolor) for module, deps in graph_data.items(): for dep in deps: if dep in graph_data: dot.edge(module, dep) output_path f{self.output_dir}/frame_{frame_index:03d} dot.render(output_path, formatpng, cleanupTrue) return f{output_path}.png这里有一个关键点每一帧都是完整图但高亮色标记了“新增的变更节点”。这样做的好处是架构图始终显示整体结构但动画过程能区分“已有模块”和“本次变更涉及模块”。4.4 合成动画 GIF渲染出多帧 PNG 后用 imageio 合成为 GIF。这里需要注意所有帧的尺寸必须一致帧率不宜太高否则评审者看不清建议每秒 1 到 2 帧每个提交停留至少半秒。动画合成代码放在src/pr_arch_animator/animator.pyimport glob import imageio.v2 as imageio class AnimationBuilder: 将多帧 PNG 合成为 GIF。 def __init__(self, frame_dir: str frames): self.frame_dir frame_dir def build_gif(self, output_path: str pr_architecture.gif, duration: float 0.8): 合成 GIFduration 控制每帧停留秒数。 frame_files sorted(glob.glob(f{self.frame_dir}/frame_*.png)) if not frame_files: raise RuntimeError(没有找到任何帧图片请先执行渲染步骤) frames [] for frame_file in frame_files: frames.append(imageio.imread(frame_file)) imageio.mimsave(output_path, frames, durationduration) print(fGIF 已生成{output_path})如果生成的 GIF 太大可以适当减少帧数或者用 Pillow 压缩每个 PNG 的尺寸。还有一个提示GitHub 的 PR 评论对图片大小有限制超过 10MB 的文件可能无法正常预览建议控制分辨率。4.5 在 PR 评论中发布 GIF动画生成后我们需要把它发布到 PR 评论里。使用 PyGithub 的参考代码如下放在src/pr_arch_animator/publisher.pyfrom github import Github from github.Repository import Repository class PRPublisher: 把 GIF 上传到 PR 评论区。 def __init__(self, token: str, repo_name: str): self.client Github(token) self.repo: Repository self.client.get_repo(repo_name) def publish_gif(self, pr_number: int, gif_path: str, message: str ): 上传 GIF 并发表评论。 pr self.repo.get_pull(pr_number) with open(gif_path, rb) as f: # GitHub 的评论接口支持直接引用图片 URL # 简单起见这里先打印提示。 # 实际项目中可以先把 GIF 传到 GitHub Release 或对象存储 # 然后在评论中使用 Markdown 图片语法。 print(f上传 GIF 到 PR #{pr_number}) default_message 这是本次 PR 的架构变更动画请结合动画查看影响范围。 comment_message message or default_message pr.create_issue_comment(f{comment_message}\n\n![]({gif_url}))要注意的是GitHub API 不能在评论中直接上传二进制文件作为图片。通常的做法是把 GIF 上传到 GitHub Releases或者使用第三方图床/对象存储或者使用 GitHub Actions 的 Artifacts在注释中给出下载链接。由于各团队的安全策略不同这部分往往需要结合实际部署环境调整。上面代码中的gif_url是示意需要替换成你自己的托管地址。5. 接入 GitHub Actions 实现自动化5.1 配置工作流为了让每次 PR 自动生成架构动画我们需要配置 GitHub Actions。核心思路是用pull_request事件触发然后执行上面的 Python 脚本。Workflow 配置文件位置.github/workflows/pr-arch-animator.ymlname: PR Architecture Animator on: pull_request: types: [opened, synchronize] jobs: generate-animation: runs-on: ubuntu-latest permissions: contents: read pull-requests: write steps: - name: 检出代码 uses: actions/checkoutv4 with: fetch-depth: 0 - name: 设置 Python uses: actions/setup-pythonv5 with: python-version: 3.10 - name: 安装 Graphviz run: | sudo apt-get update sudo apt-get install -y graphviz - name: 安装项目依赖 run: | pip install gitpython pygithub graphviz imageio pillow - name: 生成 PR 架构动画 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} BASE_BRANCH: ${{ github.event.pull_request.base.ref }} HEAD_BRANCH: ${{ github.event.pull_request.head.ref }} run: | python scripts/run.py \ --repo-path . \ --base $BASE_BRANCH \ --head $HEAD_BRANCH \ --pr $PR_NUMBER - name: 上传产物 uses: actions/upload-artifactv4 with: name: pr-architecture-gif path: pr_architecture.gif这个 Workflow 有几个关键点fetch-depth: 0必须完整拉取分支历史否则git diff拿不到目标分支的对比permissions只申请了contents: read和pull-requests: write避免暴露过大的 Token 权限GITHUB_TOKENGitHub 自动生成的临时 Token不需要手动配置但权限范围需要在 permissions 中声明事件类型opened, synchronize表示 PR 创建或代码更新时触发。5.2 主脚本设计上面的 Workflow 调用了scripts/run.py这个脚本是用来串联整个流程的入口。示例代码如下#!/usr/bin/env python3 PR 架构动画生成入口脚本。 import argparse import os from pr_arch_animator.collector import ChangeCollector from pr_arch_animator.parser import DependencyParser from pr_arch_animator.renderer import ArchitectureRenderer from pr_arch_animator.animator import AnimationBuilder from pr_arch_animator.publisher import PRPublisher def main(): parser argparse.ArgumentParser(description生成 PR 架构动画) parser.add_argument(--repo-path, default.) parser.add_argument(--base, defaultmain) parser.add_argument(--head, defaultfeature/demo) parser.add_argument(--pr, typeint, requiredTrue) args parser.parse_args() collector ChangeCollector(args.repo_path) changed_files collector.get_changed_files(args.base, args.head) print(f变更文件列表{changed_files}) parser_engine DependencyParser(args.repo_path) graph_data parser_engine.parse_files(changed_files) print(f依赖图节点数{len(graph_data)}) renderer ArchitectureRenderer(frames) # 这里做简化处理把全部变更文件作为高亮节点 renderer.render_frame(graph_data, 1, set(graph_data.keys())) builder AnimationBuilder(frames) builder.build_gif(pr_architecture.gif, duration1.0) token os.environ.get(GITHUB_TOKEN, ) repo_name os.environ.get(GITHUB_REPOSITORY, ) if token and repo_name: publisher PRPublisher(token, repo_name) publisher.publish_gif(args.pr, pr_architecture.gif) if __name__ __main__: main()注意上面的脚本做了大量简化。真实的版本需要在每次提交后生成一帧而不是只生成一帧。你可以把get_commit_sequence结合起来每个提交处理一批文件渲染一帧逐渐叠加节点。5.3 权限与安全注意事项使用 GitHub Token 操作 PR 评论时务必注意不要使用个人 Token 写死在代码里优先使用 GitHub Actions 自带的GITHUB_TOKEN并限制权限如果必须使用其他平台的密钥请保存到 GitHub Secrets 中脚本中不要打印 Token 或任何敏感环境变量对于 fork 仓库的 PR默认情况下GITHUB_TOKEN是只读的需要额外配置信任范围建议谨慎处理。安全边界是所有自动化工具的第一优先级尤其是能写评论、改状态的自动化任务。6. 常见问题与排查思路在实际开发和部署过程中我整理了下面几个高频问题。问题现象常见原因解决思路git diff结果为空本地分支没有更新或 fetch 深度不足检查是否执行了git fetch --depth0或完整拉取Graphviz 报dot: command not found环境缺少 Graphviz 本体安装graphviz系统包不只是 Python 包生成的 GIF 帧大小不一致PNG 尺寸不同用 Pillow 统一 resize 或设置固定画布大小PR 评论没有收到图片Token 权限不足或图片 URL 无法访问检查pull-requests: write权限确认图床地址可公网访问Python 解析 import 不准ast 只解析静态 import改用 Tree-sitter 或 combination 静态分析工具动画帧数过多导致 GIF 体积过大提交数量多且分辨率高降低帧率、压缩尺寸、或按模块聚合提交CI 中 Python 版本过低Workflow 未设置版本在setup-python中明确指定版本本地调试和 CI 结果不一致分支差异、绝对路径差异在 CI 中打印关键环境变量启动日志输出6.1 排查清单如果你遇到“脚本在本地正常CI 里失败”的问题按以下顺序排查先检查 Git 拉取深度有没有fetch-depth: 0再检查工作目录run-path是否在仓库根目录接着确认依赖安装Graphviz 系统包有没有安装然后看环境变量PR_NUMBER 是否传入最后看权限Token 是否能写 PR 评论。7. 最佳实践与工程建议7.1 变更集粒度控制不是每个 PR 都值得生成动画架构图。如果一个 PR 只改了一个 README 文件动画只有一帧意义不大。我建议在工具中增加一个过滤条件只有满足以下条件之一才触发动画生成变更文件数量大于 3变更文件包含核心源码目录如src/、app/变更涉及至少两个不同的业务模块。这样可以减少无意义的 CI 任务也避免污染 PR 评论区。7.2 节点聚合策略微服务架构中一个服务可能包含几十个类。如果每个类都作为图上的一个节点架构图会非常拥挤。建议按服务名或包名做聚合只显示一级或二级依赖层级。例如把com.example.order.controller.OrderController聚合为order把com.example.payment.service.PaymentService聚合为payment只展示order → payment这条边。这样动画图才具备“架构”层面的概括能力而不是变成类关系图。7.3 缓存与增量计算大型仓库中每次 PR 都全量解析所有文件是不明智的。你可以利用 CI 缓存把解析过的基础分支依赖图缓存下来PR 运行时只解析变更文件然后合并到基础图上的增量这样既能画出“全量架构图”又能突出“本次变更点”效率也高。GitHub Actions 中可以使用actions/cache来缓存中间 JSON 数据。7.4 可观测性与日志自动化工具最怕“跑了但不知道跑没跑”。建议设计时输出结构化日志[collector] 获取到 12 个变更文件 [parser] 解析完成生成 8 个节点5 条依赖边 [renderer] 第 1 帧渲染完成frame_001.png [renderer] 第 2 帧渲染完成frame_002.png [animator] GIF 生成成功pr_architecture.gif (2.3MB) [publisher] 已发布评论到 PR #123同时把生成 GIF 的下载链接附在 PR 评论中方便需要详细查看的评审者点击查看原图。7.5 安全边界与权限最小化所有涉及 API 调用的代码都遵循最小权限原则只让 Token 拥有操作 PR 评论的权限不要开放repo全部权限如果工具要下载其他平台的数据使用短期凭证不要在日志中输出 Token、密钥、用户隐私信息对来自 fork 仓库的 PR优先验证“变更文件是否在允许列表内”再运行解析脚本。这一点尤其重要因为恶意 PR 可能构造一个特殊的代码文件来触发解析器漏洞。解析外部输入时要做好异常捕获和超时控制。8. 总结与扩展方向整个“把 PR 变成动画架构图”的方案核心价值不在动画本身而在于让代码评审者可以用更短的时间理解变更影响范围。实现上并不复杂获取变更文件、解析依赖、渲染帧、合成 GIF、发布评论五步闭环。把它接入 GitHub Actions 之后每次 PR 都会自动生成架构动画团队成员在评审时可以直接看到“哪个服务被改了、依赖了谁、新增了哪些调用”这对维护大型仓库尤其有帮助。如果要把这个方案做到生产级下一步可以往这些方向深入使用更精准的静态分析工具支持多语言解析在架构图动画中加入新增、删除、修改的区分例如用不同颜色表示改动类型支持 GitLab CI 和自建 GitLab扩大适用范围把架构图数据沉淀下来支持跨 PR 的架构演进对比结合 CHANGELOG 或 commit message自动生成架构变更说明文本。我建议你从一个小型仓库开始先把流程跑通再加入过滤条件、聚合策略和增量解析逐步完善。动手写一遍比看十篇文章更有效。如果这篇文章对你有帮助可以收藏备用后续遇到 PR 评审不直观的问题时翻出来按照步骤搭建一套试试。