读书笔记 · 约 13,976 字 · 预计推送 2 部分

Git 高级工作流:worktree、rebase、bisect、镜像备份与单人协作(主题精读) 未开始

目录

题记与说明


1. 核心问题:单人开发者缺什么,Git 内置能力补什么

团队场景里,Git 的很多坑由流程兜底:Code Review 拦住烂提交,CI 拦住回归,运维团队管备份。单人开发者没有这些兜底,于是四类高频痛点全部落在 Git 本身:

痛点传统做法(低效)本主题的对口技术
同时推进两件事(如改 bug 时突然要修线上急件)git stash 或 WIP 提交 污染历史worktree:多工作树并行
提交历史里混着 "typo"、"fix typo again"、半成品不整理 → bisect 失效、回溯困难rebase / fixup / autosquash:推送前整理
上线后出回归,不知哪个提交引入翻 git log 逐个人肉排查(线性 O(n))bisect / bisect run:二分 O(log n)
觉得"代码在 GitHub 上就是有备份"平台停机/误删时无路可退clone --mirror + bundle:本地镜像+离线档案

GitProtect.io 一文的论断点破第三行:

"there is a huge difference between 'copy' and 'backup'. Copy is fine for daily work, but it's not enough for the real protection of your data."

这四项不是并列的工具清单,而是一个闭环:worktree 管"空间"(并行),rebase 管"顺序"(历史整洁),bisect 管"回溯"(定位),mirror/bundle 管"冗余"(灾备)。单人协作的"协作对象"是未来的自己——原子提交 + 干净历史就是给未来自己的 bisect 留的可二分性。


2. 主要主张(各来源的核心论点)

2.1 worktree:一个仓库,多棵工作树

官方文档定义(git-worktree):

"Manage multiple working trees attached to the same repository."
"A git repository can support multiple working trees, allowing you to check out more than one branch at a time."

机制:主工作树(main worktree)之外创建的是"linked worktree",共享对象库、只隔离 per-worktree 状态:

"The new worktree is linked to the current repository, sharing everything except per-worktree files such as HEAD, index, etc."

所以新增一棵树几乎不占磁盘(对象库共享),但每个树有独立的 HEAD/index/stash——这正是"两个任务互不踩脚"的根基。

硬约束:同一分支不能同时被两棵树检出。官方:"add refuses to create a new worktree when \<commit-ish\> is a branch name and is already checked out by another worktree"。本地实验复现了精确报错:fatal: 'feature' is already used by worktree at 'D:/temp/.../wt1'(见第 6 节)。

官方明示的实验性警告:

"Multiple checkout in general is still experimental, and the support for submodules is incomplete. It is NOT recommended to make multiple checkouts of a superproject."

社区经验一致:新 worktree 里子模块目录是空的,需逐树重新 git submodule update --init,且有已知 bug(Stack Overflow 75769128 等)。另外每个树要各自装 node_modules/构建产物——共享的是 Git 对象,不是依赖。

2.2 rebase:金律只有一条

Pro Git(3.6 Rebasing)的机制描述精确到步骤:

"This operation works by going to the common ancestor of the two branches..., getting the diff introduced by each commit of the branch you're on, saving those diffs to temporary files, resetting the current branch to the same commit as the branch you are rebasing onto, and finally applying each change in turn."

金律原文(粗体是原文自带的):

"Do not rebase commits that exist outside your repository and that people may have based work on.
If you follow that guideline, you'll be fine. If you don't, people will hate you, and you'll be scorned by friends and family."

为什么:rebase 是"抛弃旧提交、造出相似但不同的新提交"。别人基于你的旧提交工作后,你强推改写版,对方 pull 会得到内容相同的重复提交(作者、日期、message 一样但 SHA 不同),历史分叉且难以调和。Pro Git 给出的补救依赖 patch-id 匹配(git pull --rebase),且明说 "This only works if C4 and C4' that your partner made are almost exactly the same patch."

收益与代价同样明确:

"Rebasing replays changes from one line of work onto another in the order they were introduced, whereas merging takes the endpoints and merges them together."
"If you examine the log of a rebased branch, it looks like a linear history: it appears that all the work happened in series, even when it originally happened in parallel."
最终快照一样:"it's only the history that is different."

两全法原文:"rebase local changes before pushing to clean up your work, but never rebase anything that you've pushed somewhere."

单人场景的推论(本文归纳):金律自动放宽一档——"只在你自己电脑上的提交"可以随便 rebase(Pro Git 原文认可:"If you only ever rebase commits that have never left your own computer, you'll be just fine");但只要推送过,即便没有"别人",未来的你 + 你的多台机器 + 你的 agent 也可能基于旧 SHA 工作,改写成本仍在。

2.3 bisect:把回归定位从 O(n) 降到 O(log n)

官方一句话定义(git-bisect):

