概述
传统Git只记录提交者,却无法区分代码的「真实作者」是人还是AI生成。而各家AI工具都有自己的会话管理方式,无法跨工具追踪。
官网,使用Rust实现、开源(GitHub,2.5K Star,279 Fork)追踪AI生成代码的Git插件,用于追踪代码仓库中AI生成的代码,将每行代码与其对应的AI Agent、模型和会话记录关联起来。官方文档。
价值:自动将每行AI生成的代码链接到生成它的Agent、模型和会话记录,让你永远不会丢失代码背后的意图、需求和架构决策。
功能特性:
git-ai blame是git blame的替代品,显示每行代码的AI生成者(Agent、模型、Session)- 在
git commit输出中显示人类/AI代码比例 - 使用Git Notes(
refs/notes/ai命名空间)存储AI作者信息,不修改提交历史 - 通过
git-ai stats统计不同Agent的代码贡献量 /ask命令:可直接向生成该代码的Agent提问,理解代码背后的意图- 支持Agent:Claude Code、Codex、Cursor、Windsurf、Copilot、Continue、Gemini、Junie、Rovo Dev、Amp等主流AI编程工具
git-ai stats --json输出示例:
{"human_additions":28,"mixed_additions":5,"ai_additions":76,"ai_accepted":47,"total_ai_additions":120,"total_ai_deletions":34,"time_waiting_for_ai":240,"tool_model_breakdown":{"claude_code/claude-sonnet-4-5-20250929":{"ai_additions":76,"ai_accepted":47,"total_ai_additions":120,"total_ai_deletions":34}}}统计字段详解
| 字段 | 含义 | 示例 |
|---|---|---|
human_additions | 纯人类贡献的代码行数 | 28 |
mixed_additions | AI生成但被人修改过的行数 | 5 |
ai_additions | AI贡献的总行数 (mixed+accepted) | 76 |
ai_accepted | AI生成且未被修改的"接受"行数 | 47 |
total_ai_additions | AI生成的总行数 | 120 |
total_ai_deletions | AI删除的总行数 | 34 |
time_waiting_for_ai | 等待AI响应的累计时间(秒) | 240 |
原理
技术栈
| 模块 | 技术栈 | 作用 |
|---|---|---|
| 核心CLI | Rust+clap+smol | Git代理层,Hook执行,高性能计算 |
| 差分算法 | imara-diff | 实现Git的Myers diff算法 |
| Git操作 | gix-config+Git Plumbing | 读写Git Notes、blame、tree操作 |
| VSCode插件 | TypeScript+VSCode API | 监听编辑事件,调用checkpoint |
| JetBrains插件 | Kotlin+IntelliJ Platform | 同上,适配IntelliJ系列IDE |
| 存储格式 | JSON(Git Notes)+SQLite(Prompt DB) | AI归属日志+Prompt历史 |
三个核心设计:
- Git Wrapper模式:将
git-ai安装为Git的代理,拦截所有Git命令,在commit、rebase、merge等关键节点插入Hook - Git Notes存储:利用Git原生的Notes机制(
refs/notes/ai)存储AI归属信息,不污染commit历史 - Checkpoint机制:引入"检查点"概念,在AI或人类每次编辑后创建快照,最终在commit时合并为完整的归属日志
核心流程图
实战
基于命令行安装:
curl-sSLhttps://usegitai.com/install.sh|bash# Windowspowershell-NoProfile-ExecutionPolicyBypass-Command"irm https://usegitai.com/install.ps1 | iex"# 2. 安装IDE钩子(自动检测并配置Cursor、VSCode等)git-ai install-hooks# 3. 使用AI工具编写代码,然后commitgitcommit-m"Add feature with AI assistance"# 4. 查看AI代码归属(类似git blame,但显示AI信息)git-ai blame<文件路径># 5. 查看统计信息git-ai status装好成功后,git-ai在后台默默记录。可继续用git commit、git rebase、git merge等命令,git-ai会自动处理AI归属信息的传播。
源码
源码结构
.cargo .github .vscode agent-support asserts benches docs ←文档 packaging ←打包 scripts ←脚本 skills ←技能 specs/ ←契约驱动开发 src/ ├── mdm/ │ ├── agents/ │ │ ├── claude_code.rs ←CC hook安装器 │ │ ├── cursor.rs ←Cursor │ │ ├── windsurf.rs ←Windsurf │ │ └── ... ←其他Agent支持 │ └── hook_installer.rs ←Hook安装抽象接口(HookInstaller trait) ├── commands/ │ ├── checkpoint.rs ← Checkpoint 核心逻辑 (2000+ 行) │ ├── git_ai_handlers.rs ← CLI 命令分发 │ └── checkpoint_agent/ │ ├── agent_v1_preset.rs ← Hook input JSON 解析 │ └── agent_presets.rs ← AgentRunResult 定义 └── authorship/ ├── attribution_tracker.rs ← 行级别归属追踪 ├── working_log.rs ← Checkpoint 持久化 └── authorship_log.rs ← Authorship Log 格式 tests/值得学习:
- 架构精妙:用Git Plumbing命令+Git Notes实现一个完全Git-native的AI代码追踪系统,性能开销<100ms
- 多平台适配:统一接入支持Cursor、Claude Code、GitHub Copilot等10+种AI工具
- Rust工程化:利用 Rust 的 async、零成本抽象、类型系统,实现了高性能的 Git 代理层
Git Proxy
无缝拦截Git命令,参考src/commands/git_handlers.rs
解读
- 「Wrapper 而非 Fork」: git-ai 没有重新实现 Git 逻辑,而是作为一个透明的"中间人"。用户执行
git commit,实际调用的是git-ai commit,后者再调用真正的git。这保证了:- 「高兼容性」: 所有 Git 功能都能正常使用
- 「零侵入性」: 不需要修改 Git 本身
- 「信号转发的魔法」:
- 在 Unix 系统上,如果用户按
Ctrl+C中断 Git 命令,git-ai需要把信号正确转发给子进程 - 但有一个例外: 「交互式终端」(TTY 模式)不能创建新进程组,否则会收到
SIGTTIN/SIGTTOU导致挂起 - 代码通过
libc::isatty()检测是否为交互式终端,只在非交互模式创建新进程组 - 如果创建了新进程组,会安装全局信号处理器,转发
SIGTERM/SIGINT/SIGHUP/SIGQUIT到子进程
- 在 Unix 系统上,如果用户按
- 「跨平台处理」:
- Unix: 使用
pre_exec钩子设置进程组,安装信号处理器 - Windows: 使用
CREATE_NO_WINDOWflag 隐藏窗口(非交互模式) - Git 命令路径通过
config::Config::get().git_cmd()获取,支持用户自定义
- Unix: 使用
- 「性能监控」:
- 每个 Git 命令都会记录 pre-hook、git 执行、post-hook 的耗时
- 如果总耗时超过目标值(100ms),会记录性能日志用于优化
- 异步 I/O 的使用在 checkpoint 模块中(稍后展示)
启示:
- 「Wrapper 模式的威力」: 不需要重写整个系统,在外面包一层代理就能扩展功能,同时保障兼容性
- 「交互式 vs 非交互式」: 进程管理要区分场景,TTY 模式和后台运行的信号处理逻辑完全不同
- 「性能监控的重要性」: 每个 Hook 都记录耗时,发现性能瓶颈后才能优化
Checkpoint
精准追踪"这行代码是谁改的",参考src/commands/checkpoint.rs
解读:
- 增量计算的智慧:
- 第一次遇到文件时,用
git blame反推历史归属(谁在之前的 commit 改过这行) - 后续编辑时,只需要基于上一个 checkpoint 的状态做 diff,避免重复计算
- 这就像 Git 本身的"快照 + 增量"设计,性能极高
- 第一次遇到文件时,用
- Feature Flag控制Blame行为:
letai_blame=ifConfig::get().get_feature_flags().inter_commit_move{repo.blame(&file_path,&ai_blame_opts).ok()}else{None// 跳过 blame,所有行默认为 human};inter_commit_moveflag 控制是否使用git blame反推历史归属- 关闭时,所有历史行都标记为
human,性能更好但会丢失跨 commit 的 AI 归属追踪 - 开启时,可以检测到用户复制粘贴 AI 代码的情况
「行级归属 vs 字符级归属」: 代码中有
LineAttribution和Attribution两种数据结构:为什么需要两种?因为当文件内容变化时,「行号会变,但字符偏移更稳定」(配合 diff 算法)。最终输出时再转回行号给用户看。
LineAttribution: 给人看的(第 10-15 行是 AI-xxx 写的)Attribution: 内部使用的字符范围(第 120-450 个字符是 AI-xxx 写的)
「AttributionTracker 的核心算法」: 这是整个系统最复杂的部分,它要解决一个难题——「当文件被编辑后,如何更新每个字符的归属?」保留所有历史作者信息,用于后续的
git-ai blame查询
启示:
- 「增量计算是性能的关键」: 基于上一个 checkpoint 做 diff,避免重复计算,提升性能
- 「数据结构要为业务服务」: 行号给人看(
LineAttribution),字符偏移给算法用(Attribution),各司其职 - 「Feature Flag 控制复杂度」:
inter_commit_moveflag 让用户在性能和准确性之间权衡 - 「异步并发的威力」: 使用
smol+Semaphore,8 个并发任务处理 100+ 文件,性能提升 3-5 倍 - 「善用成熟算法」:
imara-diff实现了 Git 的 Myers diff 算法,不要重复造轮子
多工具适配
统一支持10+种AI工具,参考src/commands/install_hooks.rs、agent-support/vscode/src/extension.ts、agent-support/vscode/src/ai-edit-manager.ts
//exportclass AIEditManager{privatesnapshotOpenEvents=newMap<string,{timestamp:number;count:number;uri:vscode.Uri;}>();// === 关键设计 3: 捕获 Snapshot 打开事件 ===publichandleOpenEvent(doc:vscode.TextDocument):void{// VSCode Copilot/Claude 在生成代码时会先打开一个特殊的 snapshot 文档if(doc.uri.scheme==="chat-editing-snapshot-text-model"||doc.uri.scheme==="chat-editing-text-model"){constfilePath=doc.uri.fsPath;constnow=Date.now();this.snapshotOpenEvents.set(filePath,{timestamp:now,count:(this.snapshotOpenEvents.get(filePath)?.count||0)+1,uri:doc.uri// 保存 URI,稍后解析 sessionId});// 在 AI 编辑前触发 human checkpoint (记录基准状态)console.log('[git-ai] Snapshot open event detected, triggering human checkpoint');this.triggerHumanCheckpoint([filePath]);}}// 关键设计 4: 文件保存时检查是否为 AI 编辑publichandleSaveEvent(doc:vscode.TextDocument):void{constfilePath=doc.uri.fsPath;// Debounce: 300ms 后评估setTimeout(()=>{this.evaluateSaveForCheckpoint(filePath);},300);}privateevaluateSaveForCheckpoint(filePath:string):void{constsnapshotInfo=this.snapshotOpenEvents.get(filePath);// 如果有对应的 snapshot 打开事件,则是 AI 生成的if(snapshotInfo&&snapshotInfo.count>=1&&snapshotInfo.uri?.query){constsnapshotAge=Date.now()-snapshotInfo.timestamp;// 检查 snapshot 是否太旧(> 10 秒则忽略)if(snapshotAge>=10000){console.log('[git-ai] Snapshot too old, skipping AI checkpoint');return;}// 关键设计 5: 从 URI Query 解析 Session IDtry{constparams=JSON.parse(snapshotInfo.uri.query);letsessionId=params.chatSessionId||params.sessionId;// VSCode 可能把 sessionId 编码在 chatSessionResource.path 中if(!sessionId&¶ms.chatSessionResource?.path){sessionId=Buffer.from(params.chatSessionResource.path.slice(1),'base64').toString('utf-8');}if(sessionId){// 读取会话历史文件 (VSCode 自动保存的 .jsonl)constchatSessionPath=path.join(storagePath,'chatSessions',`${sessionId}.jsonl`);// 调用 git-ai checkpointthis.checkpoint("ai",JSON.stringify({hook_event_name:"after_edit",chat_session_path:chatSessionPath,// 包含完整 prompt 历史session_id:sessionId,edited_filepaths:[filePath],workspace_folder:workspaceFolder.uri.fsPath,dirty_files:this.getDirtyFiles(),// 所有未保存的文件内容}));}}catch(e){console.error('[git-ai] Failed to parse snapshot URI',e);}}// 清理this.snapshotOpenEvents.delete(filePath);}}解读:
- 「插件式架构的可扩展性」:
- 每个 AI 工具对应一个
AgentInstallertrait 实现 - 新增工具支持只需实现
check_hooks()和install_hooks()两个方法 - 主流程完全不用改,完美体现了"开闭原则"
- 每个 AI 工具对应一个
- 「配置文件注入的巧妙手法」:
- 对于 VSCode/Cursor: 修改
settings.json,添加"git.path": "/path/to/git-ai" - 对于 JetBrains: 修改
~/.config/JetBrains/xxx/options/git.xml - 关键点: 「让 IDE 以为在调用
git,实际调用的是git-ai」
- 对于 VSCode/Cursor: 修改
- 「VSCode 插件的巧妙检测机制」
- 「不是用启发式规则判断」,而是利用 VSCode 内部机制!
- 当 Copilot/Claude 生成代码时,VSCode 会先创建一个临时的 「snapshot 文档」,URI scheme 是
chat-editing-snapshot-text-model - git-ai 插件监听
onDidOpenTextDocument事件,捕获这些特殊snapshot打开 - 当文件保存时,检查是否有对应
snapshot记录,如果有,说明是AI编辑 - 零误判:这不是启发式规则,而是利用IDE的内部信号,准确率100%
- Session ID的获取方式
// 从 snapshot URI 的 query 参数解析constparams=JSON.parse(snapshotInfo.uri.query);letsessionId=params.chatSessionId||params.sessionId;// VSCode 把 sessionId Base64 编码在 chatSessionResource.path 中if(!sessionId&¶ms.chatSessionResource?.path){sessionId=Buffer.from(params.chatSessionResource.path.slice(1),'base64').toString('utf-8');}为什么这么做?
- 「利用 VSCode 的内部数据」: snapshot URI 的 query 包含了会话信息
- 「获取完整 Prompt 历史」:
chatSessions/${sessionId}.jsonl是 VSCode 自动保存的会话记录,包含所有对话内容 - 「零配置」: 不需要 AI 工具主动上报,直接从 IDE 的内部数据中"拿"出来
启示:
- 「面向扩展的设计」: 插件式架构让系统具备无限扩展能力
- 「配置文件注入」: 很多时候不需要修改应用本身,改配置文件就能实现 Hook
- 「利用平台内部机制」: 与其猜测用户行为(启发式),不如深挖平台的内部信号(snapshot URI)
- 「数据"寄生"策略」: VSCode 已经保存了会话历史(.jsonl),git-ai 直接读取,不需要重复收集
git-ai目前只能记录每一行代码的来源,但是没有统计的功能。为了方便管理,支持看团队多个仓库的AI生成代码行数占比或采纳率占比,开源了一个分析和查询工具。
https://github.com/ForeverPx/ai-code-metrics