git stash -u 命令会在工作目录中保留未跟踪的文件

4
如果我理解正确,git stash -u会将所有内容(包括未跟踪的文件)储藏,并使你的工作目录回到最后一次提交(即HEAD位置)时的状态。
但我运行时,它删除了除一个文件夹外的所有未跟踪文件(当然也删除了所有已跟踪和修改的文件)。那个特定的文件夹在任何一个.gitignore中都没有被忽略。在我运行命令之前和之后,它都显示为未跟踪状态。这是一个错误吗?还是有原因导致这种情况发生?
进一步地说,我从同一存储库中的其他地方复制了该文件夹,但我用多个文件夹做了同样的操作,这些文件夹都被储藏和删除了。我的目标是将所有本地更改(包括已跟踪和未跟踪的文件)储藏起来,以便我可以得到与最新提交完全相同的工作目录。

你是否仍需要更改后的文件,以便稍后将其放入“stash”中?那么你的目标是让你的工作目录看起来与上次提交相同。因此,如果你不需要自上次提交以来更改过的文件,请运行git clean -fdf表示文件,d表示目录)。注意:这些更改将永远消失! - SwissCodeMen
我可能以后需要那些文件,这就是为什么我决定使用 git stash -u 的原因。同时,我不认为它们处于可以提交的状态。我的困惑在于,为什么 git stash -u 会在我的工作树中留下一个未跟踪的文件夹? - Skawang
没有更多的信息很难提供帮助。您说未被隐藏的文件夹不在此存储库中的任何.gitignore中。您尝试过以下命令吗:git stash -u --all,以将所有更改的文件都隐藏起来,其中包括.gitignore中的文件?也许.gitignore中的文件夹最终被忽略了。 - SwissCodeMen
1个回答

1

TL;DR

运行命令后,除了一个文件夹外,它消除了所有未跟踪的文件(当然还删除了工作目录中所有已跟踪和修改的文件)。在我任何 .gitignore 文件中都没有这个特定的文件夹,在运行命令之前和之后,它都显示为未跟踪。这是一个 bug 还是有原因的?

这取决于这个文件夹里面有什么。Git 从一开始就不会存储文件夹,所以如果文件夹为空,那就是它仍然存在的原因;但是 Git 在 git status 中也不会提及空文件夹,所以你可能指的是非空文件夹。

整个事情有点神秘。也许你可以解决它。

Long

我认为,从使用 git stash 时没有使用 -u-a 选项(它们都有更长的拼写方式:--include-untracked--all)开始是有帮助的。这种类型的 git stash 所做的是:

  1. 提交 Git 索引中的所有内容:这与 git commit 命令相同,但是 git stash 在当前分支上不会进行提交,而是在无分支状态下进行提交;然后
  2. 提交 Git 索引中使用 git add 命令添加的所有文件,即等效于 git add -u && git commit 的提交:这类似于你执行此操作后得到的提交,但是 git stash 也会在当前分支上不进行提交,而是在无分支状态下进行提交,并且由于内部原因,使得 Git 认为这个提交是一个合并提交。1 然后,git stash 将会
  3. 运行 git reset --hard 命令,以重置 Git 索引和工作树。
这个最后的 git reset --hard 命令会清除所有暂存和未暂存的修改。2 这是可以做的,因为 你需要恢复这些修改(包括已暂存和未暂存的)的所有内容都在步骤1和步骤2中 git stash 所创建的提交中。 然而,要真正理解这一点,你需要很好地掌握 Git 的索引是什么,这样才能理解步骤1和步骤2。让我们花点时间定义一下,因为很多 Git 教程都没讲清楚。(如果你已经非常了解索引是什么以及它对你有什么作用,可以跳过下一节。)

1如果Git认为这个提交是一个合并提交,那么也许它就是一个合并提交。但它并没有使用任何Git的合并机制来制作。那么它是一个合并提交吗?这重要吗?如果是,那么什么时候重要?这些都是有用的问题,在你拥有了Git的其他方面的丰富经验之后再去思考。

