许多 GUI 隐藏了许多细节,通常是为了让 Git 看起来更简单。我认为这是一个错误,因为底层的 Git 细节仍然不断出现。我不知道 VS2017 试图隐藏到什么程度——一般来说,我只是避免使用 GUI,除非特殊情况。
无论如何,这里发生的事情是您正在处理这样一个事实,即在 Git 中,每个文件都有 三个 (!) 副本。一种是只读的、永久提交的版本,它是当前提交的一部分。第二个是 Git 调用的那个,不同的是,index、staging area 或 cache,这取决于编写文档的人以及何时。
每个文件的这两个版本都保存在内部的 Git-ty 压缩格式中。 (它压缩得非常好,以至于大多数时候实际上只有一个副本。)如果你的 GUI 很好,它会让你查看这两个,尽管它们的格式只有 Git 本身可以直接处理。
文件的第三个副本是您工作树中的那个。这是一个普通文件,在普通文件系统中,以普通程序可以以普通方式处理的方式存储。这是文件中唯一真正占用大量空间的版本(除了一些 Git 不能很好压缩的二进制文件)。所以三个副本多半只是管理上的头疼。
显然,文件的工作树版本是可以更改的。毕竟,这只是一个普通的文件。您可以对其进行编辑、重写,甚至删除它。您还可以创建新文件,这些文件尚未在索引中,也可能不在任何提交中。
每个文件的索引版本也是可写的。您使用git add 编写它:这会将当前工作树中的任何内容复制到索引中的同名文件中。如果索引中已经有一个,这将替换索引副本。如果索引中还没有一个,这会将一个放入索引中。
除了使用git add 将内容复制到其中之外,处理索引的最重要方法是运行git commit 时会发生什么。此时,Git 查看您的索引,而不是您的工作树,以进行新的提交。无论索引中有什么文件,这些文件都会进入新的提交。如果您从索引中删除一个文件(使用git rm),则该文件不在新提交中。每个文件的内容都是你复制到索引中的内容:不多也不少。
一旦新提交安全地保存在存储库中,Git 就会更改当前分支(如 HEAD 中记录的那样),以便分支命名新提交。新提交的父提交是是当前提交之前的任何提交。由于 Git 刚刚使用索引进行了新提交,因此提交和索引现在匹配。
这为您的四个问题奠定了基础:
如果我不提交,为什么我在切换回 LocalMaster 后会看到这些更改?
在工作树和/或索引中所做的更改只是在工作树和/或索引中。
当您要求 Git 检查某个 other 提交时,Git 会将当前 (HEAD) 提交与其他提交进行比较。如果某些文件不同,Git 必须替换索引和工作树版本。如果没有,Git 可以不用管它。
让我们看一个简短的例子,你要求 Git(通过 git checkout)从提交 badbeef... 移动到提交 ac0ffee...。每个文件中包含三个文件:README、a.txt 和 b.txt。两次提交中README 的版本是相同的。 a.txt 和 b.txt 的版本不是。
那么,要执行此操作,Git 必须替换索引和工作树中现有的 a.txt 和 b.txt。那安全吗?好吧,如果您没有对a.txt 进行任何更改,那 部分是安全的。如果您对 b.txt 进行了更改,则不是,并且您会收到错误消息。
但是README 在两次提交中是相同的。 Git 不必将其换掉。因此,您在索引和/或工作树中对README 所做的任何更改都可以保留在原处。只要a.txt和b.txt不受影响,Git就可以替换它们;由于README 不需要替换,Git 可以保持不变。
这使您可以在这两个提交中携带未提交的README 更改,但不能携带任何未提交的a.txt 或b.txt。除非它们与切换前提交中的内容相匹配,否则这两个不会被安全保存。
假设我在 BugFix 分支中进行更改,并且我只将它们提交到该分支。如何查看此分支中存在哪些更改?
当您进行提交时,这是一个完整且完整的快照(索引中的任何内容)。提交本身会导致当前分支名称更改,以便名称解析为新提交。这根本没有任何“变化”:它是一个快照。要找出“改变了什么”,您必须选择一些 other 快照并让 Git 进行比较他们。
让 Git 将任何提交与其直接父提交进行比较很容易:这就是 git log -p 或 git show 将显示的“更改”。但是,为了显示这些 as 更改,Git 必须每次都从两个快照中重新计算差异。
如果您想查看自一些特定较早提交以来的所有更改,您需要git diff。这需要两个快照并比较它们。就像git log -p 一样,除了git log 你总是在比较父母和孩子,而git diff 你在比较两个提交你选择。
Git 不跟踪“分支开始的位置”,因此要查找“分支上的更改”,您必须定义自己的起点。
假设我在步骤 2 中进行了更改。现在出现了其他问题,我只想在 LocalMaster 分支中进行快速修复。我可以做到这一点并将其推回网络。如何将我的 BugFix 分支与相同的更改同步?
唉,现在我们进入提交图。这实际上是一些 GUI 大放异彩的地方(有些真的很糟糕......)。
我们在上面提到,当您进行新的提交时,这会将 当前的 分支名称推进以指向新的提交。让我们绘制一个包含三个提交的存储库:
A <-B <-C <--master
这些提交使用简单的大写字母名称,而不是大而丑陋的哈希 ID。提交C 是最新的,所以名称master 会记住它的哈希ID。提交 C 记住其父 B 的哈希 ID,B 记住第一次提交的哈希 ID A。
由于A 是第一个提交,它根本没有父提交。这是一个 root 提交(这只是说“没有父提交”的一种奇特方式)。
提交中的内部箭头始终是固定的,与提交有关的所有其他内容也是如此。 (这有很好的技术原因:基本上,哈希 ID 本身就是提交内容的加密校验和,所以如果你改变任何东西,你会得到一个新的、不同的提交。)所以内部箭头不是很有趣——我们只需要记住它们总是指向后面。所以:
A--B--C <-- master
然而,分支名称会随着时间而改变。这个当前存储C 的哈希ID。如果我们进行 new 提交,我们会得到:
A--B--C--D <-- master
现在master 存储D 的哈希ID。 (要找到C,Git 从D 开始。D 存储C 的ID——“指向”C——这让我们到达C,然后是B,并且等等。)
当您创建一个新分支时,Git 会将一些 ID 复制到新分支名称中。默认情况下,我们从当前提交开始(现在是D):
A--B--C--D <-- master, newbranch (HEAD)
现在有两个名称,我们需要知道哪一个是当前。 Git 将其存储在特殊名称 HEAD(实际上是一个文件,.git/HEAD)中:HEAD 字面意思是包含分支的名称。
现在,如果我们进行新的提交,Git 会像往常一样进行新的提交,并更新 current 分支,即newbranch:
A--B--C--D <-- master
\
E <-- newbranch (HEAD)
我们在一个新的分支上有一个新的提交,从视觉上看,它看起来像分支。除了它只是一条带有扭结的直线,真的。为了让它正确地分支,我们必须检查master(所以HEAD说master)并进行另一个新的提交:
A--B--C--D--F <-- master (HEAD)
\
E <-- newbranch
现在我们确实有一个分支。
这里的关键是要注意,实际的分支是一个属性,而不是names(master vs newbranch),而是提交 及其嵌入的图表链接。这些名称只是让我们开始进入图表,然后我们执行所有这些反向链接跟踪。
因此,要正确回答问题 3,我们必须查看图表的外观。它可能看起来像这样:
...--G--H--I <-- LocalMaster (HEAD)
\
J <-- BugFix
您现在可以在 LocalMaster 上进行新的提交:
...--G--H--I--K <-- LocalMaster (HEAD)
\
J <-- BugFix
现在您希望 BugFix 从提交 K 开始。您根本无法更改 J,但可以将其复制到一个新的临时分支。复制到新的类似提交后,您会得到:
J' <-- tmp (HEAD)
/
...--G--H--I--K <-- LocalMaster
\
J <-- BugFix
(使用名称J' 表示它是J 的副本)。您现在可以通过强制 Git 重新定位名称 BugFix 以指向 J' 来开始忽略原来的 J:
J' <-- BugFix (HEAD), tmp
/
...--G--H--I--K <-- LocalMaster
\
J [abandoned]
现在您不需要临时名称,因此您可以将其删除。
为您一次性完成所有这些操作的命令是git rebase。
你没有有变基,但是,在某些情况下它是不明智的。特别是,假设您将提交J 提供给其他人,并将其推回某处。其他人现在拥有他们的 提交J 的副本。您建议用这个新的J' 替换它。你必须让其他人来做同样的替换!否则你的旧J 可能会回来:他们可能认为原来的J 很重要,而不是J' 的重复,稍后重新引入。
如果不能选择变基,您可以改用git merge。合并所做的事情很复杂,但最终,它会生成一个新的merge commit,它不是指向一个父级,而是指向两个父级的提交。您可以像以前一样开始:
...--G--H--I--K <-- LocalMaster (HEAD)
\
J <-- BugFix
然后您检查BugFix 并运行git merge LocalMaster。这说明了如何将自合并基础(提交I,分支聚集在一起)与两个分支提示(提交J和K,以某种顺序排列——顺序对于组合无关紧要,但在以后确实很重要)。如果合并成功,Git 会进行新的合并提交。它的第一个父级将是J,因为这是HEAD 在git merge 期间的提交。结果看起来像这样:
...--G--H--I--K <-- LocalMaster
\ \
J--M <-- BugFix (HEAD)
假设我在第 2 步中进行了更改,我不想合并到 LocalMaster。我如何才能取消该更改?
您在这里建议您在BugFix 上进行了更改并提交了它。还没有什么可以退出的:
...--G--H--I <-- LocalMaster
\
J <-- BugFix
在这里,哪个分支是当前的并不重要。名称LocalMaster 记录了提交I,因此提交至I 并包括LocalMaster。名称BugFix 记录了提交J,因此通过G--H--I--J 的提交在BugFix 上。请注意,许多(在这种情况下除了一个之外)提交都在 both 分支上——这是 Git 独特的另一种方式。