
写给每周五对着
git log发呆、把 commit 一条条抄进周报的你:把"拉提交、归类、成稿"交给一条脚本,周五下午省下半小时,顺手还能接 WorkBuddy 定时跑。代码全部实测可运行,复制就能用。
先说痛点。我以前写周报,流程是这样的:打开 git log,用眼睛挑"看起来重要"的提交,把 commit message 复制进文档,再逐条改写成人话;哪天忘了干了啥,就靠回忆硬凑。一顿操作,半小时起步,而且——
这类"规则明确、数据已在、但要加工成报告"的活,正是智能体(Agent)工作流的经典猎物。本篇用一条可运行脚本,把 git log 直接变成一份按类型分好类、按时间窗卡好边的周报草稿,跑通之后再交给 WorkBuddy 到点自动跑。
核心思路一句话:把"写周报"拆成 拉取提交 → 按时间窗过滤 → 按类型归类 → 模板渲染 → 成稿归档 五步,每步单一职责、可单独验证;全部跑通后,接 WorkBuddy 定时执行。

flowchart LR
A["① 拉取提交 fetch<br />git log 取 subject/author/date<br />判定:只读信息,绝不读 diff"] --> B["② 时间窗过滤 filter<br />Python 侧按日期比较<br />判定:窗口起点可复现"]
B --> C["③ 按类型归类 classify<br />约定式提交前缀 feat/fix/...<br />判定:无前缀归'其他'单列"]
C --> D["④ 模板渲染 render<br />按分类生成 Markdown<br />判定:输出可复现"]
D --> E["⑤ 成稿归档 archive<br />按日期存文件<br />判定:0条判失败 exit 1"]
E -.->|"可选"| F["接 WorkBuddy 定时<br />每周五自动跑<br />判定:失败有人喊,不静默"]
F -.->|"可选"| G["接 ima 知识库<br />打 周报 标签<br />判定:攒一月能问答"]注释(判定标准):五步各自独立——拉取出错不影响归类,归类错了不影响归档。每个节点的"判定"就是它的自检点,哪步出问题一眼定位,不用回翻整条脚本。最后两跳(接定时 / 进知识库)是可选升级,呼应本系列 #14、#12。
只用标准库 + 本地 git 命令,零第三方依赖,Python 3.9+ 直接跑(代码用了 dict[str, list[dict]] 等内置泛型注解,3.8 及以下会在定义函数时报 TypeError):
"""
weekly_report.py —— 周报自动生成器(示例代码,已实测可运行)
流程:git 拉取全部提交 -> Python 侧按时间窗过滤 -> 按类型归类 -> 模板渲染 -> 成稿归档
依赖:仅 Python 标准库 + 本地 git 命令(git 须在 PATH 中)
重要:脚本只读取提交信息(subject/author/date),绝不读取 diff,避免泄露源码与密钥。
为何时间窗在 Python 侧算、不交给 `git --since`:`git --since` 的边界判定依赖提交时间戳时区与本地时区,
相对表述(如 7 days ago)在不同环境下容易产生边界漂移,实测中无法稳定复现同一窗口结果;
把窗口比较放在 Python 里、以显式日期为起点,结果可复现。
"""
import argparse
import datetime as dt
import re
import shutil
import subprocess
from pathlib import Path
# 约定式提交(Conventional Commits)前缀 -> 中文分类
TYPE_LABELS = {
"feat": "新功能",
"fix": "缺陷修复",
"docs": "文档",
"refactor": "重构",
"perf": "性能优化",
"test": "测试",
"style": "格式调整",
"chore": "杂项",
"build": "构建/依赖",
"ci": "CI/构建",
}
ORDER = list(TYPE_LABELS.keys()) + ["other"]
def git_available() -> bool:
return shutil.which("git") is not None
def pull_all(repo: str, author: str) -> list[dict]:
"""拉取全部提交,仅取 hash/author/committer日期/subject,绝不取 diff。"""
# 字段用 \x1f(单元分隔符)分隔,避免与提交内容中的逗号/空格冲突
# %ci = committer 日期,类 ISO 8601(ISO 8601-like,形如 2026-10-22 10:00:00 +0800);过滤时只取前 10 位日期部分。若只需 YYYY-MM-DD,可直接改用 %cs
fmt = "%H%x1f%an%x1f%ci%x1f%s"
cmd = ["git", "-C", repo, "log", f"--pretty=format:{fmt}"]
if author:
cmd.append(f"--author={author}")
proc = subprocess.run(cmd, capture_output=True, text=True)
if proc.returncode != 0:
raise RuntimeError(proc.stderr.strip() or "git log 执行失败")
rows = []
for line in proc.stdout.splitlines():
parts = line.split("\x1f")
if len(parts) != 4:
continue
h, who, cdate, subject = parts
rows.append({"hash": h[:8], "author": who, "cdate": cdate.strip(), "subject": subject.strip()})
return rows
def parse_since(since: str) -> dt.date:
"""支持 '7 days ago' / '14 days ago' / '2026-10-20' 三种写法,返回窗口起始日期(含)。
'N days ago' 按「过去 N 个自然日(含今天)」解释:起点 = 今天 - (N-1) 天,
配合 in_window 的 >= 比较,窗口正好覆盖 N 个自然日。
"""
s = since.strip().lower()
m = re.match(r"^(\d+)\s*days?\s*ago$", s)
if m:
return dt.date.today() - dt.timedelta(days=int(m.group(1)) - 1)
m2 = re.match(r"^(\d{4}-\d{2}-\d{2})$", s)
if m2:
return dt.date.fromisoformat(m2.group(1))
raise ValueError(f"无法解析 --since 参数:{since!r}(支持 '7 days ago' 或 '2026-10-20')")
def in_window(cdate: str, cutoff: dt.date) -> bool:
# cdate 形如 '2026-10-22 10:00:00 +0800',取前 10 位按提交者本地日期比较,不做跨时区换算,保证边界稳定
return dt.date.fromisoformat(cdate[:10]) >= cutoff
def classify(subject: str) -> str:
# scope 用字符类 [(] / [)] 表示括号,避免反斜杠转义在渲染或复制时丢失
m = re.match(r"^(feat|fix|docs|refactor|perf|test|style|chore|ci|build)([(][^)]+[)])?!?:", subject, re.I)
return m.group(1).lower() if m else "other"
def render(rows: list[dict], out_dir: Path, repo_name: str, window: str) -> Path:
by_type: dict[str, list[dict]] = {}
for r in rows:
by_type.setdefault(classify(r["subject"]), []).append(r)
lines = [
f"# 周报 · {repo_name}(窗口:{window})",
"",
"> 数据来源:git log(仅提交信息,不含 diff)",
f"> 提交总数:{len(rows)}",
"",
]
for t in ORDER:
items = by_type.get(t)
if not items:
continue
label = TYPE_LABELS.get(t, "其他")
lines.append(f"## {label}({len(items)})")
for r in items:
lines.append(f"- [{r['cdate'][:10]}] {r['subject']} _{r['hash']}_")
lines.append("")
out_dir.mkdir(parents=True, exist_ok=True)
path = out_dir / f"周报-{repo_name}-{window.replace(' ', '_')}.md"
path.write_text("\n".join(lines), encoding="utf-8")
return path
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--repo", default=".", help="仓库路径,默认当前目录")
ap.add_argument("--since", default="7 days ago", help="时间窗起点:'7 days ago' 或 '2026-10-20',默认近 7 天(含当天共 7 个自然日)")
ap.add_argument("--author", default="", help="按作者过滤,留空不过滤")
ap.add_argument("--out", default="weekly", help="成稿输出目录")
args = ap.parse_args()
if not git_available():
print("[error] 未找到 git 命令,请先安装并加入 PATH")
return 2
try:
cutoff = parse_since(args.since)
except ValueError as e:
print(f"[error] {e}")
return 2
all_rows = pull_all(args.repo, args.author)
rows = [r for r in all_rows if in_window(r["cdate"], cutoff)]
if not rows:
# 守卫:空窗口直接判失败,避免生成"空白周报"还假装成功
print(f"[error] 窗口({cutoff.isoformat()} 起)内没有提交,周报无法生成(请检查 --since 或 --author)")
return 1
path = render(rows, Path(args.out), Path(args.repo).resolve().name, args.since)
print(f"[done] 拉取 {len(all_rows)} 条提交,窗口内 {len(rows)} 条,分 {len({classify(r['subject']) for r in rows})} 类")
print(f"[done] 周报已生成:{path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())我用托管 Python 3.13 在一个演示仓库(含 6 条约定式提交,其中 4 条提交于 2026-10-23 及以后、2 条在 9 月)真跑了一遍。该次运行日期为 2026-10-29;脚本里 7 days ago 按「过去 7 个自然日(含今天)」计算,起点 = 今天 - 6 天,即 2026-10-23,窗口覆盖 10-23 至 10-29:
$ python weekly_report.py --repo ./repo --since "7 days ago" --out weekly
[done] 拉取 6 条提交,窗口内 4 条,分 4 类
[done] 周报已生成:weekly/周报-repo-7_days_ago.md(示例在我的演示仓库 repo 下运行,你的路径会是自己的仓库名与输出目录;示例各条提交的日期按该窗口排列,你重跑时以自己的提交日期与条数为准。)
生成的周报长这样,按类型自动分好类,日期、提交哈希一目了然:
# 周报 · repo(窗口:7 days ago)
> 数据来源:git log(仅提交信息,不含 diff)
> 提交总数:4
## 新功能(1)
- [2026-10-23] feat(auth): 支持企业微信扫码登录 _d0f6f37e_
## 缺陷修复(1)
- [2026-10-24] fix(export): 修复导出大文件时内存溢出 _d6694598_
## 文档(1)
- [2026-10-26] docs: 补充定时任务配置说明 _c232aa07_
## 性能优化(1)
- [2026-10-28] perf(report): 周报渲染耗时从 800ms 降到 120ms _a786b045_空窗口守卫也实测过——时间窗内没有提交时,脚本直接判失败、退出码 1,绝不生成"空白周报"还假装成功:
$ python weekly_report.py --repo ./repo --since "2026-10-30" --out weekly
[error] 窗口(2026-10-30 起)内没有提交,周报无法生成(请检查 --since 或 --author)两点如实说明:
git --since。原因不是"git 有 bug",而是边界语义不该交给脚本猜:git --since 按提交时间戳与本地时区判定边界,相对表述在不同环境下会漂移,我无法在同一份仓库上稳定复现同一窗口结果。所以我把窗口比较放进 Python:7 days ago 按"过去 7 个自然日(含今天)"解释,起点取今天减 6 天,再用 %ci 的日期部分做 >= 比较——只要运行日期确定,窗口就是确定的 7 个自然日;换个日期跑,窗口整体平移,窗口内条数自然不同。1. 数据早就在 git 里——提交即记录。 周报的本质是"一段时间内我做的工作",而一次 Commit 恰好是带作者、时间戳、意图(subject)的最小工作单元。不利用它、反而去聊天记录里考古,是本末倒置。脚本做的,只是把这份"已存在的事实"翻译成"人能直接交差的格式"。
2. 约定式提交(Conventional Commits)是免费的自动分类器。 feat/fix/docs/refactor/perf/... 这些前缀,本来就是给工具看的"语义标签"。一条正则 ^(feat|fix|docs|...)!?: 就能把提交分流到对应栏目,零人工打标。团队只要推行约定式提交,周报分类就自动完成一半。

flowchart TD
S["一条 commit subject<br />例: feat(auth): 支持扫码登录"] --> M{"匹配正则<br />^(feat|fix|docs/...)!?:"}
M -->|"匹配"| T["取前缀作分类<br />feat 新功能 / fix 缺陷修复"]
M -->|"不匹配"| O["归入'其他'<br />判定:人工补分类或推行约定式提交"]注释(判定标准):分类只看 subject 开头的约定式前缀,零人工打标;匹配不到的统一进"其他"并单列,既不丢提交、也提醒你补全规范。
3. 时间窗自己算,不交给 git。 关键就一句:把"相对时间"先换算成明确日期,再自己比较,最稳。代码里 parse_since() 先支持 '7 days ago' 和 '2026-10-20' 两种写法,算出窗口起点 cutoff,再用 in_window() 逐条比日期。这样窗口起点是一个显式日期,结果只取决于提交日期本身,与本地时区、git 版本无关;唯一变量是运行当天的日期,写进结论时要说明这一点——可复现是自动化的最低要求。
4. 只读 subject 不读 diff,既是安全也是聚焦。 周报读者(老板/同事)关心"你这周交付了什么功能、修了多少 bug",不关心你改了哪几行。把 git show/diff 排除在脚本之外,既避免把仓库里的密钥、内部逻辑误带进文档,也让输出更干净。
5. 脚本跑通,才是接 Agent 的起点。 和 #13 早报流水线、#14 定时任务一个道理:先让脚本在本地手动跑熟、日志可见,再交给 WorkBuddy 定时执行。没验证的步骤交给自动调度,等于让自动系统定时放大你的 bug。
周报脚本出问题时,按这张决策树定位,别从头翻代码:

flowchart TD
E["周报不对劲"] --> Q1{"生成的周报是空的?"}
Q1 -->|"是"| C1["时间窗/作者过滤太窄<br />看 [error] 提示<br />判定:放宽 --since 或去掉 --author"]
Q1 -->|"否"| Q2{"分类大量落到'其他'?"}
Q2 -->|"是"| C2["提交未用约定式前缀<br />判定:推行 Conventional Commits"]
Q2 -->|"否"| Q3{"含敏感/内部信息?"}
Q3 -->|"是"| C3["脚本误读了 diff<br />判定:确认只读 subject,绝不 git show"]
Q3 -->|"否"| Q4{"接定时后不按时跑?"}
Q4 -->|"是"| C4["本地任务关机即停/权限不足<br />判定:看运行记录+开启推送"]注释(判定标准):每个症状先对应一个信号(
[error]提示 / 分类分布 / 内容是否含 diff / 运行记录)——排错入口永远是信号,不是猜。
这 5 个坑按踩到的顺序排:
git --since 不可信,时间窗放 Python。 这是本篇最大的坑:git --since 的边界判定随提交时区与本地时区变化,相对表述在不同环境下会漂移,第一版脚本因此抓不到预期的提交。解法是把窗口比较移出 git、用 Python 按 %ci 的显式日期比——窗口结果从此稳定可复现。feat:、fix: 前缀的提交会被归到"其他"并单列。解法:团队推行约定式提交;脚本对"其他"单独成栏,方便你补一句分类或事后复盘提交习惯。git show 把改动贴进去,一旦仓库含密钥、内部逻辑就泄露。本脚本从设计上只读 subject,从根上规避——周报只写"做了什么",不写"怎么做的"。git log 全量拉取在大仓库可能很慢。解法:用 --author 按人过滤(脚本已透传该参数),或给 git log 加 --since/--max-count 兜底减少数据量。真实可加的一行是在 pull_all() 里 cmd.append(f"--since={args.since}")(注意:这只是性能优化,窗口判定仍以 Python 为准)。exit 1 守卫把空窗口判为失败,接 WorkBuddy 后失败会推送通知,不生成空白周报骗你。跑通之后,这条周报流水线有三个自然的升级方向,顺序别搞反——先"自动跑",再"进知识库",最后"多项目汇总":

flowchart LR
L1["第1级 手动脚本<br />你敲命令它跑<br />判定:代码实测跑通+日志可见"] --> L2["第2级 定时自动<br />WorkBuddy 到点执行<br />判定:失败有推送,不静默"]
L2 --> L3["第3级 沉淀知识库<br />周报进 ima 打标签<br />判定:攒一月能问答检索"]
L3 --> L4["第4级 多项目汇总<br />多仓库合并成月报<br />判定:每步仍可单独验证"]注释(管控要求):升级顺序的硬规矩还是每一步稳定跑至少一周再进下一步——脚本没跑熟就定时,等于让自动系统定时放大 bug。
#周报 标签,攒一个月你就能"问"自己这个月到底干了啥——流水线输出的终点不该是文件夹,而是可检索的知识库;今晚就能做的行动清单(按顺序,别跳):
weekly_report.py 放进项目旁固定目录(如 D:\NewsBot),--repo 用绝对路径,手动跑通一次:python weekly_report.py --repo "D:\your-repo" --since "7 days ago" --out weekly;--since 设成明天)跑一次,确认失败会通知你;#周报 标签,攒够一个月做第一次"问答式复盘"。披露声明(AI 辅助创作,含 AI 生成内容):本文由作者基于真实的脚本搭建与实跑过程撰写,使用 AI 工具辅助起草与润色;文中含 AI 生成内容,经作者审核修订。核心观点、代码设计与踩坑结论均由作者负责。示例代码已用 Python 3.13 实测运行,文中命令行输出与周报示例为演示仓库的实际输出,各条提交日期按当次窗口排列,重跑时取值随你自己的仓库而定;
git --since的时区边界问题与 Python 侧窗口过滤方案均为实测排查所得,未编造。git 命令与 WorkBuddy 界面随版本更新,请以软件内实际为准。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。