技术

Mac 下用 Git 提交自动生成 Obsidian 工作日报

告别每个仓库单独配置 githooks:用全局扫描、白名单、中文别名和业务分组,把 Git 提交自动汇总进 Obsidian 工作记录,同时保留人工目标与项目判断。

发布
阅读约 7 分钟
Mac 下用 Git 提交自动生成 Obsidian 工作日报
文章阅读阅读导航1 / 14

手头的 Git 仓库一多,写日报就容易变成一件「想起来才补、补了也不全」的事。

我最早在每个仓库里配置 .githooks/post-commit,提交后把记录追加到一个 JSONL 文件,再手动生成 Markdown。它能工作,但新项目要重复安装,嵌套仓库容易遗漏,还可能与 Husky 等项目级 hook 冲突。

后来我把流程收敛成一个独立工具:定时扫描固定工作区,只采集白名单仓库,把原始提交留在本机,再把适合阅读的结果写入 Obsidian。

这套流程后来又演进了几次。现在 Git 提交只是工作记录中的自动证据;目标、进展、非 Git 产出、风险和下一步仍由人维护,项目状态也不会由提交数量自动推断。


要解决的不是「自动写日报」

更准确地说,这套工具负责自动生成日报底稿,而不是替人判断一天完成了什么。

问题当前做法
每个仓库都要安装 hook在 Mac 上定时扫描固定工作区
历史仓库太多,噪音大allowlist 只纳入活跃仓库
仓库目录名不适合直接写进日报repo_aliases 映射为中文组件名
同一业务由多个仓库组成repo_groups 供工作台按业务项目聚合
只有 commit 标题,信息不足保留提交正文,展示范围、影响和验证方式
自动内容覆盖手写记录只替换 HTML 标记之间的 Git 区块
提交多就被误判为优先级高项目阶段、优先级和风险由项目页人工维护

这里最重要的边界是:

Git 负责提供可追溯的代码事实;Obsidian 负责承载人工工作判断;自动化只连接两者,不替代判断。


当前的数据链路

flowchart LR
  R[工作区 Git 仓库] --> F[白名单过滤]
  F --> C[collect]
  C --> A[raw.jsonl]
  C --> S[state.json]
  A --> P[report]
  P --> D[每日工作项 Git 自动区块]
  A --> G[dashboard]
  G --> W[仓库动态与工作台]
  A --> Q[周报]
  D --> Q
  Q --> M[月报]
  A -.补漏核验.-> M
  D -.补漏核验.-> M
mermaid

数据分成四层:

  1. data/raw.jsonl 保存纳入范围内已采集提交的完整字段,是 Git 工作事实的原始来源。
  2. data/state.json 保存每个仓库上次采集到的 HEAD,用于增量扫描。
  3. work/每日工作项/ 展示适合人阅读的提交标题与正文。
  4. work/仓库动态/ 保存按仓库生成的近期活动快照,供 Obsidian 工作台聚合。

work/仓库动态/ 是生成物,只反映代码活动。项目阶段、优先级、下一步和风险仍以人工维护的项目页为准。

周报以 raw.jsonl 中已采集的 Git 记录作为已完成代码工作的主要事实来源,再用日报补充非 Git 产出、业务背景、风险和计划。月报则以当月周报为成果主线,再回查日报与原始 Git 记录补漏。


日报里哪些内容自动写,哪些必须手写

当前每日工作项采用下面的结构:

## 今日目标

- [ ]

## 工作进展

## 非 Git 产出

## Git 提交(自动)

<!-- git-work-log:start -->
_等待 git-work-log 同步。_
<!-- git-work-log:end -->

## 风险与阻塞

## 下一步
markdown

git-work-log 只管理两个 HTML 注释之间的内容,绝不改写其他栏目。需要调整 Git 展示时,应修改生成器和配置后重新执行 report,不要手工编辑自动区块。

早上可用 $work-daily 创建当天记录。它会从最近一个工作日迁移未完成目标与下一步,而不是机械地读取自然日的「昨天」;周末、节假日和补录日期因此不会打乱迁移。

Git 不能表达的工作,例如需求澄清、会议结论、联调、上线观察和文档交付,写进「非 Git 产出」或「工作进展」。这样生成周报时,代码证据和人工事实都不会缺席。


目录与安装

工具独立放在用户配置目录,不进入任何业务仓库:

~/.config/git-work-log/
  config.yaml
  git_work_log.py
  requirements.txt
  data/
    raw.jsonl
    state.json
    launchd.log
    launchd.err.log

~/.local/bin/git-work-log
~/Library/LaunchAgents/com.xio.git-work-log.plist
text

raw.jsonlstate.json 和 launchd 日志都是本机运行数据,不应放进 Obsidian Vault,也不应提交到博客仓库。

命令入口可以保持很薄:

#!/usr/bin/env bash
set -euo pipefail
exec python3 "$HOME/.config/git-work-log/git_work_log.py" "$@"
bash

安装依赖并赋予执行权限:

python3 -m pip install -r ~/.config/git-work-log/requirements.txt
chmod +x ~/.local/bin/git-work-log
bash