2Git实际上不处理“更改”,而是处理“快照”。区别很简单:给定两个快照,我们可以找到更改,但是给定一个更改,我们找不到任何快照。可以将其类比于天气预报。如果他们告诉你今天比昨天温度高10度或低10度,昨天的温度是多少,今天的温度是多少?你能回答这个问题吗?但是如果他们告诉你昨天是20度,今天是30度,那么昨天的温度是多少,今天的温度是多少,这两个温度差异有多大?

两个快照可以让你看到差异。差异并不能让你看到任何快照。这就是为什么Git处理的是快照。但作为人类,我们通常更关心差异。这就是为什么Git告诉你差异。


索引,也称为暂存区或缓存区

在Git中,索引是一个非常重要的概念,你必须了解它。虽然有一些方法可以尝试避免学习它——比如使用git commit -a——但如果你要使用Git,跳过这个步骤会对自己不利。问题在于,Git喜欢搞笑,有时候会用它的索引制作一条鱼来打击你,至少是在比喻上;你需要能够反击。

在Git中,索引有许多角色,在冲突合并时会被扩展(此后你就无法提交)。我们不会在这里详细介绍这些细节,尽管我会提到,解决冲突的过程本质上是将索引缩小到可提交的大小(此后你可以再次提交)。相反,我们将集中讨论Git如何使用索引“跟踪”文件,以及这意味着Git索引中的内容是提交的内容。

索引还有三个名称。这反映了它的重要作用,或者说“索引”是一个糟糕的名称,或者两者都有。另外两个名称是“暂存区”,它指的是你如何使用它,以及“缓存”。最后一个名称今天很少使用:它主要出现在标志中,比如“git rm --cached”。但是,索引是什么呢?
我认为解释索引的最好方法是通过示例。如果您运行“git ls-files --stage”,Git将在您正在使用的任何Git存储库中以其目前存在的状态将整个索引的内容倾倒出来。这是在Git存储库中的索引的一部分:
$ git ls-files --stage | sed -n -e 10,19p
100644 cbeebdab7a5e2c6afec338c3534930f569c90f63 0       .gitmodules
100644 bde7aba756ea74c3af562874ab5c81a829e43c83 0       .mailmap
100644 05f3e3f8d79117c1d32bf5e433d0fd49de93125c 0       .travis.yml
100644 5ba86d68459e61f87dae1332c7f2402860b4280c 0       .tsan-suppressions
100644 fc4645d5c08bd005238fc72cfa709495d8722e6a 0       CODE_OF_CONDUCT.md
100644 536e55524db72bd2acf175208aef4f3dfc148d42 0       COPYING
100644 ddb030137d54ef3fb0ee01d973ec5cee4bb2b2b3 0       Documentation/.gitattributes
100644 9022d4835545cbf40c9537efa8ca9a7678e42673 0       Documentation/.gitignore
100644 45465bc0c98f5d88cfe1ade092d29b5dc32c1e23 0       Documentation/CodingGuidelines
100644 b9804070594d9cd33dfc1e30cdd925c6e83a2187 0       Documentation/Makefile

如果我现在运行git status,我会得到nothing to commit, working tree clean。然而,索引中充满了文件。如上所示,索引包含每个文件的四个内容:mode字符串(通常为100644),一个哈希ID,一个暂存号码 - 这需要为零才能提交,并且除了中间合并之外,总是为零 - 和一个文件名,例如 COPYINGDocumentation/Makefile

这些是将进入您下一次提交的文件。就是这样:很简单。对于普通文件和可执行文件,mode分别为100644和100755,它是Git如何在具有可执行文件的系统上恢复文件模式。3 文件名(包括嵌入的斜杠;索引中没有“文件夹”)告诉Git要命名的文件,哈希ID表示去重后的文件内容,这些内容将出现在提交的所有文件的快照中。

