
Git Batch Commit
- 34 installs
- 543 repo stars
- Updated August 5, 2026
- cat-xierluo/legal-skills
Split mixed staged changes into multiple focused commits and generate a clear message for each.
About
Splits mixed working changes into multiple focused, logically clean commits and generates a conventional message for each instead of one large commit. A developer uses it when they want to break staged changes into separate focused commits.
- Splits mixed changes into focused commits
- Generates conventional commit messages
Git Batch Commit by the numbers
- 34 all-time installs (skills.sh)
- Ranked #345 of 733 Git & Pull Requests skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cat-xierluo/legal-skills --skill git-batch-commitAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 34 |
|---|---|
| repo stars | ★ 543 |
| Last updated | August 5, 2026 |
| Repository | cat-xierluo/legal-skills ↗ |
What it does
Split mixed staged changes into multiple focused commits and generate a clear message for each.
Files
Git 批量提交工具
概述
将混合的修改自动拆分为多个聚焦的、逻辑清晰的提交。而不是创建一个包含"更新各种文件"的大提交,而是创建多个清晰的提交,如"docs: 更新 README"、"chore: 更新依赖"、"license: 更新 license 文件"。
与 git-workflow 的职责边界
git-batch-commit 是提交拆分工具,不是完整 Git 工作流控制器。
| 场景 | 使用哪个 Skill | 说明 |
|---|---|---|
| 将已暂存的混合变更拆成多个 commit | git-batch-commit | 本 Skill 的核心职责 |
| 判断是否能 merge / push / close PR | git-workflow | 本 Skill 不做合并门禁 |
PR 合入 main 的 commit 标题是否带 (#N) | git-workflow | 本 Skill 只在生成普通 commit 时保留 Issue/Task 引用 |
直接解决 GitHub Issue 是否应写 Closes #N | git-workflow | 本 Skill 只写 Refs #N,不关闭 Issue |
| 项目本地任务引用 | cross-agent-collab 定任务来源,git-batch-commit 写引用 | 使用 --local-ref "project-task Issue #13" |
当用户只是说“把这些改动提交一下 / 拆分提交”,使用本 Skill;当用户说“合并 PR / 拉 PR 到 main / 推送 / 关闭 issue”,同时遵循 git-workflow。
使用场景
- 用户暂存的文件来自多个类别(文档 + 代码 + 配置)
- 用户希望保持清晰、标准化的提交历史
- 用户提到"批量提交"、"拆分提交"或"整理提交"
- 用户修改了许多文件,希望按逻辑分组
快速开始
方式一:使用交互式脚本
# 首先暂存你的文件
git add file1.py file2.md package.json
# 运行交互式批量提交工具(需要确认)
python3 skills/git-batch-commit/scripts/interactive_commit.py
# 或使用 --yes 参数自动确认(适用于非交互式环境)
python3 skills/git-batch-commit/scripts/interactive_commit.py --yes
# 使用 --dry-run 仅查看分组,不实际提交
python3 skills/git-batch-commit/scripts/interactive_commit.py --dry-run
# 这组提交关联 GitHub Issue #13:每个标题追加 (#13),正文写 Refs #13
python3 skills/git-batch-commit/scripts/interactive_commit.py --issue 13
# 这组提交关联项目本地任务,不误关 GitHub Issue
python3 skills/git-batch-commit/scripts/interactive_commit.py --local-ref "project-task Issue #13"命令行参数:
--yes,-y:跳过交互式确认,自动创建提交--dry-run:仅显示分组建议,不实际创建提交--issue N:关联 GitHub Issue,提交标题追加(#N),正文写Refs #N--local-ref "...":关联项目本地任务,如project-task Issue #13,只写Refs: ...,不会关闭 GitHub Issue
方式二:手动分类
python3 skills/git-batch-commit/scripts/categorize_changes.py
python3 skills/git-batch-commit/scripts/categorize_changes.py --json提交分类
支持类型:docs, feat, fix, refactor, style, chore, license, config, test
完整定义和检测逻辑详见 references/commit-types.md
技能核心文件的特殊处理
重要规则:SKILL.md 虽然是 Markdown 格式,但它是技能的核心功能文件,不应归类为 docs 类型。
| 文件类型 | 正确分类 | 理由 |
|---|---|---|
SKILL.md | feat/style/fix | 技能核心文件,修改它相当于修改功能/代码 |
AGENTS.md | docs | 项目协作规范,属于文档 |
DECISIONS.md | docs | 决策记录,属于文档 |
CHANGELOG.md | docs | 变更日志,属于文档 |
TASKS.md | docs | 任务列表,属于文档 |
判断依据:
- 如果修改的是定义行为/功能的文件(如
SKILL.md、.py、.ts),视为代码变更 - 如果修改的是记录/说明性质的文件(如
README.md、CHANGELOG.md),视为文档变更
同 Skill 内伴随变更合并规则
核心原则:同一 Skill 内的功能变更及其直接关联的配套文件更新,应合并为一条提交,不要按文件类型拆分。
具体规则:
- 当
SKILL.md有功能变更(feat/fix/style),同 Skill 下的CHANGELOG.md更新应合并进同一条提交,不单独拆出docs提交 - 同理,版本号变更、配置微调(如删除
.clawhubignore)等配套小改动也一并合并 - 判断标准:这些文件变动是否由同一个功能变更引起?如果是,就是一条提交
- 只有当 CHANGELOG 独立更新(如补录历史版本)且无对应功能变更时,才单独使用
docs类型
提交信息格式
所有提交遵循格式:
<类型>: <标题>
<正文描述>重要规则:每个提交必须包含正文(body),不能只有标题。 正文用于补充变更的具体内容和原因,方便后续追溯。
使用英文前缀加中文内容,确保 GitHub 能识别并显示彩色标签。完整示例见 references/conventional-commits.md
Multi-Module/Multi-Skill 仓库规则:
- 描述中应包含模块名称:
docs: course-generator 更新 CHANGELOG - 如果一次修改涉及多个模块,必须按模块分别提交
- 描述中的模块名称使用原始英文名称,不要翻译
Issue / Task 引用规则:
- 若一组批量提交关联 GitHub Issue,使用
--issue N。每个提交标题会包含(#N),正文写Refs #N。 - 本 Skill 不生成
Closes #N。是否关闭 GitHub Issue 属于git-workflow的判断范围。 - 若编号来自项目本地任务源而非 GitHub Issue,使用
--local-ref "project-task Issue #N"或项目约定的等价引用,不要写Closes #N。 - PR 合并提交的
(#PR编号)规则不由本 Skill 决定,遵循git-workflow。
工作流程
1. 暂存文件 - 使用 git add 正常暂存 2. 运行交互式脚本 - 查看分类结果 3. 审核 - 检查提议的提交分组 4. 确认 - 创建提交或取消以调整 5. ClawHub 同步检查 - 仅当 skills/clawhub-sync/ 存在时执行,详见 references/clawhub-sync-check.md。不存在则静默跳过 6. Subtree 推送检查 - 仅当 skills/subtree-publish/config/subtree-skills.json 存在时执行,详见 references/subtree-push-check.md。不存在则静默跳过 7. 完成 - 获得清晰历史的聚焦提交
资源文件
scripts/
- `categorize_changes.py` - 分析 git diff 并按类别分组文件
- `generate_commit_message.py` - 生成约定式提交信息
- `interactive_commit.py` - 批量提交的主交互式工具
references/
- `commit-types.md` - 详细的类别定义和检测逻辑
- `conventional-commits.md` - 提交信息规范
- `clawhub-sync-check.md` - ClawHub 同步检查详细流程(工作流第5步)
- `subtree-push-check.md` - Subtree 推送检查详细流程(工作流第6步)
变更日志
[1.4.1] - 2026-05-19
改进
- 明确
git-batch-commit是提交快捷按钮,只负责已暂存变更的拆分和提交信息生成。 - 新增轻量 Issue/Task 引用:
--issue N在标题追加(#N)并写入Refs #N,--local-ref写入本地任务引用。 - 明确本 Skill 不生成
Closes #N;Issue 关闭、PR 合并、push 和分支安全规则仍由git-workflow管理。
[1.4.0] - 2026-05-15
变更
- 将
references/issue-pr-format.md(Issue 与 PR 命名规范)迁移到git-workflowskill,该规范与 Git 全流程工作流更相关 - git-batch-commit 聚焦于"提交"本职,不再管理 Issue/PR 命名
[1.3.0] - 2026-05-14
新增
- 工作流新增 Subtree 推送检查(第6步):当
skills/subtree-publish/config/subtree-skills.json存在时自动执行
[1.2.5] - 2026-04-10
新增
- 新增
references/issue-pr-format.md:定义 Issue 与 PR 命名规范 - Issue 类型前缀:
feat、bug、enhancement、docs、question - Issue 状态标记:
[done](自己完成)、[resolved]/[answered](外部) - PR 格式与 Commit 保持一致:
类型(模块): 描述 - AI 读取规则:根据状态标记区分已处理和待处理任务
[1.2.4] - 2026-04-06
新增
- ClawHub 同步检测新增「检测 B:新增 MIT 技能首次同步」:当提交新增 MIT 技能时,提示用户是否加入白名单并同步
- ClawHub 同步检测新增「检测 C:白名单新增但未同步」:检测白名单中有条目但同步记录缺失的情况
- 新增发布前敏感文件检查:确保临时发布目录不含 .env、密钥等敏感文件
[1.2.1] - 2026-03-26
修复
- ClawHub 同步工作流添加
--slug和--name参数 - 避免因临时目录名导致的 skill 标识符和显示名称错误
[1.2.0] - 2026-03-26
新增
- ✨ SKILL.md 新增「ClawHub 同步工作流」章节
- ✨ 支持提交后自动检测并同步版本更新的 skills 到 ClawHub
- ✨ 触发条件检测:clawhub-sync 存在、涉及 skills 目录、版本号更新、在白名单中
变更
- 📝 更新执行步骤,使用 prepare-publish.sh + clawhub sync 组合方式
- 📝 添加示例场景表格,清晰说明各种情况的处理方式
[1.1.0] - 2026-02-07
改进
- 📝 提交格式规范:使用小写英文前缀(docs:, feat:, fix: 等)加英文冒号,支持 GitHub 彩色标签显示
- 📝 描述保持中文:提交信息描述部分使用中文,保持内容一致性
- 📝 更新文档:SKILL.md 和 references/commit-types.md 反映新的提交格式
技术优化
- 更新
scripts/generate_commit_message.py- 从中文前缀改为小写英文前缀 - 更新
CATEGORY_TO_TYPE映射 - 使用小写英文类型名称 - 提交信息格式从
类型:描述改为type: 描述
文档更新
- 更新 SKILL.md - 反映新的提交格式规范
- 更新 references/commit-types.md - 添加 GitHub 彩色标签支持说明
- 新增 CHANGELOG.md - 版本变更记录
---
[1.0.0] - 2025-12-15
新增
- ✨ 初始版本发布:智能 Git 批量提交工具
- ✨ 自动分类功能:按文件类型和内容自动分类修改
- ✨ 交互式提交:支持确认预览后再创建提交
- ✨ 命令行工具:categorize_changes.py 和 generate_commit_message.py
- ✨ 提交分类支持:deps, docs, license, config, test, chore, feat, fix, refactor, style
核心功能
- 文件模式匹配:基于路径和扩展名进行分类
- Diff 内容分析:对源代码进行深度分析,区分功能、修复、重构和风格变更
- 关键字检测:识别 fix/bug/error 和 add/new/implement 等关键字
- 行变更比率:比较添加与删除行数推断变更意图
ClawHub 同步检查(工作流第5步)
核心规则:仅当项目目录下存在 `skills/clawhub-sync/` 时才执行——不存在则静默跳过,不输出任何提示。
完成 Git 提交后,执行以下检查。
触发条件
完成提交后,依次执行以下三类检测。任一命中即提示用户是否同步。
检测 A:已有技能版本升级
全部满足时触发:
1. 本地存在 clawhub-sync 技能
- 检查
skills/clawhub-sync/目录是否存在 - 不存在则静默跳过,不提示用户
2. 提交涉及 skills 目录
- 提交的文件中包含
skills/<skill-name>/下的文件
3. 版本号有更新
- 读取
skills/<skill-name>/SKILL.md的 frontmatter 中的version字段 - 比较
skills/clawhub-sync/config/sync-records.yaml中记录的版本 - 如果新版本 > 已记录版本(或记录中无版本),则需要同步
4. 在白名单中
- 检查
skills/clawhub-sync/config/sync-allowlist.yaml - skill 必须在白名单中(未被
#注释)
检测 B:新增 MIT 技能首次同步
当提交新增了 skills/<skill-name>/ 目录时触发:
1. 识别新增技能:检查提交中是否有 skills/<skill-name>/SKILL.md 为新文件(untracked → committed)
2. 许可证为 MIT:读取新技能 SKILL.md frontmatter 中的 license 字段,判断是否为 MIT
3. 未在白名单中:sync-allowlist.yaml 中无此 skill 的条目(无论是否被注释)
4. 未在同步记录中:sync-records.yaml 中无此 skill 的条目
满足全部条件时,向用户提示:
🆕 发现新增 MIT 技能:<skill-name>
该技能尚未加入 ClawHub 同步白名单。是否将其加入白名单并同步到 ClawHub?
选项:
y - 加入白名单并同步
n - 跳过,暂不发布
s - 加入白名单但暂不同步用户选择 y 时:
- 在
sync-allowlist.yaml中添加该 skill(添加到对应分类区域) - 执行 prepare-publish → publish → 更新 sync-records 流程
- 注意:发布前检查临时目录,确保不含
.env、密钥等敏感文件
用户选择 s 时:
- 仅在
sync-allowlist.yaml中添加该 skill(被注释),下次版本更新时再同步
检测 C:白名单新增但未同步
当白名单中有未被注释的 skill,但 sync-records.yaml 中没有对应记录时:
1. 遍历 sync-allowlist.yaml 中未被注释的 skill 2. 检查 sync-records.yaml 中是否有该 skill 的记录 3. 如果白名单有但记录中没有,且 SKILL.md 存在,提示用户执行首次同步
执行步骤
对于每个需要同步的 skill,按照 clawhub-sync 的"单个 Skill 同步工作流"执行:
步骤 1:准备发布目录
bash skills/clawhub-sync/scripts/prepare-publish.sh skills/<skill-name>步骤 2:执行发布(使用 publish 命令)
clawhub publish /tmp/clawhub-publish-<skill-name> \
--slug <skill-name> \
--name "<Display Name>" \
--version "<新版本号>" \
--changelog "<变更说明>"⚠️ 必须指定 --slug 和 --name
- 临时目录名可能包含前缀,使用 --slug 确保正确的 skill 标识符- 使用 --name 确保 ClawHub 上显示正确的名称为什么用 `publish` 而不是 `sync`?
- clawhub sync 会扫描所有目录的 skills,可能遇到 slug 冲突- clawhub publish <path> 只发布指定路径的单个 skill,更精确步骤 3:更新同步记录
更新 skills/clawhub-sync/config/sync-records.yaml:
- 更新
version为新版本号 - 更新
last_sync为当前时间 - 更新
git_hash为当前 commit hash - 更新
status为synced - 添加
url和publish_id(从命令输出获取)
失败处理
- 同步失败时仅显示警告信息
- 不影响 Git 提交结果
- 继续处理其他 skills
版本比较逻辑
new_version = SKILL.md frontmatter 中的 version(如 "1.2.0")
recorded_version = sync-records.yaml 中记录的版本(如 "1.1.0")
if new_version > recorded_version:
执行同步版本号按语义化版本规则比较(major.minor.patch)。
示例场景
| 场景 | 版本变化 | 白名单 | 同步记录 | 结果 |
|---|---|---|---|---|
| 版本升级(检测A) | "1.0.0" → "1.1.0" | 在白名单 | 有记录 | ✅ 执行同步 |
| 无版本变化(检测A) | "1.1.0" → "1.1.0" | 在白名单 | 有记录 | ❌ 跳过 |
| 不在白名单(检测A) | 任意 | 被注释 | - | ❌ 跳过 |
| 白名单内首次发布(检测A/C) | "1.0.0" | 在白名单 | 无记录 | ✅ 执行同步 |
| 新增 MIT 技能(检测B) | "0.1.0" | 无条目 | 无记录 | ✅ 提示用户选择 |
| 新增 CC 技能(检测B) | "0.1.0" | 无条目 | 无记录 | ❌ 非 MIT 跳过 |
| clawhub-sync 不存在 | - | - | - | ❌ 静默跳过整个工作流 |
提交类别参考
本文档定义了 git-batch-commit skill 使用的修改类别及其检测方法。
类别
deps (chore: 依赖管理)
触发文件: package.json、requirements.txt、go.mod、Cargo.toml 等
提交格式: chore: 更新依赖
描述: 依赖管理文件的变更,包括包管理器的 lockfile。
docs (docs: 文档)
触发文件: *.md、*.rst、docs/、README*、CHANGELOG*
提交格式: docs: 更新 <主题> 文档
描述: 文档变更。当单个文档文件被修改时,提交信息会包含具体的文档名称。
license (license: License 文件)
触发文件: LICENSE、LICENSE.txt、COPYING
提交格式: license: 更新 license 文件
描述: License 文件的更新。
config (config: 配置)
触发文件: *.env.*、*.conf、*.config、*.yaml、config/
提交格式: config: 更新配置
描述: 配置文件的变更。
test (test: 测试)
触发文件: test_*.py、*_test.go、*.test.ts、test/、__tests__/
提交格式: test: 更新测试
描述: 测试文件的添加或修改。
chore (chore: 工具)
触发文件: Makefile、Dockerfile、.gitignore、.github/
提交格式: chore: 更新工具
描述: 构建工具、CI/CD 和开发基础设施的变更。
feat (feat: 新功能)
触发: 包含大量添加内容的源代码文件
提交格式: feat: 添加新功能
描述: 添加到代码库的新功能。通过分析 diff 内容中的功能相关关键字和添加/删除比率来检测。
fix (fix: Bug 修复)
触发: diff 中包含 bug 相关关键字的源代码文件
提交格式: fix: 修复 bug
描述: Bug 修复。通过 diff 中的 "fix"、"bug"、"issue"、"error" 等关键字检测。
refactor (refactor: 代码重构)
触发: 删除内容多于添加内容的源代码文件
提交格式: refactor: 重构代码
描述: 无功能性变更的代码重构。
style (style: 代码风格)
触发: 源代码文件(默认类别)
提交格式: style: 更新代码风格
描述: 代码风格变更、格式化或其他小的源代码修改。
提交格式规范
所有提交信息遵循格式:`<类型>: <描述>`
- 使用小写英文类型前缀(docs:, feat:, fix: 等)
- 使用英文冒号 (:)
- 描述使用中文,保持简洁明确
GitHub 彩色标签支持: 使用英文前缀可以让 GitHub 自动识别并显示彩色标签,便于快速浏览提交历史。
检测逻辑
1. 文件模式匹配: 文件首先按路径模式分类 2. Diff 内容分析: 对于源代码,分析实际的 diff 以确定是功能、修复、重构还是风格变更 3. 关键字检测: 查找修复相关关键字("bug"、"fix"、"error")与功能关键字("add"、"new"、"implement") 4. 行变更比率: 比较添加与删除以推断意图
约定式提交规范
本 skill 遵循约定式提交(Conventional Commits)规范进行提交信息格式化。
格式
<类型>: <标题>
<正文描述>正文(body)是必填项,用于补充变更的具体内容和原因。正文应列出关键变更点,使用列表格式(- 开头)。
支持的类型
| 类型 | 描述 |
|---|---|
docs | 文档变更 |
feat | 新功能 |
fix | Bug 修复 |
refactor | 代码重构 |
style | 代码风格变更 |
chore | 构建工具、依赖、工具链 |
test | 测试添加或修改 |
config | 配置变更 |
license | License 文件更新 |
示例
docs: 更新 README 文档
- 补充安装说明
- 添加使用示例
feat: 添加用户认证
- 实现 JWT Token 签发与验证
- 添加登录/登出 API 端点
fix: 修复解析器内存泄漏
- 修复大文件解析时缓冲区未释放的问题
chore: 更新依赖Issue / Task 引用
git-batch-commit 只负责轻量引用,不负责关闭 Issue:
docs: 更新 README 文档 (#13)
Refs #13
- 补充安装说明
- 添加使用示例项目本地任务引用使用正文中的 Refs:,避免误关 GitHub Issue:
docs: 更新任务材料
Refs: project-task Issue #13
- 补充素材包说明若需要使用 Closes #N 关闭 GitHub Issue,或需要合并 PR、推送远端、拉取 PR 到 main,遵循 git-workflow。
为什么要使用标准化提交?
1. 更易阅读: 快速理解改动内容 2. 更好的 git log: 更清晰、更有意义的历史记录 3. 自动化 changelog: 工具可以自动生成 CHANGELOG.md 4. 语义化版本控制: 帮助确定版本升级 5. 团队一致性: 所有人使用相同的格式
# 提交消息生成数据配置
# 用于 generate_commit_message.py 生成约定式提交信息
file_to_function_map:
- pattern: 'search\.py$'
description: '代码语义搜索模块(支持函数/类/导入/文档搜索)'
- pattern: 'qa\.py$'
description: '智能问答模块(意图分类 + 结构化回答生成)'
- pattern: 'query\.py$'
description: '查询构建器(生成结构化查询语句)'
- pattern: 'parser\.py$'
description: '解析器(文本/代码/数据解析)'
- pattern: 'analyzer\.py$'
description: '分析器(代码/数据/性能分析)'
- pattern: 'architecture\.py$'
description: '架构分析器(目录结构/模块划分/设计模式检测)'
- pattern: 'quality\.py$'
description: '代码质量分析器(注释覆盖率/技术债务/潜在问题检测)'
- pattern: 'utils\.py$'
description: '通用工具函数库'
- pattern: 'helpers\.py$'
description: '辅助函数(特定场景的便捷方法)'
- pattern: 'common\.py$'
description: '公共模块(共享常量和函数)'
- pattern: 'config\.py$'
description: '配置管理模块(加载/解析/验证配置)'
- pattern: 'models\.py$'
description: '数据模型定义(ORM/Schema/Entity)'
- pattern: 'types\.py$'
description: '类型定义(Type Hints/Interfaces)'
- pattern: 'constants\.py$'
description: '常量定义(枚举/配置值/魔法数字)'
- pattern: 'schemas\.py$'
description: '数据 Schema 定义(验证规则/序列化)'
- pattern: 'exceptions\.py$'
description: '自定义异常类(错误码/错误消息)'
- pattern: 'validators\.py$'
description: '数据验证器(输入校验/格式检查)'
- pattern: 'converters?\.py$'
description: '数据转换器(格式转换/编码转换)'
- pattern: 'formatters?\.py$'
description: '格式化器(输出美化/模板渲染)'
- pattern: 'renderers?\.py$'
description: '渲染器(HTML/PDF/图片生成)'
- pattern: 'generators?\.py$'
description: '生成器(代码/文档/数据生成)'
- pattern: 'extractors?\.py$'
description: '提取器(从文本/网页/文件提取信息)'
- pattern: 'transformers?\.py$'
description: '数据转换管道(ETL/清洗/标准化)'
- pattern: 'loaders?\.py$'
description: '加载器(文件/网络/数据库加载)'
- pattern: 'savers?\.py$'
description: '保存器(持久化/导出/备份)'
- pattern: 'storage\.py$'
description: '存储层抽象(本地/云存储/缓存)'
- pattern: 'handlers?\.py$'
description: '事件处理器(HTTP/WebSocket/信号处理)'
- pattern: 'managers?\.py$'
description: '资源管理器(生命周期/连接池/状态)'
- pattern: 'services?\.py$'
description: '业务服务层(核心业务逻辑)'
- pattern: 'processors?\.py$'
description: '数据处理器(批处理/流处理/异步处理)'
- pattern: 'clients?\.py$'
description: 'API 客户端(HTTP/RPC/GraphQL)'
- pattern: 'servers?\.py$'
description: '服务端实现(路由/中间件/错误处理)'
- pattern: 'api\.py$'
description: 'API 接口定义(端点/参数/响应)'
- pattern: 'routes?\.py$'
description: '路由配置(URL 映射/参数提取)'
- pattern: 'views?\.py$'
description: '视图层(页面渲染/模板上下文)'
- pattern: 'controllers?\.py$'
description: '控制器(请求处理/响应封装)'
- pattern: 'middleware\.py$'
description: '中间件(请求/响应拦截器)'
- pattern: 'auth\.py$'
description: '认证模块(登录/登出/Token 管理)'
- pattern: 'permissions?\.py$'
description: '权限控制(角色/资源/操作检查)'
- pattern: 'security\.py$'
description: '安全模块(加密/签名/防护)'
- pattern: 'database\.py$'
description: '数据库连接管理(连接池/事务/会话)'
- pattern: 'db\.py$'
description: '数据库操作层(CRUD/查询构建)'
- pattern: 'cache\.py$'
description: '缓存层(Redis/Memcached/内存缓存)'
- pattern: 'repository\.py$'
description: '仓储层(数据访问抽象)'
- pattern: 'logger\.py$'
description: '日志模块(格式化/分级/输出)'
- pattern: 'monitoring\.py$'
description: '监控模块(指标收集/告警)'
- pattern: 'metrics\.py$'
description: '指标定义(计数器/仪表/直方图)'
- pattern: 'test.*\.py$'
description: '单元测试/集成测试'
- pattern: 'conftest\.py$'
description: 'Pytest 配置与 Fixtures'
- pattern: 'mock.*\.py$'
description: 'Mock 对象与测试替身'
- pattern: '\.tsx?$'
description: 'TypeScript/React 前端组件'
- pattern: '\.vue$'
description: 'Vue.js 组件(模板/脚本/样式)'
- pattern: '\.svelte$'
description: 'Svelte 组件(编译时优化)'
- pattern: '__init__\.py$'
description: 'Python 模块初始化(导出/版本)'
- pattern: '__main__\.py$'
description: '命令行入口(python -m module)'
- pattern: 'SKILL\.md$'
description: 'Claude Code 技能定义文件'
- pattern: 'CHANGELOG\.md$'
description: '版本变更日志'
- pattern: 'README\.md$'
description: '项目说明文档'
message_templates:
deps:
patterns:
- pattern: 'package\.json'
message: '更新 JavaScript 依赖'
- pattern: 'requirements\.txt'
message: '更新 Python 依赖'
- pattern: 'go\.(mod|sum)'
message: '更新 Go 依赖'
- pattern: 'Gemfile'
message: '更新 Ruby 依赖'
- pattern: 'Cargo\.toml'
message: '更新 Rust 依赖'
- pattern: 'pyproject\.toml'
message: '更新 Python 项目配置'
default: '更新依赖'
docs:
patterns:
- pattern: 'README'
message: '更新 README 文档'
- pattern: 'CHANGELOG'
message: '更新变更日志'
- pattern: 'CONTRIBUTING'
message: '更新贡献指南'
- pattern: 'ARCHITECTURE'
message: '更新架构文档'
- pattern: 'AGENTS\.md'
message: '更新协作规范文档'
- pattern: 'SKILL-GUIDE\.md'
message: '更新 Skill 开发指南'
- pattern: 'skills/([^/]+)/SKILL\.md'
message: '添加 \1 技能文档'
default: '更新文档'
license:
default: '更新许可证文件'
config:
patterns:
- pattern: '\.env\.example'
message: '更新环境变量示例'
- pattern: '\.yaml|\.yml'
message: '更新 YAML 配置'
- pattern: 'toml'
message: '更新 TOML 配置'
default: '更新配置'
test:
default: '更新测试'
chore:
patterns:
- pattern: '\.gitignore'
message: '更新 gitignore 忽略规则'
- pattern: 'Dockerfile'
message: '更新 Docker 配置'
- pattern: '\.github/'
message: '更新 GitHub 工作流'
- pattern: 'Makefile'
message: '更新 Makefile'
default: '更新工具配置'
feat:
patterns:
- pattern: 'scripts/([^/]+)'
message: '添加 \1 脚本'
- pattern: 'test/([^/]+)'
message: '添加 \1 测试'
default: '添加新功能'
fix:
default: '修复 Bug'
refactor:
default: '重构代码'
style:
default: '调整代码风格'
function_prefix_actions:
detect_: '检测'
analyze_: '分析'
parse_: '解析'
extract_: '提取'
identify_: '识别'
infer_: '推断'
generate_: '生成'
create_: '创建'
build_: '构建'
make_: '制作'
process_: '处理'
convert_: '转换'
transform_: '转换'
handle_: '处理'
format_: '格式化'
get_: '获取'
fetch_: '获取'
load_: '加载'
read_: '读取'
update_: '更新'
modify_: '修改'
edit_: '编辑'
delete_: '删除'
remove_: '移除'
validate_: '验证'
check_: '检查'
verify_: '验证'
is_: '验证'
optimize_: '优化'
improve_: '改进'
enhance_: '增强'
fix_: '修复'
config_key_meanings:
space_before: '段前间距'
space_after: '段后间距'
margin: '边距'
padding: '内边距'
gap: '间隙'
line_height: '行高'
spacing: '间距'
font: '字体'
font_size: '字号'
font_family: '字体族'
bold: '加粗'
italic: '斜体'
size: '尺寸'
align: '对齐方式'
indent: '缩进'
color: '颜色'
background: '背景色'
fill: '填充色'
width: '宽度'
height: '高度'
layout: '布局'
content: '内容'
text: '文本'
title: '标题'
value: '值'
Subtree 推送检查(工作流第6步)
核心规则:仅当项目目录下存在 `skills/subtree-publish/config/subtree-skills.json` 时才执行——不存在则静默跳过,不输出任何提示。
完成 Git 提交后,检查本次提交是否涉及已注册的 subtree 子项目。
触发条件
1. 配置文件存在
- 检查
skills/subtree-publish/config/subtree-skills.json是否存在 - 不存在则静默跳过,不提示用户
2. 提交涉及已注册的子目录
- 读取
skills/subtree-publish/config/subtree-skills.json中的prefix和skills列表 - 获取本次提交涉及的文件列表(
git diff --name-only HEAD~1 HEAD,或批量提交时使用最后一次 commit) - 检查是否有文件路径以
<prefix>/<skill-name>/开头 - 如果命中,收集所有命中的 skill 名称
3. remote 已配置
- 检查是否存在对应的
<name>-standaloneremote - 如果 remote 不存在,说明该子项目尚未完成首次注册,跳过
提示与执行
满足触发条件时,向用户提示:
本次提交涉及已注册的 subtree 子项目:<name1>, <name2>
是否推送到独立仓库?
选项:
y - 推送所有命中的子项目
n - 跳过,暂不推送
s - 选择性推送用户选择 y 时,对每个命中的子项目执行:
git subtree push --prefix=<prefix>/<name> <name>-standalone main用户选择 s 时,逐个询问是否推送。
失败处理
- 推送失败时仅显示警告信息
- 不影响 Git 提交结果
- 继续处理其他子项目
示例场景
| 场景 | 配置文件 | 提交涉及子目录 | remote 存在 | 结果 |
|---|---|---|---|---|
| 正常推送 | 存在 | 是 | 是 | ✅ 提示用户推送 |
| 未涉及子目录 | 存在 | 否 | - | ❌ 静默跳过 |
| 配置文件不存在 | 不存在 | - | - | ❌ 静默跳过 |
| 首次注册未完成 | 存在 | 是 | 否 | ❌ 跳过(提示用户先完成首次注册) |
#!/usr/bin/env python3
"""
Categorize Git changes by type for batch committing.
Analyzes git diff to group modifications by logical categories:
- deps: Dependency management (package.json, requirements.txt, go.mod, etc.)
- docs: Documentation changes
- license: LICENSE file updates
- config: Configuration files
- test: Test files
- chore: Build scripts, tooling
- feat: New features in source code
- fix: Bug fixes in source code
- refactor: Code refactoring
- style: Code style changes
"""
import subprocess
import json
import os
import re
from pathlib import Path
from typing import Dict, List, Tuple
# File pattern to category mapping
FILE_PATTERNS = {
'deps': [
r'package\.json',
r'package-lock\.json',
r'yarn\.lock',
r'pnpm-lock\.yaml',
r'requirements\.txt',
r'poetry\.lock',
r'Pipfile',
r'pyproject\.toml',
r'go\.mod',
r'go\.sum',
r'Gemfile',
r'Gemfile\.lock',
r'Cargo\.toml',
r'Cargo\.lock',
r'composer\.json',
r'composer\.lock',
r'\.gradle',
],
'license': [
r'LICENSE',
r'LICENSE\.txt',
r'LICENSE\.md',
r'COPYING',
],
'docs': [
r'\.md$',
r'\.rst$',
r'\.txt$',
r'docs/.*',
r'DOC.*',
r'README.*',
r'CHANGELOG.*',
r'CONTRIBUTING.*',
],
'config': [
r'\.env\.',
r'\.conf',
r'\.config',
r'\.yaml$',
r'\.yml$',
r'\.toml$',
r'\.json$',
r'\.xml$',
r'\.ini$',
r'config/.*',
],
'test': [
r'test_.*\.py$',
r'.*_test\.go$',
r'.*\.test\.ts$',
r'.*\.test\.js$',
r'.*\.spec\.ts$',
r'.*\.spec\.js$',
r'tests?/.*',
r'__tests?__/.*',
r'test/.*',
],
'chore': [
r'Makefile',
r'Dockerfile',
r'\.dockerignore',
r'\.gitignore',
r'\.gitattributes',
r'\.github/.*',
r'\.gitlab-ci\.yml',
r'\travis\.yml',
r'jenkinsfile',
r'\.editorconfig',
],
}
# Skill core files that should be treated as code, not docs
# These define behavior/functionality, not just documentation
SKILL_CORE_FILES = [
r'SKILL\.md$', # Skill definition file (defines behavior)
r'skills/.*/SKILL\.md$', # Skill files in skills directory
]
# Files that should ALWAYS be categorized separately, even inside skills/
# These are cross-cutting concerns that apply to the whole project
ALWAYS_SEPARATE_CATEGORIES = {
'license': [
r'LICENSE',
r'LICENSE\.txt',
r'LICENSE\.md',
r'COPYING',
],
}
# Source code extensions
SOURCE_EXTENSIONS = [
r'\.py$', r'\.js$', r'\.ts$', r'\.tsx$', r'\.jsx$',
r'\.go$', r'\.rs$', r'\.java$', r'\.kt$', r'\.swift$',
r'\.c$', r'\.cpp$', r'\.h$', r'\.hpp$',
r'\.cs$', r'\.php$', r'\.rb$', r'\.scala$',
]
def get_staged_files() -> List[str]:
"""Get list of staged files using git diff --cached --name-only."""
result = subprocess.run(
['git', 'diff', '--cached', '--name-only'],
capture_output=True,
text=True,
check=True
)
files = result.stdout.strip().split('\n')
return [f for f in files if f]
def get_unstaged_files() -> List[str]:
"""Get list of unstaged modified files using git diff --name-only."""
result = subprocess.run(
['git', 'diff', '--name-only'],
capture_output=True,
text=True,
check=True
)
files = result.stdout.strip().split('\n')
return [f for f in files if f]
def categorize_file(filepath: str) -> str:
"""Categorize a file based on its path and patterns."""
# Special handling for skill core files
# SKILL.md defines behavior/functionality, should be treated as code, not docs
for pattern in SKILL_CORE_FILES:
if re.search(pattern, filepath):
return 'code'
# Check each category's patterns
for category, patterns in FILE_PATTERNS.items():
for pattern in patterns:
if re.search(pattern, filepath):
return category
# Check if it's a source file
for ext_pattern in SOURCE_EXTENSIONS:
if re.search(ext_pattern, filepath):
return 'code'
# Default to 'other'
return 'other'
def detect_code_change_type(filepath: str) -> str:
"""
Detect if a source code change is feat, fix, refactor, or style.
This analyzes the git diff content.
"""
try:
result = subprocess.run(
['git', 'diff', '--cached', filepath],
capture_output=True,
text=True,
check=True
)
diff_content = result.stdout
# Simple heuristic based on diff patterns
added_lines = len([l for l in diff_content.split('\n') if l.startswith('+')])
removed_lines = len([l for l in diff_content.split('\n') if l.startswith('-')])
# IMPORTANT: Check for fix-related keywords FIRST
# Even if adding new functions, fix keywords indicate a bug fix
# English + Chinese fix keywords
fix_keywords = ['fix', 'bug', 'issue', 'error', 'patch', 'hotfix',
'修复', '错误', '问题', '补丁']
if any(keyword in diff_content.lower() for keyword in fix_keywords):
return 'fix'
# Check for new function definitions (strong indicator of new feature)
# Match patterns like: +def function_name(
new_func_pattern = r'^\+def\s+\w+'
new_funcs = re.findall(new_func_pattern, diff_content, re.MULTILINE)
if new_funcs:
return 'feat'
# Check for feature-related keywords (English + Chinese)
feat_keywords = ['add', 'new', 'implement', 'feature', 'support',
'添加', '新增', '实现', '功能', '支持', '增加']
if any(keyword in diff_content.lower() for keyword in feat_keywords):
return 'feat'
# Based on line changes
if added_lines > removed_lines * 1.5:
return 'feat'
elif removed_lines > added_lines * 1.5:
return 'refactor'
else:
return 'style'
except Exception:
return 'style'
def extract_skill_name(filepath: str) -> str:
"""
Extract skill name from a file path if it's under skills/ directory.
Returns skill name if found, None otherwise.
"""
match = re.match(r'skills/([^/]+)/', filepath)
if match:
return match.group(1)
return None
def check_always_separate(filepath: str) -> str:
"""
Check if file should always be categorized separately, even inside skills/.
Returns the category if matched, None otherwise.
"""
for category, patterns in ALWAYS_SEPARATE_CATEGORIES.items():
for pattern in patterns:
if re.search(pattern, filepath):
return category
return None
def group_changes(files: List[str], staged: bool = True) -> Dict[str, List[str]]:
"""
Group files by category.
Special handling for skill directories: all files under the same skill
directory are grouped together as they represent a single logical change.
IMPORTANT: Some files (like LICENSE) are ALWAYS categorized separately,
even if they are inside a skill directory. This ensures that cross-cutting
concerns like licensing changes get their own focused commits.
Args:
files: List of file paths
staged: Whether files are staged (True) or unstaged (False)
Returns:
Dictionary mapping category to list of files
"""
groups = {}
skill_groups = {} # Temporary storage for skill-based grouping
# First pass: separate skill files from others
for filepath in files:
# Check if file should ALWAYS be separate (e.g., LICENSE)
separate_category = check_always_separate(filepath)
if separate_category:
if separate_category not in groups:
groups[separate_category] = []
groups[separate_category].append(filepath)
continue
skill_name = extract_skill_name(filepath)
if skill_name:
if skill_name not in skill_groups:
skill_groups[skill_name] = []
skill_groups[skill_name].append(filepath)
else:
# Non-skill files: categorize normally
category = categorize_file(filepath)
# For source code, further categorize
if category == 'code' and staged:
subcategory = detect_code_change_type(filepath)
category = subcategory
if category not in groups:
groups[category] = []
groups[category].append(filepath)
# Second pass: process skill groups
for skill_name, skill_files in skill_groups.items():
# Determine the change type based on the most significant change
# If any file has 'feat' keywords, use 'feat'
# Else if any file has 'fix' keywords, use 'fix'
# Else use 'style'
change_type = 'style'
for filepath in skill_files:
if staged:
file_type = detect_code_change_type(filepath)
if file_type == 'feat':
change_type = 'feat'
break
elif file_type == 'fix' and change_type != 'feat':
change_type = 'fix'
# Use skill:<name> as the category key
category_key = f'skill:{skill_name}:{change_type}'
groups[category_key] = skill_files
return groups
def main():
"""Main entry point for command-line usage."""
import argparse
parser = argparse.ArgumentParser(
description='Categorize Git changes for batch committing'
)
parser.add_argument(
'--unstaged',
action='store_true',
help='Analyze unstaged changes instead of staged'
)
parser.add_argument(
'--json',
action='store_true',
help='Output as JSON'
)
args = parser.parse_args()
# Get files
if args.unstaged:
files = get_unstaged_files()
else:
files = get_staged_files()
if not files:
print("No changes found.")
return
# Group files
groups = group_changes(files, staged=not args.unstaged)
if args.json:
print(json.dumps(groups, indent=2))
else:
print("Categorized changes:")
print("-" * 40)
for category, file_list in sorted(groups.items()):
print(f"\n{category.upper()}:")
for f in file_list:
print(f" - {f}")
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""
Generate conventional commit messages based on change type and content.
使用小写英文前缀 + 中文冒号 + 中文描述的格式:
- docs:文档变更
- feat:新功能
- fix:Bug 修复
- refactor:代码重构
- style:代码风格调整
- chore:构建工具、依赖更新
- test:测试变更
- config:配置变更
- license:许可证文件更新
注意:实际输出使用英文冒号 (:) 以支持 GitHub 彩色标签
"""
import subprocess
import re
import argparse
import yaml
from pathlib import Path
from typing import List, Dict
# Category to commit type mapping (小写英文)
CATEGORY_TO_TYPE = {
'deps': 'chore',
'docs': 'docs',
'license': 'license',
'config': 'config',
'test': 'test',
'chore': 'chore',
'feat': 'feat',
'fix': 'fix',
'refactor': 'refactor',
'style': 'style',
'code': 'style', # Default for uncategorized code
'other': 'chore',
}
# Load configuration from YAML
_DATA_PATH = Path(__file__).parent.parent / 'references' / 'message-data.yaml'
with open(_DATA_PATH, encoding='utf-8') as _f:
_DATA = yaml.safe_load(_f)
# Build FILE_TO_FUNCTION_MAP from YAML (list of {pattern, description})
FILE_TO_FUNCTION_MAP = _DATA['file_to_function_map']
# Build MESSAGE_TEMPLATES from YAML (convert patterns from list to tuple format)
MESSAGE_TEMPLATES = {}
for _cat, _tpl in _DATA['message_templates'].items():
if 'patterns' in _tpl:
MESSAGE_TEMPLATES[_cat] = {
'patterns': [(p['pattern'], p['message']) for p in _tpl['patterns']],
'default': _tpl['default'],
}
else:
MESSAGE_TEMPLATES[_cat] = {'default': _tpl['default']}
FUNCTION_PREFIX_ACTIONS = _DATA['function_prefix_actions']
CONFIG_KEY_MEANINGS = _DATA['config_key_meanings']
def parse_skill_category(category: str):
"""
Parse skill:<name>:<type> format.
Returns (skill_name, commit_type) if skill category, else (None, None).
"""
if category.startswith('skill:'):
parts = category.split(':')
if len(parts) == 3:
return parts[1], parts[2] # (skill_name, commit_type)
return None, None
def detect_skill_name(files: List[str]) -> str | None:
"""
检测文件是否属于某个技能,返回技能名称。
Args:
files: 文件列表
Returns:
技能名称,如果不属于任何技能则返回 None
"""
if not files:
return None
for filepath in files:
# Check if this is a skill file (skills/<skill-name>/...)
if '/skills/' in filepath:
parts = filepath.split('/skills/')
if len(parts) > 1:
skill_path = parts[1]
skill_name = skill_path.split('/')[0]
if skill_name and skill_name != 'skills':
return skill_name
return None
def is_new_skill_being_added(skill_name: str, files: List[str]) -> bool:
"""
检测是否正在添加新技能(而非更新现有技能)。
判断逻辑:
1. 检查 SKILL.md 文件是否已存在于 HEAD 提交中
2. 如果不存在 → 新技能
3. 否则 → 更新现有技能
Args:
skill_name: 技能名称
files: 本次提交涉及的文件列表
Returns:
True 表示新技能,False 表示更新现有技能
"""
# 检查 SKILL.md 是否是新文件(不存在于 HEAD 提交中)
for filepath in files:
if filepath.endswith('SKILL.md'):
try:
# 使用 git cat-file 检查文件是否在 HEAD 中存在
result = subprocess.run(
['git', 'cat-file', '-e', f'HEAD:{filepath}'],
capture_output=True,
text=True,
)
# 如果返回码非 0,说明文件不在 HEAD 中,是新技能
if result.returncode != 0:
return True
except Exception:
# 出错时保守处理,视为更新
pass
return False
def analyze_changes(files: List[str], category: str) -> str:
"""
分析文件变更以生成具体的描述信息。
返回变更的具体描述。
"""
if not files:
return ""
# Handle skill:<name>:<type> format - extract the actual type
skill_name_parsed, actual_category = parse_skill_category(category)
is_skill_format = skill_name_parsed is not None
if not is_skill_format:
actual_category = category
# 优先处理技能文件(跳过通用 patterns 匹配)
# 技能文件需要特殊处理,不能被 scripts/、test/ 等通用模式提前匹配
if len(files) == 1:
filepath = files[0]
if 'skills/' in filepath:
filename = filepath.split('/')[-1]
skill_name_from_path = filepath.split('skills/')[1].split('/')[0]
# 检测是否为新技能(SKILL.md 未被 git 跟踪)
if is_new_skill_being_added(skill_name_from_path, files):
return f'添加 {skill_name_from_path} 技能'
else:
# 分析具体变更内容,生成有意义的描述
diff = get_file_diff(filepath)
specific_change = analyze_diff_content(diff, filename)
return specific_change
# Try to match patterns
if actual_category in MESSAGE_TEMPLATES:
templates = MESSAGE_TEMPLATES[actual_category]
if 'patterns' in templates:
for pattern, message in templates['patterns']:
for filepath in files:
if re.search(pattern, filepath):
# If message contains regex group reference, substitute it
if r'\1' in message or r'\2' in message:
match = re.search(pattern, filepath)
if match:
try:
result = message
for i in range(1, len(match.groups()) + 1):
result = result.replace(f'\\{i}', match.group(i))
return result
except IndexError:
pass
return message
# Use default template
if 'default' in templates:
base_msg = templates['default']
# Enhance with file-specific info
if len(files) == 1:
filepath = files[0]
filename = filepath.split('/')[-1]
# For markdown docs, extract doc name
if actual_category == 'docs' and filename.endswith('.md'):
doc_name = filename.replace('.md', '')
# Handle special cases
if doc_name == 'README':
return '更新 README 文档'
elif doc_name == 'CHANGELOG':
return '更新变更日志'
elif doc_name == 'AGENTS':
return '更新协作规范文档'
elif doc_name == 'SKILL-GUIDE':
return '更新 Skill 开发指南'
else:
return f'更新 {doc_name} 文档'
# For config files, mention specific config
if actual_category == 'config':
if filename.endswith(('.yaml', '.yml')):
return f'更新 {filename} 配置'
elif filename.endswith('.toml'):
return f'更新 {filename} 配置'
# Multiple files: mention count
return f'{base_msg}({len(files)} 个文件)'
# Fallback: generic message based on actual_category
return f'更新 {actual_category} 文件'
def generate_commit_message(category: str, files: List[str]) -> str:
"""
生成约定式提交信息,包含详细信息。
格式:
<type>(<技能名>): <描述>
- 详细变更说明1
- 详细变更说明2
Args:
category: 变更类别 (deps, docs, feat 等,或 skill:<name>:<type>)
files: 该类别中的变更文件列表
Returns:
格式化的提交信息(包含详细信息)
"""
# Check if this is a skill category
skill_name, skill_type = parse_skill_category(category)
if skill_name:
# Skill-based commit: 使用 analyze_changes 生成具体描述
commit_type = skill_type
description = analyze_changes(files, category)
else:
# Regular category
commit_type = CATEGORY_TO_TYPE.get(category, 'chore')
description = analyze_changes(files, category)
# Detect skill name from files for automatic formatting
detected_skill = detect_skill_name(files)
# Generate detailed body based on files
detail_lines = generate_detail_lines(files, category)
# Format: type(skill-name): description (使用英文冒号以支持 GitHub 彩色标签)
# 如果已有 skill_name 或从文件中检测到技能名,则使用括号格式
if skill_name:
message = f"{commit_type}({skill_name}): {description}"
elif detected_skill:
message = f"{commit_type}({detected_skill}): {description}"
else:
message = f"{commit_type}: {description}"
# Add detail lines if available
if detail_lines:
message += "\n\n" + detail_lines
return message
_diff_cache: dict = {}
def get_file_diff(filepath: str) -> str:
"""Get git diff for a specific file (cached)."""
if filepath in _diff_cache:
return _diff_cache[filepath]
try:
result = subprocess.run(
['git', 'diff', '--cached', filepath],
capture_output=True,
text=True,
)
_diff_cache[filepath] = result.stdout
return result.stdout
except Exception:
return ""
def analyze_diff_content(diff: str, filename: str) -> str:
"""
分析 diff 内容,生成具体的变更描述。
Args:
diff: git diff 内容
filename: 文件名
Returns:
具体的变更描述
"""
if not diff:
return f"更新 {filename}"
lines = diff.split('\n')
added_lines = []
removed_lines = []
for line in lines:
if line.startswith('+') and not line.startswith('+++'):
content = line[1:].strip()
if content and not content.startswith('\\'):
added_lines.append(content)
elif line.startswith('-') and not line.startswith('---'):
content = line[1:].strip()
if content and not content.startswith('\\'):
removed_lines.append(content)
# Analyze based on file type
if filename.endswith('.md'):
return analyze_markdown_changes(added_lines, removed_lines, filename)
elif filename.endswith('.py'):
return analyze_code_changes(added_lines, removed_lines, filename)
elif '.gitignore' in filename:
return analyze_gitignore_changes(added_lines, removed_lines)
elif filename.endswith(('.yaml', '.yml', '.json', '.toml')):
return analyze_config_changes(added_lines, removed_lines, filename)
else:
return analyze_generic_changes(added_lines, removed_lines, filename)
def analyze_markdown_changes(added: List[str], removed: List[str], filename: str) -> str:
"""分析 Markdown 文件变更。"""
# Check for new sections/headers - collect all headers for better context
added_headers = [l.lstrip('#').strip() for l in added if l.startswith('#')]
# SKILL.md is a skill core file, treat differently from regular docs
if filename == 'SKILL.md' or 'SKILL.md' in filename:
# Extract skill name from path if available
skill_name = ""
if 'skills/' in filename:
parts = filename.split('skills/')
if len(parts) > 1:
skill_name = parts[1].split('/')[0]
# If we found added headers, generate specific description
if added_headers:
# Get the most important header (first h1 or h2)
header_text = added_headers[0]
if skill_name:
return f"{skill_name} 技能 - 添加 {header_text} 部分"
return f"添加 {header_text} 部分"
# Check for bullet points or list items (often contain具体的变更内容)
added_items = [l.strip() for l in added if l.strip().startswith('- ') or l.strip().startswith('* ')]
if added_items:
# Use first meaningful item as description
item_text = added_items[0][2:].strip()[:50] # Limit length
if skill_name:
return f"{skill_name} 技能 - {item_text}"
return f"更新 {filename} - {item_text}"
if skill_name:
return f"更新 {skill_name} 技能"
return f"更新技能定义文件"
# For regular markdown files with headers
if added_headers:
header_text = added_headers[0]
return f"更新 {filename} - 添加 {header_text} 部分"
# Check for doc updates
if added:
return f"更新 {filename} 文档内容"
elif removed:
return f"修改 {filename} 文档内容"
return f"更新 {filename}"
def get_function_description(filename: str) -> str | None:
"""
根据文件名获取功能描述。
优先使用预定义的映射,如果没有匹配则返回 None。
"""
for entry in FILE_TO_FUNCTION_MAP:
if re.search(entry['pattern'], filename, re.IGNORECASE):
return entry['description']
return None
def extract_class_names(added: List[str]) -> List[str]:
"""提取新增的类名。"""
class_names = []
for line in added:
# Python/JavaScript/TypeScript: class ClassName
match = re.search(r'class\s+(\w+)', line)
if match:
class_names.append(match.group(1))
return class_names
def extract_function_names(added: List[str]) -> List[str]:
"""提取新增的函数名。"""
func_names = []
for line in added:
# Python: def function_name(
match = re.search(r'def\s+(\w+)\s*\(', line)
if match:
func_names.append(match.group(1))
return func_names
def infer_intent_from_function_name(func_name: str) -> str | None:
"""根据函数名前缀推断意图。
示例:
- detect_skill_name -> 检测技能名
- infer_intent -> 推断意图
- analyze_code_changes -> 分析代码变更
- extract_function_names -> 提取函数名
- detect_modified_functions -> 检测被修改的函数
"""
for prefix, action in FUNCTION_PREFIX_ACTIONS.items():
if func_name.startswith(prefix):
# 提取函数名中剩余的部分
rest = func_name[len(prefix):]
# 如果剩余部分很短(如单个词),直接返回动作描述
if len(rest) < 4:
return action
# 常见的"新功能"类词组,这些情况下直接返回简洁动作
simple_suffixes = ('new', 'test', 'feature', 'item')
if rest.lower().endswith(simple_suffixes) or rest.lower() in simple_suffixes:
return action
# 按下划线分割
parts = rest.split('_')
# 过滤掉常见词,保留核心名词
filtered = [p for p in parts
if p.lower() not in ('name', 'file', 'path', 'content',
'data', 'message', 'lines', 'from',
'function', 'the', 'a', 'an', 'list',
'new', 'test', 'feature', 'removed',
'changes', 'added', 'modified', 'items')]
# 如果过滤后只剩通用词(names, functions),使用函数名本身
generic_terms = ('names', 'functions', 'data', 'items', 'values')
if not filtered or all(f.lower() in generic_terms for f in filtered):
# 返回函数名的后半部分(去掉前缀后的部分),用更友好的格式
return rest.replace('_', ' ')
readable = '_'.join(filtered) # 保留完整的剩余部分
return f"{action}({readable})"
return None
def extract_removed_function_names(removed: List[str]) -> List[str]:
"""提取被删除的函数名。"""
func_names = []
for line in removed:
match = re.search(r'def\s+(\w+)\s*\(', line)
if match:
func_names.append(match.group(1))
return func_names
def detect_modified_functions(added_funcs: List[str], removed_funcs: List[str]) -> List[str]:
"""检测被修改的函数(既被删除又新增的同名函数)。"""
modified = []
for func in added_funcs:
if func in removed_funcs:
modified.append(func)
return modified
def analyze_code_changes(added: List[str], removed: List[str], filename: str) -> str:
"""分析代码文件变更,生成有意义的描述。"""
# 先尝试获取功能描述
func_desc = get_function_description(filename)
# 提取新增和删除的类名和函数名
added_classes = extract_class_names(added)
removed_classes = extract_class_names(removed)
added_funcs = extract_function_names(added)
removed_funcs = extract_removed_function_names(removed)
added_imports = [l for l in added if 'import ' in l or 'from ' in l]
# 判断是否为修改(既有删除又有新增)
is_modification = len(removed) > 0 and len(added) > 0
has_new_funcs = len(added_funcs) > 0
has_new_classes = len(added_classes) > 0
# 检测被修改的函数
modified_funcs = detect_modified_functions(added_funcs, removed_funcs)
# 构建描述
# 优先处理修改场景(既有新增又有删除)
if is_modification:
if modified_funcs:
# 有函数被修改
intent = infer_intent_from_function_name(modified_funcs[0])
if intent:
return f"改进 {modified_funcs[0]}() - {intent}"
return f"改进 {modified_funcs[0]}() 函数"
if has_new_funcs:
# 新增了函数
intent = infer_intent_from_function_name(added_funcs[0])
if intent:
return f"改进 {filename} - 新增 {intent}"
return f"改进 {filename} - 新增 {added_funcs[0]}() 函数"
if func_desc:
return f"改进 {func_desc}"
return f"改进 {filename} 代码"
# 新增函数场景
if has_new_funcs:
intent = infer_intent_from_function_name(added_funcs[0])
if intent:
if len(added_funcs) == 1:
return f"新增 {intent}"
else:
return f"新增 {len(added_funcs)} 个函数 - {intent}"
return f"新增 {added_funcs[0]}() 函数"
if has_new_classes:
if func_desc:
return f"新增 {added_classes[0]} 类到 {func_desc}"
return f"新增 {added_classes[0]} 类"
# 检查关键词 - 生成更具体的描述
fix_keywords = ['fix', 'bug', '修复', '错误', '问题']
if any(any(kw in l.lower() for kw in fix_keywords) for l in added + removed):
if func_desc:
return f"修复 {func_desc} 中的问题"
return "修复代码问题"
refactor_keywords = ['refactor', '重构', '优化', 'improve', 'optimize']
if any(any(kw in l.lower() for kw in refactor_keywords) for l in added + removed):
if func_desc:
return f"重构 {func_desc}"
return "重构代码"
update_keywords = ['update', '改进', 'enhance', 'modify']
if any(any(kw in l.lower() for kw in update_keywords) for l in added + removed):
if func_desc:
return f"改进 {func_desc}"
return f"改进 {filename}"
if added_imports and func_desc:
return f"改进 {func_desc} - 新增导入"
if func_desc:
return f"改进 {func_desc}"
return f"改进 {filename} 代码"
def analyze_gitignore_changes(added: List[str], removed: List[str]) -> str:
"""分析 .gitignore 变更。"""
if added:
# Extract patterns that were added
patterns = [l.lstrip('#').strip() for l in added if l and not l.startswith('#')]
if patterns:
# Summarize the types of patterns
summary = []
for p in patterns[:3]: # Show up to 3 patterns
if '__pycache__' in p or '.pyc' in p:
summary.append('Python缓存')
elif '.log' in p:
summary.append('日志文件')
elif '.db' in p or 'sqlite' in p:
summary.append('数据库文件')
elif 'node_modules' in p:
summary.append('Node依赖')
elif 'logs' in p:
summary.append('日志目录')
elif '.playwright' in p:
summary.append('Playwright数据')
else:
summary.append(p)
if summary:
return f"更新 gitignore 忽略规则 - 添加 {', '.join(summary)}"
return "更新 gitignore 忽略规则"
def analyze_config_changes(added: List[str], removed: List[str], filename: str) -> str:
"""分析配置文件变更,生成具体的描述。"""
if not added:
if removed:
removed_keys = [l.split(':')[0].strip() for l in removed if ':' in l]
if removed_keys:
return f"更新 {filename} - 移除 {', '.join(removed_keys[:2])} 配置"
return f"更新 {filename} 配置"
# 分析添加的配置项
added_keys = []
semantic_descriptions = []
for line in added:
if ':' in line:
key = line.split(':')[0].strip()
added_keys.append(key)
# 查找语义描述
key_lower = key.lower()
for config_key, meaning in CONFIG_KEY_MEANINGS.items():
if config_key in key_lower:
if meaning not in semantic_descriptions:
semantic_descriptions.append(meaning)
break
# 分析上下文:查找配置所属的分组(如 level3, level4)
context_info = []
for line in added:
line_stripped = line.strip()
# 检测 YAML 层级标识(如 "level3:", "level4:")
if line_stripped and not line_stripped.startswith('#'):
# 检测缩进级别,判断是否是子配置
indent = len(line) - len(line.lstrip())
if indent == 0 and line_stripped.endswith(':'):
# 顶级配置项
section_name = line_stripped[:-1]
context_info.append(section_name)
# 构建描述
if semantic_descriptions:
# 有语义描述
desc = '、'.join(semantic_descriptions[:3])
# 如果有上下文信息,添加到描述中
if context_info:
context = context_info[0]
# 将 level3 转换为更友好的描述
context_map = {
'level1': '一级标题',
'level2': '二级标题',
'level3': '三级标题',
'level4': '四级标题',
'level5': '五级标题',
'level6': '六级标题',
}
context_desc = context_map.get(context, context)
return f"更新 {filename} - 为 {context_desc} 添加 {desc} 配置"
return f"更新 {filename} - 添加 {desc} 配置"
if added_keys:
# 没有语义匹配,使用原始 key
return f"更新 {filename} - 添加 {', '.join(added_keys[:3])} 配置"
return f"更新 {filename} 配置"
def analyze_generic_changes(added: List[str], removed: List[str], filename: str) -> str:
"""分析通用文件变更。"""
if added:
return f"更新 {filename}"
elif removed:
return f"删除 {filename} 中的内容"
return f"修改 {filename}"
def generate_detail_lines(files: List[str], category: str) -> str:
"""
根据变更文件生成详细信息行。
Args:
files: 变更文件列表
category: 变更类别
Returns:
详细信息字符串
"""
lines = []
for filepath in files:
filename = filepath.split('/')[-1]
# Get diff content for analysis
diff = get_file_diff(filepath)
# Analyze and generate description
description = analyze_diff_content(diff, filename)
lines.append(f"- {description}")
return "\n".join(lines)
def generate_commit_messages(groups: Dict[str, List[str]]) -> Dict[str, str]:
"""
为所有分组生成提交信息。
Args:
groups: 变更类别到文件列表的映射
Returns:
类别到提交信息的映射
"""
messages = {}
for category, files in groups.items():
messages[category] = generate_commit_message(category, files)
return messages
def add_issue_reference(
message: str,
github_issue: str | None = None,
local_ref: str | None = None,
) -> str:
"""
Add issue/task traceability to a generated commit message.
GitHub issues use a subject suffix like "(#13)" so git log can show the
source issue at a glance. This helper only writes references. Closing
semantics such as "Closes #13" belong to git-workflow, not this shortcut.
"""
if not github_issue and not local_ref:
return message
lines = message.split('\n')
subject = lines[0]
body = '\n'.join(lines[1:]).strip()
reference = ""
if github_issue:
issue_num = github_issue.strip().lstrip('#')
suffix = f"(#{issue_num})"
if suffix not in subject:
subject = f"{subject} {suffix}"
reference = f"Refs #{issue_num}"
elif local_ref:
reference = f"Refs: {local_ref.strip()}"
if reference and reference not in body:
body = f"{reference}\n\n{body}".strip()
if body:
return f"{subject}\n\n{body}"
return subject
def main():
"""命令行使用的主入口。"""
parser = argparse.ArgumentParser(
description='生成约定式提交信息'
)
parser.add_argument(
'--category',
type=str,
help='变更类别 (deps, docs, feat 等)'
)
parser.add_argument(
'--files',
nargs='+',
help='变更文件列表'
)
parser.add_argument(
'--issue',
type=str,
help='关联的 GitHub Issue 编号,例如 13 或 #13;标题会追加 (#13)'
)
parser.add_argument(
'--local-ref',
type=str,
help='关联的本地任务引用,例如 "project-task Issue #13",不会关闭 GitHub Issue'
)
args = parser.parse_args()
if args.category and args.files:
msg = generate_commit_message(args.category, args.files)
msg = add_issue_reference(
msg,
github_issue=args.issue,
local_ref=args.local_ref,
)
print(msg)
else:
print("用法: generate_commit_message.py --category <类型> --files <文件1> [文件2...]")
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""
Interactive batch commit tool for Git.
Groups changes by type and helps create multiple focused commits
instead of one large mixed commit.
"""
import subprocess
import sys
import json
from pathlib import Path
from typing import Dict, List
# Import sibling scripts
sys.path.insert(0, str(Path(__file__).parent))
from categorize_changes import get_staged_files, group_changes
from generate_commit_message import add_issue_reference, generate_commit_messages
def stage_files(files: List[str]) -> bool:
"""Stage files for commit."""
if not files:
return True
try:
subprocess.run(
['git', 'add'] + files,
capture_output=True,
check=True
)
return True
except subprocess.CalledProcessError as e:
print(f"暂存文件时出错: {e}", file=sys.stderr)
return False
def unstage_files(files: List[str]) -> bool:
"""Unstage files to reorganize commits."""
if not files:
return True
try:
subprocess.run(
['git', 'reset', 'HEAD'] + files,
capture_output=True,
check=True
)
return True
except subprocess.CalledProcessError as e:
print(f"取消暂存文件时出错: {e}", file=sys.stderr)
return False
def create_commit(message: str) -> bool:
"""Create a git commit with the given message (supports multi-line)."""
try:
# Use -m multiple times for multi-line commit message
# First line is the subject, subsequent lines are the body
lines = message.split('\n')
cmd = ['git', 'commit']
for line in lines:
cmd.extend(['-m', line])
subprocess.run(
cmd,
capture_output=True,
check=True
)
return True
except subprocess.CalledProcessError as e:
print(f"创建提交时出错: {e}", file=sys.stderr)
print(f"stderr: {e.stderr.decode()}", file=sys.stderr)
return False
def display_groups(groups: Dict[str, List[str]], messages: Dict[str, str]):
"""Display grouped changes with proposed commit messages."""
print("\n" + "=" * 60)
print("提议的提交分组")
print("=" * 60)
for i, (category, files) in enumerate(sorted(groups.items()), 1):
msg = messages.get(category, f"{category.title()}: 更新文件")
print(f"\n[分组 {i}] {msg}")
print(f"类别: {category}")
print(f"文件 ({len(files)} 个):")
for f in sorted(files):
print(f" - {f}")
print("\n" + "=" * 60)
def is_interactive() -> bool:
"""Check if running in an interactive terminal."""
return sys.stdin.isatty()
def confirm_groups(skip_confirm: bool = False) -> bool:
"""Ask user to confirm the proposed grouping.
Args:
skip_confirm: If True, skip confirmation and proceed automatically
"""
if skip_confirm:
return True
print("\n选项:")
print(" y - 是,创建这些提交")
print(" n - 否,取消")
while True:
try:
response = input("\n是否继续创建这些提交? [y/n]: ").strip().lower()
except (EOFError, OSError):
print("\n检测到非交互式环境,已取消操作。")
print("提示:使用 --yes 参数跳过确认,或使用 --dry-run 仅查看分组")
return False
if response in ['y', 'yes', '是']:
return True
elif response in ['n', 'no', '否']:
return False
else:
print("请输入 'y' 或 'n'。")
def decorate_messages(
groups: Dict[str, List[str]],
messages: Dict[str, str],
issue: str | None = None,
local_ref: str | None = None,
) -> Dict[str, str]:
"""Add issue/task references to generated commit messages."""
if not issue and not local_ref:
return messages
decorated = {}
for category, message in messages.items():
decorated[category] = add_issue_reference(
message,
github_issue=issue,
local_ref=local_ref,
)
return decorated
def batch_commit(
skip_confirm: bool = False,
issue: str | None = None,
local_ref: str | None = None,
):
"""Main function to perform batch commit.
Args:
skip_confirm: If True, skip confirmation and proceed automatically
"""
print("Git 批量提交工具")
print("=" * 60)
# Get currently staged files
staged = get_staged_files()
if not staged:
print("未发现已暂存的变更。")
print("请先使用 git add <files> 暂存一些变更")
return 1
print(f"发现 {len(staged)} 个已暂存文件")
# Group changes by category (using already staged files)
groups = group_changes(staged, staged=True)
# Generate commit messages for each group (files are already staged)
messages = generate_commit_messages(groups)
messages = decorate_messages(
groups,
messages,
issue=issue,
local_ref=local_ref,
)
# Display proposed groups
display_groups(groups, messages)
# Confirm with user
if not confirm_groups(skip_confirm=skip_confirm):
print("\n已取消。")
return 0
# Unstage everything first to regroup
if not unstage_files(staged):
print("错误:无法取消暂存文件。")
return 1
# Create commits for each group
print("\n正在创建提交...")
success_count = 0
total_count = len(groups)
for category, files in sorted(groups.items()):
msg = messages.get(category, f"{category.title()}: 更新文件")
# Stage files for this commit
print(f"\n → {msg}")
if not stage_files(files):
print(f" 无法为 {category} 暂存文件")
continue
# Create commit
if create_commit(msg):
print(f" ✓ 已提交 {len(files)} 个文件")
success_count += 1
else:
print(f" ✗ 提交失败")
# Summary
print("\n" + "=" * 60)
print(f"批量提交完成:{success_count}/{total_count} 个提交已创建")
print("=" * 60)
return 0 if success_count == total_count else 1
def main():
"""Entry point."""
import argparse
parser = argparse.ArgumentParser(
description='Interactive batch commit tool for Git',
epilog='示例: %(prog)s --yes # 自动确认并创建提交'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='显示将要提交的内容而不实际提交'
)
parser.add_argument(
'--yes', '-y',
action='store_true',
help='跳过交互式确认,自动创建提交(适用于 CI/CD 或非交互式环境)'
)
parser.add_argument(
'--issue',
type=str,
help='关联的 GitHub Issue 编号,例如 13 或 #13;每个提交标题会追加 (#13)'
)
parser.add_argument(
'--local-ref',
type=str,
help='关联的本地任务引用,例如 "project-task Issue #13",不会关闭 GitHub Issue'
)
args = parser.parse_args()
if args.dry_run:
# Just show grouping without committing
staged = get_staged_files()
if not staged:
print("未发现已暂存的变更。")
return 0
groups = group_changes(staged, staged=True)
messages = generate_commit_messages(groups)
messages = decorate_messages(
groups,
messages,
issue=args.issue,
local_ref=args.local_ref,
)
display_groups(groups, messages)
return 0
else:
return batch_commit(
skip_confirm=args.yes,
issue=args.issue,
local_ref=args.local_ref,
)
if __name__ == '__main__':
sys.exit(main())