使用 git 制作工作目录的快照

Make a snapshot of working directory with git

我有时需要为当前(可能是脏的)工作目录做一个快照。 git stash save -u 和我需要的非常相似,但是有两个问题:

  1. 我希望我的工作目录保持相同状态(保持未跟踪文件未跟踪)
  2. 万一我需要回到保存的状态(可能一个月后),git stash apply就不容易了,因为我需要先找到git stash之前的状态。

我目前有以下适合我的命令序列,但我想知道是否有更优雅的方法。

# on branch master
git add .
git commit -m "Temporary commit on the original branch"
git checkout -b snapshot_of_master_yyyy-mm-dd-hh-mm-ss HEAD~
git merge master
git checkout master
git reset HEAD^

谢谢大家的解答和解释!我将主要根据@XavierGuihot 的回答做类似的事情

git stash -u # -u to include untracked files
git checkout -b snapshot_of_master_yyyy-mm-dd-hh-mm-ss
git stash apply
git add --all
git commit -m "Snapshot"
git checkout master
git stash pop --index # --index to recover the state of indexed files

您可以存储更改、创建新分支、应用更改、仅在新分支上执行提交,然后在最终再次应用更改之前再次检出 master。

git stash
git checkout -b snapshot_of_master_yyyy-mm-dd-hh-mm-ss
git stash apply
git add --all
git commit -m "Snapshot"
git checkout master
git stash pop

没有工作树变动的最有效方法是直接使用核心命令执行此操作,假设您没有任何飞行中的合并冲突或索引中的 intent-to-add 标记它是

statenow=`git write-tree`
stashedindex=`git commit-tree -p @ -m "index snap" $statenow`
git add -f .
git tag snap-`date +%Y-%m-%dT%H.%M.%S` \
            $(git commit-tree -p @ -p $stashedindex \
                    -m "worktree snap" \
                    $(git write-tree)
            )
git read-tree $statenow

但如果您不关心被忽略的文件或 no-effect 工作树变动,最简单的方法是

