记录今天的进度看起来是一件很小的事但它直接影响到个人复盘、团队协作和项目排期。很多开发者并不是没有干活而是到了下班前说不清自己究竟完成了什么。原因在于进度信息散落在多个地方任务看板里只有状态Git 提交里只有片段聊天记录和本地笔记又很难汇总。如果能把“今天的进度”从一个模糊回忆变成一个可查询、可统计、可回溯的数据记录研发协作的质量会提升不少。这篇文章会围绕如何采集 Git 提交、补充人工备注、持久化到 SQLite、生成 Markdown 日报逐步搭建一套个人开发进度记录工具。适用读者是后端开发者、技术负责人以及每天需要写日报或周报的工程师。文章的默认场景是单仓库、单人开发或小团队协作不依赖数据库服务也不需要额外部署。所用技术是 Python 3.10 标准库和 Git 命令核心目标是让日报不再是口头描述而是从真实工程数据中沉淀出来。1. 为什么要把“今天的进度”当成一个工程问题1.1 常见记录方式的缺口开发进度的传统保存方式大致有三种口头汇报、任务看板、Git 日志。它们各自都有明显的信息缺口。口头汇报的问题在于不可校验。汇报说“今天在改订单模块”但改了什么、是否提交、是否遇到阻塞都没有证据。任务看板能记录任务状态比如从“进行中”变成“已完成”但它不会自动记录代码变更、提交时间和具体改动范围。Git 日志是最可靠的代码变更来源它天然包含提交人、提交时间、提交说明和 commit hash缺点是信息碎片化一条提交只代表一次操作不能直接回答“今天整体完成了什么”。所以一个合格的进度记录系统必须把不同来源的数据拼接起来。Git 提供客观事实任务备注补充主观上下文看板状态补充业务进度最后统一成一份结构化记录。1.2 一条有效进度记录的数据结构在设计工具之前要先定义“今天”的数据结构。一条有效的每日进度记录至少需要包含这几个字段字段含义来源report_date日期精确到天当前日期或指定日期author提交人Git 日志total_commits当日提交总数Git 统计commit_list提交明细Git 日志type_stats按提交类型分组统计提交信息解析blocks阻塞事项手工补充tomorrow_plan明日计划手工补充notes备注与上下文手工补充日期和提交数据是客观的阻塞和明日计划是主观的。两者结合才能让日报既有数据支撑又有业务上下文。1.3 采集、存储、展示的分层思路从工程上看这个工具可以分成四层。第一层是采集层负责从 Git 仓库读取当天提交同时读取本地的备注文件。第二层是统计层负责对提交信息做分类汇总。第三层是存储层把统计结果写入 SQLite保证历史数据可以回溯。第四层是展示层把数据渲染成 Markdown 日报。这样分层的好处是每一层都可以独立替换。如果后续想接入团队内部的研发管理系统只需要替换采集层。如果不想用 Markdown可以换成 HTML 或直接推送到企业微信、飞书、钉钉的 webhook。核心数据模型不变扩展成本很低。2. 让 Git 日志成为今日进度的数据源2.1 为什么首选 Git 日志Git 日志是开发者每天都会产生的客观记录它有几大优势。第一提交时间由版本控制工具记录难以手工篡改。第二提交人与作者信息明确可以区分个人贡献。第三提交内容与代码变更强关联能追溯到一个具体的改动。第四Git 已经集成在开发流程中不需要额外录入。但 Git 日志也有局限。它只记录已提交的改动工作区中未提交的代码不会被统计。如果一个人写了一天代码但始终没有 commitGit 日志里就是空白。此外提交信息质量参差不齐如果大家习惯写update、fix、test这类模糊信息统计出来也没意义。因此把 Git 日志作为数据源时要先解决两个问题如何精确过滤“今天”的提交以及如何让提交信息具备可解析的结构。2.2 用 git log 筛选当天提交Git 本身提供基于时间的过滤参数可以直接获取某个时间段的提交记录。常用命令如下git log \ --since2026-01-01 00:00:00 \ --until2026-01-01 23:59:59 \ --prettyformat:%h|%an|%ad|%s \ --dateformat:%Y-%m-%d %H:%M:%S \ --all这条命令的含义是查询指定日期内所有提交输出格式为commit hash|作者|提交时间|提交标题并且包含所有分支的提交。参数说明参数作用--since起始时间包含该时间点之后的提交--until截止时间包含该时间点之前的提交--pretty自定义输出格式--date指定日期显示格式--all统计所有分支不只看当前分支要注意--since和--until是包含边界还是排除边界在不同 Git 版本中表现略有差异。实际使用时应先跑一条命令确认结果是否包含临界时间点的提交。2.3 提交信息规范与类型分组采集到原始提交之后还需要把提交信息分类才能回答“今天完成了哪些类型的工作”。常见的做法是采用 Conventional Commits 规范。feat(用户模块): 增加手机号登录 fix(支付服务): 修复支付回调重复通知 docs(README): 补充部署说明 refactor(订单): 抽出价格计算逻辑 test(购物车): 增加结算测试用例 chore(build): 升级依赖版本提交信息的第一部分是类型第二部分是模块第三部分是描述。解析时只需要读取冒号前的类型词就能完成分类。中英文混合场景下中文关键词也需要纳入判断规则。分类规则可以维护成一张映射表类型对应关键词典型场景featurefeat, feature, 新增, 添加新功能bugfixfix, bugfix, 修复缺陷修复docsdocs, documentation, 文档文档变更refactorrefactor, 重构, 优化结构调整testtest, tests, 测试测试代码chorechore, build, ci构建与工具链没有匹配到任何类型时可以归类为other在报表中单独展示提示提交信息可能不够规范。2.4 未提交的工作怎么办Git 日志无法覆盖未提交的代码变更。为了让日报更接近真实情况采集器还需要记录工作区状态。git status --porcelain这个命令会输出所有未提交的文件变更包含修改、新增、删除和未跟踪文件。它可以作为一个“进行中”指标写入日报的备注区域。这样即使当天没有提交也能看到代码仓库中还有尚未落盘的改动。不过要注意不要把未提交变更直接当作“已完成的工作”。未提交只能说明代码有改动不能说明改动可运行、已通过测试。日报中应该把它标记为“进行中”或“待提交”而不是最终结果。3. 用 Python 采集并统计当日提交3.1 环境准备和目录结构为了实现采集器建议使用 Python 3.10 及以上版本因为下面的代码会用到dict类型以及date | None这样的联合类型写法。Git 版本建议 2.20 以上以保证--dateformat可用。项目目录结构如下daily-progress/ ├── progress/ │ ├── __init__.py │ ├── collector.py │ ├── storage.py │ ├── report.py │ └── cli.py ├── data/ ├── reports/ └── today_notes.json其中progress是 Python 包data存放 SQLite 数据库reports存放生成的日报today_notes.json是人工补充信息的入口。创建虚拟环境并检查依赖python3 -m venv venv source venv/bin/activate python --version git --version这个项目只用 Python 标准库不需要安装第三方包。这样在任意一台有 Python 的机器上都能跑降低环境依赖。3.2 采集器实现采集器放在progress/collector.py中。它负责调用 Git 命令解析输出并返回结构化的提交列表。from __future__ import annotations import subprocess from datetime import date from pathlib import Path def _run_git(repo_path: Path, args: list[str]) - str: cmd [git, -C, str(repo_path)] args proc subprocess.run( cmd, capture_outputTrue, textTrue, encodingutf-8, errorsreplace, ) if proc.returncode ! 0: raise RuntimeError(fgit 命令执行失败: {proc.stderr}) return proc.stdout def collect_commits(repo_path: str, target_date: date | None None) - list[dict]: target_date target_date or date.today() repo_path Path(repo_path).resolve() since f{target_date.isoformat()} 00:00:00 until f{target_date.isoformat()} 23:59:59 output _run_git( repo_path, [ log, --since, since, --until, until, --prettyformat:%h%x1f%an%x1f%ad%x1f%s, --dateformat:%Y-%m-%d %H:%M:%S, --all, ], ) commits [] for line in output.splitlines(): if not line: continue parts line.split(\x1f) if len(parts) 4: continue commit_hash, author, commit_time, subject parts[0], parts[1], parts[2], parts[3] commits.append({ hash: commit_hash, author: author, time: commit_time, subject: subject, }) return commits def uncommitted_files(repo_path: str) - list[str]: repo_path Path(repo_path).resolve() output _run_git(repo_path, [status, --porcelain]) return [line for line in output.splitlines() if line]代码中有几个关键点。git -C用来指定仓库目录避免依赖当前工作目录。--prettyformat中使用%x1f作为字段分隔符而不是直接使用|是为了防止提交标题里出现分隔符导致解析错误。maxsplit不需要额外设置因为split(\x1f)后如果 subject 里真的出现了这个控制字符也可以人工识别。errorsreplace是为了防止非 UTF-8 环境的编码报错解析中文提交信息时尤其有用。3.3 提交类型统计实现采集到提交列表后需要按类型统计。继续在collector.py中添加分类函数。TYPE_KEYWORDS { feature: [feat, feature, 新增, 添加], bugfix: [fix, bugfix, 修复, hotfix], docs: [docs, documentation, 文档], refactor: [refactor, 重构, 优化], test: [test, tests, 测试], chore: [chore, build, ci, 依赖, 构建], } def classify_subject(subject: str) - str: lowered subject.lower() for type_name, keywords in TYPE_KEYWORDS.items(): for keyword in keywords: if lowered.startswith(keyword): return type_name return other def build_type_stats(commits: list[dict]) - dict[str, int]: stats: dict[str, int] {} for commit in commits: type_name classify_subject(commit[subject]) stats[type_name] stats.get(type_name, 0) 1 return stats这里有一个需要注意的地方startswith会匹配fix同时也会匹配fixture这类前缀相同的词。如果提交信息足够规范影响不大。如果希望更精确可以改成先用冒号切分def classify_subject(subject: str) - str: if : in subject: prefix subject.split(:, 1)[0].strip().lower() for type_name, keywords in TYPE_KEYWORDS.items(): if prefix in keywords or prefix.startswith(type_name): return type_name return other推荐先让提交信息符合类型(模块): 描述的规范再配合基于前缀的解析准确率会更高。3.4 补充阻塞和明日计划Git 数据只能回答“代码做了什么”不能回答“为什么没完成”和“下一步做什么”。所以需要一个手工备注文件默认放在项目根目录下的today_notes.json。{ blocks: [等待测试环境更新, 第三方支付接口未开放], tomorrow_plan: [完成订单导出, 联调支付回调], notes: 核心逻辑已写完还剩补测 }在生成日报时先读取这个文件再与 Git 统计结果合并。如果文件不存在就使用空列表。这样设计的好处是日报既不会丢失客观数据也不会忽略主观异常。4. 用 SQLite 保存每日进度历史4.1 表结构设计日报生成之后如果只写 Markdown 文件历史数据无法高效查询。比如想统计最近 7 天每天提交了多少次用 Markdown 很难做。SQLite 适合这种单机、小数据量、结构化查询场景。表结构设计如下CREATE TABLE IF NOT EXISTS daily_report ( report_date TEXT PRIMARY KEY, author TEXT NOT NULL, total_commits INTEGER NOT NULL DEFAULT 0, commit_summary TEXT NOT NULL, type_stats TEXT NOT NULL, blocks TEXT NOT NULL, tomorrow_plan TEXT NOT NULL, notes TEXT, created_at TEXT NOT NULL );字段解释字段类型说明report_dateTEXT日报日期作为主键authorTEXT提交人total_commitsINTEGER当日提交总数commit_summaryTEXT提交明细以 JSON 数组保存type_statsTEXT类型统计以 JSON 对象保存blocksTEXT阻塞事项以 JSON 数组保存tomorrow_planTEXT明日计划以 JSON 数组保存created_atTEXT写入时间report_date使用文本类型并作为主键是因为日期本身就是唯一的避免同一天重复生成多条记录。4.2 写入逻辑存储模块放在progress/storage.py。import json import sqlite3 from datetime import datetime from pathlib import Path def get_connection(db_path: str | Path) - sqlite3.Connection: path Path(db_path) path.parent.mkdir(parentsTrue, exist_okTrue) conn sqlite3.connect(path) conn.execute( CREATE TABLE IF NOT EXISTS daily_report ( report_date TEXT PRIMARY KEY, author TEXT NOT NULL, total_commits INTEGER NOT NULL DEFAULT 0, commit_summary TEXT NOT NULL, type_stats TEXT NOT NULL, blocks TEXT NOT NULL, tomorrow_plan TEXT NOT NULL, notes TEXT, created_at TEXT NOT NULL ) ) return conn def upsert_report( conn: sqlite3.Connection, report_date: str, author: str, total_commits: int, commit_summary: list[dict], type_stats: dict[str, int], blocks: list[str], tomorrow_plan: list[str], notes: str, ) - None: conn.execute( INSERT INTO daily_report ( report_date, author, total_commits, commit_summary, type_stats, blocks, tomorrow_plan, notes, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(report_date) DO UPDATE SET author excluded.author, total_commits excluded.total_commits, commit_summary excluded.commit_summary, type_stats excluded.type_stats, blocks excluded.blocks, tomorrow_plan excluded.tomorrow_plan, notes excluded.notes, created_at excluded.created_at , ( report_date, author, total_commits, json.dumps(commit_summary, ensure_asciiFalse), json.dumps(type_stats, ensure_asciiFalse), json.dumps(blocks, ensure_asciiFalse), json.dumps(tomorrow_plan, ensure_asciiFalse), notes, datetime.now().isoformat(timespecseconds), ), ) conn.commit()这里使用ON CONFLICT(report_date) DO UPDATE可以让同一天的日报重复执行时覆盖更新而不是插入重复数据。对于只关注当天汇总的场景幂等写入更安全。4.3 为什么选 SQLite何时换数据库SQLite 适合单人或多人的小团队场景数据文件可以随仓库备份不需要独立服务。如果日后要接入统一研发平台或者需要多人并发写入同一份进度数据再考虑迁移到 PostgreSQL 或 MySQL。迁移时只需要改storage.py的数据库连接方式和 SQL 方言。核心字段不需要大改因为日报的数据结构是稳定的。注意SQLite 数据库文件如果放在 Git 仓库中建议加入.gitignore避免每天都在数据库文件里产生二进制变更污染仓库提交记录。5. 自动生成 Markdown 日报5.1 渲染逻辑报告渲染模块放在progress/report.py作用是把 Python 字典转换为 Markdown 文本。def render_markdown(data: dict) - str: lines [] lines.append(f{data[report_date]} 开发日报) lines.append() lines.append(f- 作者: {data[author]}) lines.append(f- 提交数: {data[total_commits]}) lines.append(f- 未提交文件数: {data[uncommitted_count]}) lines.append() lines.append(## 类型统计) lines.append() for type_name, count in data[type_stats].items(): lines.append(f- {type_name}: {count}) lines.append() lines.append(## 提交明细) lines.append() for commit in data[commits]: lines.append(f- {commit[hash]} {commit[time]} {commit[subject]}) lines.append() if data[blocks]: lines.append(## 阻塞) lines.append() for block in data[blocks]: lines.append(f- {block}) lines.append() if data[tomorrow_plan]: lines.append(## 明日计划) lines.append() for plan in data[tomorrow_plan]: lines.append(f- {plan}) lines.append() if data[notes]: lines.append(## 备注) lines.append() lines.append(data[notes]) lines.append() return \n.join(lines)这个渲染逻辑没有使用模板引擎是因为日报结构足够简单直接拼接字符串更容易控制格式也方便后续扩展。5.2 输出示例生成的 Markdown 文件内容类似下面这样2026-01-01 开发日报 - 作者: zhangsan - 提交数: 5 - 未提交文件数: 2 ## 类型统计 - feature: 2 - bugfix: 1 - docs: 1 - other: 1 ## 提交明细 - a1b2c3d 2026-01-01 10:12:00 feat(用户模块): 增加手机号登录 - e4f5g6h 2026-01-01 11:30:00 fix(支付服务): 修复回调重复通知 - i7j8k9l 2026-01-01 14:05:00 docs(README): 补充部署说明 ## 阻塞 - 等待测试环境更新 ## 明日计划 - 完成订单导出 ## 备注 核心逻辑已写完还剩补测从这份日报可以一眼看出当天的工作量、工作类型、具体改动和下一步计划。相比口头汇报它更完整也更容易追踪。5.3 命令行入口为了方便日常使用把整个流程封装成 CLI 命令。实现放在progress/cli.py。import argparse from datetime import date from pathlib import Path from .collector import collect_commits, build_type_stats, uncommitted_files from .storage import get_connection, upsert_report from .report import render_markdown import json def load_notes(notes_path: Path) - dict: if not notes_path.exists(): return {blocks: [], tomorrow_plan: [], notes: } with open(notes_path, r, encodingutf-8) as f: return json.load(f) def main() - None: parser argparse.ArgumentParser(description生成每日开发进度日报) parser.add_argument(--repo, requiredTrue, helpGit 仓库路径) parser.add_argument(--date, help目标日期格式 YYYY-MM-DD默认今天) parser.add_argument(--db, defaultdata/progress.db, helpSQLite 数据库路径) parser.add_argument(--out, defaultreports, help日报输出目录) parser.add_argument(--notes, defaulttoday_notes.json, help手工备注文件) parser.add_argument(--author, defaultdeveloper, help日报作者标识) args parser.parse_args() target_date date.fromisoformat(args.date) if args.date else date.today() report_date target_date.isoformat() commits collect_commits(args.repo, target_date) uncommitted uncommitted_files(args.repo) type_stats build_type_stats(commits) notes load_notes(Path(args.notes)) data { report_date: report_date, author: args.author, total_commits: len(commits), uncommitted_count: len(uncommitted), commits: commits, type_stats: type_stats, blocks: notes.get(blocks, []), tomorrow_plan: notes.get(tomorrow_plan, []), notes: notes.get(notes, ), } conn get_connection(args.db) upsert_report( conn, report_datereport_date, authorargs.author, total_commitslen(commits), commit_summarycommits, type_statstype_stats, blocksdata[blocks], tomorrow_plandata[tomorrow_plan], notesdata[notes], ) conn.close() out_dir Path(args.out) out_dir.mkdir(parentsTrue, exist_okTrue) out_file out_dir / f{report_date}.md out_file.write_text(render_markdown(data), encodingutf-8) print(f日报已生成: {out_file}) if __name__ __main__: main()CLI 参数保证了脚本不依赖当前目录。--repo指向被统计的仓库--date用于补生成历史日报--db和--out分别指定数据库和输出位置。6. 验证与定时自动化6.1 手动运行和结果核验先在一个真实 Git 仓库中运行命令cd daily-progress python -m progress.cli \ --repo /path/to/your/project \ --date 2026-01-01 \ --db data/progress.db \ --out reports运行成功后用 SQLite 查询数据库sqlite3 data/progress.db \ select report_date, author, total_commits from daily_report where report_date2026-01-01;再查看生成的日报cat reports/2026-01-01.md预期结果是数据库中有且只有一条当天的记录Markdown 文件中的提交数和类型统计与git log手动查询结果一致。6.2 边界情况验证要确认脚本在边界情况下不会误报可以构造几个场景。第一当天没有提交。此时脚本应该生成一份零提交日报并且commit_summary是空数组total_commits是 0。这个过程不应该报错。第二提交信息没有规范前缀。所有提交会被归入other日报中会显示大量other提示工程规范需要加强。第三日期切换。如果从2026-01-01切换到2026-01-02应该生成两条独立记录而不是覆盖同一天的数据。第四时区问题。脚本使用date.today()时时间来自运行脚本的机器。如果机器时区不是本地时区生成的报告日期可能与业务日期不一致。生产环境建议在配置中显式指定时区而不是依赖系统默认值。6.3 用 cron 和计划任务定时生成在 Linux 环境下可以用 crontab 每天下午自动生成当天日报30 18 * * 1-5 cd /opt/daily-progress /usr/bin/python3 -m progress.cli --repo /data/app --db /data/progress.db --out /data/reports /data/logs/progress.log 21这段配置的含义是每周一到周五的 18:30 执行一次日报生成任务日志写入progress.log。在正式启用前要确认几点。第一Python 路径要使用绝对路径避免 cron 环境中 PATH 变量不同。第二--repo路径要确保有读取权限。第三如果仓库有多个分支--all参数会把所有分支都统计进来需要确认这不是你想看到的。若只想统计当前分支可以去掉--all。在 Windows 上则可以使用“任务计划程序”操作步骤简单易上手但要注意 Python.exe 和脚本路径中的空格需要用引号包裹。7. 常见问题排查7.1 现象与处理对照表问题现象常见原因检查方式处理建议git log 没有输出仓库路径错误、日期没有提交、分支不对检查--repo路径手动执行git -C 路径 log --since...修正路径确认提交确实在目标日期统计到其他日期的提交时区不一致--since/--until使用了 UTC 时间用date命令查看系统时区用git log --dateiso核对提交时间在脚本中显式指定本地时区中文提交信息乱码系统编码不是 UTF-8执行locale查看编码给subprocess.run增加encodingutf-8, errorsreplace重复生成同一张日报脚本被重复执行或 cron 任务冲突查询daily_report表记录数使用主键和ON CONFLICT DO UPDATE保证幂等当天提交未统计到提交在工作区未 commit执行git status先提交代码再生成日报或把未提交文件作为“进行中”备注--all统计结果多于预期统计了所有本地分支和远端分支执行git branch -a查看分支列表按需求改为--branches或去掉--all7.2 一个典型排查路径假设用户反馈“日报显示今天没有提交”。按照这条路径排查。第一步确认仓库路径是否正确在项目目录执行git log --oneline -5如果能看到提交说明仓库正常。第二步确认日期是否合理如果系统日期是 UTC而本地业务日期比 UTC 早八小时凌晨的提交会被归到前一天。第三步确认提交是否已经 commitgit status --porcelain如果有很多未提交文件则说明当天工作还没有落盘。第四步确认是否在目标分支如果提交在dev分支而当前处于main分支使用--all可以包含但不用--all时就不会统计。这套顺序从输入、路径、时区到数据来源逐层检查能快速定位绝大多数问题。注意工具永远只能记录“已经发生的事实”不能替开发者判断“这件事是否算完成”。阻塞、风险和变更影响仍然需要人补充到日报中。8. 可落地的实践建议8.1 从个人日报到团队协作个人可以先从 Git 提交 备注文件开始跑通流程后再推广到团队。团队推广时要统一提交信息规范至少包含类型、模块和描述。规范不是强制所有人背文档而是在提交模板中约定格式。git config commit.template .gitmessage.gitmessage文件示例type(scope): subject # 示例: # feat(用户模块): 增加手机号登录 # fix(支付服务): 修复支付回调重复通知配置提交模板后开发者执行git commit会看到示例信息能显著减少无效提交信息。8.2 扩展方向当日报数据积累到一定量级可以基于这些数据做更多事。第一自动生成周报。通过按report_date分组求和直接在 SQLite 中查询过去七天的汇总数据再渲染一份周报。第二接入团队消息系统。日报生成后可以把 Markdown 内容发送到群机器人减少手动粘贴。第三结合 CI 数据。在日报中补充构建状态、测试通过率把“开发进度”延伸到“质量进度”。第四用提交粒度做研发效能分析时一定要谨慎。提交数量不能直接等同于产出更合理的做法是结合代码变更行数、拆分的任务数量、评审时间等多项指标。8.3 上线前检查清单检查项说明完成状态Python 和 Git 版本确认 Python 3.10Git 2.20待确认仓库路径使用绝对路径避免依赖工作目录待确认提交规范已配置.gitmessage或团队规范待确认日期时区确认业务日期与系统时区一致待确认数据库目录data目录可写已加入.gitignore待确认输出目录reports目录可写历史文件不覆盖待确认手动验证已用--date补生成历史日报并核对结果待确认定时任务cron 已配置日志可查看待确认异常处理仓库路径错误、无提交时不会崩溃待确认记录“今天的进度”最终不是为了汇报给谁看而是让开发者的工作过程变得可追溯、可复盘、可改进。从一次git log查询开始到一套自动生成日报的脚本再到可以用于周报和团队协作的数据沉淀整个过程并不复杂但它能把每天消失在工作流里的信息重新变成工程资产。