ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

拆解ticket源码:单文件bash脚本如何实现完整工单系统

拆解ticket源码:单文件bash脚本如何实现完整工单系统

【免费下载链接】ticket

Fast, powerful, git-native ticket tracking in a single bash script. Dependency graphs, priority levels, zero setup.

项目地址:https://gitcode.com/gh_mirrors/ticket11/ticket
点击查看免费下载

🔍 今天我们来拆解 ticket 源码——一个用单文件 bash 脚本实现的完整工单系统:内置依赖图、优先级、零配置启动,不依赖数据库、不跑后台进程,1383 行代码跑遍所有 POSIX 系统。这篇文章带你读懂 ticket 源码中每一个关键设计决策。

📌 ticket 是什么?为什么值得关注

ticket(命令名tk)是一个git 原生的工单追踪器(issue tracker),灵感来自 Joe Armstrong 的"最小可行程序"理念。它和大多数工单工具的根本区别在于存储方式:

维度传统工单工具ticket
数据存储SQLite / 服务端数据库Markdown 文件 + YAML frontmatter
同步方式后台常驻进程(daemon)Git 提交,天然版本化
依赖多种运行时仅 coreutils(bash/sed/awk/find)
部署成本安装服务、配置连接把一个脚本丢进 PATH 即可
可检索性需要 API纯文本,grep和 AI 直接可读

每张工单就是 .tickets/ 目录下的一个 Markdown 文件,文件头带 YAML frontmatter 记录id、status、deps、priority等元数据。这种设计带来了两个"免费"的福利:

  • IDE 直达:工单 ID 即文件名,在 VS Code 里 Ctrl+Click 日志中的工单 ID 即可跳转阅读;
  • AI 友好:AI Agent 用自然语言即可搜索工单内容,而不必解析成千上万字符的 JSONL。

关于完整用法,可阅读 README.md。

📁 仓库结构:一个脚本如何撑起整个系统

ticket/ ├── ticket # 核心:1383 行的单文件 bash 脚本 ├── plugins/ # 官方插件(edit / ls / query / migrate-beads) ├── features/ # behave BDD 行为测试(Gherkin 场景) ├── pkg/ # Homebrew / AUR 打包模板 ├── scripts/ # 发布脚本 ├── Makefile # make test 一键跑测试 └── CHANGELOG.md

源码导读地图可直接看 CLAUDE.md,作者在其中明确总结了架构要点:核心是一个约千行的单文件 bash 实现,用 awk 做批量高性能操作。整份脚本可拆成五块:

模块关键函数职责
目录定位find_tickets_dir()/init_tickets_dir()向上遍历父目录寻找.tickets/
ID 系统generate_id()/ticket_path()生成与模糊解析工单 ID
数据读写yaml_field()/update_yaml_field()用 sed 增删改 YAML frontmatter
命令实现cmd_create()/cmd_dep()/cmd_show()等14 个内建子命令
插件分发主 case 分支 +super旁路PATH 中同名可执行文件自动接管

🎯 设计决策一:Markdown 即数据库,sed 即 ORM

传统做法是"文件当数据库",但 ticket 更激进:连 ORM 都不要。

frontmatter 的读取只有一个函数 yaml_field():用sed截取---之间的区块,再grep出目标字段;写入则交给 update_yaml_field()——字段已存在就正则替换,不存在就插到首个---之后。整个"数据库引擎"就这几十行。

更妙的是create命令 cmd_create():它不解析任何模板文件,直接用一个{ echo ...; } > "$file"块把 frontmatter 和正文(描述、Design、Acceptance Criteria 小节)一次性写出。写文件本身就是数据建模——没有 schema 迁移问题,因为格式是纯文本。

🆔 设计决策二:为 IDE 和 AI 定制的 ID 系统

工单 ID 的生成逻辑 generate_id() 只有十几行,但处处体现产品思维:

  1. 取当前目录名,提取每个-/_分段的首字母作为前缀(my-app→ma);
  2. 单段目录名取前 3 个字符兜底(plan→pla,避免前缀太短);
  3. 从/dev/urandom取 4 位a-z0-9随机后缀,提高熵值防碰撞。

最终得到形如ma-5c46的短 ID。配套的 ticket_path() 实现部分 ID 匹配:tk show 5c46能唯一定位到完整文件;匹配到多个则报ambiguous ID错误,匹配不到则报not found。

这两个细节都是为 AI Agent 服务的——CHANGELOG.md 里甚至专门有一条:trim ID 首尾空白,"修复 AI Agent 传入多余空格的问题"。

🕸️ 设计决策三:依赖图——用 awk 跑图算法

这是整个源码里技术含量最高的部分。依赖关系存成每个工单 frontmatter 里的deps: [a, b, c]数组,但没有任何中心索引——图是从全部 Markdown 文件现场算出来的。

