1. 为什么我要给 Codex 单独做一个 Git 面板
用 Codex 写代码这件事,最反直觉的一点是:它改代码很快,但你管理这些改动很慢。我平时的工作流是让 Codex 在终端里跑任务,它一口气改七八个文件,然后我切到另一个终端窗口敲git status、git diff、git add、git commit,来回切窗口切到怀疑人生。更麻烦的是,Codex 有时候会改到一半停下来,工作区里一半是它的改动、一半是我自己没提交的调试代码,这时候如果直接git add .就是灾难。
所以我花了几个晚上,给 Codex 做了一个配套的 Git 面板:左边是分支树,中间是提交历史,右边是工作区操作。核心目标就三个——看得见分支结构、理得清提交历史、管得住工作区改动。它不是一个通用的 Git 客户端,而是专门为"和 Codex 协作"这个场景设计的,重点解决的是"AI 改完代码之后,我怎么安全地把这些改动收进来"。
这篇文章我会把整个面板的设计思路、每个模块的实现细节、踩过的坑全部摊开讲。适合两类人看:一类是天天用 Codex 或类似 AI 编程工具、被 Git 操作拖慢节奏的开发者;另一类是想自己动手做一个垂直场景工具、但不知道从哪下手的同学。哪怕你 Git 只会add和commit,看完也能明白这套东西为什么这么设计,以及怎么照着搭一个自己的版本。
先说清楚一个前提:这个面板是本地工具,跑在你自己的机器上,通过调用本地git命令和读取.git目录来工作,不涉及任何远程服务。这一点很重要,因为它决定了整个架构可以做得非常轻。
2. 整体设计与技术选型拆解
2.1 为什么不做成 IDE 插件
最开始我确实想过做成 VS Code 插件,毕竟 Codex 很多时候就在编辑器里用。但试了两天就放弃了,原因有三个。
第一,IDE 插件受宿主限制太强。VS Code 的插件 API 对文件树、面板布局的控制是有限制的,我想做一个"分支树 + 提交历史 + 工作区"三栏联动的布局,用 Webview 硬做出来体验很割裂,滚动、拖拽、右键菜单都要自己模拟一遍。第二,Codex 的使用场景不局限于某一个编辑器,有人用 CLI,有人用桌面版,有人挂在别的工具里,做成独立应用反而通用。第三,独立应用可以常驻,我可以把它放在第二个显示器上,Codex 在终端跑,我在这边实时看工作区变化,这个体验是插件给不了的。
所以最终选型是:独立桌面应用,前端用 Web 技术栈渲染界面,后端通过本地进程调用git命令。这样界面开发效率高,Git 操作又完全复用系统里已经装好的git,不用自己实现一套 Git 协议。
2.2 核心架构:命令层、解析层、状态层
整个应用我拆成了三层,这个分层是后面所有功能能稳定跑起来的基础。
命令层负责执行git命令。这里有个关键决策:不用任何 Git 库,直接调系统git。像nodegit、libgit2这类库看起来很美好,但版本兼容问题非常多,尤其是遇到git lfs、git commit --amend、复杂 merge 这些场景,库的行为和命令行经常对不上。直接调命令行,行为 100% 和你在终端里敲的一样,出问题也好排查。
解析层负责把git的文本输出变成结构化数据。这是整个项目最脏最累的部分,因为 Git 的输出格式是给人看的,不是给程序看的。比如git log --graph的输出,那些*、|、\、/字符画出来的分支线,要还原成树结构需要一套专门的解析逻辑。后面我会详细讲这块。
状态层负责维护界面状态和 Git 真实状态的一致性。Codex 在后台改文件,面板要能感知到,这里用的是文件系统监听 + 定时轮询兜底的双保险。
2.3 分支树为什么不用现成库
市面上有现成的 Git 图形库,但我最后自己写了分支树的渲染。原因是现成库大多基于git log --graph的字符画直接转 SVG,遇到复杂分支(比如多个分支交叉 merge)时线条会错乱。我自己实现的方式是:先用git log --all --pretty=format:...拿到所有提交的父子关系,在内存里构建一张有向图,再用拓扑排序算出每个提交的"泳道"位置,最后才画线。这样无论分支多复杂,线条都是数学计算出来的,不会错。
这个决策的代价是要自己处理很多边界情况,比如同一个提交被多个分支引用、分支合并后又分叉、游离提交(detached HEAD)等等。但收益是分支树的可控性极强,后面加"点击分支高亮整条链路""折叠某个分支"这些功能都很容易。
3. 分支树模块的实现细节
3.1 数据从哪来:一次命令拿全所有提交
分支树的数据源是这条命令:
git log --all --date-order --pretty=format:"%H|%P|%an|%ae|%at|%s|%D"逐个字段解释一下,这些字段的选择直接决定了后面能做什么:
%H是完整提交哈希,作为唯一 ID。%P是父提交哈希,多个父提交用空格分隔,这是构建树结构的关键。%an、%ae是作者名和邮箱,用于显示。%at是作者时间戳,用于排序。%s是提交信息第一行。%D是引用信息,也就是哪些分支、标签指向这个提交。
--all保证拿到所有分支的提交,--date-order保证按时间顺序输出,避免拓扑顺序导致的时间线错乱。用|分隔是因为提交信息里几乎不会出现这个字符,比用空格安全得多。
注意:如果你的仓库里有提交信息包含
|的情况(虽然罕见),解析会出错。更稳妥的做法是用%x00这种不可见字符做分隔符,我在第二版里换成了\x00,解析稳定性明显提升。
3.2 泳道算法:把提交排成好看的树
拿到提交列表后,核心问题是怎么给每个提交分配一个横向位置(泳道),让分支线不交叉、不重叠。我用的是一个简化版的泳道分配算法,思路如下。
维护一个"活跃泳道"数组,每个元素记录当前这个泳道正在等待哪个父提交。遍历提交列表(已经按时间倒序),对每个提交:
- 先看它的哈希是否在活跃泳道里。如果在,说明这个提交是某个泳道的延续,直接占用那个泳道。
- 如果不在(说明是个新分支的头),分配一个新的空泳道。
- 处理它的父提交:第一个父提交继承当前泳道,其余的父提交(merge 产生的)各分配新泳道。
- 清理已经没有任何提交等待的泳道,回收位置。
这个算法跑下来,主分支永远在最左边一条直线上,feature 分支从主分支分出去后往右排,merge 回来时线条自然收拢。实测在几百个提交、十几个分支的仓库里,渲染出来的树非常清晰。
3.3 渲染性能:虚拟滚动是必须的
一开始我天真地直接把所有提交渲染成 DOM 节点,结果在一个有 8000 多个提交的仓库里,界面直接卡死。后来改成虚拟滚动:只渲染视口内可见的 30 到 50 个提交节点,滚动时动态替换内容。
具体做法是给容器一个总高度(提交数 × 每行高度),监听滚动事件算出当前应该显示哪一段,只渲染那一段的 DOM。分支线的绘制用 SVG,同样只画可见区域。改完之后,8000 个提交的仓库滚动起来也是丝滑的。
这里有个细节:分支线的 SVG 路径要跨行连接,如果只画可见区域,上下边缘的线会断掉。我的处理是多渲染视口上下各 5 行的缓冲,让线条在视口外自然延伸,视觉上就连续了。
3.4 分支操作:右键菜单里能干什么
分支树上每个分支标签都支持右键,菜单项包括:切换分支、新建分支、重命名、删除、合并到当前分支、查看该分支独有的提交。这些操作背后对应的命令我列一下,方便你对照:
| 操作 | 对应命令 |
|---|---|
| 切换分支 | git checkout <branch> |
| 新建并切换 | git checkout -b <branch> |
| 重命名 | git branch -m <old> <new> |
| 删除 | git branch -d <branch> |
| 强制删除 | git branch -D <branch> |
| 合并 | git merge <branch> |
实操心得:删除分支前一定要检查它是否已经合并。
git branch -d会拒绝删除未合并的分支,这是保护机制,别图省事直接用-D。我在面板里做了个判断,如果分支有未合并提交,删除按钮会变成黄色警告,需要二次确认。
4. 提交历史模块的关键实现
4.1 提交列表要显示什么
提交历史列表每一行我放了这些信息:提交哈希前 7 位、提交信息、作者、相对时间(比如"3 小时前")、以及这个提交关联的分支标签。点击某一行,右侧展开这个提交的完整 diff。
相对时间的计算有个小坑:git log给的是绝对时间戳,要转成"3 小时前"这种格式,得自己算。我写了个函数,按秒、分、时、天、月、年逐级判断,超过 30 天就直接显示日期。这个逻辑不复杂,但要注意时区问题,统一用本地时区转换。
4.2 diff 渲染:语法高亮和折叠
diff 的渲染是提交历史模块里最费功夫的部分。git show <hash>输出的 diff 是纯文本,要变成好看的高亮视图,需要解析每一行的前缀:+是新增、-是删除、 是上下文、@@是 hunk 头。
我做的处理是:按文件拆分 diff,每个文件一个可折叠的区块,默认只展开有改动的文件。文件内按 hunk 分组,每个 hunk 显示行号范围。新增行绿色背景,删除行红色背景,行内的字符级差异用更深的颜色标出来。
字符级差异这块我用了一个简单的算法:对删除行和新增行做逐字符对比,找出公共前缀和公共后缀,中间不同的部分高亮。虽然不是最优的 diff 算法,但对代码这种结构化文本效果已经足够好。
4.3 提交历史的筛选和搜索
提交多了之后,找某个提交很痛苦。我加了三个筛选维度:按作者筛选、按时间范围筛选、按提交信息关键词搜索。这些都可以直接翻译成git log的参数:
git log --author="xxx" --since="2024-01-01" --until="2024-06-01" --grep="fix bug"搜索这块有个体验优化:输入时做防抖,用户停止输入 300 毫秒后才真正执行查询,避免每敲一个字符就查一次导致卡顿。查询结果用缓存存起来,同样的条件第二次查直接读缓存。
4.4 提交详情里的那些操作
在提交详情面板里,我放了几个高频操作:复制完整哈希、回退到这个提交(git reset)、撤销这个提交(git revert)、从这个提交创建分支、cherry-pick 到当前分支。
这里必须强调一个安全设计:所有会改变历史的操作都要二次确认,并且明确告诉用户后果。比如git reset --hard会丢弃工作区改动,我在确认弹窗里会列出具体会丢失哪些文件。git revert相对安全,因为它生成一个新提交而不是改写历史,所以确认级别低一些。
踩过的坑:早期版本我把
git reset --hard做成了单击执行,结果自己手滑点了一次,丢了一下午的改动。从那以后所有破坏性操作都加了确认,而且默认焦点在"取消"按钮上,防止回车误触。
5. 工作区操作模块的实战设计
5.1 工作区状态怎么实时感知
工作区模块要解决的核心问题是:Codex 在后台改文件,面板怎么第一时间知道。我用的是文件系统监听 + 定时轮询的组合方案。
文件系统监听用的是操作系统的原生能力,文件一有变动就触发事件。但监听有个问题:Codex 可能一次性改很多文件,会触发大量事件,如果每个事件都去跑一次git status,性能会很差。所以我加了事件合并:收到事件后不立即执行,而是等 200 毫秒,把这段时间内的事件合并成一次git status查询。
轮询作为兜底,每 5 秒跑一次git status,防止某些文件系统监听漏掉的情况。实测下来,Codex 改完文件后,面板基本在 1 秒内就能刷新出新的改动。
5.2 工作区改动的分类展示
git status --porcelain的输出是机器友好的,我用它来获取工作区状态。输出格式是每行两个状态字符加文件路径,比如M表示已暂存的修改,M表示未暂存的修改,??表示未跟踪文件。
我把改动分成三组展示:已暂存的改动、未暂存的改动、未跟踪的文件。这个分组和git status的默认输出一致,用户一看就懂。每个文件前面有个复选框,勾选后可以批量暂存或取消暂存。
这里有个细节:重命名和删除的文件要特殊处理。git status --porcelain对重命名会输出两行(旧路径和新路径),解析时要合并成一个条目。删除的文件在 diff 里显示为整个文件被删,要给出明确的视觉提示。
5.3 暂存和提交的完整流程
工作区操作的核心流程是:选择文件 → 暂存 → 写提交信息 → 提交。每一步我都做了对应的界面。
暂存单个文件用git add <file>,暂存全部用git add -A,取消暂存用git restore --staged <file>。这里要注意git add -A和git add .的区别:前者会处理删除的文件,后者在某些 Git 版本里不会。我统一用-A,行为更可预期。
提交信息输入框我做了几个贴心设计:支持多行输入(第一行是标题,空行后是正文)、实时显示字符数、提供常用前缀的快捷按钮(feat、fix、docs、refactor 等)。提交命令是git commit -m "标题" -m "正文",用两个-m参数分别传标题和正文。
实操心得:如果你经常需要修改上一条提交信息,
git commit --amend很好用,但它会改写提交历史。如果这条提交已经推送到远程,amend 后需要强制推送,团队协作时慎用。我在面板里对 amend 加了"仅本地提交可用"的限制,检测到已推送的提交就禁用这个功能。
5.4 和 Codex 协作的特殊处理
这是整个面板最有价值的部分。Codex 改代码时,工作区里往往混着两类改动:它改的和你自己改的。如果直接全选提交,就把自己的调试代码也提交进去了。
我的解决方案是改动来源标记。面板会记录 Codex 开始工作前的工作区快照,Codex 工作结束后对比快照,把新增的改动标记为"可能来自 Codex"。这个标记不是 100% 准确,但能帮你快速区分。标记为 Codex 的改动默认勾选,你自己的改动默认不勾选,提交时一目了然。
另一个功能是改动预览对比。选中一个文件,面板会同时显示"当前工作区版本"和"最后一次提交版本"的并排对比,让你在暂存前就看清 Codex 到底改了什么。这个功能救过我很多次,有一次 Codex 把一个配置文件里的密钥字段改成了占位符,我在预览里一眼看到,避免了提交事故。
6. 常见问题与排查技巧实录
6.1 面板显示的分支和终端里不一致
这是最常见的问题,原因通常是面板的缓存没刷新。Git 的分支信息存在.git/refs和.git/packed-refs里,外部操作(比如你在终端里git checkout)改了这些文件,面板如果只靠自己的操作触发刷新,就会滞后。
解决办法是监听.git/HEAD和.git/refs目录的变化。.git/HEAD文件内容变化意味着分支切换了,.git/refs目录变化意味着有分支创建或删除。监听这两个位置,基本能覆盖所有分支变动场景。
6.2 提交历史里出现重复提交
如果你看到同一个提交在历史里出现两次,大概率是解析逻辑把 merge 提交的父提交处理错了。merge 提交有两个父提交,第一个是当前分支的父提交,第二个是被合并分支的父提交。泳道算法里如果没区分这两个父提交,就会把同一个提交分配到两条泳道。
排查方法是打印出问题提交的%P字段,看父提交数量。如果父提交数量大于 1,就是 merge 提交,需要特殊处理。
6.3 大仓库加载慢
仓库提交超过一万条时,git log --all会明显变慢。优化手段有几个:限制初始加载的提交数量(比如只加载最近 500 条),滚动到底部时再加载更多;用--no-merges过滤掉 merge 提交(如果用户不需要看);开启 Git 的 commit-graph 功能,能大幅加速历史查询。
git config core.commitGraph true git commit-graph write --reachable这个 commit-graph 是 Git 官方提供的加速机制,对大型仓库的历史查询提升非常明显,建议所有大仓库都开一下。
6.4 中文文件名显示乱码
Git 默认会对非 ASCII 文件名做转义,输出成\344\270\255\346\226\207这种八进制形式。解决办法是设置:
git config core.quotepath false设置后 Git 直接输出原始 UTF-8 文件名,面板就能正常显示了。这个配置建议全局设置,一劳永逸。
6.5 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 分支列表不更新 | 未监听 refs 变化 | 监听.git/HEAD和.git/refs |
| 提交重复显示 | merge 父提交处理错误 | 区分第一父提交和第二父提交 |
| 大仓库卡顿 | 一次性加载全部提交 | 分页加载 + commit-graph |
| 中文乱码 | quotepath 转义 | git config core.quotepath false |
| 工作区不刷新 | 文件监听失效 | 增加定时轮询兜底 |
| diff 显示错位 | 行尾符不一致 | 统一 CRLF/LF 处理 |
7. 一些实操层面的经验补充
做这个面板的过程中,我最大的体会是:工具的价值不在于功能多,而在于它是否贴合你的真实工作流。市面上成熟的 Git 客户端很多,功能比我这套全得多,但它们都不是为"和 AI 协作"这个场景设计的。Codex 改代码的速度远超人手,工作区状态变化极快,传统的"改完再统一处理"的节奏已经跟不上了,你需要一个能实时反映变化、能区分改动来源、能快速收拢改动的工具。
另一个体会是关于破坏性操作的处理。Git 的强大在于它能改写历史,但这也意味着误操作代价很高。我在面板里对所有会丢改动的操作都做了三重保护:二次确认、明确列出影响范围、默认焦点在取消按钮。这套设计后来帮我避免了好几次事故,强烈建议你自己做工具时也这么处理。
最后分享一个我常用的组合操作:Codex 跑完一个任务后,我会先在面板里看工作区改动,用来源标记快速筛出 Codex 的改动,预览几个关键文件的 diff,确认没问题后批量暂存,写一条规范的提交信息,提交。整个过程不到一分钟,比在终端里来回敲命令快得多。如果你也在用 Codex 或类似的工具,真的建议花点时间搭一个属于自己的面板,哪怕功能简单,只要能省下每天切窗口的时间,就值了。