使用git对工作目录进行快照

7

有时我需要对当前(可能是脏的)工作目录进行快照。 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

你是否不想在主分支上进行提交? - Xavier Guihot
@XavierGuihot,是的,在最后一行中撤销了主分支上的提交。 - Ryota Tomioka
刚想到这个方法并尝试了一下,我同意上面展示的stash方法,它似乎可以完成所有需要的操作。 - Kristjan Jonasson
4个回答

5

你可以将你的更改暂时存储,创建一个新分支,应用你的更改,只在新分支上提交,然后再切换回主分支,最后再重新应用你的更改。

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

在执行 git checkout master 后,您需要执行 git stash pop 吗? - Ryota Tomioka
是的,确实如此!修正答案。 - Xavier Guihot
1
深入挖掘后,我发现这篇文章,其中提到git stash branch <branch_name>可以替代git checkout -b <branch_name> + git stash apply。此外,它还提供了一个很好的shell别名来完成你所需要的操作。 - Xavier Guihot
git stash branch 似乎也会删除存储,这将导致我无法在最后执行 git stash pop - Ryota Tomioka
这就是我最终要做的事情:git stash git checkout -b snapshot_of_master_yyyy-mm-dd-hh-mm-ss git stash apply git add --all git commit -m "快照" git checkout master git stash pop --index - Ryota Tomioka
你是对的,git stash branch 不幸地会弹出(pop)stash。 - Xavier Guihot

4

如果您没有任何进行中的合并冲突或意向添加标记,那么使用核心命令直接执行操作是最有效的,而且不会产生额外的工作树更改。

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

但是,如果您不关心被忽略的文件或者没有影响工作区的变化,最简单的方法就是:

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

1
请注意,git stash save -u 不会添加被忽略的文件,因此不使用 git clean -x;您在这里使用的 git add -f . 确实添加了被忽略的文件,因此在这里使用 -x 是适当的。但是,您可能需要确保您在树的顶部,或者使用 git add -f $(git rev-parse --show-top-level) 或类似命令来确保您不仅仅在子目录中工作。 - torek
1
哦,还有:你不想要 git stash save -a -u,后面的 -u 会覆盖 -a(至少在旧版本的 git stash 中是这样,我没有检查新版本是否检测到冲突的选项)。 - torek

2

另一种干净简洁的方法。甚至不需要更改分支或使用stash

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

使用commit命令拍摄快照,可以按照您喜欢的方式进行。
使用reset命令将分支指向以前的提交,这样您就回到了起点。

2

简述

使用jthill的答案,他在我写下面长答案的过程中离开了键盘。

详细解释

在你选择这个问题的任何一个答案之前(参见jthill的答案,我认为这些答案在某种意义上是“最好的”),请考虑以下内容:每当您有一个不仅仅是“工作树中所有内容与HEAD完全匹配”的状态时,就需要担心每个文件的三个版本。

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

git clone <url>

或者,从完全干净的状态(没有未跟踪的文件,没有修改过的文件等)开始执行以下操作:
git checkout <somebranch>

你开始时有每个文件的三个副本,例如 READMEMakefile等,现在可以立即使用:

  • 一个在 HEAD(你当前检出的任何分支的尖端提交)中:这是只读的,并且当然与在 HEAD 中的文件匹配,因为它就是在 HEAD 中的那个。这个 HEAD:README 文件以 Git 独有的格式存储,(使用 git show HEAD:README 命令可查看)。

  • 一个在索引中。索引是下一次提交的地方,但现在它只包含当前提交中的所有内容的副本。因此,:0:README - 你可以使用 git show :0:README 来查看此副本,与 HEAD:README 完全匹配。这个额外的副本也存储在 Git 独有的格式中,这意味着几乎不占用任何空间。 :0:READMEHEAD:README 的区别是你可以覆盖前者:例如,git add README 将工作树中的 README 复制到 :0:README 中。(你在这里制作的副本将占用一些空间,但之后它将成为下一个 git commit 的内容,并且直到你再次复制另一个副本,它们将共享那个冻结/只读版本。)

  • 每个文件的最后一个副本,例如 README,在工作树中。此文件以其正常的日常格式存在,因此所有程序都可以读取和写入它。没有必要使用 git show 查看它,因为它只是一个普通文件!

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

所以:

我有时需要对当前(可能是脏的)工作目录进行快照。

对于路径为 P 的每个文件,除了未跟踪的文件,我们有:
  • 位于 HEAD 提交中的版本 P:你不需要保存这个,因为它已经通过提交被永久保存了;
  • 位于索引中的版本 P:你想保存这个吗?
  • 位于工作树中的版本 P:你无疑想要保存这个。
