首页 > 解决方案 > git 分支中的案例管理

问题描述

我不明白当我

git checkout <branch_with_missed_upper_to_lower_case>
git checkout <branch_with_proper_case>
git branch -d <branch_with_missed_upper_to_lower_case>

然后我在说“initial commit”,只能git checkout -f <elsewhere>离开这个陷阱。以下所有命令都因未提交的更改或未完成的首次提交(未设置 HEAD)而失败:

git stash
git stash -q
git checkout .
git checkout <elsewhere>
git clean -xfd
git reflog

我什至在某个时候

Rename from 'C:/wordirGit/ps-fw-fps/.git/HEAD.lock' to 'C:/wordirGit/ps-fw-fps/.git/HEAD' failed. Should I try again? (y/n)

不能y,当git checkout -f <elsewhere>

谁能解释删除后会发生什么?

标签: git

解决方案


请注意,这里的问题不是您是否可以拥有名称仅在大小写不同的分支,例如branchvs BRANCH。(答案是肯定的和否定的:有时可以,有时不能。)这就是为什么,创建了一个名为 的分支BRANCH,然后创建了另一个名为branch的分支,branch签出了名为的分支,然后删除了名为 的分支BRANCH,Git 本身会感到困惑,现在处于错误状态。

在这种情况下,答案是“因为 Windows”,尽管更准确地说是因为您在 Windows 上使用的文件系统。默认情况下,MacOS 的文件系统也具有这种行为方式。

简短的版本是您遇到了 Git 自己的内部混淆,即大小写在分支名称中是否重要。

在开始之前,您需要知道特殊文件.git/HEAD包含当前分支的名称。它是一个纯文本文件,由一行组成,格式为. 该部分包含当前分支名称。(还有另一种选择:该文件可以存在并包含原始哈希 ID。这表明您的存储库处于分离 HEAD 模式。但该文件必须存在,否则该目录根本不被视为存储库。)ref: refs/heads/namenameHEAD.git

您还需要知道,在 Git 中,每个分支名称都只是存储了一些现有提交的哈希 ID。除非存储的哈希 ID 是某个现有提交的哈希 ID,否则拥有分支名称是无效的。

各种文件中的分支名称

Git 非常强烈地认为,分支名称xyzXYZ完全不同。微软和苹果倾向于不同意:文件命名xyzXYZ代表同一个文件。

Git 最初是在 Linux 上编写的,Linux 文件系统倾向于与 Git 一致:一个名为 的文件xyz与另一个名为 的文件完全分开XYZ,因此您可以同时拥有两者。Git 利用了这个特性。

现在,Git 不会将所有分支名称存储为文件名。事实上,在许多情况下,分支名称不是文件名。例如,当你第一次克隆一个存储库时,你的 Git 有另一个 Git——你正在克隆的那个——列出了它的分支名称。如果该 Git 位于 Linux 系统或其他大写和小写名称始终分开的系统上,则该 Git始终可以有两个不同的分支名称,它们仅在这种情况下不同。即使该 Git 位于文件名被大小写折叠的系统上,该 Git也可以将其分支名称存储在不发生大小写折叠的地方。

此时clone运行过程中,你自己的Git创建了两个数据库。这些不是很花哨:例如,它们不是 SQL。但它们确实可以作为简单的键值存储。

这两个之一持有 Git对象。Git 对象的内部名称在这里不会引起任何问题,因此无论底层文件系统如何,该数据库始终可以正常工作。

然而,另一个数据库包含分支名称、标签名称和其他此类名称。它将这些存储为键,并将 Git 内部哈希 ID 存储为每个键的值。 最初,这个“数据库”是一个简单的纯文本文件:您可以在.git/packed-refs. 由于这是一个仅包含文本行的单个文件,因此它可以包含两个仅大小写不同的名称。例如,.git/packed-refs可以有这样的两行:

b994622632154fc3b17fb40a38819ad954a5fb88 refs/heads/branch
282ce92448e25cfbf1b399c9d33eb290f2331814 refs/heads/BRANCH

注意:它不会,因为你的 Git重命名了他们的分支。所以你实际上有:

b994622632154fc3b17fb40a38819ad954a5fb88 refs/remote/origin/branch
282ce92448e25cfbf1b399c9d33eb290f2331814 refs/remotes/origin/BRANCH

在这里,如果这是新克隆的结果。事实上,这在某些情况下确实会发生,并且会导致更奇怪的行为,如下所示。但让我们暂时想象一下它确实做到了。)

这存储了两个分支名称,branchBRANCH,并赋予它们不同的值。(键值对存储在.git/packed-refsas 中value key,出于内部原因,每行一个键。)

但是 Git 并不总是将 name/id 键值对存储在.git/packed-refs.

特别是,一旦更新了分支名称(或任何其他名称) ,Git 会将其值存储在独立文件中。对于名为 的分支branch,此文件为.git/refs/heads/branch. 在此文件中,Git 将存储作为该分支名称值的原始哈希 ID。Git 使用这个单独的文件是因为它没有适当的键值名称数据库。这些文件提供了一种模拟适当数据库的方法。

这里有一个大问题。如果 Git 需要创建或更新 branch-name BRANCH,它将使用另一个单独且不同的文件名为.git/refs/heads/BRANCH. 这在一个典型的 Linux 系统上工作得很好,其中文件系统很乐意存储两个单独的文件,一个名为branch,一个名为BRANCH. 这在两个文件实际上是同一个文件的典型 Windows 或 MacOS 文件系统上不起作用。操作系统坚持将任何新数据写入旧文件,从首先创建的文件中保留名称大小写。

