git stash是否只存储暂存区文件还是包括未暂存和未跟踪的文件?

4
我把一些代码更改放在了错误的分支(dev)上。我希望将所有更改转移到 master 分支,从这里获得了一些领导,但我不确定 git stash 是否只保留已经 暂存 的文件,还是连未暂存未追踪的文件也会保留?
因为在切换分支后,我需要将未暂存未追踪的文件都提交到 master 分支。 dev 分支的状态:
 git status
On branch dev
Your branch is up to date with 'origin/dev'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   __init__.py
    modified:   src/App.py
    modified:   src/analysis/insis.py
    modified:   src/access/via/db.py
    modified:   src/process/DM.py
    modified:   start.sh
    modified:   utility/utils.py

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    docs/
    process.py
    src/flow.py
no changes added to commit (use "git add" and/or "git commit -a")

切换到master分支时,我遇到了以下错误:
 git checkout master
error: Your local changes to the following files would be overwritten by checkout:
    start.sh
Please commit your changes or stash them before you switch branches.
Aborting

一个安全的切换可以如何发生在 `master` 分支上,将所有未提交和未跟踪的更改从 `dev` 分支传输到 `master` 分支?
2个回答

11
默认情况下:git stash 会存储暂存的文件(索引)和未暂存的文件(已跟踪的文件,已修改但未添加),但不会存储未跟踪的文件。
你可以使用以下命令:
  • git stash -k 告诉stash只存储未暂存的修改,保留暂存的文件。
  • git stash -u 告诉stash同时包括未跟踪的文件。
  • git stash -a 告诉stash同时包括未跟踪和被忽略的文件。
参考资料:git-stash选项

1
git stash -k告诉存储区将暂存的文件保持原样,只存储未暂存的修改。”第二部分是不正确的:此命令会存储所有修改。 - Géry Ogam
1
@Maggyero:我认为你(正确地)指的是存储中创建的提交包含所有修改。但就你的工作树和索引而言,git stash -k将保留你的索引不变,并且仅丢弃未暂存的更改。 - LeGEC
1
没错,我只是在指你句子的第二部分,第一部分是正确的。你知道有什么方法可以只隐藏未暂存的修改吗? - Géry Ogam
1
阅读被接受的答案是个好主意。它提供了一个解释 git 机制的方法,这实际上是有意义的,并有助于预测和记忆 git 的行为。大多数 git 解释似乎非常注重让它看起来像他们理解你永远不会理解的东西,通过用箭头指向错误方向的图表来解释抽象术语。而这个解释实际上解释了 git。这是一种行之有效的方法。 - NeilG
1
@NeilG:我同意,你可以从torek的回答中学到很多东西(我知道我曾经学到了,现在仍在学习)。 - LeGEC
显示剩余6条评论

6
git stash 命令的作用是创建一些提交。这意味着它并没有做其他任何你不能通过创建提交完成的事情。
“我不清楚 git stash 仅存储暂存区文件还是未暂存和未跟踪的文件?” 这并不是一个确切的问题,除了最后一部分之外。默认情况下,git stash 不会存储未跟踪的文件,但是有一个选项可以让 git stash 这样做。您可能不应该使用此选项。要真正理解 git stash 的工作原理以及您可以做什么,您需要了解许多内容。
Git 关注的是提交而不是分支。分支名称很有用:它们帮助您(和 Git)查找提交。但是提交才是最重要的。每个提交都包含所有已跟踪文件的完整快照。这些文件以特殊的只读、Git-only、冻结压缩和去重形式存储在提交中。这种去重意味着当您创建一个新提交并重新使用先前提交中的大多数文件时,新提交实际上不需要文件的另一个副本:它只是重用早期提交中的冻结文件。每个提交都有一个唯一的哈希 ID。这个哈希 ID 是根据对提交的所有二进制位(1 和 0)计算出来的加密校验和。这意味着更改提交是不可能的。如果您从 Git 对象的大型数据库中取出提交,修改一些位,并将其放回,则新提交具有不同的哈希 ID。因此,之前的提交仍然存在。因此,提交就像 Git 的冻结存储文件一样不可能被更改。
除了完整复制您的每个文件(但是去重),每个提交还存储一些元数据:关于提交本身的一些信息,包括进行提交的人的姓名和电子邮件地址、日期时间戳以及他们解释为什么要进行该提交的日志消息; 但它还包括仅供Git使用的内容:上一个提交的哈希ID。 这使得Git可以向后工作。
分支名称仅保存分支上最后一个提交的哈希ID。 我们不会在这里详细介绍,因为我们更感兴趣的是存储提交,除非描述Git通常如何创建新提交。
您在每个提交中存储的文件是只读的,并且而且,它们处于大多数计算机上的程序无法读取的格式中。那么你怎么可能在你的文件上工作呢?换句话说,提交是只读的,那么你怎么能创建新提交呢?
Git对此的答案是为您提供一个工作树或工作目录。您可以通过使用git checkout或git switch选择某个提交开始。 Git从提交中获取所有冻结的,压缩的,去重的Git-only文件,并将其展开成普通的日常读/写文件,您可以读取和编写。 这些来自提交的每日使用副本文件位于您的工作树中。
这意味着您看到和使用的文件实际上不是Git的文件。 Git的文件隐藏在.git目录中(大多数也没有正常的文件名:它们存储在具有哈希ID名称的.git/objects中,或者移动到特殊的pack文件中,这些pack文件会存储更多压缩的多个对象)。
这必须是这种情况,因为Git的文件是只读的且仅适用于Git,您需要可以使用所有内容的读/写文件。 因此,Git填充您的工作树,然后您开始工作。 工作树文件属于您:当您请求时,Git只是填充它们。
这就足够了:Git可以只有每个文件的两个副本,一个在当前提交中冻结,另一个在您的工作树中解冻并可用。 实际上,其他版本控制系统也是这样做的。 但是Git没有。 Git保留每个文件的第三个副本。 这个额外的副本以Git的冻结格式存在,但与提交中的副本不同,它并不完全被冻结:您可以替换它。

