首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >周报全自动:从 Git 记录到成稿的一条龙

周报全自动:从 Git 记录到成稿的一条龙

原创
作者头像
华东子
发布于 2026-10-31 10:05:12
发布于 2026-10-31 10:05:12
1080
举报
文章被收录于专栏:WorkBuddy知识库WorkBuddy知识库

写给每周五对着 git log 发呆、把 commit 一条条抄进周报的你:把"拉提交、归类、成稿"交给一条脚本,周五下午省下半小时,顺手还能接 WorkBuddy 定时跑。代码全部实测可运行,复制就能用。

一、问题场景:周报是"Git 考古 + 手工搬运"

先说痛点。我以前写周报,流程是这样的:打开 git log,用眼睛挑"看起来重要"的提交,把 commit message 复制进文档,再逐条改写成人话;哪天忘了干了啥,就靠回忆硬凑。一顿操作,半小时起步,而且——

  • 重复且易遗漏:哪些 commit 算"本周成果"全凭手感,漏一条自己都不知道;
  • 不可复现:"上周周报怎么写的"没人记得,换个写法就前后不一致;
  • 本末倒置:git 里早就有答案——你的每一次提交(Commit)就是一份带作者、时间戳、意图的最小工作记录,周报要的"一段时间内我做了什么"它全有。问题只是它"不是周报"。

这类"规则明确、数据已在、但要加工成报告"的活,正是智能体(Agent)工作流的经典猎物。本篇用一条可运行脚本,把 git log 直接变成一份按类型分好类、按时间窗卡好边的周报草稿,跑通之后再交给 WorkBuddy 到点自动跑。

二、解决方案:五步一条龙,先拆后跑

核心思路一句话:把"写周报"拆成 拉取提交 → 按时间窗过滤 → 按类型归类 → 模板渲染 → 成稿归档 五步,每步单一职责、可单独验证;全部跑通后,接 WorkBuddy 定时执行。

2.1 全景图:每步的判定标准

代码语言:javascript
复制
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。

2.2 完整代码(已实测跑通)

只用标准库 + 本地 git 命令,零第三方依赖,Python 3.9+ 直接跑(代码用了 dict[str, list[dict]] 等内置泛型注解,3.8 及以下会在定义函数时报 TypeError):

