git - 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>
。
谁能解释删除后会发生什么?
- 编辑,我在 Windows 上使用 GitBash
解决方案
请注意,这里的问题不是您是否可以拥有名称仅在大小写不同的分支,例如branch
vs BRANCH
。(答案是肯定的和否定的:有时可以,有时不能。)这就是为什么,创建了一个名为 的分支BRANCH
,然后创建了另一个名为branch
的分支,branch
签出了名为的分支,然后删除了名为 的分支BRANCH
,Git 本身会感到困惑,现在处于错误状态。
在这种情况下,答案是“因为 Windows”,尽管更准确地说是因为您在 Windows 上使用的文件系统。默认情况下,MacOS 的文件系统也具有这种行为方式。
简短的版本是您遇到了 Git 自己的内部混淆,即大小写在分支名称中是否重要。
长
在开始之前,您需要知道特殊文件.git/HEAD
包含当前分支的名称。它是一个纯文本文件,由一行组成,格式为. 该部分包含当前分支名称。(还有另一种选择:该文件可以存在并包含原始哈希 ID。这表明您的存储库处于分离 HEAD 模式。但该文件必须存在,否则该目录根本不被视为存储库。)ref: refs/heads/name
name
HEAD
.git
您还需要知道,在 Git 中,每个分支名称都只是存储了一些现有提交的哈希 ID。除非存储的哈希 ID 是某个现有提交的哈希 ID,否则拥有分支名称是无效的。
各种文件中的分支名称
Git 非常强烈地认为,分支名称xyz
和XYZ
完全不同。微软和苹果倾向于不同意:文件命名xyz
并XYZ
代表同一个文件。
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
在这里,如果这是新克隆的结果。事实上,这在某些情况下确实会发生,并且会导致更奇怪的行为,如下所示。但让我们暂时想象一下它确实做到了。)
这存储了两个分支名称,branch
和BRANCH
,并赋予它们不同的值。(键值对存储在.git/packed-refs
as 中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 通过做两件事来做到这一点:
首先,它检查文件中是否有名称条目
packed-refs
。这使用了区分大小写的比较,即它不匹配refs/heads/branch
条目,仅匹配refs/heads/BRANCH
条目。如果存在这样的条目,Git 必须重写或销毁 packed-refs 文件,正如我们将看到的。然后,要么验证一切正常——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/branch
并origin/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 可以通过以下三种方式中的任何一种来解决此问题:
停止将文件系统用作廉价的数据库形式。
如果 Git 作者放入一个不依赖于操作系统的“原子文件创建”和“原子重命名”操作的真实数据库,问题就会消失。这是令人不快的,因为真正的数据库往往需要大量代码。Git 或许可以使用诸如Berkeley DB之类的库,但即使这样也有点复杂。不过,有一些明显的好处。
编码文件名。
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
.放弃案例意义。
Git 可以规定所有分支名称始终都是小写的,如果您将分支名称写为
BRANCH
Git,则只需将其存储为branch
。
方法 2 可能是最可口的,但即使这样也是操作上的一个相当大的转变。core.refVersion
为了处理向后兼容性,只有在变量设置为至少 2时才可以启用它,并且也可能只有在core.ignoreCase
设置时才启用。
推荐阅读
- c# - 如何使用 Source Generator 获取数据成员默认值?
- java - DockerforWindows:JenkinsInstallation:java.security.SignatureException:签名长度不正确:得到512但期待256
- excel - 水晶报表虚线
- google-cloud-platform - Terraform:“每个”对象不支持此操作
- python - 如何保存 DNN 中间层的输出并将其作为输入提供给另一个 DNN?
- c# - 无法使用 Nunit ITestEngine 运行测试
- php - Codeception 查看和点击函数 运行 ChromeDriver 的 Selenium 出错
- c++ - c++中链表的结构定义
- c++ - 如何构建一个数据指向数组的向量,这对于统一内存是否可行?
- php - PHP Curl FTPS 隐式传输在防火墙后面被拒绝