当您首次检查某个提交时,Git会使用该提交中的快照将其索引填充到所有文件中。然后,Git将相同的文件填充到您的工作树中。因此,现在您在Git的索引和您的工作树中都拥有来自该提交的所有文件。
当文件在Git的索引中时,Git称该文件为已跟踪文件。如果您的工作树中有一个不在Git的索引中的文件,则为未跟踪文件。您的工作树是计算机上的普通目录(或文件夹)。从很实际的意义上讲,您的工作树根本不在Git存储库中。因此,您可以随意创建和销毁文件和文件夹。Git对此一无所知,并且没有影响。稍后,您可能会运行git add,例如告诉Git现在查看我的工作树。或者您可能不这样做!
当你运行git commit时,Git使用在Git的索引中的文件——而不是你的工作树中的文件!——来创建新的提交。因此,如果你git checkout一些提交,或使用git switch检出一些提交,那么会用该提交填充Git的索引,并同时填充你的工作树。如果你随后修改了工作树中的内容,但没有运行git add,并尝试git commit,Git会告诉你没有新的提交。Git刚才做的是将Git的索引中的文件与当前提交中的文件进行比较。它们都是相同的!你可能已经改变了你的工作树,但在仓库中没有发生任何事情。因此没有新的提交可供提交。
当你运行 git add 命令时,你正在告诉 Git: 现在请查看我的工作树文件。 特别地,你告诉 Git 在其索引中将 它的 每个准备提交的文件与你在工作树中更新或新创建的文件匹配。如果你创建了一个全新的文件,Git 将复制该全新文件到其索引中。如果你更新了某个现有文件,则 Git 会将更新的内容复制到其索引中。无论哪种方式,你现在都使得 Git 的索引副本 你的工作树副本相匹配。
最终,索引函数作为您的“拟议下一个提交”功能。将文件复制到舞台(暂存区)上方(或下方),即可“上演”文件:对其进行新快照的排列,就像在舞台上移动家具和假人、拍摄广告照片一样。您可以对某些内容进行“上演”,然后退后一步看看照片效果。如果您喜欢它,现在可以通过运行“git commit”来实际“拍照”。如果您不喜欢它,则可以操作工作树文件的副本,再次运行“git add”,并再次回到之前的起点。
这里的关键是:
  • 索引作为您的下一个建议提交。索引中始终有文件的副本。它们最初来自某个提交。当您运行git add时,它们会得到更新git commit命令会快照“在舞台上”的所有内容。 当索引中的副本与当前提交中的副本不同时,git status命令将文件列为准备提交

  • 文件可以存在于您的工作树中,而从未进入“舞台”。这些未跟踪的文件将不会出现在将来的提交中。它们可能根本不存在于存储库中。7

  • 索引仅保存文件名称,而不是目录名称。这就是为什么您无法提交空目录的原因:Git从索引构建提交,并且索引仅保存文件。名称可以包含斜杠 - 例如Documentation/Makefile - 并且主机系统可能要求在您的工作树中存在此目录(文件夹),其中包含名为Makefile的文件,但在索引中,它只是一个名为Documentation/Makefile文件

由于git commit只会提交索引中列出的文件,因此您对未跟踪的文件所做的任何更改都是无关紧要的。另外,由于git commit将提交索引中的内容,因此您对已跟踪的文件所做的任何更改仅在您使用git add添加这些文件时才有意义。8


3在不支持此功能的系统上,Git仍会在索引中保留模式,以便可以进入提交记录。Git会注意到系统不支持文件模式,并将core.fileMode设置为false;要更改Git索引中的模式,您将不得不使用Git的低级别update-index命令。如果您使用的是Linux或macOS,则通常不会出现任何问题。Samba也可以在Windows系统上模拟可执行权限,尽管由于我避免使用Windows,因此我不确定这在实践中效果如何。

4我故意忽略了很多细节:Git有能力在某些情况下填写特定文件的索引,以便让您在具有未提交工作的情况下切换提交记录。这种想法-在携带未提交工作的同时切换提交记录-非常棘手并充满了角落情况。有关所有细节,请参见Checkout another branch when there are uncommitted changes on the current branch

5技术上,Git只定义了术语“未跟踪的文件”,但如果一个不在索引中的文件是未跟踪的,那么一个在索引中的文件必须是已跟踪的。

6仓库本身通常位于您工作树的顶层的.git子目录中。该.git文件夹内部的所有文件和目录都是仓库,并受Git的控制;它外面的东西不受Git的控制,因此不是仓库的一部分。