"This command uses a binary search algorithm to find which commit in your project's history introduced a bug."

效率证据直接写在命令输出里(官方文档示例):

"Bisecting: 675 revisions left to test after this (roughly 10 steps)"

Pro Git 补充:"This is a powerful tool that can help you check hundreds of commits for an introduced bug in minutes."

git bisect run 的退出码契约(官方原文,这是自动化脚本的关键):

"should exit with code 0 if the current source code is good/old, and exit with a code between 1 and 127 (inclusive), except 125, if the current source code is bad/new."
"The special exit code 125 should be used when the current source code cannot be tested."(126/127 被 POSIX shell 占用,故留 125 作 skip)

三个进阶点:

  1. 不止找 bug:"git bisect can be used to find the commit that changed any property of your project"——性能变慢、某个修复何时进入,都可二分;语义不搭时可换 old/new 甚至自定义词(--term-old fast --term-new slow)。
  2. 出错可悔:git bisect log 导出会话、编辑后 git bisect replay 重放。
  3. 结束必须复位(Pro Git):"you should run git bisect reset to reset your HEAD to where you were before you started, or you'll end up in a weird state"。

配套工具分工(Pro Git):知道问题在哪几行 → git blame(-C 还能追出跨文件复制的来源:"Git tells you the original commit where you wrote those lines, even if it was in another file");不知道在哪 → bisect 全历史搜索。

2.4 镜像备份:clone --mirror + git bundle

为什么普通 clone 不够:普通 clone 只拿一个默认分支 + 跟踪分支;--bare 拿裸仓库但 refspec 不覆盖全部 refs。官方 --mirror 定义(git-clone):

"--mirror — Set up a mirror of the source repository. This implies --bare. Compared to --bare, --mirror not only maps local branches of the source to local branches of the target, it maps all refs (including remote-tracking branches, notes etc.) and sets up a refspec configuration such that all these refs are overwritten by a git remote update in the target repository."

同步一条命令:镜像仓库内 git remote update 即可全量覆盖 refs。

离线档案:git-bundle 自述:

"git-bundle - Move objects and refs by archive"
"Bundles are used for the 'offline' transfer of Git objects without an active 'server' sitting on the other side of the network connection."
"They can be used to create both incremental and full backups of a repository"

恢复端直接 git clone backup.bundle,还能 fetch/pull。但 bundle 的边界要背下来:

"You will not include other local state, such as the contents of the index, working tree, the stash, per-repository configuration, hooks, etc."

以及:bundle 没有写支持("a git push into a bundle is not supported");范围 bundle(old..new)依赖前提提交存在;官方还警告直接 cp 活仓库不可靠("the repository could be written to during the copy operation")——bundle/clone --mirror 才是安全快照。

"托管平台 ≠ 备份"(GitProtect.io):clone "makes a complete copy of the repository",但作者反问:如果 clone 就够,Git 为什么要发明 bundle?并列举平台停机事实:2020-04 GitHub、2020-06 GitHub 与 Bitbucket、2020-12 Bitbucket 均发生过代码不可用事件。结论口号:"We should treat our repositories as production environments."(注:该站为备份工具厂商,有商业动机,见第 7 节局限。)

2.5 单人协作:和未来的自己协作

Mikkel Paulson 的 solo 指南给出一套一致的原则:


3. 方法:本次研究怎么做的

  1. 检索:4 组关键词(worktree 限制与子模块坑、rebase 金律、bisect run 退出码、mirror/bundle 备份策略 + solo workflow),筛选出官方文档优先、社区实践交叉验证的 7 个来源。
  2. 精读原文:对 5 个核心页面做全文抓取,逐条摘录带引号的定义、警告、机制描述(保留英文原文,避免翻译失真)。
  3. 本地实验:在 Git Bash 里用临时仓库(/tmp/gitlab-4okgaw4b-UXii)对文档声称的四个关键行为做最小复现(见第 6 节)——文档说什么是"主张",跑出来什么才是"证据"。
  4. 交叉审视:对有商业动机的来源(GitProtect.io)单独标注可信度折扣。

4. 数据与证据

文献证据:第 2 节所有引文均来自本次抓取的原文;关键数字:

实验证据(第 6 节全部命令与输出为本次实测):


5. 结论:一套可执行的单人日常流程

把四件套串成一天的工作循环:

开工      git worktree add ../hotfix -b hotfix/x     # 新任务新树,别 stash
编码      小步原子提交(一个逻辑改动一个提交)
整理      git commit --fixup 旧提交;git rebase -i --autosquash main   # 仅限未推送部分
自测      每次提交都保持"可构建+可测",这是 bisect run 的入场券
回溯      回归出现:git bisect start HEAD <已知good>; git bisect run sh check.sh; git bisect reset
收工      git remote update(镜像仓库);git bundle create repo-$(date +%F).bundle --all  # 离线档案
灾备      bundle 推到另一块盘/云存储(配合 3-2-1:3 份、2 种介质、1 份异地)