如果使用 pyenv,应在命令入口中写明实际 Python 路径。launchd 不会读取交互式 shell 的全部环境,不能假设它能找到终端里可用的解释器。


配置:白名单、中文名和业务分组

目前所有规则集中在 config.yaml,不再单独维护 repo_aliases.yaml。下面使用虚构项目展示结构:

vault_path: "/Users/YOUR_USER/Library/Mobile Documents/iCloud~md~obsidian/Documents/Obsidian Vault"

work_daily:
  dir: "work/每日工作项"

scan_roots:
  - ~/Projects/company

ignore_dir_names:
  - node_modules
  - target
  - build
  - dist

max_depth: 6

repo_filter:
  mode: allowlist
  include_repos:
    - user-portal
    - billing-service
  include_paths:
    - acme-stack/catalog-api
    - acme-stack/notify-worker
  exclude_repos: []

repo_aliases:
  user-portal: 用户门户
  billing-service: 计费服务
  acme-stack/catalog-api: 商品目录 API
  acme-stack/notify-worker: 消息投递 Worker

repo_groups:
  user-portal: 客户平台
  billing-service: 结算平台
  acme-stack/catalog-api: 商品中心
  acme-stack/notify-worker: 商品中心

dashboard:
  enabled: true
  repo_activity_dir: work/仓库动态
  active_days: 7
  recent_days: 30

markers:
  start: "<!-- git-work-log:start -->"
  end: "<!-- git-work-log:end -->"

report:
  include_branch: false
  include_commit_body: true
  strip_co_authored: true

initial_since_days: 30
yaml

对子目录中的独立仓库,include_paths 最好写相对 scan_roots 的完整路径。只写末级目录名虽然有时也能匹配,但同名仓库一多就容易产生歧义。

repo_aliases 解决「组件叫什么」,并决定日报自动区块的分组标题;repo_groups 解决「组件属于哪个业务项目」,只用于仓库动态和工作台聚合,不会把日报中的多个组件合并为一个项目。

所有纳入白名单的仓库都应同时配置中文别名和业务分组。否则日报可能退回英文目录名,工作台则会把仓库标记为「未分组」。

当前日报不显示分支,也会过滤 Co-authored-by: 元数据,但这些信息仍保留在 raw.jsonl。日报因此更适合阅读,原始记录仍可用于追溯。


为什么还要保留 raw.jsonl

采集时使用 git log 的控制字符分隔字段,一次读取完整 hash、短 hash、作者时间、标题和正文,再交给标准库序列化为 JSON Lines。

下面只摘录消息字段的解析核心。实际函数还会读取仓库名和分支,并由后续逻辑补充仓库相对路径,因此这段代码用于解释分隔方式,不是可直接替换的完整实现。

def parse_git_log(repo: Path, rev_range: str) -> list[dict[str, str]]:
    fmt = "%H%x1f%h%x1f%ai%x1f%s%x1f%b%x1e"
    out = run_git(repo, "log", rev_range, f"--format={fmt}", "--reverse")
    entries = []

    for record in out.split("\x1e"):
        record = record.strip("\n")
        if not record:
            continue
        parts = record.split("\x1f", 4)
        if len(parts) < 4:
            continue
        full_hash, short_hash, author_date, subject = parts[:4]
        body = parts[4].rstrip("\n") if len(parts) > 4 else ""
        entries.append({
            "commit_full": full_hash,
            "commit": short_hash,
            "time": format_author_time(author_date),
            "message": f"{subject}\n\n{body.strip()}" if body.strip() else subject,
        })

    return entries
python

旧 hook 常把 commit message 直接插进 shell 或 Here-Doc。正文一旦包含引号、反斜杠或特殊结束符,就可能损坏日志。现在由 Python 读取 Git 输出并调用 json.dumps,不再手工拼接 JSON。

state.json 只负责记录增量位置,不能替代 raw.jsonl。首次采集还受白名单和 initial_since_days 限制,所以这里的「完整」指已纳入、已采集提交的字段完整,不代表所有仓库的全部 Git 历史。

日报可以重新生成,工作台快照也可以刷新;但如果 raw.jsonl 丢失,历史报告的事实依据就不完整了。


输出效果

假设某天有两次规范提交,自动区块会呈现为:

## Git 提交(自动)

<!-- git-work-log:start -->
**商品目录 API**
- 10:32:18 `a1b2c3d` feat(api): 为商品列表增加游标分页
  - 新增 CursorPageRequest 与 nextCursor 响应字段
  - 查询改为 keyset 分页,避免深分页性能问题
  - 验证:集成测试覆盖空结果与末页边界

**用户门户**
- 15:05:42 `e4f5a6b` fix(cache): 修正会话缓存过期策略
  - 未配置 Redis 时使用 30 分钟默认 TTL
  - 为热点 key 增加防击穿处理
  - 验证:缓存单元测试与登录回归通过
<!-- git-work-log:end -->
markdown

推荐使用 Conventional Commits 风格的标题,并在正文写清范围、行为变化和验证方式。自动化能保留信息,但无法替糟糕的提交说明补出业务语义。