7技术上,未暂存的文件只是不存在于“下一个拟议”的提交中。它可能存在于其他现有的提交中。未跟踪的文件通常不在任何提交中。

注意,git commit -a会在git commit步骤期间自动运行git add步骤,但是这个add只会添加已经在索引中的文件,所以你仍然需要对任何新文件执行git add。虽然一开始使用git add -a非常有吸引力——一开始,它使得使用Git像使用Mercurial一样容易——但是如果你试图使用它来避免学习索引,它最终会让你失望。我们将在下一节中看到,关于git stash -u

-u-a 选项

git stash命令中添加-u-a,更准确地说是在保存或推送子命令中添加,告诉Git要向通常组成存储区的两个无分支提交中添加第三个提交。这第三个提交在文档中没有得到恰当描述。文档有一个简短的部分标题为DISCUSSION,讨论了在IW提交中保存索引和工作树文件的内容(我通常在自己的描述中将它们小写),但它从未提到为这些存储操作添加的U提交。

我们需要从提交的第一条规则开始:Git的提交总是从索引中进行的。9请注意,这里说的是一个索引,而不是索引:我们可以将Git的内部位指向临时索引,我们可以按照自己的方式创建它。例如,我们可以将主/真实索引复制到新的临时索引中,然后对临时索引进行调整。这就是旧版的git stash程序的工作原理,当git stash还是一个shell脚本时。现在已经被重写为C程序,仍然这样做,只是现在更难看出它是如何做到的。

这意味着,保存一个存储区所需的两个或三个提交 i wu都是从索引中进行的,因此只能保存文件。它们不能保存目录。它们保存的文件名可以包含斜杠,但像所有提交一样,没有目录,只有带有斜杠的文件名。

当使用-u-a时,git stash仍会像往常一样创建 i w提交,并像往常一样使用git reset --hard。 但是,在完成之前,git stash将枚举所有未跟踪的文件。这也是-u-a之间的区别所在。
我们可以将未跟踪的文件分为两组。有未跟踪但未被忽略的文件,其中git status会对这些文件发出警告;还有未跟踪和被忽略的文件,其中git status不会有任何提示。 10 -u选项列出了被警告的未跟踪文件列表,而-a选项列出了所有未跟踪的文件列表,即使要抑制警告。
做完未跟踪文件列表后,git stash push -u-a现在创建了第三个u提交。它通过以下方式实现:
  1. 创建一个空的临时索引;
  2. git add所有列出的文件到临时索引中;
  3. git commit此为特殊的u提交;和
  4. 从您的工作树中删除这些文件。
就是这样。这真的是所涉及的全部。11现在三次提交的存储存在后,当git stash -u完成后,您的未跟踪文件将从您的工作树中消失。但是还有一个缺口。

9可以使用git mktree来制作一个不使用索引的快照,但实际上并不是这样做的。相反,内部命令使用git write-tree将至少一个索引转换为快照,然后使用git commit-tree将快照打包成提交。

10请注意,不存在被跟踪但被忽略的文件:被跟踪的文件本质上不会被忽略。这意味着.gitignore这个名称可能不太准确:它应该被称为.git-do-not-whine-about-these-untracked-files。这个名称已经太长了(并且足够描述性的文件名非常荒谬),所以Git将此文件称为.gitignore

11最初实现这个功能的代码非常复杂,当添加了部分树储藏功能后,在执行git stash push -u时就会出现错误。这将无法恢复地删除未放入u提交中的文件。虽然这个问题已经修复,但这是我建议尽可能避免使用git stash的众多原因之一。


Git如何清理,及其奥秘

当您运行git reset --hardgit clean时(两者在内部使用相同的代码),Git有时会从工作树中删除一些文件。 正如我们之前所提到的,Git从未在第一时间存储文件夹。 相反,在提交中,Git拥有带有斜杠名称的文件,例如Documentation/Makefile

