git - Git切换分支问题:无法获取远程分支更改
问题描述
我一直在使用 git 并且能够创建一个分支并推送原点。我对基本的了解很少,但仍在学习。
今天我在一个分支上工作,可以说B
是并行调用,但有时我正在做一些调试分支文件夹A
,但没有在分支之间切换,只是处理文件并将它们保存到驱动器。
所以我想切换回分支A
以将更改推送到git
所以我做了
git checkout A
错误:以下未跟踪的工作树文件将被结帐覆盖:cc.py dd.py ....其他一些文件并不真正理解为什么我会收到此错误,因为我的分支是
B
并且错误下方的那些文件属于分支-A
文件夹。反正我做了
git checkout -f A
切换到分支'A' 你的分支是最新的'origin/A'。
怎么会这样?我已经在A
本地更新了分支中的文件,但它说你是最新的??
然后我做了
git status
没有要提交的文件。一切都是最新的。所以我想如果我fetch
是这个分支的远程版本,它会识别本地版本和分支的远程版本之间的差异A
然后我做了
git remote update
Fetching origin
remote: Enumerating objects: 27, done.
remote: Counting objects: 100% (27/27), done.
remote: Compressing objects: 100% (14/14), done.
remote: Total 14 (delta 11), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (14/14), 1.76 KiB | 39.00 KiB/s, done.
做过
git fetch origin A
- 分支 A -> FETCH_HEAD
基本上无论我尝试什么,我都无法让更改后的文件状态在我的本地存储库分支 A 中显示为红色。所以我尝试fetch
从远程获取分支的版本local
和remote
版本之间的差异A
。那也是失败的。
我真的很困惑为什么会发生这种情况,并且真的在寻求帮助来解决这个问题!谢谢
解决方案
TL;博士
切换分支可能需要更改 Git 索引和工作树的内容。这可能会丢失您正在做的工作。你遇到过这样的情况。通常,您必须强制 Git 丢失工作(尽管旧git checkout
命令有一些小问题,使销毁未保存的工作变得太容易,在新命令中已修复git switch
)。
这里有很多要知道的。
长
您将许多概念混合在一起,当您使用 Git 时,您需要在脑海中保持独立。特别是,您似乎对 Git 的介绍很糟糕。一个好的将从这个开始:
Git 是关于提交的。
提交包含文件,但 Git 与文件无关。Git 是关于提交的。
分支——或者更准确地说,分支名称——帮助你和 Git找到提交,但 Git 也不是关于分支的。
所以 Git 基本上只是一个充满提交的大数据库(和其他支持对象,还有一些较小的数据库)。提交是 Git 存在的理由。
众所周知,有人告诉你三遍都是真的,所以接下来要学习的是什么是commit。这有点抽象:很难指向房间里的某个东西并在那里说,这是一个提交!因为没有现实世界的模拟。但在 Git 中:
每个提交都有编号,有一个唯一的编号,看起来像随机垃圾。它实际上是一个密码校验和(让人想起加密货币,这里实际上存在关系),以十六进制表示,但我们可以将其视为显然是随机的垃圾字符串,没有人会记住。然而,它对于那个特定的提交是唯一的:一旦一个数字被任何一个提交使用,任何地方的任何人都不能将它用于任何其他提交。1
这就是两个不同的 Git(实现 Git 的两个软件,使用两个不同的存储库)如何判断它们是否都有一些提交。他们只是看对方的提交编号。如果数字相同,则提交相同。如果不是,则提交是不同的。所以从某种意义上说,数字就是提交,除了数字只是提交的哈希值,如果你没有数字,你需要获取整个提交(从拥有它的人那里)。
同时,每个提交都存储两件事:
每个提交都有每个文件的完整快照。更准确地说,每个提交都有它拥有的所有文件的完整快照。这听起来是多余的,但提交
a123456
可能有 10 个文件,而提交b789abc
可能有 20 个文件,所以很明显一些提交可能比另一个有更多的文件。这一点要注意,只要您有提交,您就会拥有所有文件的完整快照,就像存档一样。提交中的文件以特殊的仅 Git 形式存储。它们被压缩并且——更重要的是——<em>重复数据删除。这可以防止存储库变得非常臃肿:大多数提交主要重用以前提交的文件,但是当他们这样做时,文件都被删除了重复数据,因此新提交几乎不占用任何空间。只有真正不同的文件需要进入;与以前相同的文件只是被重新使用。
除了快照之外,每个提交都有一些元数据。元数据只是关于提交本身的信息。这包括诸如提交人的姓名之类的内容。它包括一些日期和时间戳:他们何时提交。它包括一条日志消息,他们在其中说明了提交的原因。
对于 Git 本身至关重要的是,Git 在此元数据中添加了先前提交的提交编号列表——“哈希 ID”或“对象 ID”(OID)。
大多数提交只存储一个哈希 ID,用于(单数)先前或父提交。这种形式提交到链中。这些链条向后工作,这是有充分理由的。
1这种完全唯一性的想法在实践中是正确的,但在理论上是不正确的,但只要它在实践中是正确的就可以。为了让它在实践中发挥作用,这些数字需要和它们一样大——或者很快,更大,Git 人员现在正在努力让它们变得更大。
每个提交的所有部分都是只读的
为了使提交编号(加密哈希 ID)起作用,Git 需要确保任何提交的任何部分都不能更改。事实上,你可以从 Git all-commits 数据库中取出一个提交,并用它来更改内容或元数据并将其放回去,但是当你这样做时,你只会得到一个新的不同的提交,并带有一个新的唯一哈希ID。旧提交保留在旧 ID 下的数据库中。
因此,提交是由两部分组成的东西——快照和元数据——它是只读的,或多或少是永久的。你真正使用 Git 所做的只是添加更多的提交。你真的不能拿出任何东西,2但添加新的非常容易,因为这就是 Git 的目的。
2但是,您可以停止使用提交,如果提交不仅未使用而且无法找到,Git 最终会意识到该提交是垃圾,并将丢弃它。因此,如果需要,这就是您摆脱提交的方式:您只需确保无法找到它们,Git 最终——这需要一段时间!——将它们扔掉。不过,我们不会在这里详细介绍。
让我们多谈谈父母和后链的事情
尽管这与您现在正在做的事情无关,但它确实很重要,所以让我们看看提交链是如何工作的。我们已经说过,大多数提交记录了一个较早提交的原始哈希 ID。我们还说过哈希 ID 又大又丑,对人类来说是不可能的(这是真的:这到底是什么e9e5ba39a78c8f5057262d49e261b42a8660d5b9
意思?)。所以让我们假设我们有一个包含一些提交的小型存储库,但不是他们真正的哈希 ID,让我们使用单个大写字母来代表这些提交。
我们将从一个只有三个提交的存储库开始,我们将它们称为A
、B
和C
。 C
将是最新的提交。让我们把它画进去:
<-C
C
包含较早提交的原始哈希 ID B
。我们喜欢将这些绘制为从提交中出来的箭头,并说它C
指向 B
. 我们B
现在也画进去:
<-B <-C
当然B
有这些箭头之一,指向较早的提交A
:
A <-B <-C
这就是我们完整的提交链。 A
,作为第一次提交,它没有指向任何更早的东西,因为它不能,所以链在这里停止。
为了添加一个新的提交,我们告诉 Git 用 commit 做一些事情——我们C
稍后会更完整地描述这个——然后使用 C
来进行新的提交,然后它会指向C
:
A <-B <-C <-D
现在我们的链中有四个提交,新的提交D
指向C
.
除了这些向后的箭头之外,每个提交都有一个完整的快照。当我们制作.D
_ 我们大概留下了一些文件。我们现在可以让 Git 向我们展示. D
C
D
为此,Git 将 和 提取到 C
一个 D
临时区域(在内存中)并检查包含的文件。当他们匹配时,它什么也没说。Git 执行的重复数据删除使这个测试变得容易,Git 实际上可以完全跳过这些文件的提取。只有对于不同的文件, Git 实际上才需要提取它们。然后它比较它们,玩一种Spot the Difference游戏,并告诉我们这些更改的文件有什么不同。那是 a git diff
,也是我们从git log -p
or看到的git show
。
当我们git show
在一次提交上运行时,Git:
- 以某种格式打印元数据或其中的某些选定部分;和
- 运行这种差异来查看这个提交的父级和这个提交之间有什么不同。
当我们运行时git log
,Git:
- 从最后一次提交开始
D
; - 向我们展示了该提交,如果我们使用;也可能带有
git show
样式差异-p
然后 - 向后移动一跳到上一个提交,
C
,然后重复。
只有当我们厌倦了查看git log
输出,或者 Git 到达第一个提交时(A
),这个过程才会停止。
查找提交
让我们再画几个提交。我将对提交之间的内部箭头变得懒惰:它们是每个提交的一部分,因此不能更改,所以我们知道它们总是指向向后。我将H
在这里用哈希结束我的链:
...--F--G--H
一旦我们有很多提交——超过这个暗示的八个左右——就很难确定哪个看起来随机的哈希 IDH
实际具有. 我们需要一种快速查找哈希的方法,H
.
Git 对此的回答是使用分支名称。分支名称就是任何符合名称限制的旧名称。该名称包含一个哈希 ID,例如 commit 的哈希 ID H
。
给定一个包含 commit 的哈希 ID 的名称H
,我们说这个名称指向 H
,并将其绘制在:
...--G--H <-- main
如果我们愿意,我们可以拥有多个指向 commit 的名称H
:
...--G--H <-- develop, main
我们现在需要一种方法来知道我们正在使用哪个名称。为此,Git 将一个非常特殊的名称 ,HEAD
以这样的全大写形式写在一个分支名称上。HEAD
附加到它的名称是当前分支,该分支名称指向的提交是当前提交。所以:
...--G--H <-- develop, main (HEAD)
on branch main
正如将要说的那样,我们git status
是 ,并且我们正在使用哈希 ID 为 的提交H
。如果我们运行:
git switch develop
作为一个 Git 命令,它告诉 Git 我们应该停止使用该名称main
并开始使用该名称develop
:
...--G--H <-- develop (HEAD), main
当我们这样做时,我们从 commit 移动H
到 ... commit H
。我们实际上哪儿也不去。这是一个特殊情况,Git 确保除了更改HEAD
附加位置之外什么都不做。
现在我们已经“开启”了分支develop
,让我们进行一个新的提交。我们不会谈论我们如何做到这一点,但我们会回到那个,因为这是你当前问题的核心。
无论如何,我们将引入新的commit I
,它将指向现有的 commit H
。Git 知道父级 forI
应该是H
因为,当我们开始时,名称develop
选择了 commit H
,所以这H
就是我们开始整个“make new commit”过程时的当前提交。最终结果是这样的:
I <-- develop (HEAD)
/
...--G--H <-- main
也就是说,名称 develop
现在选择了 commit I
,而不是 commit H
。存储库中的其他分支名称没有移动:它们仍然选择之前所做的任何提交。但现在develop
意味着commitI
。
如果我们再次提交,我们会得到:
I--J <-- develop (HEAD)
/
...--G--H <-- main
也就是说,名称develop
现在选择了 commit J
。
如果我们现在运行git switch main
或者git checkout main
——两者都做同样的事情——Git 将删除所有附带的文件J
(尽管它们被安全地永久存储在 J
其中)并提取所有附带的文件H
:
I--J <-- develop
/
...--G--H <-- main (HEAD)
我们现在on branch main
又得到了文件H
。如果我们愿意,我们现在可以创建另一个新的分支名称,例如feature
,然后进入该分支:
I--J <-- develop
/
...--G--H <-- feature (HEAD), main
请注意如何H
在所有三个分支上通过并包含提交,而I-J
仅在上提交develop
。当我们进行新的提交时:
I--J <-- develop
/
...--G--H <-- main
\
K--L <-- feature (HEAD)
当前分支名称向前移动,以适应新提交,并且新提交仅在当前分支上。我们可以通过移动分支名称来改变这一点:名称移动,即使提交本身是一成不变的。
提交是只读的,那么我们如何编辑文件呢?
我们现在来到您问题的核心部分。我们不——事实上,我们不能——直接处理提交,因为它们是这种奇怪的仅 Git 格式。我们必须让 Git 来提取提交。我们已经看到git checkout
或git switch
可以做到这一点,但现在是全面了解情况的时候了。
为了完成新工作,Git 为您提供了 Git 所谓的工作树或工作树。这是一个目录(或文件夹,如果您更喜欢该术语),其中包含您计算机的普通文件格式的普通文件。 这些文件不在 Git 中。 可以肯定的是,其中一些来自Git:git checkout
orgit switch
进程填充了您的工作树。但它通过这个过程做到这一点:
- 首先,如果你有一些现有的提交被签出,Git 需要删除所有来自该提交的文件。
- 然后,由于您要进行其他提交,Git 现在需要创建(刷新)存储在该提交中的文件。
因此,Git 根据两次提交之间的差异删除旧文件并放入新文件。
但是你的工作树是一个普通的目录/文件夹。这意味着您可以在此处创建文件,或在此处更改文件的内容,而 Git 对此过程没有任何控制或影响。您创建的一些文件将是全新的:它们不在 Git 中,它们不是来自 Git,Git 从未见过它们。其他文件实际上可能在很久以前的某个旧提交中,但不是来自该提交。一些文件确实来自这个提交。
使用 时git status
,Git 需要将工作树中的内容与某些内容进行比较。现在这个过程变得有点复杂了,因为 Git 实际上并没有从工作树中的文件进行新的提交。3 相反,Git 保留了所有文件的另一个副本。
请记住,提交的文件——当前文件或HEAD
提交文件中的文件——是只读的,并且是 Git 化的、去重复的格式,只有 Git 本身可以读取。因此,Git 将这些文件提取为普通文件,为每个文件留下两个副本:
- 提交中的 Git-only 只读文件,以及
- 你工作树中的那个。
但实际上,Git 偷偷地在这两个副本之间插入了一个副本,因此每个文件都有三个副本:
- 中有一个 Git 化的
HEAD
,不能更改; - 在中间位置有一个 Git-ified准备提交副本;和
- 您的工作树中有一个可用的副本。
因此,如果您有一些文件,例如README.md
and main.py
,实际上每个文件都有三个副本。中间那个在 Git 调用的地方,不同的地方是index,或者staging area,或者cache。这个东西一共有三个名字,可能是因为index这个名字太烂了,cache也不好。暂存区这个术语可能是最好的术语,但我会在这里使用索引,因为它更短且无意义,有时无意义是好的。
那么,我们的文件的三个副本是:
HEAD index work-tree
--------- --------- ---------
README.md README.md README.md
main.py main.py main.py
Git索引中的文件是 Git 将提交的文件。因此,我想说的是,Git 的索引就是你提议的下一次提交。
当 Git 第一次提取提交时,Git 会填写它的索引和您的工作树。Git 索引中的文件是预压缩和预去重的。由于它们来自提交,它们都是自动重复的,因此不占用空间。4 你工作树中的那些确实占用空间,但你需要那些,因为你必须让它们去 Git 化才能使用它们。
当您修改工作树中的文件时,不会发生其他任何事情: Git 的索引没有改变。提交本身当然是没有改变的:它实际上是不能改变的。但是索引中的文件也没有发生任何事情。
一旦你做了一些更改并希望这些更改被提交,你必须告诉 Git:嘿,Git,将旧版本的文件从索引中踢出。阅读我的工作树版本,main.py
因为我改变了它!立即将其压缩为您的内部压缩格式! 你用git add main.py
. Git 读取并压缩文件,并检查结果是否重复。
如果结果是重复的,Git 会踢出当前main.py
并使用新的重复。如果结果不是重复的,则保存压缩文件以便准备好提交,然后执行相同的操作:踢出当前main.py
文件并放入现在已删除重复(但第一次出现)的文件副本. 因此,无论哪种方式,索引现在都已更新并准备就绪。
因此,索引始终准备好提交。如果您修改某些现有文件,您必须git add
:这会通过更新索引来压缩、重复数据删除和准备提交。如果您创建一个全新的文件,您必须git add
:这会压缩、删除重复并准备好提交。通过更新 Git 的索引,您可以准备好提交的文件。
这也是删除文件的方式。它保留在当前提交中,但如果你使用git rm
,Git 将同时删除索引副本和工作树副本:
git rm main.py
产生:
HEAD index work-tree
--------- --------- ---------
README.md README.md README.md
main.py
您进行的下一次提交不会有main.py
.
3这实际上很奇怪:大多数非 Git 版本控制系统确实使用您的工作树来保存提议的下一次提交。
4索引条目本身会占用一些空间,通常每个文件大约 100 字节左右或略低于 100 字节,以保存文件名、内部 Git 哈希 ID 和其他使 Git 快速运行的有用内容。
现在我们看看是如何git commit
工作的
当你运行时git commit
,Git:
- 收集任何需要的元数据,例如
user.name
和user.email
fromgit config
,以及进入新提交的日志消息; - 当前提交的哈希 ID 是新提交的父级;
- Git索引中的任何内容都是快照,因此 Git 将索引冻结为新快照;和
- Git 写出快照和元数据,从而获得新提交的哈希 ID。
在您运行之前,我们不知道哈希 ID 是git commit
什么,因为进入元数据的部分内容是当时的当前日期和时间,我们不知道您何时提交。所以我们永远不知道未来的提交哈希 ID 是什么。但我们确实知道,因为它们都是一成不变的,所有过去的提交哈希 ID 是什么。
所以现在 Git 可以写出 commit I
:
I
/
...--G--H <-- develop (HEAD), main
一旦 Git 将其写出并获得哈希 ID,Git 就可以将该哈希 ID 填充到分支名称 develop
中,因为这HEAD
是附加的位置:
I <-- develop (HEAD)
/
...--G--H <-- main
这就是我们的分支的成长方式。
index或staging area确定下一次提交的内容。您的工作树允许您编辑文件,以便您可以git add
将它们放入 Git 的索引中。checkout 或 switch 命令从索引中删除当前提交的文件,然后转到所选提交,填写 Git 的索引和您的工作树,并选择哪个分支名称和提交作为新的当前提交。这些文件来自该提交并填写Git 的索引和您的工作树,您已准备好再次工作。
但是,在您实际运行之前git commit
,您的文件不在Git中。一旦你运行git add
,它们就在 Git 的index中,但这只是一个临时存储区域,将被 nextgit checkout
或git switch
. 这git commit
是真正拯救他们的步骤。这也将新提交添加到当前分支。
介绍其他 Git 存储库
现在,除了上述所有内容之外,您还使用git fetch
. 当至少有两个 Git 存储库时使用它。我们之前提到过,我们将使用两个存储库将两个 Git(Git 软件的两个实现)相互连接并让它们传输提交。一个 Git 可以通过显示哈希 ID 来判断另一个 Git 是否有某个提交:另一个 Git 要么在其所有提交的大数据库中具有该提交,要么没有。如果缺少提交的 Git 说我没有那个, gimme,那么发送Git 必须打包该提交 - 以及任何所需的支持对象 - 并将它们发送过来,现在接收Git 也有那个提交。
我们在这里总是使用单向传输:我们运行git fetch
以从其他 Git获取git push
提交,或者将提交发送到其他 Git。这两个操作(获取和推送)与 Git 的对立一样接近,尽管这里存在某种根本的不匹配(我不会讨论,因为这已经很长了)。我们只谈fetch
。
当我们将我们的 Git 连接到其他 Git 时——让我们在这里使用 GitHub 的 Git 软件和存储库作为我们的示例,尽管使用正确的 Git 软件协议的任何东西都适用——使用git fetch
,我们:
让另一个 Git 列出它的所有分支(和标签)名称以及与这些分支名称相关的提交哈希 ID(标签使事情变得更加复杂,所以我们将在这里忽略它们)。
对于我们没有但感兴趣的每个提交哈希 ID——我们可以在这里限制我们打扰的分支名称,但默认是所有都是有趣的——我们要求他们发送那个提交!. 他们现在有义务提供这些提交的父提交。我们检查我们是否有这些提交,如果没有,也询问这些提交。这种情况一直持续到他们得到我们确实有的提交,或者完全用完提交。
这样,我们将从他们那里得到他们拥有的每一个我们没有的提交。然后,他们将这些与任何所需的支持内部对象一起打包,并将它们全部发送出去。现在我们有了他们所有的承诺!
但是还记得我们如何在存储库中使用分支名称查找提交吗?我们现在有一个问题。
假设我们的存储库中有这些提交:
...--G--H--I <-- main (HEAD)
也就是说,我们只有一个分支名称,main
. 我们早些时候从他们那里得到了提交,但后来我们自己做出了承诺。H
I
同时,当我们进行 commitI
时,他们做出了 commitJ
并将其放在了他们的main 上,所以他们有:
...--G--H
\
J <-- main (HEAD)
我把它画J
了一条线,因为当我们结合我们的提交和他们的提交时,我们最终得到:
...--G--H--I <-- main (HEAD)
\
J
我们将附加什么名称来提交J
以便能够找到它?(请记住,它的真实名称是一些看起来很丑的随机散列 ID。) 他们正在使用他们命名的分支main
来查找它,但是如果我们将分支移动到指向main
,J
我们将失去自己的 I
!
所以我们不会更新任何分支名称。相反,我们的 Git 将为它们的每个分支名称创建或更新一个远程跟踪名称:
...--G--H--I <-- main (HEAD)
\
J <-- origin/main
我们的远程跟踪名称显示为git branch -r
, 或git branch -a
(它显示了我们自己的分支名称和我们的远程跟踪名称)。远程跟踪名称只是我们的 Git 记住他们的分支origin/
名称的方式,我们的 Git 通过粘贴在他们的分支名称前面来弥补它。5
现在我们既有他们的提交,也有我们的提交,加上远程跟踪名称,如果他们不完全重叠我们的提交,可以帮助我们找到他们的提交,现在我们可以对他们的提交做一些事情。我们所做的“某事”取决于我们想要完成的事情,而这里的事情实际上开始变得复杂——所以我会在这里停下来。
5从技术上讲,我们的远程跟踪名称位于单独的命名空间中,因此即使我们做一些疯狂的事情,比如创建一个名为的(本地)分支origin/hello
,Git也会保持这些名称不变。但是不要这样做:即使使用 Git 为不同名称着色的技巧,您也可能会感到困惑。
那么你的改变发生了什么?
我们再来看看这部分:
$ git checkout A error: The following untracked working tree files would be overwritten by checkout: cc.py dd.py ....
这些是您创建的文件,不是来自之前的提交。它们在您的工作树中,但不在 Git 中。(“未跟踪”的意思是“甚至不在 Git 的索引中”。)
checkout 命令给了你这个错误,让你可以在 Git 中(通过添加和提交它们)或其他地方保存文件。但你没有提到这样做:
$ git checkout -f A
这里的-f
, 或--force
, 标志意味着继续,覆盖这些文件。所以你创建的文件消失了:分支名称A
选择了一个包含这些文件的提交,所以它们从提交中出来,进入 Git 的索引,并被扩展到你的工作树中。
以前的工作树文件从未在 Git 中,因此 Git 无法检索它们。如果您有其他检索它们的方法(例如,如果您的编辑器保存备份),请使用它。如果没有,你可能不走运。
推荐阅读
- spring - 将 Spring Feign 与 Spring Cloud LoadBalancer 一起使用
- jira - 有没有办法让用户故事成为任务的子任务?
- python - 如何在 matplotlib 极坐标 2D 直方图中创建弯曲的 bin?
- angular - 使用 ngrx 测试服务
- c - 以最经济的方式保存一组 0-127 整数的数据结构
- r - 有没有办法用一个 for 循环计算多个数据帧的平均值,然后将其放入数据帧中?
- python-3.x - 在 groupby sum 之后将数据帧索引显示为整数
- hadoop - Apache Pig 问题 - 使用顺序限制数据不起作用
- bash - Bash,如果 for 循环在第一个元素上崩溃,如何移动到下一个元素
- r - 如何在不退出远程服务器上的 R 的情况下恢复 X11 连接