git stash -a -u
git tag snap-`date +%Y-%m-%dT%H.%M.%S stash
git stash pop

然后对于任何一个,恢复你所做的状态,例如

git clean -dfx
git checkout snap-2018-02-18T14.19.15           # to move HEAD + worktree there
# or
git read-tree -um @ snap-2018-02-18T14.19.15    # just the worktree

git read-tree @ snap-2018-02-18T14.19.15^2      # then restore the index

TL;DR

使用 ,这是他在我写下面的长答案之间离开键盘时写的。

在你决定这个问题的任何一个答案之前(参见 我认为在某种意义上是 "the best" 的集合),考虑一下:每当你有一个状态不是只是 "everything tracked in the work-tree exactly matches HEAD",每个文件有 三个 个版本需要担心。

也就是说,当你第一次 运行:

git clone <url>

或者,从一个完全干净的(没有未跟踪的文件,没有修改的文件等)状态做:

git checkout <somebranch>

每个文件的三个副本开始,例如READMEMakefile等等,现在可用:

  • HEAD中的一个(您签出的任何分支的提示提交):这个是read-only,当然与HEAD中的匹配,因为它 HEAD 中的那个。此 HEAD:README 文件以 Git 使用的特殊 Git-only 格式存储。 (使用git show HEAD:README可以看到。)

  • 索引中的一个。索引是您将构建下一次提交的地方,但现在,它只包含 current 提交中所有内容的副本。所以 :0:README——你可以使用 git show :0:README 查看这个副本——完全匹配 HEAD:README。这个额外的副本也以特殊的 Git-only 格式存储,这意味着它基本上不需要 space。 :0:READMEHEAD:README 的区别是你可以 覆盖 这个: git add README 复制 work-tree README:0:README,例如。 (你在这里制作的副本将占用一些 space,但随后将进入下一个 git commit,之后他们将共享冻结/read-only 版本,直到你复制另一个。)

  • 每个文件的最后一个副本,如README,在work-tree中。该文件采用其正常的日常格式,因此所有程序都可以读取和写入它。不需要用git show查看,因为它只是一个普通文件!

最初,所有三个版本都匹配,并且没有未跟踪的文件。

所以:

I sometimes need to make a snapshot of the current (possibly dirty) working directory.

对于路径为 P 的每个文件,除了 untracked 文件,我们有:

  • HEAD 提交中 P 的版本:您永远不需要保存这个版本,因为它已经通过提交永久保存;
  • 索引中P的版本:要保存这个吗?;和
  • P 的版本在 work-tree 中:你无疑想要保存这个。

这也留下了如何处理 未跟踪 文件的问题。

让我们在这里注意,未跟踪的文件只是索引中不存在的 work-tree 中的文件。 (这包括 HEAD 中但您小心地 索引中删除的文件——它们当前未被跟踪,只要该文件的 work-tree 版本存在。)

git stash save -u is very similar to what I need ...

这是一个很好的线索,因为 git stash save -u 节省了:

  • 当前索引(作为一次提交);
  • 当前work-tree(作为另一个提交,仅跟踪文件);和
  • 未跟踪的文件(作为第三次提交)。请注意,第三次提交省略了 untracked-and-ignored 的文件(不存在既被跟踪又被忽略的文件;"ignored" 仅适用于已经未跟踪的文件)。如果您还想要忽略的文件,则必须在此处使用 -a 而不是 -u

... but there are two problems: [1. git stash -u removes the untracked files after committing them, and 2. git stash -u makes it rather hard to un-stash again later]

请注意 git stash -u 只需 运行s git clean 即可删除未跟踪的文件。您必须执行 git reset --hard && git clean -df 才能返回到 unstash 状态(但请参阅我在下面提到的问题案例)。

现在,进行提交(任何 提交)的主要问题是您通过将文件复制到索引中来完成。但我们刚刚注意到,可能存在各种文件的索引版本,例如 :0:README:0:path/to/important.data,它们不同于 HEAD: 对应的 work-tree同行。如果您要保存 work-tree 个副本,则必须通过覆盖索引副本来完成!

如果这没问题,您可能有一条比使用 git stash 或等效方法更简单的前进道路。但是您仍然遇到未跟踪文件的问题!如果不行,你必须先保存索引,就像 git stash 一样,在这种情况下你可能只想使用 git stash.

我们在上面已经注意到,未跟踪的文件是 work-tree 中的文件——有一些路径 U——但不在索引中:没有 :0:<em>U</em>。这会产生一些问题:要保存这些文件,我们必须将它们复制到索引中。当然,这会破坏(覆盖)我们精心准备的与 HEAD 和 work-tree 版本不同的任何内容。这就是所有并发症的来源。

如果您确实出于任何原因想要保留索引状态, 索引状态会记录以后应该跟踪和取消跟踪哪些文件,那么我们有我们的解决方案(即jthill 的解决方案也是如此),这很像 git stash,但略有修改:

  • 写出当前索引状态:git write-tree.
  • 使用结果进行提交(永久直到它没有名字,当它可以被垃圾收集时):git commit-tree。此提交可以将当前提交(HEAD@)作为其父项,尽管实际上并不需要。
  • 将所有未跟踪的文件(可能包括被忽略的文件)添加到索引:git add -Agit add -f -A 等,具体取决于您在第二次提交中想要的内容。
  • 写入这个更新的索引,然后使用结果进行第二次提交,其父项是保存的索引状态,并为第二次提交命名以使其永久化。 (在 jthill 的回答中,就像 git stash 所做的那样,他将第二个提交存储为父项,索引提交作为第二个提交的第二个父项。这迫使我们使用后缀 ^2符号稍后,它的优点是它可以与 git stash 脚本一起使用。)

写完这些后,我们必须立即将索引恢复到原来的状态——我们在第一步中保存的那个。否则所有 previously-untracked 文件现在都是跟踪文件!

要恢复这些东西之一,我们遇到了与 git stash save -u 相同的问题:我们进行的第二次提交中的文件(用于保存未跟踪的文件)将至少暂时变为, 跟踪 个文件。如果现在 work-tree 中有同名文件,Git 将非常不愿意覆盖它们——所以我们需要 git clean -dfgit clean -dfx 来销毁它们。这里有一点问题,因为这将 删除第二次提交中 没有 的文件:例如,假设当你保存所有内容时,有一个名为 important-1 的未跟踪文件,但没有任何名为 important-2 的文件。 现在有一个important-2

如果您现在天真地 运行 git clean -dfgit clean -dfx,Git 将删除 both 这些未跟踪的 important-* 文件。然后我们将指示 Git 从第二次提交中提取所有文件,包括 previously-untracked important-1。 Git 会将文件复制到索引和 work-tree 中。由于没有保存 important-2,Git 不会 复制该文件。

这是使用比较大的缺陷:

git clean -dfx
git checkout snap-2018-02-18T14.19.15

这就是为什么:

git read-tree -um @ snap-2018-02-18T14.19.15
git read-tree @ snap-2018-02-18T14.19.15^2

更好。第一步,git read-tree -um @ snap-...,执行 merge-and-update 将我们所做的第二次提交(保持所有 work-tree 状态)引入索引并更新 work-tree。这样important-2就不会被破坏

之后需要第二步来修复索引,因为从第二次提交中读取所有那些未跟踪的文件会导致它们成为 已跟踪 文件。我们希望将索引恢复到制作快照时的状态,或者至少从索引中拉出 out 现在不应该在其中的所有文件它。

我们有确切的索引状态:它在我们所做的第一个提交中,即snap-...^2(快照或存储的第二个父级)。我们可以直接将其读入索引:

git read-tree snap-2018-02-18T14.19.15^2

(注意此处缺少 @ / HEAD),或者执行 two-tree 合并以尽可能保留我们对索引所做的修改:

git read-tree @ snap-2018-02-18T14.19.15^2

请注意,我们可以只重置索引以匹配您当前所在的提交:

git reset HEAD

或者可能将其重置为保存的第一个提交的父级:

git read-tree snap-2018-02-18T14.19.15^1

如果您真的不想保存索引状态。无论哪种方式,未跟踪的文件再次未跟踪,因为它们不再在索引中。

另一种简洁明了的方法。即使没有分支更改也不需要 stash:

git commit
git branch name-of-my-snapshot-branch
git reset HEAD^

使用 commit 只需提交您想要快照的所有内容,以您喜欢的任何方式执行此操作。

reset 将您的分支指向之前的提交,因此您回到了起点。