1从技术上讲,这第三份副本是预先去重的,因此实际上不直接存在于索引中。相反,索引保存每个文件的模式、文件名、blob哈希ID、分段编号和其他使Git运行快速的信息。然而,通常情况下您不需要知道任何这些内容:您可以假装索引包含每个文件的冻结格式副本,随时准备提交。只有在开始使用低级索引操作命令git ls-files --stagegit update-index时,您才需要了解有关blob哈希的信息。


索引或暂存区

索引是Git中另一个非常重要的东西,通常没有很好地解释清楚。问题的一部分是它有点复杂:例如,在合并过程中它承担了更大的角色。但总体而言,有一个好的、相对简单的描述Git索引的方式:Git的索引保存了您提议的下一个提交。

注意:索引或者被如此重要,或者最初命名得太糟糕了,以至于现在它有三个名称。它被称为索引(如此处所示)、暂存区(表示其在创建新提交时的作用)或者极少使用的缓存。所有这三个名称都指的是同一件事情。2

换句话说,在任何时候,每个文件都有三份活动副本:

  • 一个在当前或HEAD提交中,这个副本是冻结的:它不会改变。您可以创建一个新的提交,其中包含一个不同的拷贝,但旧的提交将继续保留旧的拷贝。

  • 第二个是准备好冻结的在Git索引中。它最初与冻结的副本相匹配。

  • 最后一个处于日常格式并且是您要处理的:它在您的工作目录中。

当您运行git commit并创建一个新的提交时,Git打包所有在Git索引中的文件,并将其放入新的提交中。所以新的提交完全由此时在Git索引中的每个文件版本组成。

当您在文件上运行git add时,您实际上告诉Git:将工作目录版本的文件复制到索引中。 Git将它压缩到冻结格式(并且如果已经有一个拷贝,则去重)。如果这是一个全新的文件,现在它在索引中。如果它已经在索引中,那么旧副本就被排挤出了索引;现在索引副本与工作目录副本匹配。

你还可以让Git从索引中删除一个文件,如果它之前在那里(从提交或工作树中复制),现在它就会被删除。执行此操作的主要命令是git rmgit rm不仅会删除索引副本,还会删除你的工作树副本。由于工作树副本根本不在Git中,除非你将其复制到索引中,否则无法找到它,或者除非它来自于提交,并且你刚刚还删除了索引副本,请注意此操作。
要从索引中删除文件的副本而不删除工作树副本,可以使用git rm --cached。由于仍有工作树副本,因此这种情况比较安全,但请记住,工作树副本不在Git中,在进行新提交时,新提交将不会包含该文件的副本。
因此,在索引/暂存区与提交匹配后,通常会修改文件并然后使Git更新索引。您对索引所做的更改会导致“准备提交的文件”。如果您没有更新索引但已更新了工作树中的文件,则结果是“未准备提交的文件”。请注意,您可以同时执行两个操作:
  1. 提取提交:现在文件F的所有三个副本都匹配。
  2. 修改F的工作树副本。现在,HEAD和索引中的F匹配,但工作树不匹配:该文件为“未准备提交的文件”。
  3. 运行git add F。现在,HEAD与F的索引副本不匹配,但索引和工作树副本匹配:文件被“准备提交”。
  4. 再次修改F的工作树副本。现在,所有三个副本都不同,因此文件F既为“准备提交”又为“未准备提交的文件”!
