上个月我在终端里跑 Codex 改一个老项目它一口气动了 40 多个文件连续跑了 3 次测试中间还在 main 分支上自动做了 5 次提交。我在旁边看着它的操作日志越看越心虚这 5 次提交里每次到底改了什么工作区还有多少文件没进暂存区哪些改动是我 review 时亲手加的哪些又是它偷偷补的终端输出刷得飞快我唯一能做的就是在两个终端之间来回切换反复敲git log、git status、git diff敲到手指发酸。那会儿我就意识到Codex 把“写代码”这件事自动化得很好但也把仓库状态变成了一个黑盒。我需要一个能把 Git 内部结构直接铺到面前的工具而且这个工具最好能和我正在用的 Codex 工作流无缝配合。于是就有了这个项目一个专门面向 Codex 编码场景的 Git 面板核心功能就三个——分支树、提交历史、工作区操作。这篇文章我会把整个项目的设计决策、技术选型、实现细节和踩坑过程都写出来适合正在用 Codex 写代码又嫌git log --graph不够直观的人参考也适合想自己造一个 Git GUI 的同学当路线图。1. 项目从哪来Codex 把 Git 变成黑盒之后1.1 最不透明的不是代码生成而是仓库状态Codex 这类编码代理和普通 AI 补全有很大区别它会自己读文件、改文件、执行测试、运行命令甚至自己创建提交。听起来很爽但实际用起来有一个很尴尬的问题每次会话结束你根本不知道仓库处于什么状态。我遇到的具体场景是这样的。Codex 在一个 session 里做了这么一串操作改了 3 个业务模块的代码补了一个测试文件把 package.json 里的依赖升级了然后在 main 分支上提交了 2 次。表面上它汇报“已完成后退出”但我想知道的是第一次提交和第二次提交分别改动什么提交前它有没有做过 lint工作区现在是不是干净的如果我想把它生成的某个提交回滚会不会连累我自己改的文件这些问题用命令行当然都能回答但答案分散在很多条命令里。git log只有提交信息git diff只看当前改动git branch -a告诉你分支但没有可视化关系。在 Codex 这种高频自动提交的工作流里我几乎每隔几分钟就想看一眼仓库态势纯靠命令行的效率太低。面板存在的意义不是取代 Git 命令而是在“信息获取”这一步消除摩擦。1.2 我对比过的现成 Git GUI为什么都觉得差点意思做之前我当然先试了市面上现成的工具。GitKraken 和 SourceTree 的分支图谱做得很漂亮VS Code 自带的时间线也能看一部分提交历史。但用了几天我发现自己真正的需求它们都没完全覆盖。第一这些工具都是“通用 Git 客户端”它们不知道 Codex 的存在。我希望面板能展示哪些提交是 Codex 生成的、哪个分支是 Codex 正在工作的分支、当前会话修改了哪些文件。这个上下文在通用工具里没有。第二刷新机制。GitKraken 对磁盘文件的变更反馈有延迟而 Codex 的操作频率很高我需要面板里的分支树和工作区状态每秒都在实时变化。第三重量级工具功能太多。我一个做带界面的 Git 面板结果先被一堆 Pull Request、发布管理、代码行级暂存的功能淹没了光熟悉界面就要半天。综合下来自己做一个轻量、贴近 Codex 场景、完全可控的面板反而是成本最低的方案。真正动手后我发现一个能用的 Git 面板核心并不复杂——就是“读 Git 数据 画图/列表 调 Git 命令”难的只是细节处理。2. 技术选型为什么是 Tauri React TypeScript git CLI2.1 桌面壳层Tauri 还是 Electron面板的形态我考虑过三条路线纯终端 TUI用 Ink 或 Bubble Tea、浏览器网页、桌面应用。TUI 最贴合 Codex 的终端场景但分支树这种视觉化的东西在字符界面里画起来很别扭网页需要自己起服务Codex 每次会话要关关开开。最后我选了桌面应用壳层在 Tauri 和 Electron 之间二选一。我最终用了 Tauri 2核心考量是系统资源占用和后端能力。Electron 跑一个空壳窗口就要吃掉一百多兆内存而 Codex 本身已经要跑模型、跑 node 进程我不希望旁边再堆一个桌面应用。Tauri 的 WebView 复用系统渲染引擎内存占用低了一个量级更重要的是Tauri 的后端是 Rust 进程我可以在 Rust 侧安全地管理 git 子进程而不是像 Electron 那样直接把命令拼接交给 Node 的 child_process。这里多说一句Git 操作属于高风险动作尤其切换分支、丢弃修改这种命令如果参数处理不好很容易误伤整个仓库。Rust 的std::process::Command天然支持参数数组传参不会像 shell 拼接那样出现空格、引号导致的注入问题。这个安全感在开发过程里比什么都重要。2.2 读取 Git 数据直接调 CLI而不是引 git 库确定壳层之后接下来要决定的是 Git 数据怎么读。主流方案有三个调 git CLI 解析输出、用 isomorphic-git 这类纯 JS 实现、用 Rust 的 git2-rslibgit2 绑定。我先试了 git2-rs因为 Rust 侧直接操作很香。但它的劣势很快暴露libgit2 对某些仓库配置比如部分 worktree 操作、LFS 指针支持不完整而且它读到的配置和真实 git 命令行行为有细微差异。面板是做给用户自己的日常仓库用的兼容性必须排在第一位我不想碰上“面板显示一种状态、终端敲 git 又是另一种状态”的灵异现象。isomorphic-git 我直接排除了原因很简单它走的是自己的实现对 git 版本差异、钩子行为、远程协议的支持都有边界用来做可视化面板的数据源风险太大。最后我选择了最“笨”也最稳的方案在 Rust 侧通过std::process::Command调用系统 git解析它的结构化输出。用户机器上只要有 git 就一定能用行为与终端完全一致遇到未知情况我还能把 git 的报错原样抛给前端。代价是要认真设计命令格式和解析逻辑这一块我在后面的章节专门讲。2.3 分支树渲染手写 SVG 网格而不是引入流程图库前端部分用了 React TypeScript Vite渲染分支树时我一度考虑引入 Dagre 做自动布局。Dagre 在复杂 DAG 的层级布局上确实成熟但引入它有几个问题一是依赖的体积不小二是自动布局出来的图形是“通用图”而不是“Git 树”节点之间的连线往往是曲线的看久了反而晕。我的第一版选择是手写一个简单的 SVG 网格布局每次提交一行提交之间按父子关系竖线相连分叉点用空格占位。视觉原型很快就出来了配合 git 自带的--graph输出做对照能保证我画出的拓扑结构和命令行一致。后续如果遇到分支很多、穿插复杂的情况再迭代布局算法也不迟。3. 分支树从 git 数据到一张看得懂的图3.1 数据采集rev-list 拿拓扑for-each-ref 拿指针分支树的本质是把 commit 之间的父子关系画成有向无环图第一步是拿到完整的提交关系和 ref 指针位置。我这里用两条命令配合。# 拿到所有提交的哈希和父提交哈希用于绘制 DAG git rev-list --all --parents --date-order # 拿到分支、标签、HEAD 当前指向的提交 git for-each-ref --format%(refname:short)%00%(objectname)%00%(HEAD) refs/heads refs/remotes refs/tagsrev-list --parents的输出每行是一个提交的完整哈希后面跟所有父提交的哈希。合并提交会有两个父提交这就是分支树里“分叉汇合”的地方没有父提交的那行是初始提交。for-each-ref的输出每一行用 NUL 分隔三段引用名比如 main、origin/develop、v1.2.3、指向的对象哈希、以及该引用是否是当前 HEAD是的话输出一个*。这比解析git branch -a的美化表格要可靠得多因为--format输出是稳定的协议格式不会被终端宽度和高亮字符干扰。这两个命令的输出在 Rust 侧解析成两个数据结构一个commit_id - parents的映射一组ref_name - commit_id的指针。前端拿这两个结构就能绘制整棵树。3.2 合并提交和分叉的布局策略分支树布局最头疼的是合并提交。比如这样一个场景dev 分支从 main 分出feature 分支又从 dev 分出feature 合并回 devdev 再合并回 main。拓扑上是一个菱形网格布局里必须为这条合并线预留横向位置。我的做法是“按行从新到旧摆 commit先深度优先摆第一父链再回头处理第二父链”。具体来说从 HEAD和各分支最新提交出发沿着第一父链向下生成主干遇到合并提交时把非第一父链延后到后续空行插入。这个策略保证主干连续分叉线虽然会在早期版本里偶尔交叉但可读性已经足够。渲染这块我用 SVG 画节点圆点和连线每个 commit 是一个圆点位于左侧固定 x 坐标commit 的 message、作者、相对时间印在圆点右边分支和标签分别用不同颜色的胶囊标签挂在对应的 commit 行尾。本地分支用绿色远程分支用橙红色标签用灰色HEAD 用一个明显的描边圆点标记。3.3 提交详情与右键操作图不只是用来看的。点击某个 commit 节点右侧面板切换为该提交的完整信息作者、提交时间、完整提交信息、以及这个提交相对于父提交的 diff。右键节点会弹一个菜单我做的第一个版本支持四个动作查看 diff、复制哈希、在这个提交上创建分支、软回退到这里。软回退我特别加了一道二次确认因为git reset --soft commit会把目标提交之后的所有改动放进暂存区而你本地的未提交改动也都还在混合之后很容易把自己改了一半的文件也卷进去。面板上我会明确提示“该操作将保留工作区改动但会改变分支指针”确认后才发给后端执行。4. 提交历史让每一笔变更都能被快速定位4.1 git log 格式化输出与解析陷阱分支树负责“大局”提交历史列表负责“细节”。历史列表我采用增量加载先取最近 200 条滚动到底部再加载更多。获取列表时我自定义了 log 格式用 NUL 做字段分隔符这样能避免提交信息里包含空格甚至换行导致的解析错乱git log --all --date-order --formatCOMMIT%x00%H%x00%h%x00%an%x00%ae%x00%aI%x00%P%x00%B%x00ENDCOMMIT --dateiso-strict -n 200这个格式里%H是完整哈希%h是短哈希%an和%ae是作者名和邮箱%aI是 ISO 格式的绝对时间%P是父哈希列表%B是完整提交信息。真正的坑有两个。第一个坑是中文文件名。git 默认把非 ASCII 路径转义成八进制序列面板上显示一串\346\265\213\350\257\225根本没法读。解决方法是在调用 git 时显式加-c core.quotepathfalse。第二个坑是提交信息里的多行内容。%s只给单行标题但我列历史时希望显示完整 message 的前两行所以用%B拿全文再在解析层做截断和转义。如果直接用%sCodex 自动生成的提交往往带长 body列表里只能看到半句话信息缺一大块。4.2 diff 视图不要自己造语法高亮轮子点击某条提交后我需要展示它和父提交之间的差异。实现上Rust 侧执行git diff parent commit拿到 unified diff 文本再塞给前端的语法高亮组件。这里我的建议是diff 解析和渲染直接上成熟库前端用 diff 库 一个代码高亮组件不要自己写。Git 的 diff 格式看似简单但涉及文件头、块头、行号、上下文行、无换行符标记等一堆边界手搓很容易翻车。性能上要注意一点大文件 diff 可能上千行如果每次点击提交都实时执行git diff再从头高亮界面会明显卡顿。我做了两级优化第一级是给 commit 列表项做轻量缓存同一个提交的 diff 结果在内存里只算一次第二级是 diff 视图用虚拟滚动只渲染可见行。实测对一个有一万多次提交的大型仓库历史列表滚动流畅点开任意提交 diff 的反应时间在一百毫秒以内。4.3 筛选与按文件定位提交历史的另一个刚需是筛选。Codex 经常会说“我改了 auth 模块”这时候我想在面板里快速看 auth 相关的所有提交。第一版我实现了三个维度关键词搜索匹配作者、提交信息、日期范围、单文件过滤。关键词搜索我直接走后端Rust 侧把--grep参数和--author参数拼进 git log 命令而不是把所有提交拉进前端再做字符串匹配。数据量大的情况下在数据库层面做过滤永远比在界面层做过滤高效Git 本身对 log 的搜索已经做得很快了。单文件过滤用的是git log -- path。这里有个细节路径参数必须放在--后面避免文件名和参数混淆。前端在文件树里点选一个文件后端执行过滤后的查询分支树也会同步高亮包含该文件改动的提交。这个功能在追查某个 bug 是怎么引入的时候特别好用。5. 工作区操作把反人类的命令变成按钮5.1 状态探测git status --porcelain 的 XY 状态码工作区面板承担的不只是“看状态”还要能操作。而看清楚状态是一切操作的前提。我用的命令是git status --porcelainv1 --branch --untracked-filesnormal--porcelainv1是为了拿到机器可读的稳定输出。每一行前两个字符是 XY 状态位第一个字符表示暂存区状态第二个字符表示工作区状态后面是文件路径。我整理了一张对照表放在项目文档里状态码含义常见场景??未跟踪文件新建但没 add 的文件M工作区修改未暂存改过但没 addM暂存区有修改已 add 但没 commitMM暂存和工作区都有修改add 后又改了A新文件已暂存新文件已 addR文件重命名已暂存有改名D/D删除暂存/未暂存删了文件U合并冲突rebase/merge 冲突右上角我放了一个“刷新”按钮同时也支持 2 秒自动轮询。自动轮询这个设计考虑过监听文件系统事件在 Rust 侧用 notify 监听.git目录变化但后来发现.git目录的写入频率太高Codex 每次操作都写事件风暴会频繁触发界面刷新干脆用固定间隔轮询更省心。5.2 暂存、提交、切换分支的操作链路工作区操作面板就是一组和状态码直接绑定的按钮。文件在未暂存状态时显示“暂存”按钮在已暂存状态时显示“取消暂存”在未跟踪状态时显示“加入暂存”或“忽略此文件”。一键提交的面板做成了表单上面是 changelog 文本框下面列出所有暂存文件。提交前我会把git diff --cached的摘要显示在文本框下方提醒你这笔提交到底会带上哪些改动。这个提醒很有必要——Codex 自动git add -a的习惯非常可怕经常把意料之外的文件卷进提交我手动提交时不能再犯。切换分支的操作我在交互上做了约束如果工作区有未提交改动强制先弹一个“暂存并切换”还是“保留改动切换”的选项。git checkout在某些配置下会把未跟踪文件带过去面板必须提前预警。5.3 危险操作的兜底丢弃修改之前先留后路面板里最容易出事的是“丢弃修改”按钮。我把它做成两级防护第一级是红底白字的二次确认弹窗第二级才是真正的保护——Rust 侧在执行git restore path或者git checkout -- path之前会自动把这个文件的当前状态用git diff生成一个.patch文件存到面板的内部目录并且把路径显示在确认框里。真出现手滑的情况你还能去内部目录找到那个 patch用git apply救回来。同样git reset --hard我直接不在面板里提供。如果你想硬回退用终端自己敲。面板存在的意义是降低安全操作的成本而不是让危险操作变得更容易触发。6. 用 Codex 开发这个面板的过程复盘6.1 我是怎么给 Codex 下需求的说了半天技术选型和实现最贴题的问题来了既然我有一个 Codex那我自己写这个面板是不是只用动嘴就行我的经验是Codex 能替代大部分“实现”但替代不了“设计”和“验收”。我给 Codex 的需求不是一句话而是按阶段拆的规格说明。第一个任务是这样的目标在 Tauri 2 React 项目中实现 Git 仓库面板。 要求 1. 后端用 Rust 调用系统 git 命令禁止使用 libgit2。 2. 提供三个 APIlist_branches、list_commits、status。 3. 所有 git 命令通过 Command::new(git) 执行工作目录由参数传入。 4. 返回值全部用 JSON 序列化时间用 ISO 8601。 5. 先实现分支读取和提交历史读取不要做界面。第一阶段只要后端数据读取不做界面这样我能先验收数据格式第二阶段才让它搭 React 界面和调后端 API第三阶段处理交互细节。每个阶段独立提交出现问题能精准定位是哪一步的锅。6.2 它替我写的代码哪些被我一页页改了Codex 在通用胶水代码上确实效率极高比如 Tauri 的命令注册、React 组件骨架、JSON 序列化结构体定义这些我手写要一两个小时的量它十分钟给出一版。但有几类问题它反复犯我列一下给你打预防针。第一命令拼接注入。它早期版本直接用format!(git log {}, user_input)的方式来拼参数输入里带个空格就崩带个;就可能执行额外命令。我要求它改成Command::new(git).arg(log).arg(user_input)之后它才记住。第二git 报错处理。它喜欢假设命令一定成功output().success()不检查就直接String::from_utf8解包。真实场景里 git 会因为各种原因失败没有提交、不在仓库里、权限不足不处理 stderr 的结果就是前端小圆圈转圈转到天荒地老。第三对“数据一致性”没有感觉。它有一版为了省事前端请求分支列表时直接调git branch --format完全没有考虑这个命令的输出在 detached HEAD 状态下的怪异表现。我统计了一下最终合并进主分支的代码里我手动改过的比例大概在三成左右主要是错误处理、边缘条件和命令参数安全三类。这个比例我认为是健康的Codex 的价值在于把 70% 的体力活干完剩下 30% 的关键路径由人来把关。6.3 用它自己的逻辑开发给它的工具有哪些意外收获开发这个面板的过程里我有几次让 Codex 直接看我面板里某个模块的 bug让它在面板的代码库里自查。它在追踪“为什么这个提交 diff 在文件重命名时显示空白”这个问题时自己跑了几次git diff --find-renames实验然后定位到是我的 diff 命令少了-M参数。这个体验让我意识到一个 AI 编码代理在“有明确可执行的验证命令”的任务上非常可靠。另外我的面板也有一个 Codex 专属模式它把当前 Codex 日志目录里最近的会话计划读进来标记出“正在被 agent 修改的文件”在工作区面板里用高亮底色显示。这样我能一边看 Codex 干活一边在旁边看它的改动如何影响工作区状态。最后分享一个使用细节我在同一个仓库里通过 Codex 开发这个面板时Codex 会在 main 分支上不断自动提交我的面板分支树会被这些提交刷得“看起来项目一直在动”。后来我给它加了一条规则让它固定在dev分支上跑任务面板的主视图默认固定在main只有在需要审查时才切到dev。这个习惯让我始终保有一个干净的对照基线。实际用下来这个面板现在是我和 Codex 协作的标准配置——它在写代码我在旁边看仓库的脉搏谁也别想再偷偷改文件不让我知道。