依赖树dep tree:cmd_dep_tree() 内嵌一大段 awk 程序,单次遍历所有.md文件建图,然后用显式栈模拟 DFS(避免 awk 递归限制)计算每个节点的深度与子树深度,最终按├──/└──符号渲染出缩进树。默认模式自动去重、只保留最长路径;--full关闭去重展示完整子树。

环检测dep cycle:cmd_dep_cycle() 是教科书级的DFS 三色标记法——白色未访问、灰色访问中(遇到即成环)、黑色已完成。找到环后还会做归一化去重(以最小 ID 为起点重排),保证同一个环无论从哪里出发都只报告一次。

依赖调度ready/blocked:cmd_ready() 和 cmd_blocked() 各用一段 awk 一次读入所有工单,判断"活跃工单的全部依赖是否已 closed",再按 priority(0 最高)排序输出,还支持-a按负责人、-T按标签过滤。没有数据库聚合查询,靠的就是这张内存哈希表。

show的逆向关系:cmd_show() 展示工单时,额外扫一遍全部文件算出谁在阻塞我(Blockers)、我阻塞了谁(Blocking)、我的子工单(Children)、关联工单(Linked)——相当于现场构建索引表的外键反查。

🧩 设计决策四:插件系统——不改源码即可扩展

ticket 的主分发逻辑在脚本末尾 主 dispatch,执行顺序是:

  1. super <cmd>模式:跳过插件,直接跑内建命令;
  2. 否则先查 PATH 中是否存在tk-<cmd>或ticket-<cmd>可执行文件,存在则exec移交(通过TICKETS_DIR、TK_SCRIPT环境变量传递上下文);
  3. 都没有,才落入内建 case 分发。

这意味着扩展一个命令完全不需要改 ticket 脚本——写个可执行文件丢进 PATH 即可。官方仓库的 plugins/ 目录就有 4 个插件:

  • plugins/ticket-edit:用$EDITOR打开工单;
  • plugins/ticket-query:把工单批量输出为 JSON(JSONL),可直接接jq过滤;
  • plugins/ticket-ls / plugins/ticket-list:带过滤条件的列表;
  • plugins/ticket-migrate-beads:从 beads 的.beads/issues.jsonl一键迁移。

这些插件与核心脚本分属ticket-core/ticket-extras两个包(清单见 pkg/extras.txt),想装哪个装哪个。插件编写规范见 plugins/README.md。

🚀 设计决策五:可移植性兜底

脚本开头就埋了几处"防翻车"设计:

  • set -euo pipefail:任何一步失败立即退出,不留脏状态;
  • _iso_date():手写 UTC 时间戳格式,绕开 GNUdate -Iseconds与 BSD date 的差异;
  • _sed_i():用临时文件模拟sed -i,兼容 macOS;
  • _grep():检测到rg(ripgrep)就用它,否则回退grep;
  • 目录发现逻辑 find_tickets_dir() 支持TICKETS_DIR环境变量覆盖,未找到时只允许create这类写命令就地初始化(见WRITE_COMMANDS白名单)。

发布侧同样干净:Homebrew tap 与 AUR 各有一套打包模板(pkg/aur/ticket/PKGBUILD、scripts/publish-homebrew.sh),brew install ticket或yay -S ticket一条命令装完。

✅ 测试:用 BDD 场景守住 1383 行

没有单元测试框架?那就用行为驱动开发(BDD)。测试基于 Python 的 behave 库:

  • Gherkin 场景文件:features/ticket_dependencies.feature、features/ticket_query.feature 等 10 个文件,覆盖创建、依赖、状态流转、查询、插件等全部命令;
  • 步骤定义:features/steps/ticket_steps.py;
  • 运行方式:Makefile 里一行uv run --with behave behave,即make test。

以依赖场景为例,它断言"重复添加依赖应提示Dependency already exists(幂等)"、"对不存在的工单加依赖必须失败并给出明确错误"——错误路径也被场景锁死,这正是单文件脚本最容易腐化的地方。

📝 总结:能从一个 bash 脚本学到什么

技巧对应源码位置
纯文本当数据库,sed/grep 当 ORMyaml_field()
awk 单次遍历建图 + 显式栈 DFScmd_dep_tree()
三色标记法检测环并归一化去重cmd_dep_cycle()
PATH 约定 +exec移交的零侵入插件系统主 dispatch
BSD/GNU 差异的轻量 shim_sed_i()
为"用户"(AI Agent)打磨的容错细节ticket_path()

ticket 证明了"最小可行"不是功能残缺,而是把每一行都花在刀刃上:没有数据库、没有守护进程、没有配置项,1383 行 bash 却实现了工单创建、状态流转、依赖图、环检测、优先级调度和插件扩展的完整闭环。如果你正想给项目找一个零维护成本的轻量工单系统,或者想学习如何把 Unix 工具链用到极致,这份源码值得逐行细读。

【免费下载链接】ticket

Fast, powerful, git-native ticket tracking in a single bash script. Dependency graphs, priority levels, zero setup.

项目地址:https://gitcode.com/gh_mirrors/ticket11/ticket
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进