之前"暂存但未提交"的更改会发生什么?

4
我刚开始接触 Git,正在学习这个网站:https://dev.to/unseenwizzard/learn-git-concepts-not-commands-4gjc。文档中让我有疑问的是下面这段话:

看看 Alice.txt 文件。
它实际上包含一些文本,但 Bob.txt 没有,所以让我们更改 Bob.txt 文件,添加 "Hi!! I'm Bob. I'm new here." 。

简而言之,我们使用这个句子更改了 Bob.txt 文件:" Hi!! I'm Bob. I'm new here." ,然后我们将其被 暂存(stage) 而不是被 提交(commit) 。随后,我们又对同一个句子进行了修改,删除了 "Hi" 后面的额外 "!" , 现在该句子变成了: "Hi! I'm Bob. I'm new here." 。现在我们已经将新的更改暂存(stage)提交(commit)
我的问题是:之前使用两个 "!!" 进行修改的 Bob.txt 文件(原始更改)还存在于暂存区吗?我运行了git status 命令,但没有提到之前的修改。我能回去重新将带两个 "!!" 的更改提交(commit)吗?
3个回答

5

VonC的回答Romain Valeri对该回答的补充都是正确的,但可能在不同方面难以可视化或理解。以下是一种相对直观的理解方式。

在Git中处理提交时,每个文件最多有三个副本。一旦您知道提交包含每个文件的完整快照,以只读(且仅限于Git、压缩和去重)格式,您就会明白为什么需要两个副本:

Git's read-only copy in HEAD        your working tree copy
----------------------------        ----------------------
file1.ext                           file1.ext
file2.ext                           file2.ext
readme.md                           readme.md

即使 "提交" 中的 file1.ext 内容与您的工作树中的 file1.ext 内容匹配,您的工作树是您可以查看、处理/使用 Git 从提交中提取的文件的地方,但 Git 复制是以某种特殊、奇怪、压缩和只读格式进行的。 只有 Git 自身才能读取此文件,甚至 Git 自身也不能覆盖它。1 您的工作树包含普通的日常文件,每个人都可以按照普通方式阅读和编写,因此每次检出提交时,Git 确实需要复制它。
同样的原则也适用于其他版本控制系统:在 Mercurial、SVN、CVS、ClearCase 或其他任何 VCS 中,您通常会发现存在多个文件副本(具体细节大为不同取决于 VCS)。不过 Git 的特别之处在于,它不仅提供了两个文件副本,而是提供了三个副本:
HEAD (r/o)    staging area    working tree
----------    ------------    ------------
file1.ext     file1.ext       file1.ext
file2.ext     file2.ext       file2.ext
readme.md     readme.md       readme.md
HEAD 复本已经 提交且无法更改,这就是它的状态。工作目录里的复本则可以按照需要修改。奇怪的是,在 HEAD 和工作目录版本之间还有一个额外的复本存在。
这个额外的版本不容易被发现,至少不容易直接看到。2 但它确实存在。为什么呢?嗯,一个答案是:“没有原因”——毕竟,那些其它的版本控制系统并不这样做。3 但是Git却这么做,并且Git会有一些除了“与众不同”之外的理由。特别地,这个“暂存区”的存在允许你使用 git add -p部分地添加文件。还有一整套类似的操作(如 git reset -pgit checkout -p),尽管我个人对这些操作不太感冒,但它们确实存在,并且常常作为暂存区存在的理由。
在暂存区中存储的数据实际上和Git在提交中所使用的只读、压缩和去重复形式是相同的。这对Git本身来说意味着 git commit的速度可以非常快(至少相比于所有其它的版本控制系统而言)。当你运行 git commit时,暂存区中的文件复本即已经准备好被提交。几乎不需要额外的工作。5 当你运行 git checkout 时,Git会用所选提交版本的所有文件来预填充索引/暂存区。当你运行git add时,Git会进行以下操作:
  • 压缩并散列工作目录文件的内容;
  • 检查是否有重复的文件;并且
  • 如果存在重复的文件,则直接复用旧的文件,否则保存新的文件。

因此,该文件已经准备就绪,Git可以仅更新其索引条目/暂存复本中的新或重用的内部散列ID。6 这意味着索引/暂存区现在已准备好被提交。
将这些放在一起,我们可以看到以下内容:

  • full-commit-checkout (or git switch): fills in the index / staging-area and your working tree:

    HEAD         staging      working tree
    ----         ----------   -------------
    file1.ext -> file1.ext -> file1.ext
    file2.ext -> file2.ext -> file2.ext
    readme.md -> readme.md -> readme.md
    
  • git add: copies from working tree to index / staging-area. Let's say we just git add file2.ext:

    HEAD         staging      working tree
    ----         ----------   -------------
    file1.ext    file1.ext    file1.ext
    file2.ext    file2.ext <- file2.ext
    readme.md    readme.md    readme.md
    
现在其他各种操作也开始有意义了:
  • git rm --cached: removes the index copy, leaving everything else untouched. Let's say we git rm --cached file2.ext:

    HEAD         staging      working tree
    ----         ----------   -------------
    file1.ext    file1.ext    file1.ext
    file2.ext                 file2.ext
    readme.md    readme.md    readme.md
    
  • git reset, in one of its modes: restores the staged copy (only) from the HEAD copy, with git reset -- file2.ext for instance:

    HEAD         staging      working tree
    ----         ----------   -------------
    file1.ext    file1.ext    file1.ext
    file2.ext -> file2.ext    file2.ext
    readme.md    readme.md    readme.md
    
  • git rm without --cached: removes both index and working tree copies:

    HEAD         staging      working tree
    ----         ----------   -------------
    file1.ext    file1.ext    file1.ext
    file2.ext
    readme.md    readme.md    readme.md
    
  • git restore: restores staging, working-tree, or both, depending on flags; let's say we now run git restore -SW file2.ext to restore both:

    HEAD         staging      working tree
    ----         ----------   -------------
    file1.ext    file1.ext    file1.ext
    file2.ext -> file2.ext -> file2.ext
    readme.md    readme.md    readme.md
    
