从 GNU Stow 迁移到 Chezmoi

2026-06-18 1 阅读 speckx
几年来我一直使用 GNU stow 管理我的点文件。早在 2023 年,我什至就该设置写了一篇带有老生常谈标题的文章。Stow 对我很有帮助,但管理多个设备上的符号链接慢慢地变成了一件痛苦的事情。所以我开始寻找更好的工具,甚至考虑编写自己的工具。然后一位同事向我推荐了 chezmoi,到目前为止我非常喜欢它。它可以满足我需要的一切,而且我也开始用它跟踪我的特工技能文件。机器 # 我运行三台 Mac:一台用于工作的 MacBook Pro,一台用于个人使用的 MacBook Air,以及一台用作小型个人服务器的 Mac Mini。 Mini 大部分是从另外两台通过 SSH 连接到的。它仍然是一台带有我的 shell 的 Mac,所以同样的点文件也适用。我还保留了一些 Linux 虚拟机,但我很少需要在服务器上使用我的点文件。 Ansible 提供了这些。此工作流程仅适用于台式机。当我不再需要 Stow 时 # Stow 的模型是符号链接。配置文件位于 git 存储库中,分组到存储调用包的目录中,并且存储包将其文件链接到主目录中。对于单台机器来说它仍然成立。这些命令是幂等的,几乎没有什么可学习的。问题在于符号链接是双向的。每台机器上的每次编辑都会通过链接直接写入该机器的存储库克隆。几个月后,我会在空中发现肮脏的工作树,其中有我不记得做过的改变。其中一半与专业人士已经推动的内容相冲突。让三个克隆体聚集在一起变成了一件苦差事。新机器是问题的另一半。 Stow 不会链接真实文件。当 Homebrew 和一些工具在新 Mac 上运行时,像 ~/.zprofile 和 ~/.gitconfig 这样的文件已经存在。引导意味着克隆存储库,手动删除冲突的文件,并重新加载每个包,同时尝试记住我给它们命名的内容。并且 stow 只处理文件。 Homebrew 软件包和 macOS 设置位于单独的脚本中,我必须记住以正确的顺序运行它们。 chezmoi 如何工作 # Chezmoi 在 ~/.local/share/chezmoi 保留源目录,这是一个常规的 git 存储库。 chezmoi add ~/.zshrc 将实时文件复制到其中并将副本命名为 dot_zshrc 。添加 ~/.config/gh/config.yml 会创建 dot_config/gh/config.yml ,包括父目录。我从不手动创建这些名称,因为 chezmoi add 是从真实路径中派生出它们的。该树最终镜像主目录,每个前导点都拼写为 dot_ 前缀。 dot_ 是 chezmoi 编码到文件名中的几个属性之一。 private_ 前缀会从文件中删除组和世界权限。 .tmpl 后缀将文件转换为可以读取每台机器数据的 Go 模板。我很少使用模板,每个模板都会在本文后面出现。 chezmoi apply 则相反。它将每个跟踪的文件写回其名称拼写的主路径,因此 dot_zshrc 位于 ~/.zshrc 。副本是真实文件,而不是符号链接。源目录是唯一的事实来源。当主目录中的文件停止匹配其源副本时,chezmoi diff 会显示差异,下一个应用会将其恢复。失去符号链接的自动直写功能是我最喜欢的事情。除非我故意将更改放在那里,否则存储库中不会发生任何变化。我跟踪的内容 # 所有这些都位于该源目录中。 chezmoi cd 将我放入那里的子 shell 中,这是整个树: ~/.local/share/chezmoi ├── .chezmoi.toml.tmpl ├── .chezmoiignore ├── .chezmoiscripts │ └── macos │ ├── run_onchange_after_disable-macos-animations.sh │ ├── run_onchange_after_init-macos-machine.sh.tmpl │ └── run_onchange_before_install-homebrew-bundle.sh.tmpl ├── .gitignore ├── Brewfile ├── README.md ├── dot_agents │ └── 技能 │ ├── 现代化 │ ├── go-styleguide │ └── Meatspeak ├── dot_claude │ ├── settings.json │ └── symlink_skills.tmpl ├── dot_codex │ └── private_config.toml ├── dot_config │ ├── gh │ │ ├── config.yml │ │ └── private_hosts.yml │ └── Ghostty │ └── config ├── dot_gitconfig ├── dot_gitconfig-pers ├── dot_gitconfig-werk ├── dot_shellcheckrc ├── dot_zsh_aliases └── dot_zshrc 列表很短,因为我不喜欢自定义工具并尽可能坚持默认值。正确的点文件是 zsh、git、shellcheck、ghostty 和 GitHub CLI 配置。我还跟踪 Claude Code 的 settings.json 和 Codex 的 config.toml,因此代理在每台机器上的行为都是相同的。 gh 的hosts.yml 上的 private_ 前缀和 Codex 配置将这两个保持在 0600 。最后讲一下dot_agents下的技巧。三个 gitconfig 分割了我的身份。我的所有项目都位于两个目录下, ~/canvas/werk/ 用于工作, ~/canvas/pers/ 用于个人所有内容,并且两者都存在于每台计算机上。主要 gitconfig 通过存储库所在位置路由身份: [includeIf "gitdir:~/canvas/pers/"] path = ~/.gitconfig-pers [includeIf "gitdir:~/canvas/werk/"] path = ~/.gitconfig-werk Repos under ~/canvas/pers/ get m