sync 现在会做三件事

常用命令如下:

命令作用
git-work-log collect增量扫描纳入范围的仓库,写入 raw.jsonl
git-work-log report更新今日工作记录的 Git 自动区块
git-work-log report 2026-08-05重新生成指定日期的自动区块
git-work-log dashboard刷新 work/仓库动态/
git-work-log sync执行 collectreport,并在启用时刷新 dashboard
git-work-log sync 2026-08-01 2026-08-05回扫并重新生成一个闭区间
git-work-log repos查看纳入仓库及其中文名
git-work-log aliases --check检查缺少中文别名的仓库
git-work-log doctor检查配置、Vault、仓库范围和 launchd 状态

以前的 sync 可以理解成 collect + report。启用 dashboard.enabled 后,它还会刷新仓库动态,因此工作台与日报会在同一次同步中更新。

需要补历史时,可先指定采集起点,再生成目标日期:

git-work-log collect --since 2026-08-01
git-work-log sync 2026-08-01 2026-08-05
bash

用 launchd 定时执行

LaunchAgent 在登录时、每 30 分钟以及每天 18 执行一次 git-work-log sync。下面省略与用户名相关的绝对路径:

<key>Label</key>
<string>com.xio.git-work-log</string>

<key>ProgramArguments</key>
<array>
  <string>/Users/YOUR_USER/.local/bin/git-work-log</string>
  <string>sync</string>
</array>

<key>RunAtLoad</key>
<true/>

<key>StartInterval</key>
<integer>1800</integer>

<key>StartCalendarInterval</key>
<array>
  <dict>
    <key>Hour</key>
    <integer>18</integer>
    <key>Minute</key>
    <integer>0</integer>
  </dict>
</array>
xml

现代 macOS 可使用 bootstrap 加载:

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.xio.git-work-log.plist
bash

这里使用 com.xio.git-work-log,因为当前 doctor 会按这个文件名和 Label 检查任务。如果改成自己的 Label,还要同步修改 cmd_doctor 中的检查值,或者进一步把 Label 做成配置项。

修改 plist 后,应先 bootoutbootstrap。否则磁盘上的文件虽然变了,launchd 仍可能继续使用旧的内存配置。

launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.xio.git-work-log.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.xio.git-work-log.plist
git-work-log doctor
bash

一天的实际使用方式

  1. 早上运行 $work-daily,创建今日工作记录并迁移最近工作日的遗留任务。
  2. 白天正常提交 Git,不需要为每个仓库配置采集 hook。
  3. 工作过程中补充工作进展、非 Git 产出、风险和下一步。
  4. launchd 定时运行 sync,刷新 Git 自动区块和仓库动态。
  5. 下班前运行一次 git-work-log doctor,必要时手动执行 sync
  6. 写周报时以 raw.jsonl 中已采集的 Git 记录为代码事实,用人工栏目补充上下文和非代码产出。

这也意味着,日报里出现一条提交,不等于项目已经完成;没有提交,也不等于当天没有产出。


新增一个迭代项目

  1. repo_filter.include_reposinclude_paths 中加入仓库。
  2. repo_aliases 中加入中文组件名。
  3. repo_groups 中加入业务项目分组。
  4. 执行 git-work-log aliases --check,确认没有遗漏中文名。
  5. 执行一次 git-work-log sync,检查日报和仓库动态。
  6. 最后运行 git-work-log doctor,确认白名单、Vault 与 launchd 状态正常。

仓库活动只用于辅助观察。若项目阶段、下一步或风险发生变化,还要回到对应项目页人工更新。


故障排查

现象优先检查
doctor 显示纳入仓库为 0检查 include_reposinclude_paths 和扫描根目录
日报显示英文目录名config.yamlrepo_aliases 补映射
raw.jsonl 有记录,日报没有可能只运行了 collect;执行 reportsync
日报显示当天无 Git 提交确认提交日期与仓库是否在白名单中
工作台缺少仓库检查 dashboard.enabledrepo_groups,再运行 dashboard
同一提交重复出现检查旧 hook 是否仍在写入;报告会按提交 hash 去重
launchd 没有执行launchctl print 检查任务,再查看 data/launchd.*.log
修改 plist 后日志路径仍旧bootout 后重新 bootstrap,再运行 doctor
自动区块内容异常修复配置或生成器后重新执行 report,不要手改标记区间

迁移或修复前,应备份 raw.jsonlstate.json。普通同步问题不需要通过删除历史数据来解决。


小结

这套工作流真正有价值的地方,不是把 git log 复制进 Obsidian,而是建立了清晰的数据职责:

自动化负责减少遗漏,人仍然负责解释工作的意义。这个边界稳定之后,工具怎么演进,日报都不容易再次变成一堆难以复用的提交标题。

配套文章


正在加载留言…


上一篇
理解PKI(二):从 ASN.1 到 DER,数字证书为什么是一串字节
下一篇
为了使用微信读书,我把Kindle(kpw3)刷成了安卓

搜索文章...

⌘K / Ctrl K

使用上下方向键选择结果,按 Enter 打开,按 Esc 关闭

搜索结果