这里问题的根源是 Git 尝试使用不同的文件名,.git/refs/heads/branch并且.git/refs/heads/BRANCH,当操作系统坚持认为它们是相同的文件名时。Git 继续认为这是两个不同的文件,而 OS-and-file-system-combination 继续坚持不,这是一个文件。

当您删除其他案例名称时,Git 会删除(单个)文件

您现在指示 Git 删除这两个分支名称之一。哪个并不重要,但为了具体起见,假设您告诉 Git 删除 name BRANCH。Git 通过做两件事来做到这一点:

  1. 首先,它检查文件中是否有名称条目packed-refs。这使用了区分大小写的比较,即它不匹配refs/heads/branch条目,仅匹配refs/heads/BRANCH条目。如果存在这样的条目,Git 必须重写或销毁 packed-refs 文件,正如我们将看到的。

  2. 然后,要么验证一切正常——packed-refs 文件中没有这样的条目,要么重写或销毁它——Git 要求操作系统删除.git/refs/heads/BRANCH.

如果操作系统文件系统中实际存在的文件名为.git/refs/heads/BRANCH,则操作系统会删除该文件。如果实际存在的文件名为.git/refs/heads/branch,则操作系统会删除文件。无论哪种方式,由于操作系统实际上无法存储这两个文件,因此现在根本没有任何拼写的文件。

这意味着由于该不在 packed-refs 文件中并且该文件不存在,因此分支名称本身不再存在。Git 的 names-to-hash-IDs 数据库不再有该分支名称的任何条目,因此该分支名称不再存在。

当分支名称不存在但HEAD文件包含该分支名称时,Git 会说您位于尚未创建的分支上。这种状态在一个新的、完全空的存储库中是正常的。由于没有提交,因此命名的分支master不能存在:名称必须包含有效提交的哈希 ID,并且没有提交。master但无论如何你都在分支上。所以这是正常的,如果不是很常见的话。(您多久使用一次完全空的存储库?)

您也可以使用 . 故意进入此状态git checkout --orphan。因此,当 Git 删除某个分支名称的唯一副本时,Git 稍后会认为您必须习惯于git checkout --orphan进入这种状态。

(请注意,删除名称会同时删除单独的文件打包引用文件中的条目,否则旧值会返回。)

还有另一个问题,它实际上发生在远程跟踪名称上

请注意,您不能“启用”远程跟踪名称:

git checkout origin/master

让您进入“分离 HEAD”模式。但是你确实有远程跟踪名称,并且由于我们之前提到的——它们进入了.git/packed-refs——你可以远程跟踪名称开始origin/branchorigin/BRANCH在这个文件中。因此,您可以同时拥有两个远程跟踪名称。

不过,您的 Git 从另一个Git 获取更新。这些更新可能会影响这些远程跟踪名称中的一个或两个。例如,假设他们的 Gitbranch更改了值,而您的 Git 获取了新值。你的 Git 现在更新了它 origin/branch,这意味着你的 Git创建了文件.git/refs/remotes/origin/branch。该文件的存在隐藏但不删除打包参考文件中的旧值。

如果另一个 Git 也更新了它的refs/heads/BRANCH,你的 Git 会尝试创建一个新.git/refs/remotes/origin/BRANCH的来存储新值。当然,这会错误地覆盖现在存在的.git/refs/remotes/origin/branch文件,更新错误的名称,并且不会隐藏 packed-refs 文件中的旧BRANCH值。

如果另一个 Git 现在删除了它的refs/heads/BRANCH,你的 Git 将重写你的 packed-refs 文件以删除该refs/remotes/origin/BRANCH行,并尝试删除.git/refs/remotes/origin/BRANCH,这当然会错误地删除.git/refs/remotes/origin/branch。结果是 的refs/remotes/origin/branch成为您的当前值origin/branch。因此,可能还需要一个时间git fetch来纠正这种状态。

当两种情况的名称都存在并标识不同的提交,并且两个远程跟踪名称都存储在一个文件中时,Git 认为这是两个单独的文件,每个都git fetch认为本地 Git 中的一个名称已过时,并对其进行更新。这会导致名称在两个值之间来回交替。

所有这些行为都非常奇怪。这一切都源于 Git 试图使用文件名来存储分支名称同时,坚持这种情况在分支名称中很重要。

三种可能的解决方案

Git 可以通过以下三种方式中的任何一种来解决此问题:

  1. 停止将文件系统用作廉价的数据库形式。

    如果 Git 作者放入一个不依赖于操作系统的“原子文件创建”和“原子重命名”操作的真实数据库,问题就会消失。这是令人不快的,因为真正的数据库往往需要大量代码。Git 或许可以使用诸如Berkeley DB之类的库,但即使这样也有点复杂。不过,有一些明显的好处。

  2. 编码文件名。

    branch例如.git/refs/heads/branch,Git 可以将其存储为 ,而不是存储为.git/refs/heads/6272616e6368。数字对是表示每个字符的十六进制值。或者,将名称存储BRANCH为 ,.git/refs/heads/_b_r_a_n_c_h同时将名称存储branch.git/refs/heads/branch。将下划线编码为双下划线,使分支名称a_12变为.git/refs/heads/a__12.

  3. 放弃案例意义。

    Git 可以规定所有分支名称始终都是小写的,如果您将分支名称写为BRANCHGit,则只需将其存储为branch

方法 2 可能是最可口的,但即使这样也是操作上的一个相当大的转变。core.refVersion为了处理向后兼容性,只有在变量设置为至少 2时才可以启用它,并且也可能只有在core.ignoreCase设置时才启用。


推荐阅读