【问题标题】:Handling of local changes when switching branch切换分支时处理本地更改
【发布时间】:2022-11-28 08:11:18
【问题描述】:

这个简单的工作流程会发生什么:

x@PC MINGW64 /c/Temp/tests/git/branches/changes
$ git init
Initialized empty Git repository in C:/Temp/tests/git/branches/changes/.git/

x@PC MINGW64 /c/Temp/tests/git/branches/changes (master)
$ echo "CHANGE #1" >> test.txt

x@PC MINGW64 /c/Temp/tests/git/branches/changes (master)
$ git add test.txt

x@PC MINGW64 /c/Temp/tests/git/branches/changes (master)
$ git commit -m "."
[master (root-commit) 439c0f8] .
 1 file changed, 1 insertion(+)
 create mode 100644 test.txt

x@PC MINGW64 /c/Temp/tests/git/branches/changes (master)
$ git branch branch-1

x@PC MINGW64 /c/Temp/tests/git/branches/changes (master)
$ echo "CHANGE #2" >> test.txt

x@PC MINGW64 /c/Temp/tests/git/branches/changes (master)
$ cat test.txt
CHANGE #1
CHANGE #2

x@PC MINGW64 /c/Temp/tests/git/branches/changes (master)
$ git switch branch-1
Switched to branch 'branch-1'
M       test.txt

x@PC MINGW64 /c/Temp/tests/git/branches/changes (branch-1)
$ git add test.txt

x@PC MINGW64 /c/Temp/tests/git/branches/changes (branch-1)
$ git commit -m "."
[branch-1 4c62bc9] .
 1 file changed, 1 insertion(+)

x@PC MINGW64 /c/Temp/tests/git/branches/changes (branch-1)
$ git switch master
Switched to branch 'master'

x@PC MINGW64 /c/Temp/tests/git/branches/changes (master)
$ cat test.txt
CHANGE #1

用词:

  • master 中工作时创建一个带有“CHANGE #1”的文件
  • 添加并提交
  • 创建另一个分支branch-1
  • 添加“CHANGE #2”进行另一项更改
  • 切换到branch-1
  • 添加并提交文件
  • 切换回master

(创建分支和进行第二次更改的顺序似乎没有任何重要性)

我很惊讶:

  • branch-1 中看到“在master 的上下文中”所做的本地更改
  • 切换回master 时不再看到更改

所以我有两个问题:

  1. 当切换到 branch-1 时,本地更改未受影响,因此它们与 master 没有关联,但似乎只是被 Git 忽略了,这种行为记录在哪里?
  2. branch-1 提交更改并切换回master 后,第二个更改从master 不再可见:总的来说,更改已在branch-1 上捕获,确切的术语是什么(快照)?

