删除 git worktree 之前,怎么备份未提交的改动

git worktree remove --force 会把未提交和未跟踪的文件永久删掉。先用临时 index 把它们快照到一个 ref,再按需恢复。命令都实测过,包括 .env 的处理。

一句话回答:在 git worktree remove --force 之前,用临时 index 把 worktree 里未提交和未跟踪的文件存成一个 commit,挂在你自己的 ref 下,这样 worktree 里的东西一点都不会变。要恢复时,从快照的父提交建一个新 worktree,再执行 git read-tree -u --reset <ref>。被 ignore 的文件(比如 .env)不在快照里,要另外复制。

为什么不能只靠 git stash

删除 worktree 后,分支和提交仍留在仓库里。被删除的是工作区目录里的文件,包括未提交的改动和未跟踪的文件。强制删除之前,先把这些文件备份下来。

看起来 git stash 是现成的办法,但有两个问题:

  • 普通的 git stash 不包含未跟踪文件,要用 git stash push -u。
  • 同一个仓库的所有 worktree 共用一个 stash 列表。条目会带上来源分支的名字,但会和其他 worktree 的混在一起,在任何一个 worktree 里执行 git stash drop 或 git stash clear 都可能把它们删掉。

下面的做法是把快照存成一个 commit,挂在单独的 ref 下。它不碰 stash 列表、不碰分支,也不改 worktree 里的文件。

用临时 index 做快照

在即将删除的那个 worktree 里执行。把 backup/feat-search 换成之后你认得出来的名字。

(
  set -e
  index_dir=$(mktemp -d)
  trap 'rm -r -- "$index_dir"' EXIT
  export GIT_INDEX_FILE="$index_dir/index"
  git read-tree HEAD
  git add -A
  tree=$(git write-tree)
  commit=$(git commit-tree "$tree" -p HEAD -m "backup: feat/search")
  git update-ref refs/backup/feat-search "$commit" ""
)

因为 GIT_INDEX_FILE 指向别处,git add -A 不会碰 worktree 真正的 index。之后再运行 git status --short,看到的和之前一模一样。删除之前,先确认快照成功且包含所需文件。最后一条命令遇到同名备份 ref 会失败,每次备份请用新名字。

git show --stat refs/backup/feat-search

另外记下分支当时所在的 commit(git rev-parse HEAD),或者把分支留着。快照 commit 的父节点就是那个 commit,所以 git log refs/backup/feat-search 也能看到它。

被 ignore 的文件不在快照里

git add -A 会遵守 .gitignore,所以 .env、密钥和本地配置都不会进去。提交时这正是你想要的,但删目录时恰恰不是。把它们列出来,把重要的复制出去:

git ls-files --others --ignored --exclude-standard

把需要保留的文件复制到 worktree 之外的私有目录,保留它们的相对路径。确认复制成功后再删除 worktree。

node_modules 之类的构建产物也会出现在这个列表里,不用管,可以重新生成。

然后再删除 worktree

git -C /path/to/app worktree remove --force ../app-search

确认快照和单独复制的文件都已备份后,从主工作区执行这条命令。这里用 /path/to/app 作为主工作区路径。完整的流程,包括怎么确认分支已经进了 main,见如何清理 git worktree 而不丢工作。remove 的各种报错见如何删除 git worktree。

恢复

从主工作区执行下面的命令,以快照的父提交为起点创建恢复分支。即使原分支在备份后有新提交,也能恢复当时的文件内容。

(
  set -e
  git worktree add -b recover/feat-search ../app-search 'refs/backup/feat-search^'
  cd ../app-search
  git read-tree -u --reset refs/backup/feat-search
  git reset -q
)

运行 git status --short:快照恢复的是文件内容。已跟踪文件的改动会显示为未暂存,父提交里没有的文件会显示为未跟踪。一次包含已修改文件、未跟踪文件和被 ignore 的 .env 的往返测试里,前两者都恢复了,.env 没有,所以它需要单独的备份。最后再把备份的文件复制回去。

确定不需要时再删掉快照

git update-ref -d refs/backup/feat-search

之后 git 的垃圾回收会清掉不可达的对象。在那之前这个 commit 还能找回来,但别指望这一点。只要你可能还想要这些改动,就一直留着这个 ref。

每次清理都这样做

对一个 worktree 手敲这些没问题,但二十个 worktree 的清理,每次都记得先做这一步,就是工作容易丢的地方。Orchard 在删除 worktree 之前会自动做同类快照:未提交的改动、未跟踪的文件和分支顶端。它也会把被忽略的 .env、*.pem、*.key 等文件另外备份。快照保留 7 天,可以在应用里恢复。有 agent 在运行或有中断的 rebase 的 worktree,它会拒绝清理。

常见问题

git stash 会包含未跟踪文件吗?

默认不包含。git stash 只保存已跟踪文件的改动。用 git stash push -u 可以包含未跟踪文件,加 --all 连被 ignore 的文件也包含。同一个仓库的所有 worktree 共用一个 stash 列表。

git worktree remove --force 之后还能恢复文件吗?

事先做的备份或快照是可靠的恢复方式。reflog 会记录引用曾经指向哪里,可以用来找旧提交。未暂存的改动和未跟踪的文件没有这样的记录。

为什么用临时 index,而不是直接 git add -A 加 git commit?

普通提交会改动分支和 worktree 的 index。让 GIT_INDEX_FILE 指向一个临时文件,快照就在旁边构建,分支不会移动,worktree 之后也和之前一样。