git checkout 命令包含两种操作模式,可以模拟 git restore 的功能:它可以将文件从暂存区复制到工作区,或者将文件从 HEAD 复制到暂存区和工作区。7 由于这种操作即使你没有将其保存在任何地方也会覆盖工作区文件,因此使用 git switch 而不是 git checkout 更加 "安全",因为你不会意外进入这种破坏性的模式。8 因此,简而言之,你的第二个 git add 命令覆盖了第一个 git add 写入的暂存区副本,并抛弃了前一个暂存区副本。现在要恢复非常困难。
1 严格来说,只要文件以 Git 称之为 "loose object" 的形式存储,读取文件并不难:使用任何 zlib 解压程序打开底层对象并对其进行解压缩,然后丢弃 Git 添加的头信息。但就是找到这个对象很麻烦,而且它可能与 "loose" 相反,也就是 "packed",那时你就真的遇到麻烦了。

虽然覆盖文件在物理上是可能的,但由于对象的名称是对象数据的加密校验和,因此简单地覆盖文件会损坏数据,达到 Git "这个对象损坏" 的程度,从而拒绝提取它。你会知道仓库已经受损,需要找到其他未受损的克隆。

2 要想看到它不容易,请运行 git ls-files --stage 命令;请注意,在一个大的仓库中,这会产生许多输出。

3 比如 Mercurial 就没有,但是有一个名为 "dirstate" 的隐藏东西可以完成 Git 的暂存区的一部分功能。用户面向的 Mercurial 的 dirstate 和 Git 的暂存区之间的区别在于,你甚至不必知道 dirstate 的存在。Git 时不时地把其暂存区/工作区推到你的面前:看!我有这个额外的副本!它不酷吗?看,你看见了吗! 你确实需要意识到它。

4 使用 git ls-files --stage 命令可以揭示这个 "秘密",所以它并不是真正的秘密。但除非你开始使用 git ls-files --stage 本身或结合使用 git update-index,否则你不需要知道它。

5额外的一点工作是Git需要运行内部等效于git write-tree。这保存了文件的名称和模式。正如Romain Valeri所指出的,数据——文件的内容已经被"预先保存"了。

6读者练习:如果你使用 git add 添加了一些内容,然后再通过新内容覆盖它而没有提交它,会发生什么?这里有一个 Git 对象似乎永远不会被使用。请查看 git gc文档 ,了解最终会发生什么。

7 git restore 命令具有从 HEAD 到 工作树 的复制功能,如果您愿意,则可以跳过暂存区;git checkout 无法执行此操作。我不知道您是否要这样做,但如果您决定这样做,请记住,git restore 比另一种 git checkout 模式更加强大。

8在 Git 2.23 及以后的版本中,“意外的破坏性模式”不再发生,现在会提示您的 git checkout zorg 请求是有歧义的。以下是旧版 Git 中发生此情况的地方:

  • 假设您有一个名为 origin/zorg分支
  • 假设您还有一个名为 zorg文件,并且您运行了 git checkout develop 命令并得到了该文件的 develop 分支末端副本。
  • 现在假设您花了最后一个小时来计划解雇出租车司机的邪恶计划。
  • 现在您休息一下喝杯咖啡(小心别被樱桃卡住了)。当您回来时,您想:等一下,我想在 zorg 分支上。因此,您运行了 git checkout zorg 命令。
你没有一个名为zorg的分支,但你有一个叫做origin/zorg的分支,你期望 Git 从 origin/zorg 创建并切换到一个新的分支zorg,如果这是安全的话,或者给出一个错误提醒你先储存或提交你的文件。但是相反,Git说:哦,你想让我清除你在文件zorg上最近一小时的工作,因此提取文件zorg的暂存副本到你的工作目录。

如果你使用了 git switch zorg,Git 就会知道你想创建一个新的分支,并尝试安全地执行。但是相反,Git 摧毁了你的工作。糟糕!别杀很多人(甚至是 Mangalores)来发泄你的挫败感,好吗?

我只从中得到了“不要杀一群人”的建议;) 像往常一样,这是一个好建议。我也喜欢git switch,它不会错误地分离HEAD,而git checkout则可能会:https://dev59.com/CG865IYBdhLWcg3wIrFg#3965714 - VonC

3

之前对Bob.txt的修改(使用了两个!!)在暂存区中丢失了吗?

是的,暂存区是用来准备下一次提交的。
如果你对同一个文件进行更改并再次添加,那么你就修改了该暂存区,下一次提交将不知道你的初始状态。

我能回去提交带有两个“!!”的更改吗?

仅通过Git本身是不行的。
如果你使用带有 本地历史记录功能 的集成开发环境(IDE),那么该IDE可能会有所帮助。


3

同时需要指出的是,当您暂存更改时,Git 会在数据库中内部创建相应的树和Blob,即使它们尚未被任何提交引用。

因此,git fsck 应该能够找到您的“已暂存但未提交”更改作为悬空 Blob,您可以使用 git cat-file -p <hash> 命令进行检查。

所以它们是从暂存区中丢失了,但并不是完全无法恢复。


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