三条纪律:

  1. 金律:只 rebase 从未推送的提交;推送后的分支(哪怕只有你自己用)不随意改写,改则 --force-with-lease 并自担同步成本。
  2. 原子性:提交粒度 = bisect 的分辨率;糊成一团的 500 提交,bisect 也只能给你一团。
  3. copy ≠ backup:本地 --mirror + 定期 bundle --all + 异地存放,三者缺一不可;stash/hooks/config 不在 bundle 里,重要钩子单独归档。

6. 本地实验记录(本次会话实测,Git Bash / Windows)

实验仓库:/tmp/gitlab-4okgaw4b-UXii,4 个提交(c1 good → c2 work → c3 引入 BUGLINE → c4)。

① git bisect run 自动定位(测试脚本:grep -q BUGLINE app.txt && exit 1 || exit 0):

$ git bisect start HEAD good-tag && git bisect run sh ../check.sh
Bisecting: 0 revisions left to test after this (roughly 1 step)
running 'sh' '../check.sh'
Bisecting: 0 revisions left to test after this (roughly 0 steps)
running 'sh' '../check.sh'
6f813d1bed9747b60c84eb012c24da7a031a8f47 is the first bad commit
    c3 introduces bug
bisect found first bad commit

→ 4 个提交仅 2 次测试即命中 c3,与文档"roughly N steps"行为一致;退出码契约(0=good, 1=bad)实测生效。

② 同分支不能被两棵树检出(文档 safeguard 实测):

$ git worktree add ../wt1 -b feature     # 成功
$ git worktree add ../wt2 feature
fatal: 'feature' is already used by worktree at 'D:/temp/gitlab-4okgaw4b-UXii/wt1'

③ linked worktree 共享对象库的直接物证:

$ cat ../wt1/.git
gitdir: D:/temp/gitlab-4okgaw4b-UXii/lab/.git/worktrees/wt1

→ .git 变成指针文件,管理数据在主仓 $GIT_DIR/worktrees/<name>,对象库共用。

④ bundle 备份与恢复:

$ git bundle create ../lab.bundle --all
$ git clone ../lab.bundle restored
$ git -C restored log --oneline
937903b c4 after bug
6f813d1 c3 introduces bug
4a12899 c2 work
62c7521 c1 good

→ 单文件 bundle 可直接 clone,恢复出完整历史。


7. 局限

各技术本身的局限(作者/官方承认的):

本次研究的局限:


8. 对我的启发

  1. 与已有备份条目衔接:knowledge-fexxiou8-4(3-2-1 备份原则)讲的是通用灾备;本条补上"代码仓库"这一介质的专用手段——--mirror 做本地镜像、bundle --all 做单文件离线档案,正好填进 3-2-1 的"第 3 份异地"。bundle 不含 stash/hooks 的盲区,用"钩子单独归档 + stash 清零"消化。
  2. worktree 是多 agent 并行的基础设施:每个 agent 一棵树、各占一个分支,天然隔离无锁竞争;但必须给 agent 立规矩——不许 worktree remove 别人的树、不许动共享分支、收工 remove 自己的树。
  3. bisect run 反向逼迫提交纪律:想享受 O(log n),就得每个提交都能跑最小检查脚本——这把"原子提交"从美德变成了可量化的机制收益。
  4. 金律的 agent 版:agent 对已推送分支的任何历史改写(amend/rebase + force push)都应视为危险操作默认禁止;只在本地未推送的工作分支上允许 fixup/autosquash。
  5. Windows 特有注意:实验中每次写文件都报 LF will be replaced by CRLF——单人跨机同步时先定死 core.autocrlf 策略,否则 bisect 检出老提交时行尾抖动会制造假差异。

附:速查命令

# worktree
git worktree add ../wt-a -b feat/a        # 新树新分支
git worktree list [-v]                    # 查看全部树
git worktree remove ../wt-a               # 收工(脏树需 -f)
git worktree prune                        # 清理失效树记录

# rebase(仅限未推送提交)
git rebase main                           # 变基到 main
git commit --fixup <sha>; git rebase -i --autosquash main   # 自动 squash
git config --global pull.rebase true      # pull 时 rebase 而非 merge

# bisect
git bisect start HEAD v1.0                # bad 在前 good 在后
git bisect run ./test.sh                  # 0=good 1-127(除125)=bad 125=skip
git bisect log > log.txt; git bisect replay log.txt   # 悔棋
git bisect reset                          # 必做收尾

# 镜像备份
git clone --mirror git@host:me/repo.git   # 一次建档(全部 refs)
git remote update                         # 增量同步(在镜像内执行)
git bundle create repo-$(date +%F).bundle --all   # 单文件离线档案
git clone repo.bundle restored            # 从档案恢复
← 资料库今日播报 →