再次强调要记住的是每个文件有三个副本。通常情况下,其中两个或甚至全部三个副本是匹配的。当它们匹配时,Git就不会提及额外的副本。 git addgit rm命令是更新Git索引的两个主要命令。在进行新提交之前,必须更新Git的索引,因为git commit使用在Git索引中的文件的副本。这确实是这里需要了解的全部内容。

3请注意,git commit -agit commit files通过将任何要提交的文件添加到索引来工作。这可能会变得相当复杂,特别是如果您使用git commit --only;我们将不在此处详细介绍这些细节。


已跟踪和未跟踪文件

这就带我们来到了“未跟踪文件”的定义,这也非常简单:未跟踪文件是不在 Git 索引中的文件。也就是说,如果您的工作区有一个名为U的文件,但U现在不在Git索引中,那么U就是未跟踪文件。

将文件U添加到Git的索引中(同时将其保留在您的工作区),现在U是已跟踪的。从Git的索引中删除它(同时将其保留在您的工作区)U再次成为未跟踪的。由于git commit仅存储在Git索引中的文件,在新提交中,未跟踪文件不会被提交。

现在我们可以合理地谈论git stash

一个普通的git commit命令:

  • 从您那里收集一些元数据,例如您的姓名和电子邮件地址以及日志消息;
  • 将所有在 Git 索引中的文件,已经处于冻结格式的状态,写入到新提交中,并使用来自上述(和当前提交的散列 ID)的元数据使所有这些都成为新提交;并且
  • 将新提交的哈希 ID 存储在当前分支名称中,因为新提交是新的最后提交。

git stash命令开始时与此类似,但它不是从您那里获取日志消息,而是自动生成一个。然后,它从Git索引中的“现在”(right now)制作一个提交,但它根本不将此提交放在“当前分支”。

git stash现在运行相当于git add -u的操作,更新Git索引中的所有文件,基于您工作区中的相同文件。这将更新索引以匹配您的工作树文件,但只有已跟踪的文件处于索引中(按定义),因此只有这些文件会被更新。然后,stash命令从此索引中制作一个提交,保存所有已跟踪的文件。

Git将这两个提交——索引提交和工作树提交——联系在一起4,然后运行git reset --hard5。这会将所有被跟踪的文件都恢复到它们在当前提交中所处的相同状态,并使用当前提交的冻结格式副本覆盖Git的索引。因此,现在您有了一对保存先前索引和先前工作树的提交快照,但是未跟踪的文件在此处根本没有保存。
由于这个存储的提交对于任何分支都不存在,因此您可以稍后切换到新分支并使用git stash applygit stash pop或它们的任意变体将该存储应用于不同的起点。实际应用过程有些复杂,如果您在应用时忘记使用--index,则pop命令将在成功的情况下删除两个提交,即使它实际上没有使用索引提交。因此,我总是建议使用git stash的人避免使用git stash pop:先使用git stash apply,然后检查您得到了想要的结果,最后再使用git stash drop来删除存储。
4从技术上讲,Git通过将工作树提交形成合并提交。其他Git工具将认为这是一个普通合并,无法很好地处理它;您需要在存储提交上使用git stash来使它们表现良好。 5不久之前,git stash是一个花哨的脚本,直接使用各种Git命令,包括git reset --hard。现在它是一个C程序,但它仍然执行相同的操作,只是没有运行额外的命令。

git stash有选项

当您运行git stash以创建新的存储时,可以给它两个选项之一:

  • -u--include-untracked:这会创建第三个提交,我们马上就会描述它。
  • -a--all:这也会创建第三个提交。

这两个选项都告诉git stash除了通常的两个提交(索引和工作树)之外,要创建这个第三个存储提交。唯一的区别在于第三个提交中包含的内容:

  • 使用 -a 选项,将工作区中所有未在 Git 索引中的文件都提交到第三次提交记录,包括在 .gitignore 中列出的文件;
  • 使用 -u 选项,仅将未在 .gitignore 中列出的未跟踪文件提交到第三次提交记录,而已在 .gitignore 中列出的未跟踪文件不会被提交。

执行了这个第三次提交6,`stash` 命令接下来会从你的工作区中删除进入此提交的每个文件。

