“今天的进度写一下,下班前发群里。”
这句看似普通的要求,可能是很多程序员一天中最不想面对的时刻。不是因为你没干活,而是因为你在脑子里快速回溯一天的工作时,发现能说出来的东西远没有你以为的多:修了两个 Bug、调了一个接口、开会开到下午四点、还有一个需求没写完。然后你打开聊天框,开始把事实加工成一封 200 字的“汇报文学”。
问题出在哪里?出在很多团队把“进度”当成一种临场回忆,而不是一种日常沉淀的数据。开发者的代码提交、分支合并、需求关闭,这些动作本身就在产生进度信息,但它们散落在 Git 日志、项目管理工具和聊天记录里,到需要汇报时又重新靠人脑去汇总一遍。
这篇文章要讲的是,如何把“今天的进度”从一项靠记忆和作文能力完成的任务,变成一套基于 Git 提交记录自动生成的工程产物。我会从提交信息规范讲起,再给出一份可直接运行的 Python 脚本,让你能用一条命令生成当天的 Markdown 日报、一周的汇总统计,以及可直接导入 Excel 的 CSV 数据。读完你能直接在自己的电脑上跑通这套流程,并且知道在实际团队里推广时最容易踩哪些坑。
1. 为什么“今天的进度”总是写不好
先想一个问题:如果你负责的功能已经用 Git 提交了几十个 commit,为什么让你写“今天做了什么”的时候,你还是会卡住?
原因是“写代码”和“写进度”是两套完全不同的思维活动。写代码时你面对的是具体的技术问题,专注点在函数、接口、数据流上;写进度时你要做的是从时间维度回放自己的一天,把零散的改动归类、概括、翻译成别人能看懂的语言。这个转换过程有真实的认知成本,而且发生在一天最疲惫的时刻。
更麻烦的是,很多团队的进度汇报没有统一格式。有人按“做了什么”写,有人按“遇到了什么困难”写,有人写成了流水账,有人写成了邀功文学。管理者其实很难从这些文本里判断真实进展,而开发者又觉得写这些浪费时间。双方都觉得对方有问题。
但换个角度看,每个开发者一天真正交付的东西,其实已经写进了 Git 历史里。你改动了哪些文件、提交了多少次、提交信息写的是什么、对应哪个需求分支,这些都是客观存在的数据。问题不在于“今天没有进度”,而在于“进度从未被结构化采集,每次都要重新加工”。
所以我更倾向的判断是:进度管理的核心矛盾,不是人不够勤快,而是信息的产生和消费之间缺了一条自动化管道。开发者在写 commit message 的时候,实际上已经在做一次轻量级的进度记录;只需要把它变成规范和工具,就能在汇报时省掉大量回忆和整理的时间。
2. 核心思路:让 Git 提交记录成为进度的事实来源
这套方案的基本思想是:把 Git 提交记录当作开发进度的唯一事实来源,然后用脚本把它转换成日报、周报和统计表。
为什么选 Git 提交记录作为事实来源?有三个理由。
第一,它无法抵赖。代码改动在 Git 里有完整的哈希记录和历史时间线,比人脑回忆可靠得多。第二,它本来就有时间信息。每次提交都有 author 和 commit 时间,天然适合按天、按周聚合。第三,它包含了语义信息。只要你给 commit message 规定格式,提交信息本身就是在给代码变更打标签,比如“修复了什么”“新增了什么”“重构了什么”。
一个开发者的日变化轨迹其实是这样的:早晨拉分支,上午写代码,提交时写下feat: 增加订单导出功能,下午修 Bug,提交时写下fix: 修复金额精度丢失问题。这些 commit message 连起来,就是当天最真实的开发记录。
为了让机器能处理,我们还需要一个四层模型:
| 层次 | 内容 | 说明 |
|---|---|---|
| 第一层 | Commit Message | 原子变更的描述,是源数据 |
| 第二层 | 标签与类型 | type(scope): subject,例如 feat、fix、docs |
| 第三层 | 周期聚合 | 按天/人/需求汇总成日报、周报 |
| 第四层 | 复盘与度量 | 统计类型分布、提交频率、任务完成度 |
如果前面的 commit message 不遵守规范,后面的脚本解析就是空谈。所以这套方案的第一个落地动作,不是写脚本,而是先统一提交规范。
3. 环境准备与前置条件
这套工具的运行时依赖非常轻,适合在任何开发者机器上使用。本文示例以通用方式演示,版本号请以你实际环境为准。
- 操作系统:Windows / macOS / Linux 均可。Windows 建议在 Git Bash 或 PowerShell 下执行。
- Git:2.23 及以上版本即可,
git log的常用参数在旧版本也基本可用。 - Python:3.8 及以上版本,使用标准库
subprocess、datetime、collections,不需要安装第三方包。 - 代码仓库:一个已经用 Git 管理的项目仓库,或者一个专门用于记录每日工作的“进度仓库”。
如果不想使用 Python,也可以用 Shell、Node.js 或 Go 实现同样的逻辑,核心思路是调用git log拿到结构化提交数据,再按类型聚合。选择 Python 只是因为它写起来直观,跨平台也方便。
验证环境是否可用,可以执行以下命令:
git --version python --version在项目根目录执行:
git log --oneline -5如果能正常输出最近 5 条提交,说明环境没问题。
4. 第一步:统一提交规范,让 commit 从源头可解析
要让脚本准确解析“今天做了什么”,每条提交信息应该符合下面的格式:
<type>(<scope>): <subject>示例:
feat(order): 增加订单导出功能 fix(pay): 修复金额精度丢失问题 docs(readme): 补充部署说明 test(cart): 增加购物车单元测试 refactor(auth): 拆分登录校验逻辑 chore: 升级依赖版本常用类型及含义如下:
| 类型 | 含义 | 日报展示建议 |
|---|---|---|
| feat | 新功能 | 新增了…… |
| fix | Bug 修复 | 修复了…… |
| docs | 文档变更 | 更新了文档…… |
| refactor | 重构,不改行为 | 重构了…… |
| test | 测试相关 | 补充/调整了测试…… |
| chore | 构建、依赖、杂项 | 完成……维护事项 |
| style | 格式调整 | 调整了代码风格 |
| perf | 性能优化 | 优化了……性能 |
这里真正容易踩坑的地方是:如果没有 scope,也不要强求。scope 是为了快速定位模块,小型项目完全可以不写,直接写fix: 修复登录时密码校验失败的问题。解析脚本要兼容“有 scope”和“无 scope”两种格式。
在项目根目录创建.gitmessage模板文件,内容如下:
# 提交信息模板示例: # <type>(<scope>): <subject> # # 常用类型: # feat: 新功能 # fix: Bug 修复 # docs: 文档变更 # refactor: 重构 # test: 测试 # chore: 构建/依赖/杂项 # # 示例: # feat(order): 增加订单导出功能然后设置 Git 使用该模板:
git config commit.template .gitmessage这样每次执行git commit时,编辑器会自动带上提示模板,降低团队成员的记忆成本。
还可以配置一个带类型校验的提交命令,减少低级错误。例如在.bashrc或.zshrc中增加一个函数:
# 快速提交示例,格式: gc feat "增加订单导出功能" gc() { if [ $# -ne 2 ]; then echo "用法: gc <type> \"<subject>\"" return 1 fi git add -A && git commit -m "$1: $2" }对于团队项目,更规范的做法是引入 commitlint + husky,在提交时直接拦截不合法格式。但这部分依赖 Node.js 工具链,不在本文的必选范围内。个人使用阶段,用模板加 alias 就足够。
需要明确:提交规范不是目的,自动化解析才是目的。如果提交信息不统一,后面的脚本能统计到的就只有“提交次数”,无法告诉你“今天完成了什么类型的工作”。
5. 第二步:编写日报生成脚本
下面给出一个可直接运行的 Python 脚本。它的作用是:读取 Git 历史中某一天的提交记录,按类型分组,生成一份 Markdown 日报。
文件路径:daily-report.py
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 基于 Git 提交记录生成开发日报。 用法: python daily-report.py # 生成今天的日报 python daily-report.py 2025-06-10 # 生成指定日期的日报 """ import subprocess import sys from datetime import date, datetime from collections import OrderedDict # 类型与中文展示名 TYPE_LABELS = OrderedDict([ ("feat", "新增功能"), ("fix", "Bug 修复"), ("perf", "性能优化"), ("refactor", "代码重构"), ("docs", "文档变更"), ("test", "测试补充"), ("chore", "维护事项"), ("style", "样式调整"), ("build", "构建相关"), ("ci", "持续集成"), ("other", "其他"), ]) def run_git_log(target_date: str) -> str: """获取指定日期的 git 提交信息,按提交时间倒序。""" start = f"{target_date} 00:00:00" end = f"{target_date} 23:59:59" cmd = [ "git", "log", "--since", start, "--until", end, "--pretty=format:%h|%an|%s", "--date-order", ] result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8") if result.returncode != 0: raise RuntimeError(f"git log 执行失败: {result.stderr}") return result.stdout.strip() def parse_commit_line(line: str): """解析一行提交记录,返回 (hash, author, message)。""" hash_val, author, message = line.split("|", 2) return hash_val, author, message.strip() def classify_message(message: str) -> str: """从 commit message 中解析类型。""" if ":" in message: prefix = message.split(":", 1)[0].strip() # 兼容 feat(order) 这种带 scope 的写法 if "(" in prefix: prefix = prefix.split("(", 1)[0] if prefix in TYPE_LABELS: return prefix return "other" def format_items(items): """将同类型提交格式化为 markdown 列表。""" lines = [] for hash_val, author, message in items: # 去掉类型前缀,让日报更简洁 display = message if ":" in message: display = message.split(":", 1)[1].strip() lines.append(f"- [{hash_val}] {display}({author})") return lines def generate_markdown(target_date: str) -> str: raw = run_git_log(target_date) if not raw: return f"# {target_date} 开发日报\n\n当天没有提交记录。\n" grouped = OrderedDict((key, []) for key in TYPE_LABELS.keys()) for line in raw.splitlines(): hash_val, author, message = parse_commit_line(line) type_key = classify_message(message) grouped[type_key].append((hash_val, author, message)) md_lines = [f"# {target_date} 开发日报", ""] total = sum(len(v) for v in grouped.values()) md_lines.append(f"提交总数:{total} 次") md_lines.append("") for type_key, label in TYPE_LABELS.items(): items = grouped[type_key] if not items: continue md_lines.append(f"## {label}({len(items)})") md_lines.extend(format_items(items)) md_lines.append("") return "\n".join(md_lines) def main(): if len(sys.argv) > 1: target_date = sys.argv[1] datetime.strptime(target_date, "%Y-%m-%d") else: target_date = date.today().isoformat() content = generate_markdown(target_date) filename = f"daily-report-{target_date}.md" with open(filename, "w", encoding="utf-8") as f: f.write(content) print(f"日报已生成: {filename}") print(content) if __name__ == "__main__": main()这段脚本的逻辑并不复杂,关键点有三个:
第一,通过git log --since --until过滤出某一天的提交,而不是简单用“最近 24 小时”。因为开发者经常早上 9 点提交昨天的收尾代码,如果按 24 小时窗口统计,时间边界就会错乱。
第二,解析 commit message 时兼容了feat: xxx和feat(order): xxx两种格式。只要冒号前的单词属于 TYPE_LABELS 列表,就算解析成功,不支持的格式统一归为“其他”。
第三,输出的是 Markdown 文件,方便直接粘贴到企业微信、钉钉、飞书或者内部 Wiki。Markdown 是团队协作中最通用的格式,也比纯文本有结构感。
如果你想把日报直接输出到终端而不是文件,也可以把main()里写文件的部分注释掉。无论如何,先跑通一次再改细节。
6. 第三步:生成周报汇总与 CSV 统计
日报解决了“今天做了什么”的问题,但管理者通常还要看“这一周整体节奏如何”。周报不需要像日报那样逐条列出提交,而是要体现结构和趋势。
继续用 Python 写一个周报脚本,读取过去 7 天的提交记录,统计各类型提交的数量、活跃天数、涉及人数,并生成 CSV 文件用于 Excel 分析。
文件路径:weekly-report.py
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 基于 Git 提交记录生成周报汇总。 用法: python weekly-report.py # 生成最近 7 天的周报 python weekly-report.py 2025-06-10 # 以指定日期为结束日期,统计前 6 天 """ import subprocess import sys import csv from datetime import date, datetime, timedelta from collections import OrderedDict, Counter TYPE_LABELS = OrderedDict([ ("feat", "新增功能"), ("fix", "Bug 修复"), ("perf", "性能优化"), ("refactor", "代码重构"), ("docs", "文档变更"), ("test", "测试补充"), ("chore", "维护事项"), ("style", "样式调整"), ("build", "构建相关"), ("ci", "持续集成"), ("other", "其他"), ]) def get_week_range(end_date: str): """返回从 end_date-6 到 end_date 的日期列表。""" end = datetime.strptime(end_date, "%Y-%m-%d").date() start = end - timedelta(days=6) return start, end, [start + timedelta(days=i) for i in range(7)] def run_git_log_in_range(start: str, end: str) -> str: cmd = [ "git", "log", "--since", f"{start} 00:00:00", "--until", f"{end} 23:59:59", "--pretty=format:%ad|%h|%an|%s", "--date=format:%Y-%m-%d", "--date-order", ] result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8") if result.returncode != 0: raise RuntimeError(f"git log 执行失败: {result.stderr}") return result.stdout.strip() def classify_message(message: str) -> str: if ":" in message: prefix = message.split(":", 1)[0].strip() if "(" in prefix: prefix = prefix.split("(", 1)[0] if prefix in TYPE_LABELS: return prefix return "other" def main(): if len(sys.argv) > 1: end_date = sys.argv[1] datetime.strptime(end_date, "%Y-%m-%d") else: end_date = date.today().isoformat() start, end, day_list = get_week_range(end_date) raw = run_git_log_in_range(start.isoformat(), end.isoformat()) type_counter = Counter() author_counter = Counter() day_counter = Counter() rows = [] for line in raw.splitlines(): parts = line.split("|", 3) if len(parts) < 4: continue day_str, hash_val, author, message = parts type_key = classify_message(message) type_counter[type_key] += 1 author_counter[author] += 1 day_counter[day_str] += 1 rows.append([day_str, hash_val, author, message]) # 输出 CSV 明细 csv_filename = f"weekly-detail-{start}-{end}.csv" with open(csv_filename, "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["日期", "提交哈希", "作者", "提交信息"]) writer.writerows(rows) # 输出周报摘要到 Markdown md_lines = [ f"# 开发周报({start} ~ {end})", "", f"- 提交总数:{len(rows)} 次", f"- 活跃天数:{len(day_counter)} 天", f"- 参与人数:{len(author_counter)} 人", "", "## 按类型统计", "", "| 类型 | 数量 |", "| --- | --- |", ] for type_key in TYPE_LABELS.keys(): cnt = type_counter.get(type_key, 0) if cnt > 0: md_lines.append(f"| {TYPE_LABELS[type_key]} | {cnt} |") md_lines.append("") md_lines.append("## 按作者统计") md_lines.append("") md_lines.append("| 作者 | 提交数 |") md_lines.append("| --- | --- |") for author, cnt in author_counter.most_common(): md_lines.append(f"| {author} | {cnt} |") md_filename = f"weekly-report-{start}-{end}.md" with open(md_filename, "w", encoding="utf-8") as f: f.write("\n".join(md_lines)) print(f"周报已生成: {md_filename}") print(f"CSV 明细已生成: {csv_filename}") if __name__ == "__main__": main()注意 CSV 写入时使用了utf-8-sig编码,这是为了让 Excel 直接打开时中文不乱码。这个小细节在 Windows 环境下尤其重要。
周报汇总里最值得关注的是“活跃天数”。如果一个成员一周 7 天只有 2 天有提交,可能是需求周期长、提交习惯不好,也可能是确实在划水。它不能直接说明绩效,但能辅助回顾。这个指标的价值在于发现问题,而不是做评判。
7. 运行方式与效果验证
脚本写完后,建议先用一个有历史提交的仓库做一次验证。假设今天是 2025 年 6 月 10 日,执行:
python daily-report.py 2025-06-10预期会生成daily-report-2025-06-10.md,内容大致如下:
# 2025-06-10 开发日报 提交总数:5 次 ## 新增功能(2) - [a1b2c3] 增加订单导出功能(张三) - [d4e5f6] 增加批量导入接口(李四) ## Bug 修复(2) - [g7h8i9] 修复金额精度丢失问题(张三) - [j0k1l2] 修复登录态过期后跳转异常(王五) ## 文档变更(1) - [m3n4o5] 补充部署说明(李四)如何判断脚本运行成功?看三点:
- 脚本退出码为 0,无异常输出。
- 生成的 Markdown 文件存在,且提交总数与
git log手动查询的结果一致。 - 分类符合预期。如果某条提交被分到“其他”,说明 commit message 的类型前缀没写对,需要回去检查。
之后可以运行周报:
python weekly-report.py预期生成两个文件:weekly-report-2025-06-04-2025-06-10.md和对应的 CSV。打开 CSV,如果中文不乱码,说明utf-8-sig生效了。
如果想每天自动生成日报,可以把脚本加入定时任务。以 Linux/macOS 的 crontab 为例,每天 18:30 生成当天日报:
30 18 * * * cd /path/to/your/repo && python /path/to/daily-report.py >> /path/to/report-cron.log 2>&1Windows 用户可以在“任务计划程序”里创建基本任务,触发器选“每天”,操作填 Python 解释器路径和脚本路径。
8. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
git log输出为空 | 当天确实没有提交,或日期参数格式错误 | 手动执行 git log 加相同日期参数 | 检查日期格式是否为 YYYY-MM-DD;若仓库在--since时间之后才有提交,调整日期 |
| 某些提交被归为“其他” | commit message 没写类型前缀,或使用了自定义类型 | 查看原始提交信息:git log --pretty=format:%s | 统一规范;如有定制类型,在 TYPE_LABELS 中补充映射 |
| 中文乱码 | 编码不一致 | 检查终端和文件编码 | 统一使用 UTF-8;CSV 使用 utf-8-sig 编码 |
| 多行 commit message 解析异常 | --pretty=format只保留主题行,但部分提交有 body | 查看完整提交信息:git show <hash> | 脚本默认只取 subject,如需正文内容需扩展解析逻辑 |
| 时间边界不对 | 使用了错误的时区或日期窗口 | 用git log --format=%ad对比时间 | 确认--since/--until使用本地时间;如需按作者时间而非提交时间,改参数 |
| 合并导致统计虚高 | merge 提交也被计入 | 查看提交记录中是否有 Merge 行 | 在 git log 增加--no-merges参数 |
| 未推送的本地提交计入 | 脚本统计的是本地仓库历史 | 确认统计预期 | 这是正常行为;如只需已推送提交,可加origin/main..HEAD过滤 |
| 两人共用账号导致统计不准 | Git user.name 配置相同 | 查看提交者信息:git log --pretty=format:%an | 为每个成员单独配置 user.name / user.email |
关于安全提醒:这条一定要记住。提交信息中不要写敏感信息,比如账号密码、Token、密钥或者客户内部数据。因为 Git 历史是持久化数据,即使删掉旧提交,也可能残留在 reflog 或远端历史中。这套脚本本身只是读取 git log,并不修改任何 Git 数据,相对安全,但输入的原始数据如果包含敏感内容,风险在源头。
9. 最佳实践与工程建议
这套方案能跑通很容易,真正难的是长期坚持。以下几条建议来自我在实际项目中的观察,按优先级排序。
第一,提交信息要按“逻辑单位”提交,不要按时间堆砌。一次提交只做一件事,fix: 修复登录校验的 Token 过期判断就是一次提交。不要出现“提交一下,改动杂七杂八”的情况,否则类型统计和回溯定位都会失真。
第二,每天结束前花两分钟整理提交。不需要天天跑脚本,但至少执行一次git status和git log --oneline,确认今天的工作都被合理记录。如果发现某个改动忘了提交,趁当天补上,不要等到第二天再补。
第三,日报自动生成后,仍然需要人工写一句“结论”。脚本只能告诉你做了什么,不能告诉你事情做得顺不顺利。我建议的格式是:自动生成的清单 + 一句人工总结,比如“协议字段已联调完成,明天开始适配客户端”。这样既保住了数据真实性,又保留了人的判断。
第四,不要为了统计好看而拆分提交。有人为了让“提交次数”好看,把一次改动硬拆成五次提交。这是典型的指标化反噬。提交次数只是参考维度,不是 KPI。如果团队真的需要考核工作量,也应该结合需求完成数、代码评审意见、线上故障数等综合评估,而不是只看 commit 数量。
第五,脚本要放在仓库之外,或者使用独立的进度仓库。如果你把这套工具脚本直接提交到业务项目里,容易污染业务仓库的统计结果。更推荐的做法是:单独建一个work-log仓库,用来记录日常工作的提交;或者把脚本放在~/.local/bin目录下,对所有项目通用。
第六,与研发流程工具结合会更有效。如果公司使用 Jira、禅道、飞书、Teambition 之类的工具,可以在 commit message 里加上需求编号,例如feat(ORDER-123): 增加订单导出功能。这样就能从 Git 提交反查到需求卡片,实现从需求到代码的完整追踪。业界把这个模式叫做“可追溯提交”,比单独的进度文档要可靠得多。
第七,从个人推广到团队时,先说服一两个人试点。直接要求整个团队用提交规范,抵触情绪大概率会很大。更好的路径是:先自己用两周,生成几份好看的日报,在周会上展示“原来提交记录能自动形成进度报告”,让同事看到收益,再逐步推广。规范从来不是靠命令落地的,而是靠工具让遵守规范的人更省力。
10. 总结与后续方向
把“今天的进度”写清楚,本质上不是文笔问题,而是工程问题。开发者的每一天都由数十次代码变更组成,这些变更有精确的时间戳和语义描述,它们已经是最好的进度记录。你需要做的只是定一套提交规范,写一个不到 150 行的解析脚本,然后把精力留给真正需要人的判断的地方。
本文通过一个可运行的 Python 脚本,演示了从 Git 提交记录到日报、周报、CSV 统计的完整链路。你可以直接复制代码到本地验证,也可以根据自己的项目情况调整 TYPE_LABELS、日期窗口、输出格式。这里没有复杂的架构,没有新增服务,所有东西都建立在 Git 本身的能力之上,这也是它容易被采纳的原因。
下一步值得深入的方向有三个:第一,把提交信息里的需求编号解析出来,与项目管理系统打通,形成自动更新的需求状态。第二,给脚本增加增量报告逻辑,记录“连续提交天数”,帮助自己复盘节奏是否健康。第三,结合代码评审数据,把合并请求的评审耗时也纳入周报,从提交到合入的完整链路让进度记录更完整。
最后提醒一句:工具可以帮你生成报告,但没法替你判断“今天做得怎么样”。留出两分钟看一遍自动生成的日报,比让脚本自动发到群里更有价值。长期积累之后,你会发现自己对项目的感知能力和对时间的掌控能力,都会比靠记忆写日报的时候好很多。建议把这套脚本收藏下来,周末找一个小仓库试跑一次,再决定要不要用到日常工作中。