Git的做法是,当它需要创建一个文件时,它尝试这样做,如果失败,因为操作系统拒绝具有以下形式的创建新文件请求而发生错误:没有名为Documentation的目录可以放置名为Makefile的文件,Git只需继续创建目录即可。13 实际上,每当需要时,Git就会创建新文件夹。操作系统要求它们存在,因此Git满足了操作系统的需求。

这意味着为了“清理自身”,Git 应该删除目录/文件夹。所以 Git 就这样做了:如果 Git 从一个可能早先由它自己创建的文件夹中(比如 Documentation)删除了它自己最后一个文件,比如 Documentation/.gitattributes,Git 将尝试删除现在为空的文件夹。但是这里有一个复杂的问题:如果您在那里制作了一些未跟踪的文件,而 Git 没有注意到呢? Git 的答案很简单:如果删除失败,就别抱怨。如果尝试删除 Documentation 文件夹因为它不是空的而被操作系统拒绝,那么只能耸耸肩然后继续前进。
使用git clean命令时,-d选项控制Git是否应该查看未跟踪的目录。至少文档是这样描述的。问题在于,并不存在未跟踪的目录,只有未跟踪的文件。使用git status命令时,Git会找到所有未跟踪的文件并抱怨它们。只是git status通过说目录未跟踪来总结许多位于某个子目录中的文件。为了从git status获取更多信息,使用-uall选项以防止此类总结。由于git stash清理是隐藏的,我们不必担心您是使用脚本版本的git stash还是C编码版本以及任何-d选项。但请注意git status-uall选项。

最终,这里有两个有趣的问题:

  • Git是否将文件从“未跟踪的目录”中放入了u提交中?

    要找出,请列出u提交的内容:git ls-tree -r stash^3stash^3符号是gitrevisions语法,它将在存储中查找u提交。 git ls-tree -r将显示该提交中所有文件的名称。

  • Git是否未能删除其中一些文件? 如果是这样,那是因为Git默默地忽略了权限问题吗? 还是由于子目录本身包含一个Git仓库,导致Git无法保存这些文件? Git无法将Git仓库保存在另一个Git仓库内:出于安全原因,这是被禁止的。 因此,“未跟踪的目录”若是一个Git-repo-and-work-tree对的顶层目录,则会导致这种情况。

只有您拥有相关的文件系统(和Git存储库),因此只有您可以回答上述问题。


12从技术上讲,在提交表格中,名称是以类似文件夹的结构存储的。将提交提取到索引的过程会使用斜杠将名称串在一起。如果索引可以保存目录,很多东西可能会改变。不幸的是,将目录名称存储在索引中存在技术问题(因为Git已经对此做了其他处理——索引的内部格式比较复杂)。

13这忽略了许多内部问题。特别是当我们从一个提交移动到另一个提交时,我们可能需要一个名为Documentation/目录来容纳文件,但实际上只有一个名为Documentation文件,反之亦然。Git事先注意到这些问题,因为在正确的时间向操作系统发出正确的操作变得非常重要。目录/文件冲突或“d/f”冲突会使Git重新排列操作顺序,包括删除需要消失的文件(因为它们是已跟踪的文件,而不在我们要移动到的提交中)和创建文件,因为它们将成为我们要移动到的提交中的已跟踪文件。


感谢您的全面回答。作为一个Git的初学者,我学到了一些以前不知道的东西。该文件夹内没有Git存储库。我不明白为什么它会与其他文件夹不同。编辑:我运行了“git ls-tree stash^3”,看起来该文件夹中的文件在stash提交中(我之前在错误的目录中运行了它)。这意味着它正在正常工作,那么为什么“git reset --hard”部分没有起作用呢? - Skawang
请注意,git reset --hard 本身不会触及任何 未跟踪 的文件。 (不在 [主] 索引中意味着没有任何事情需要对它们进行操作/处理。)奇怪的是,在将这些文件放入额外的 u 提交后,然后应该从您的工作树中删除这些同一文件的步骤未能删除至少一个此类文件。(这假定您已经仔细整理了 git ls-tree -r stash^3 输出与残留的未跟踪文件,并发现在该提交中存在未消失的未跟踪的文件。否则,谜团就是为什么它们不在 u 提交中)。 - torek

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