告别「不知道写什么 commit」:用 Ollama 本地部署的大模型自动生成规范的 Git 提交信息

0 阅读6分钟

告别「不知道写什么 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 的一部分交给本地大模型,不是模型联网翻文件,也不是训练数据。

七、如何运行

  1. 启动 Ollama,并确保已拉取模型:
ollama pull deepseek-r1:8b
  1. 启动后端:
cd app/git-differ/server
node index.js
  1. 启动前端:
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,做成真正的提效工具。

希望这篇梳理能帮你理清这个项目的来龙去脉,也希望能给你的「提交信息焦虑」提供一个思路。