告别「不知道写什么 commit」:用 Ollama 本地部署的大模型自动生成规范的 Git 提交信息
一个前后端分离的小 Demo,调用 Ollama 部署的本地开源大模型,读取你的
git diff,自动生成一条符合 Conventional Commits 规范的提交信息。
一、这个项目解决什么痛点
很多开发者(尤其是刚开始规范提交的)在 git commit -m 时都会卡住:
- 这次到底改了什么,该用
feat还是fix? - 提交信息太随意,项目的
git log看起来一塌糊涂; - 周报、绩效考核时,leader 要翻提交记录,规范的信息能直接体现工作量。
于是就有了这个「Git 提交 AI 神器」:
把你本次改动的
git diff交给本地大模型,让它帮你总结出规范的 commit message。
二、技术栈总览
| 层 | 技术 |
|---|---|
| 前端 | React + Vite + Tailwind CSS + axios |
| 后端 | Node.js + Express |
| AI 编排 | LangChain(@langchain/ollama) |
| 本地大模型 | Ollama 部署 deepseek-r1:8b |
整体是一个前后端分离的全栈项目:
- 后端 跑在
http://localhost:3000,提供 API; - 前端 跑在
http://localhost:5173,负责交互; - AI 由 Ollama 在
http://localhost:11434提供 OpenAI 风格接口。
三、项目结构
app/git-differ/
├── readme.md # 需求笔记与学习记录
├── server/ # 后端
│ ├── index.js # Express 服务 + 核心逻辑
│ └── package.json
└── frontend/ # 前端
├── index.html
├── vite.config.js
└── src/
├── api/index.js # axios 请求封装
├── hooks/useGitDiff.js # 业务 hook
├── App.jsx # 页面组件
├── App.css
├── index.css # 引入 tailwindcss
└── main.jsx
四、后端实现详解
后端是核心,负责「读 diff → 拼 prompt → 调大模型 → 返回结果」。
1. 初始化模型与服务
import express from 'express';
import cors from 'cors';
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
import { ChatOllama } from '@langchain/ollama';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';
const execAsync = promisify(exec);
const model = new ChatOllama({
baseUrl: "http://localhost:11434",
model: "deepseek-r1:8b",
temperature: 0.1, // 低温度,输出更严谨稳定
});
const app = express();
app.use(express.json()); // 解析请求体 JSON
app.use(cors()); // 允许跨域
这里 temperature: 0.1 是为了让生成结果尽量稳定、不跑偏——生成 commit message 我们不需要创造性,需要的是准确。
2. 读取 git diff
关键点:大模型不会自己去读仓库,diff 是后端用 git 命令读出来的。
async function getGitDiff(cwd) {
try {
const { stdout } = await execAsync(`git -C "${cwd}" diff HEAD`, {
maxBuffer: 10 * 1024 * 1024,
});
return stdout;
} catch (e) {
// 仓库还没有提交历史时,diff HEAD 会报错,回退到暂存区 + 工作区
const { stdout: staged } = await execAsync(`git -C "${cwd}" diff --cached`);
const { stdout: unstaged } = await execAsync(`git -C "${cwd}" diff`);
return `${staged}\n${unstaged}`;
}
}
git -C <cwd> diff HEAD:取上次 commit 以来所有已跟踪文件的改动(已暂存 + 未暂存都包含)。- 如果仓库还没提交过,
diff HEAD会报错,就退回用diff --cached+diff拼起来。
3. 生成 commit message 的接口
app.post('/commit-message', async (req, res) => {
const cwd = req.body?.cwd || process.env.GIT_REPO_PATH || process.cwd();
try {
const diff = await getGitDiff(cwd);
if (!diff.trim()) {
return res.json({
reply: "没有检测到代码改动,请先修改文件或 git add 后再试。",
diff: "",
});
}
const prompt = ChatPromptTemplate.fromMessages([
['system', [
'你是一名专业的 Git 提交信息生成助手。',
'请根据用户提供的 git diff 内容,生成一条规范的 Git 提交信息(Conventional Commits 规范)。',
'要求:',
'1. 格式:<type>(<scope>): <subject>',
'2. type 只能从以下选择:feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert',
'3. subject 简洁明了,用中文概括本次改动的核心内容,不超过 50 个字符',
'4. 只输出提交信息本身,不要任何解释、注释、标点或代码块',
].join('\n')],
['human', 'git diff 内容如下:\n{diff}'],
]);
const chain = prompt
.pipe(model)
.pipe(new StringOutputParser());
const result = await chain.invoke({ diff });
res.json({ reply: result, diff });
} catch (e) {
res.status(500).json({ error: "读取 git diff 或调用大模型失败,请确认目标目录是 git 仓库。" });
}
});
这段代码体现了 LangChain 的经典「链式调用」:
PromptTemplate → ChatOllama → StringOutputParser
{diff} 会被替换成前面读到的 diff 原文,作为「材料」喂给大模型。
五、前端实现详解
前端负责收集用户输入、请求后端、展示结果。
1. axios 请求封装
import axios from 'axios';
const service = axios.create({
baseURL: 'http://localhost:3000',
headers: { 'Content-Type': 'application/json' },
timeout: 120000,
});
export const commitMessage = (cwd) =>
service.post('/commit-message', { cwd });
2. 业务 hook
export const useGitDiff = () => {
const [content, setContent] = useState('');
const [diff, setDiff] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const generate = async (cwd) => {
setLoading(true);
setError('');
try {
const { data } = await commitMessage(cwd);
setContent(data.reply);
setDiff(data.diff || '');
} catch (e) {
setError(e?.response?.data?.error || '请求失败,请确认后端已启动');
} finally {
setLoading(false);
}
};
return { loading, content, diff, error, generate };
};
把「请求 + 状态管理」从组件里剥离出来,组件只负责 UI,这是 React 自定义 hook 的核心价值。
3. 页面组件
export default function App() {
const { loading, content, diff, error, generate } = useGitDiff();
const [cwd, setCwd] = useState('');
return (
<div className="min-h-screen bg-gray-950 text-gray-100 p-8">
<h1 className="text-2xl font-bold">Git 提交 AI 神器</h1>
<input
placeholder="例如:C:\Users\...\你的项目"
value={cwd}
onChange={(e) => setCwd(e.target.value)}
/>
<button onClick={() => generate(cwd || undefined)} disabled={loading}>
{loading ? '生成中...' : '生成 commit message'}
</button>
{/* 错误提示 */}
{/* 生成的 commit message + 复制按钮 */}
{/* 可折叠查看本次 git diff */}
</div>
);
}
页面提供一个输入框让你填项目路径,点按钮就触发生成,结果和 diff 都能看到。
六、核心链路串联
整个功能其实就一条链路:
用户填项目路径(cwd)
↓
前端 POST /commit-message
↓
后端 git -C <cwd> diff HEAD 读改动
↓
拼进 prompt → ChatOllama(deepseek-r1:8b) 推理
↓
返回 { reply, diff }
↓
前端展示 commit message + diff
材料的来源:就是目标仓库 git diff HEAD 的输出文本,作为 prompt 的一部分交给本地大模型,不是模型联网翻文件,也不是训练数据。
七、如何运行
- 启动 Ollama,并确保已拉取模型:
ollama pull deepseek-r1:8b
- 启动后端:
cd app/git-differ/server
node index.js
- 启动前端:
cd app/git-differ/frontend
pnpm dev
打开 http://localhost:5173,输入你的 git 仓库路径,点击生成即可。
八、几个值得理解的知识点
1. Git 的三个区域
工作区 → 暂存区(staging) → 版本库(HEAD)
git diff:工作区 vs 暂存区(未暂存的改动)git diff --cached:暂存区 vs HEAD(已暂存的改动)git diff HEAD:工作区 vs HEAD(两者之和)
2. Git 会自动向上找仓库
git -C <子目录> diff HEAD 即使传的是子目录,也能向上找到 .git。但注意:git diff HEAD 默认 diff 的是整个仓库,不是那个子目录。
3. 跨域 CORS
前端(5173)访问后端(3000)属于跨域,因为「协议、域名、端口」任意一个不同都算跨域。后端通过 cors() 中间件放行。
4. 远程仓库地址与 diff 无关
git diff HEAD 是纯本地操作,读不到也不需要 Gitee/GitHub 远程地址。远程地址只在你 push/pull 时才用。
九、总结与可扩展方向
这个 Demo 已经把最核心的「git diff → LLM → 规范 commit message」链路跑通了。
可以继续优化的方向:
- 支持未跟踪文件:目前只读
git diff HEAD,新增的未跟踪文件不会进入 diff,可结合git status处理。 - 生成完整 body:目前只输出单行 subject,可扩展成「标题 + 正文」的多行提交信息。
- 只 diff 指定子目录:加
-- <path>参数,传子目录就只生成该目录的改动。 - 一键提交:生成后直接调
git commit -m,做成真正的提效工具。
希望这篇梳理能帮你理清这个项目的来龙去脉,也希望能给你的「提交信息焦虑」提供一个思路。