那么问题在于如何处理未跟踪的文件。
请注意,未跟踪的文件只是工作树中不存在于索引中的文件。(这包括那些在HEAD中但已经从索引中小心地删除的文件——只要工作树版本的该文件存在,它们就是未跟踪的。)
“git stash save -u”非常类似于我需要的内容...
这是一个很好的线索,因为“git stash save -u”会保存:
- 当前索引(作为一个提交); - 当前工作树(另一个提交,仅跟踪文件);以及 - 未跟踪的文件(第三个提交)。请注意,第三个提交省略了未跟踪和被忽略的文件(没有既被跟踪又被忽略的文件;“忽略”仅适用于已经未跟踪的文件)。如果您也想包含被忽略的文件,则必须在此处使用“-a”而不是“-u”。
...但有两个问题:[1. “git stash -u”提交后会删除未跟踪的文件,2. “git stash -u”使得稍后解除存储变得相当困难]
请注意,“git stash -u”只是运行“git clean”来删除未跟踪的文件。您需要执行“git reset --hard && git clean -df”才能回到解除存储的状态(但请注意下面提到的问题情况)。
现在,制作提交的主要问题——任何提交——是您通过将文件复制到索引中来完成的。但我们刚才指出,各种文件(例如“:0:README”和“:0:path/to/important.data”)可能存在索引版本,它们与其“HEAD:”对应项和工作树对应项不同。如果您对保存工作树对应项做任何事情,必须通过覆盖索引副本来完成!
如果这没问题,您可能有一个比使用“git stash”或等效方法更简单的前进路径。但未跟踪的文件仍然是个问题!如果这不行,您必须先保存索引,就像“git stash”所做的那样,在这种情况下,您可能只想使用“git stash”。
我们已经注意到,未跟踪的文件是指工作树中存在路径U的文件,并且该路径在索引中不存在:没有:0:U。这会带来一些问题:要保存这些文件,我们必须将它们复制到索引中。当然,这会覆盖任何与HEAD和工作树版本不同的仔细暂存的内容。这就是所有复杂性的来源。
如果您确实想保留索引状态,而索引状态记录了哪些文件稍后应被跟踪和未跟踪,则我们有了解决方案(这也是jthill的解决方案),与git stash类似,但略有修改:
1.写出当前索引状态:git write-tree。
2.使用结果进行提交(直到没有名称时才为垃圾收集):git commit-tree。此提交可以将当前提交(HEAD或@)作为其父项,尽管实际上并非必需。
3.将所有未跟踪的文件(可能包括被忽略的文件)添加到索引中:git add -A,或git add -f -A等,具体取决于第二个提交中需要什么。
4.编写此更新的索引,然后使用结果进行第二次提交,其父项为保存的索引状态,并为此第二次提交命名以使其永久化。 (在jthill的答案中,就像git stash一样,他让第二个提交将两个提交都作为父项存储,并将索引提交作为第二个提交的第二个父项。这迫使我们稍后使用后缀^2表示法,它具有与git stash脚本一起使用的优点。)
写出所有这些内容后,我们必须立即将索引恢复到其原始状态-我们保存的第一步状态。否则,所有先前未跟踪的文件现在都是已跟踪的文件!
要恢复这些文件,我们面临与 git stash save -u 相同的问题:我们创建的第二个提交(用于保存未跟踪的文件)中的文件将会成为已跟踪文件,至少是暂时的。如果工作树中现在有同名的文件,Git 将非常不愿意覆盖它们,因此我们需要使用 git clean -dfgit clean -dfx 来删除它们。这里有一个小问题,因为这也会删除那些没有在第二个提交中的文件,例如,假设当您保存所有内容时,有一个未跟踪的文件名为 important-1,但没有任何一个名为 important-2现在出现了一个 important-2
如果您现在只是简单地运行 git clean -dfgit clean -dfx,Git 将删除这两个未跟踪的 important-* 文件。然后,我们将指示 Git 从第二个提交中提取所有文件,包括先前未跟踪的 important-1。Git 将把该文件复制到索引和工作树中。由于没有保存的 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-...。这一步实现了合并和更新,将第二次提交(保存了所有工作区状态)引入索引并更新工作区。这样就不会破坏important-2

第二步需要修复索引,因为从第二次提交中读取所有这些未跟踪的文件会使它们变成已跟踪文件。我们想把索引恢复到制作快照时的状态,或者至少将目前在其中但不应在其中的任何文件从索引中剔除出来

我们正好有那个索引状态:它在我们创建的第一个提交中,即snap-...^2(快照或隐藏文件的第二个父提交)。我们可以直接将其读入索引中:

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

(请注意这里缺少@ / HEAD),或者进行两棵树的合并,尽可能保留我们对索引所做的修改。
git read-tree @ snap-2018-02-18T14.19.15^2

请注意,我们也可以将索引重置以匹配您当前所在的提交:
git reset HEAD

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

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

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


非常感谢您的详细解释!现在我对这三个版本之间的区别更加清楚了。在快照中,我不需要区分索引和工作树中的版本,所以我想我会使用 git stash - Ryota Tomioka

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接