当你尝试恢复一个存储之后,Git 会检查它是一个两次提交记录还是一个三次提交记录。如果是三次提交记录,Git 将尝试从第三次提交记录中提取所有文件。如果这些文件中有任何一个文件当前也在你的工作区中——记住,这些是在你做存储时为跟踪的文件,所以当时它们确实在你的工作区中——Git 将拒绝提取此存储。你需要把这样的文件挪开或者全部删除。

如果 Git 能够提取第三次提交记录,那么就会按照通常的方式提取其他两次提交记录。因此,你可以使用其中一个三次提交记录来存储未跟踪的文件。


6由于技术原因,这个提交必须在工作树提交之前进行,因此从某种意义上说它是第二个提交,但它被精心地移动到第三个位置,以便以后进行提取。


保留已暂存的更改

如上所述,git stash pushgit stash save 命令将创建一个包含两次提交记录的新存储:

  • 一次提交记录保存当前索引的状态。
  • 另一次提交记录保存当前工作区的状态。

所谓“已暂存”的更改实际上指的是“某个文件的索引副本与该文件所在的提交的副本不同”。通常,这又意味着该文件的工作区副本与索引副本匹配。

稍后执行 git stash apply 命令时,如果没有使用 --index 选项,则只会忽略索引提交记录。如果索引提交记录中的每个文件与工作树提交记录中的副本相同,并且你只应用了工作树提交记录,那么你将得到相同的更改应用到你的工作区中:除了忘记你之前使用过哪些 git add 命令外,没有任何损失。

git stash apply 命令后跟 --index 参数告诉 Git:在应用工作目录提交之前,尝试将暂存区提交应用到当前的暂存区。这并不总是有效的,如果无法应用,则 git stash 命令会建议您不要使用 --index。如果使用 --index 很重要,则这可能是一个糟糕的建议!
无论如何,有或没有 --index,现在使用 git stash apply 命令继续进行工作目录提交时,它将使用 Git 的合并机制来尝试应用此提交。这可能会导致合并冲突。如果发生冲突,它们可能会非常令人困惑。

我大多数时候不喜欢使用 git stash

通常情况下,我建议您只需提交代码,而不是使用 git stash
即使提交代码到“错误的分支”,稍后也可以将其“移除”(该提交本身会一直保留一段时间以备回退需要:默认情况下,Git 至少需要 30 天才能决定此提交不再有用,之后 Git 将清除它)。只要小心不要与 git push 等命令一起发送该错误提交,那么在分支上做出的“错误提交”是无害的。现在您可以切换到正确的分支并使用 git cherry-pick 命令进行复制。
拥有所有 Git 的工具,包括对正常、每天、易于找到和易于查看的提交可用的 git diffgit showgit cherry-pick,比必须在难以检查的提交上使用 git stash apply 要方便得多,因为这些提交不是日常提交,并且没有在任何分支上。
尽管如此,git stash 有时非常方便。如果出现问题,git stash branch 命令可以将现有的暂存区转换为新分支,然后使您可以访问所有常规工具。

太棒了,你的解释非常清晰!你所说的完全正确。我使用了 git add -ugit stashgit checkout mastergit stash apply 命令,结果出现了 CONFLICT (modify/delete): start-processing.sh deleted in Updated upstream and modified in Stashed changes. Version Stashed changes of start-processing.sh left in tree. 的冲突。我错误地忘记在主分支中添加这个文件,并且也没有将存储的内容添加到主分支中!但是 Git 不允许我在主分支上提交。 - aakriti
CONFLICT (modify/delete) 在你的索引中留下了合并冲突。这种情况没有被上面覆盖,但是 git stash apply 步骤失败了,因此 Git 没有删除存储。当你处于合并冲突状态时,许多 Git 操作都被完全禁用。我不喜欢 git stash 的原因之一是它会启动这个合并过程,无论它是否成功,如果它失败了,仓库可能会处于一个没有简单解决方法的状态。 - torek
话虽如此,git rm start-processing.sh 步骤将从索引中删除该文件(所有冲突的暂存副本)工作树文件,因此如果这是正确的操作,请执行此操作。 如果这是索引中唯一的冲突,则现在已解决所有冲突,您可以继续进行。 - torek
非常好的解释。非常有教育意义。 :) - KevinVictor
1
@KevinVictor:值得一提的是,git stash 最近受到了 Git 核心团队的大量关注,以至于现在 Git 2.35 中有了新功能,并且旧的存储脚本的某些微妙行为已经发生了变化。但原则仍然是相同的。 - torek
显示剩余4条评论

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