【问题讨论】:

    标签: git branch git-branch


    【解决方案1】:

    eftshift0's answer 涵盖了这里的实际方面。关于 Git 的工作原理,您错过了一些重要的解释为什么不过,这确实发生了。

    Git 新手(或偶尔使用它的人)通常认为,当您克隆存储库并签出某些提交时,您可以查看、读取、编辑等的文件是 Git 中的文件.这是错误的:你的文件工作树不在 Git 中.他们可能刚来出去的 Git,但现在他们不是混蛋。稍后我将详细说明这个想法,因为它可能会非常混乱。

    这些文件不是的事实Git 解释——或者至少是理解解释所必需的——为什么这些文件是还在那里在你切换到其他分支之后。他们只是仍然存在,但仍然不在 Git 中.你需要在精神上抓住关于什么的想法在 Git 和什么不是在 Git 中。

    什么在 Git 中

    Git 与一个存储库—一次一个存储库。1个gitglossary 中所述,存储库是:

    refs 的集合以及包含所有可从 refs 访问的对象的对象数据库......

    这个“refs 集合”实际上是第二个数据库,包含分支名称、标签名称和许多其他类型的名称。它目前的实现相当糟糕(至少在一般意义上“很差”:默认的文件和打包文件系统在 Linux 上适用于没有数万个引用的小型存储库)。因此,存储库的核心只是两个数据库。大多数存储库中都有一堆辅助文件和附加数据库,这部分对于完成任何新工作很重要——您将直接使用的大多数存储库都提供了工作树还有.

    特别地,Git 把适当的存储库—两个数据库和各种小文件之类的—里面工作树,在隐藏的.git 文件夹中。.git 文件夹中的内容是存储库。工作树不在 .git 文件夹中。因此工作树是外部存储库。

    在存储库中,一个数据库——术语表中没有称之为数据库的数据库——包含你的分支和标签以及其他名称,它们帮助你和 Git 找到你关心的提交。另一个数据库,正如它所说的“包含所有对象”的数据库,具有实际的提交和文件等。

    那么,从高层次的角度来看,存储库:

    • 包含有助于查找提交的名称,以及
    • 包含提交

    仅此而已!但显然这还不够,所以我们必须查看提交内容。每个犯罪:

    • 是有编号的,因此可以通过它的唯一编号访问它,Git 称之为它的对象编号(OID) 正式地,或散列编号不那么正式;
    • 是完全只读的:任何现有提交(或任何对象,实际上)的任何部分都不能更改;和
    • 有两部分:元数据,我们将在这里忽略,以及每个文件的完整快照.

    完整的快照是通过更多的 Git 对象间接存储的,每个对象都像提交对象一样被编号并且是只读的。

    所以文件通过存储库中的提交可以找到 Git 存储库中的内容,我们可以使用诸如分支名称之类的东西找到它们。但既然他们是对象在这个对象数据库中,它们是只读的——并且由于各种原因很重要,它们经过特殊格式化、预压缩并带有文件内容去重在提交内和跨提交。这在典型的存储库对象数据库中节省了大量空间,因为大多数提交大多与前一次提交具有大部分相同的内容,而前一次提交大多具有与下一个更早的提交相同的内容,依此类推。


    1个在内部,在 Git 的至少一个实现中——最常被描述的那个,因为它是原始的 C 版本——有一个名为the_repository 的全局变量。一个 Git 程序,在启动时,通常会弄清楚在哪里存储库是,并填充此变量的字段。过去也有一个全局的the_index,并且可以选择添加新的工作树(git worktree add),这成了一个问题,所以它被重新设计了。现在正在进行的工作是让子模块更好地工作,子模块有同样的问题:每个子模块都是一个Git 存储库,因此拥有一个全局“the”Git 存储库变量是一个问题。


    什么是不是在 Git 中

    首先让我们做一个闪电回顾。什么的一部分在 Git 中:

    • 存储库存储提交。
    • 提交存储文件:完整存档每一个文件,永久冻结。

    但是提交中的文件是一种特殊的、压缩的、只读的、Git-only、去重的格式。你从字面上不能阅读它们——只有 Git 可以阅读它们2个——没有什么,甚至 Git 本身,都不能覆盖他们。所以他们对完成任何事情完全没用!

    出于这个原因,在你真正能够任何东西,你必须有 Git从一些提交中提取文件.这是签出过程。一旦你有了一个存储库,你就可以使用git switch(2.23 中的新功能)或git checkout(2.23 之前的版本,仍然可以正常工作,只是有一些令人困惑的情况最终说服 Git 人员添加git switch)到填写一个空的工作树。顾名思义,工作树是您处理文件的地方。形式上,工作树包含普通操作系统文件.

    使用 git checkoutgit switch 选择要签出的提交的行为实质上告诉 Git:我希望您从我选择的提交中填充工作树。如果你的工作树是完全空,因为它是在一个全新的克隆中,这意味着:对于提交中的每个文件,将其展开为一个正常可用的文件。

    但是,一旦你这样做了,你现在就有了两份这些“活动”文件中的每一个:

    • 提交中有一个只读的、Git 化的、压缩的和去重复的副本(从技术上讲,在对象数据库中,提交只是为您/Git 找到它)。
    • 在您的工作树中有文件的普通读/写副本。

    这两个匹配.这样可以安全地消除工作树副本——直到你改变它,就是这样!

    那么,就 Git 而言,当您更改工作树副本时会发生什么?答案是:什么都没发生。工作树副本不是混蛋。你改变它,嗯,它改变了。 Git 甚至不知道也不关心。它不在 Git 中。你用不是 Git 的东西改变了它。

    但是现在,您已经要求 Git 切换到其他分支:

    git switch branch-1
    

    或者:

    git switch master
    

    现在事情可能会变得……复杂。


    2个Git 的内部对象有两种格式。一个不是很难读,所以用一个简单的 zlib 解压器库和一些简单的编程,很多程序都可以读这些。另一种格式压缩得更多,需要非常专门的代码来处理。


    分支名称和提交哈希 ID

    我已经提到分支名称包含在两个数据库之一的“refs”中,并且提交具有唯一性散列编号数字。哈希 ID 看起来是随机的(它们根本不是随机的,但我们将忽略这里的细节),但这里的重要部分是“唯一”的东西。每个提交都有一个独特的ID。这就是 Git 判断哪个提交是哪个提交的方式。

    因为数字又大又丑而且看起来很随意(例如63bba4fdd86d80ef061c449daa97a981a9be0792),人类对他们不好。我们请改用名称。我们说 masterbranch-1 或其他。 Git 在 refs 数据库中查找名称并获得丑陋的大数字,这就是您所说的您想要的提交。

    有时,当你说:

    git switch xyzzy
    

    对于某个名字 xyzzy,您是在告诉 Git:在记住新名称的同时切换到不同的提交哈希 ID.但是一些分支名称存储相同的大丑陋的哈希 ID,有时。当数字相同时,您是在告诉 Git:切换到相同的提交,但记住新名称.

    当你是这种情况没有进行了新的提交,但创建了新的分支名称,就像您在此处所做的那样:

    $ git branch branch-1    # while you were on "master"
    ...
    $ git switch branch-1
    

    Git 会记住哪个姓名是电流分店名称,并将使用 masterbranch-1 的 refs 数据库条目来查找丑陋的大哈希 ID。因为这两个名字目前都选择了相同的哈希 ID,你实际上并没有改变提交。 (作为记录,我们可以在上面看到,在您的问题中,此提交的缩写哈希 ID 是 439c0f8。Git 在您进行根提交时将其打印出来。)

    如果你不改变提交,Git永远不必更改任何文件.所以它不打扰。这意味着您可以轻松切换分支,即使您有未提交的工作。

    如果你但是,更改提交时,Git 可能必须替换您的工作树中的某些文件。这是当事情变得复杂。

    吉特的指数或者暂存区

    我已经提到了必须存在的每个文件的两个明显副本:

    • 当前提交中文件的冻结提交副本,以及
    • 您正在处理/使用的文件的可用普通文件副本。

    第一个在 Git 中,第二个不在。但是 Git,出于它自己的 Gitty 原因,继续保守秘密第三每个文件的副本或“副本”:

    • 每个文件的第三个“副本”在 Git 的指数或者暂存区.3个

    索引和暂存区这两个术语指的是同一件事;还有第三个术语,现在大部分已经过时了,缓存,您通常会在 git rm --cached 等标志中看到。它们都指的是这个存储每个文件的第三个副本或“副本”的地方。

    我一直把它放在这样的引号中,因为文件的索引版本是预先去重.那是,如果某些文件的索引副本是某些现有文件的副本,它已经被删除了。当您第一次签出第一个提交并第一次填充您的工作树时,这也是第一次填充 Git 的索引。

    因为进入 Git 索引的所有文件实际上都是重复的——它们是在 Git 索引中的文件的确切版本犯罪被检出——它们都被删除了,因此不占用空间。但除此之外,最容易将它们视为单独的副本,原因很简单:可以随时替换任何文件的索引副本。运行git add 告诉 Git 更新索引副本:Git 读取并压缩工作树副本,重复数据删除如果它是重复的,则它,并用结果更新索引副本。

    文件的索引副本有点像 Git 的“中途”。一旦你运行git commit,它们就变成了永久性的,它告诉 Git:使用索引中已有的预删除重复文件制作新快照。

    由于索引已经包含全部来自的文件当前的提交——除非,也就是说,你已经删除或替换了它们——新提交包含与当前提交完全相同的文件,除了你用git add-ing 替换的文件。所以新的提交是每个文件的完整快照,不变文件不占用任何额外空间,因为它们已被删除。请注意,此重复数据删除不需要时间要么因为索引副本都是预先删除的。这实际上非常聪明。

    但是,现在,当实际更改提交时,事情变得复杂了,因为现在 Git 有一种快速的方法来检测哪些文件确实需要更改。


    3个如脚注 1 所述,它不再是真正的索引,因为每个添加的工作树都有自己单独的索引。所以它是“这个工作树的索引”。但是有一个特定的主要工作树,那个特定的主要工作树得到了最初的每个 Git 存储库附带的索引,即使是没有工作树的裸存储库。在这一点上,这只是一个历史性的怪事,但为了向后兼容,必须对其进行维护。


    实际上改变提交

    假设我们现在提交4c62bc9第二你制作的一个,你在“on”分支branch-1时制作的。你现在运行:

    git switch master
    

    这意味着“切换到分支 master 并提交 439c0f8。这是一个不同的提交哈希 ID。Git 不能完全快捷地切换:它不能只存储一个新的姓名并说“全部完成”。 Git 必须从它的索引和你的工作树中取出所有与提交 4c62bc9 一起的文件,你的第二次提交,而是用来自提交 439c0f8 的所有文件填充它的索引和你的工作树,你的第一次提交.

    但是 Git 仍然可以作弊!指数在自身内部保存了每个文件从当前 (4c62bc9, branch-1) 提交,Git 可以非常快速地(通过唯一的哈希 ID 技巧)知道哪些文件在切换到commit 439c0f8 是相同的。对于每个那些文件,它可以不理会索引条目,也不理会文件本身。这就是 Git 所做的。

    所以,如果你改变了一些文件但未提交,结果证明这些文件是 Git必须删除并可能替换因为它们在你移动的提交中不一样, Git 会停止并抱怨你有未提交的更改。但如果你改变了其他文件但未提交,这可能不会阻止你:这些文件在新旧提交中是相同的,并且不必换出,所以 Git 不会。

    有用的提醒

    如果您有 Git 可以携带分支名称更改(有或没有 commit-hash-ID-change)的文件,Git 会这样做。这使您可以开始工作,然后决定,哎呀,这项工作应该发生在不同的分支.您不必现在保存它、切换分支、恢复它、切换回来、删除提交、再次切换回去……您可以切换并继续工作。

    不过,作为提醒,Git 会打印该行:

    M       test.txt
    

    请注意,尽管 Git 从一个分支名称切换到另一个分支名称,但仍有未提交的更改 Git 不必删除。它甚至对完整的快捷方式也是如此(“根本不更改任何文件,因为提交哈希 ID 相同”)。如果您愿意,可以取消提醒 (git switch -q)。

    如果你不能切换分支,因为你开始改变的文件是不同的在另一个分支的提示提交中,那是您需要保存到目前为止的工作的时候。有多种方法可以做到这一点,包括花哨的 git stash 命令。我个人推荐回避git stash:只需进行实际提交,也许在一个新的临时分支上,然后挑选它们。如果出现问题,这会为您提供完整的 Git 工具(与 git stash 相比,这可能会导致无法撤消的混乱合并,让您度过无聊的一天:这不会经常发生,但是一旦发生过一次,你可能不想再经历一次)。

    概括

    这篇很长,所以这里是一个总结:

    • 只有坚定的工作完全保存在 Git 中。
    • 你的工作树文件根本不在 Git 中。
    • (隐藏)指数文件的副本很重要。

    使用 git status 查看代表有用的部分索引中发生的事情(请参阅Plato's Cave),以及与您的工作树中发生的事情相比如何。

    在这个冗长的回答中还有更多内容,并提供了一些提示,但这三个要点,加上 git status,是这里的重要内容。

    【讨论】:

      【解决方案2】:

      只要更改未提交,如果您决定签出不同的分支,git 就会将更改的文件(或未跟踪的)带到新分支......也就是说,它不会触及工作树中的那些文件或索引....而且它不是错误,它旨在以这种方式工作,这非常方便。

      实际上有一个检查 git 运行以允许结帐以确保它不会失去你的改变。如果修改后的文件在 HEAD 和您要检出的内容之间不同,那么它会拒绝检出(为了不丢失上述更改)。这可以通过在结帐时使用 -f 来覆盖,在这种情况下,您的更改会丢失。

      【讨论】:

        猜你喜欢
        • 2020-10-23
        • 2012-07-21
        • 2011-10-02
        • 1970-01-01
        • 1970-01-01
        • 2013-08-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多