简述
使用jthill的答案,他在我写下面长答案的过程中离开了键盘。
详细解释
在你选择这个问题的任何一个答案之前(参见jthill的答案,我认为这些答案在某种意义上是“最好的”),请考虑以下内容:每当您有一个不仅仅是“工作树中所有内容与HEAD
完全匹配”的状态时,就需要担心每个文件的三个版本。
也就是说,当您第一次运行:
git clone <url>
或者,从完全干净的状态(没有未跟踪的文件,没有修改过的文件等)开始执行以下操作:
git checkout <somebranch>
你开始时有每个文件的三个副本,例如
README
和
Makefile
等,现在可以立即使用:
一个在 HEAD
(你当前检出的任何分支的尖端提交)中:这是只读的,并且当然与在 HEAD
中的文件匹配,因为它就是在 HEAD
中的那个。这个 HEAD:README
文件以 Git 独有的格式存储,(使用 git show HEAD:README
命令可查看)。
一个在索引中。索引是下一次提交的地方,但现在它只包含当前提交中的所有内容的副本。因此,:0:README
- 你可以使用 git show :0:README
来查看此副本,与 HEAD:README
完全匹配。这个额外的副本也存储在 Git 独有的格式中,这意味着几乎不占用任何空间。 :0:README
与 HEAD: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 -df
或
git clean -dfx
来删除它们。这里有一个小问题,因为这也会删除那些
没有在第二个提交中的文件,例如,假设当您保存所有内容时,有一个未跟踪的文件名为
important-1
,但没有任何一个名为
important-2
。
现在出现了一个
important-2
。
如果您现在只是简单地运行
git clean -df
或
git 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
如果您并不真正希望保存索引状态。无论哪种方式,未跟踪的文件再次成为未跟踪的,因为它们不再在索引中。