代码语言:javascript
复制
"""
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())

2.3 实测运行(真实输出,非示意图)

我用托管 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:

代码语言:javascript
复制
$ python weekly_report.py --repo ./repo --since "7 days ago" --out weekly
[done] 拉取 6 条提交,窗口内 4 条,分 4 类
[done] 周报已生成:weekly/周报-repo-7_days_ago.md

(示例在我的演示仓库 repo 下运行,你的路径会是自己的仓库名与输出目录;示例各条提交的日期按该窗口排列,你重跑时以自己的提交日期与条数为准。)

生成的周报长这样,按类型自动分好类,日期、提交哈希一目了然:

代码语言:javascript
复制
# 周报 · 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,绝不生成"空白周报"还假装成功:

代码语言:javascript
复制
$ python weekly_report.py --repo ./repo --since "2026-10-30" --out weekly
[error] 窗口(2026-10-30 起)内没有提交,周报无法生成(请检查 --since 或 --author)

两点如实说明:

  1. 时间窗是 Python 算的,不是 git --since。原因不是"git 有 bug",而是边界语义不该交给脚本猜:git --since 按提交时间戳与本地时区判定边界,相对表述在不同环境下会漂移,我无法在同一份仓库上稳定复现同一窗口结果。所以我把窗口比较放进 Python:7 days ago 按"过去 7 个自然日(含今天)"解释,起点取今天减 6 天,再用 %ci 的日期部分做 >= 比较——只要运行日期确定,窗口就是确定的 7 个自然日;换个日期跑,窗口整体平移,窗口内条数自然不同。
  2. 脚本只读提交信息(subject/author/date),绝不读 diff。周报要的是"做了什么",不是"怎么做的";读 diff 既慢又有泄露源码、密钥的风险。脚本从设计上就只取 subject,安全边界天然锁死。

三、原理解析:为什么这么拆

1. 数据早就在 git 里——提交即记录。 周报的本质是"一段时间内我做的工作",而一次 Commit 恰好是带作者、时间戳、意图(subject)的最小工作单元。不利用它、反而去聊天记录里考古,是本末倒置。脚本做的,只是把这份"已存在的事实"翻译成"人能直接交差的格式"。

2. 约定式提交(Conventional Commits)是免费的自动分类器。 feat/fix/docs/refactor/perf/... 这些前缀,本来就是给工具看的"语义标签"。一条正则 ^(feat|fix|docs|...)!?: 就能把提交分流到对应栏目,零人工打标。团队只要推行约定式提交,周报分类就自动完成一半。

代码语言:javascript
复制
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。

四、提示与避坑(5 个真实踩坑)

周报脚本出问题时,按这张决策树定位,别从头翻代码:

代码语言:javascript
复制
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 个坑按踩到的顺序排:

  1. git --since 不可信,时间窗放 Python。 这是本篇最大的坑:git --since 的边界判定随提交时区与本地时区变化,相对表述在不同环境下会漂移,第一版脚本因此抓不到预期的提交。解法是把窗口比较移出 git、用 Python 按 %ci 的显式日期比——窗口结果从此稳定可复现。
  2. 提交信息不规范 → 分类落"其他"。 没写 feat:、fix: 前缀的提交会被归到"其他"并单列。解法:团队推行约定式提交;脚本对"其他"单独成栏,方便你补一句分类或事后复盘提交习惯。
  3. 别图省事读 diff,防泄密。 有人为了"周报更详细"用 git show 把改动贴进去,一旦仓库含密钥、内部逻辑就泄露。本脚本从设计上只读 subject,从根上规避——周报只写"做了什么",不写"怎么做的"。
  4. 大仓/多分支拉取慢。 git log 全量拉取在大仓库可能很慢。解法:用 --author 按人过滤(脚本已透传该参数),或给 git log 加 --since/--max-count 兜底减少数据量。真实可加的一行是在 pull_all() 里 cmd.append(f"--since={args.since}")(注意:这只是性能优化,窗口判定仍以 Python 为准)。
  5. 空窗口静默成功 = 慢性断更。 某周没提交、脚本却"顺利"生成空白周报,你一周后才发现——这就是 #14 讲的"静默失败"。本脚本用 exit 1 守卫把空窗口判为失败,接 WorkBuddy 后失败会推送通知,不生成空白周报骗你。

五、扩展应用与系列导航

跑通之后,这条周报流水线有三个自然的升级方向,顺序别搞反——先"自动跑",再"进知识库",最后"多项目汇总":

代码语言:javascript
复制
flowchart LR
    L1["第1级 手动脚本<br />你敲命令它跑<br />判定:代码实测跑通+日志可见"] --> L2["第2级 定时自动<br />WorkBuddy 到点执行<br />判定:失败有推送,不静默"]
    L2 --> L3["第3级 沉淀知识库<br />周报进 ima 打标签<br />判定:攒一月能问答检索"]
    L3 --> L4["第4级 多项目汇总<br />多仓库合并成月报<br />判定:每步仍可单独验证"]

注释(管控要求):升级顺序的硬规矩还是每一步稳定跑至少一周再进下一步——脚本没跑熟就定时,等于让自动系统定时放大 bug。

  • 定时自动:把脚本派给 WorkBuddy(参见 #14 的自动化配置全流程),设每周五 17:00 执行,实现真正的"到点自己跑";提示词抄 #14 的示例即可;
  • 沉淀知识库:周报生成后按 #12 的方法归入 ima 知识库、打上 #周报 标签,攒一个月你就能"问"自己这个月到底干了啥——流水线输出的终点不该是文件夹,而是可检索的知识库;
  • 多项目汇总:多个仓库各跑一次,再把几份周报合并成一份"月度复盘"。到第 4 级,你离"一周工作自动成稿"就只差最后一步拼接。

今晚就能做的行动清单(按顺序,别跳):

  1. 归位:把 weekly_report.py 放进项目旁固定目录(如 D:\NewsBot),--repo 用绝对路径,手动跑通一次:python weekly_report.py --repo "D:\your-repo" --since "7 days ago" --out weekly;
  2. 接定时:WorkBuddy 新建自动化,提示词直接抄 #14 的示例,先"测试运行"通过再开定时;
  3. 听它喊:开启推送,故意指定一个空窗口(--since 设成明天)跑一次,确认失败会通知你;
  4. 进知识库:周报产出后按 #12 接进 ima 打 #周报 标签,攒够一个月做第一次"问答式复盘"。

披露声明(AI 辅助创作,含 AI 生成内容):本文由作者基于真实的脚本搭建与实跑过程撰写,使用 AI 工具辅助起草与润色;文中含 AI 生成内容,经作者审核修订。核心观点、代码设计与踩坑结论均由作者负责。示例代码已用 Python 3.13 实测运行,文中命令行输出与周报示例为演示仓库的实际输出,各条提交日期按当次窗口排列,重跑时取值随你自己的仓库而定;git --since 的时区边界问题与 Python 侧窗口过滤方案均为实测排查所得,未编造。git 命令与 WorkBuddy 界面随版本更新,请以软件内实际为准。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、问题场景:周报是"Git 考古 + 手工搬运"
  • 二、解决方案:五步一条龙,先拆后跑
    • 2.1 全景图:每步的判定标准
    • 2.2 完整代码(已实测跑通)
    • 2.3 实测运行(真实输出,非示意图)
  • 三、原理解析:为什么这么拆
  • 四、提示与避坑(5 个真实踩坑)
  • 五、扩展应用与系列导航
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档