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,以了解A分支的local版本和remote版本之间的差异。这也是失败的。

我真的很想知道为什么会发生这种情况,并真的寻求帮助来解决这个问题! 谢谢

"git pull"和"git fetch"有什么区别?

TL;博士

切换分支可能需要更改 Git 索引和工作树的内容。 这可能会丢失您正在做的工作。 你遇到过这样的情况。 通常,您必须强制 Git 丢失工作(尽管旧的git checkout命令有一些小问题,使得销毁未保存的工作变得太容易了,在新git switch中修复)。

这里有很多要知道的。

你混合了许多概念,当你使用 Git 时,你需要在脑海中保持独立。 特别是看起来你对 Git 的介绍很糟糕。 一个好的将从这个开始:

  • Git 是关于提交的

  • 提交包含文件,但 Git 与文件无关。 Git 是关于提交的。

  • 分支
  • (或者更准确地说,分支名称)可以帮助您和 Git找到提交,但 Git 也与分支无关。

所以 Git 基本上只是一个充满提交(和其他支持对象)的大型数据库,除此之外还有一些较小的数据库)。 提交是 Git 存在的理由

众所周知,某人告诉你三次是真的,所以接下来要学习的是什么是提交。这有点抽象:很难指着房间里的某个东西说,这是一个提交!因为没有现实世界的模拟。 但在 Git 中:

  • 每个提交都有编号,有一个看起来像随机垃圾的唯一编号。 它实际上是一个加密校验和(让人联想到加密货币,这里实际上有一个关系),用十六进制表示,但我们可以把它看作是一串明显的随机垃圾字符,没有人会记住。 然而,对于一个特定的提交来说,它是独一无二的:一旦一个数字被任何一个提交使用,任何地方的人都不能将其用于任何其他提交。1

    这就是两个不同的 Git(两个实现 Git 的软件,使用两个不同的存储库)可以判断它们是否都有一些提交的方式。 他们只是看彼此的提交数量。 如果数字相同,则提交相同。 如果不是,则提交是不同的。 所以从某种意义上说,这个数字就是提交,除了这个数字只是提交的哈希值,如果你没有这个数字,你需要获取整个提交(从有它的人那里)。

  • 同时,每个提交存储两件事:

    • 每次提交都有每个文件的完整快照。 更准确地说,每个提交都有它拥有的所有文件的完整快照。 这听起来是多余的,但提交a123456可能有十个文件,而提交b789abc可能有 20 个文件,所以显然一些提交可能比另一个有更多的文件。 这样做的重点是要注意,只要您有提交,您就有所有文件的完整快照,就像存档一样。

      提交中的文件以特殊的仅限 Git 的形式存储。 它们经过压缩,更重要的是,经过重复数据消除。 这可以防止存储库变得非常胖:大多数提交大多重用以前提交的文件,但是当他们这样做时,文件都会被删除重复,因此新提交几乎不占用任何空间。 只有真正不同的文件需要进入;与以前相同的文件只是被重复使用。

    • 除了快照之外,每个提交都有一些元数据。 元数据只是关于提交本身的信息。 这包括提交人员的姓名等内容。 它包括一些日期和时间戳:他们何时提交。 它包括一条日志消息,他们在其中说明他们提交的原因

      对于 Git 本身至关重要的是,Git 在此元数据中添加了以前提交的提交编号列表——"哈希 ID"或"对象 ID"(OID)。

大多数提交只存储一个哈希 ID,用于(单数)上一个或提交。 这将提交形成。 这些链条是反向工作的,这是有充分理由的。


1这种完全唯一性的想法在实践中是正确的,但在理论上不是,但只要在实践中是正确的,这就可以了。 为了使它在实践中发挥作用,数字需要像它们一样大 - 或者很快,更大,Git 人员现在正在努力使它们变得更大。


每个提交的所有部分都是只读的

为了使提交编号(加密哈希 ID)正常工作,Git 需要确保任何提交的任何部分都无法更改。 事实上,你可以从 Git all-commit 数据库中取出一个提交,并用它做一些事情来更改内容或元数据并将其放回原处,但是当你这样做时,你只会得到一个新的、不同的提交,其中包含一个新的唯一哈希 ID。 旧提交保留在数据库中的旧 ID 下。

因此,提交是由两部分组成 - 快照和元数据 - 是只读的,或多或少是永久性的。 你真正用 Git 做的就是添加更多的提交。 从字面上看,您不能删除任何内容,2但是添加新的非常容易,因为这就是 Git 的构建目的。


2但是,您可以停止使用提交,如果提交不仅未使用而且无法找到,Git 最终会意识到此提交是垃圾,并会丢弃它。 所以这就是你摆脱提交的方式,如果需要的话:你只需要确保它们找不到,Git 最终——需要一段时间!——把它们扔掉。 不过,我们不会在这里详细介绍这一点。


让我们多谈谈父母和反向链的事情

虽然这与你现在正在做的事情无关,但它真的很重要,所以让我们看看提交链是如何工作的。 我们已经说过,大多数提交都会记录一个早期提交的原始哈希 ID。 我们还说过哈希ID又大又丑,对人类来说是不可能的(这是真的:e9e5ba39a78c8f5057262d49e261b42a8660d5b9到底是什么意思? 因此,假设我们有一个包含一些提交的小型存储库,但让我们使用单个大写字母来代替这些提交,而不是它们的真实哈希 ID。

我们将从一个只有三个提交的存储库开始,我们称之为ABCC将是最新的提交。 让我们把它画进去:

<-C

C包含早期提交B的原始哈希 ID。 我们喜欢将它们绘制为从提交中出来的箭头,并说C指向B。 现在让我们也画B

<-B <-C

当然,B有这些箭头之一,指向早期的提交A

A <-B <-C

这是我们的整个提交链。A,作为第一个提交,它不会更早地指向任何东西,因为它不能,所以链到此停止。

添加新提交,我们告诉 Git 对提交C做一些事情——我们稍后会更完整地描述这一点——然后使用C进行新的提交,然后指向C

A <-B <-C <-D

现在我们的链中有四个提交,新的提交D指向C

除了这些向后箭头之外,每个提交都有一个完整快照。 当我们制作D时,我们大概更改了一些文件 - 同样,我们稍后会详细介绍 - 因此D中的某些文件与C中的文件不同。 我们大概留下了一些文件。 我们现在可以要求 Git 向我们展示D发生了哪些变化

为此,Git 将CD提取到临时区域(在内存中)并检查包含的文件。 当他们匹配时,它什么也没说。 Git 执行的重复数据消除使此测试变得容易,Git 实际上可以完全跳过这些文件的提取。 只有对于不同的文件,Git 才真正需要提取它们。 然后它比较它们,玩一种发现差异的游戏,并告诉我们这些更改文件中的不同之处。 这是一个git diff,这也是我们从git log -pgit show中看到的。

当我们在一个提交上运行git show时,Git :

  • 打印元数据或其某些选定部分,并采用一些格式;和
  • 运行这种差异以查看此提交的父级和此提交之间的区别。

当我们运行git log时,Git:

  • 从最后一个提交D开始;
  • 向我们展示了提交,如果我们使用-p,也许也会使用git show样式
  • 的差异;
  • 将一个跃点移回上一个提交,C,然后重复。

只有当我们厌倦了查看git log输出时,或者 Git 通过到达第一个 (A) 耗尽提交时,此过程才会停止。

查找提交

让我们再画几个提交。 我会对提交之间的内部箭头变得懒惰:它们是每个提交的一部分,因此无法更改,所以我们知道它们总是指向后方。 我将在这里用哈希H结束我的链:

...--F--G--H

一旦我们有很多提交- 超过这暗示的八个左右 - 就很难弄清楚H实际上具有哪个随机的哈希ID。 我们需要一种快速的方法来查找哈希,H.

Git 对此的回答是使用分支名称。 分支名称只是满足名称限制的任何旧名称。 该名称包含一个哈希 ID,例如用于提交H的哈希 ID。

给定一个包含提交H哈希ID的名称,我们说这个名称指向H,并将其绘制为:

...--G--H   <-- main

如果我们愿意,我们可以有多个指向提交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

当我们这样做时,我们从提交H移动到...提交H。 我们实际上哪儿也不。 这是一个特殊情况,Git 确保不做任何事情,而是更改HEAD附加的位置。

现在我们"在"分支develop,让我们进行一个新的提交。 我们暂时不会讨论如何做到这一点,但我们会回到这个问题,因为这是你当前问题的核心。

无论如何,我们将绘制新的提交I,它将指向现有的提交H。 Git 知道I的父级应该是H的,因为当我们开始时,名称develop选择提交H,因此H是我们开始整个"进行新提交"过程时的当前提交最终结果是这样的:

I   <-- develop (HEAD)
/
...--G--H   <-- main

也就是说,develop的名称现在选择提交I,而不是提交H。 存储库中的其他分支名称没有移动:它们仍然选择之前所做的任何提交。 但现在develop意味着提交I.

如果我们再提交一次,我们会得到:

I--J   <-- develop (HEAD)
/
...--G--H   <-- main

也就是说,名称develop现在选择提交J

如果我们现在运行git switch maingit 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 checkoutgit switch可以做到这一点,但现在是时候了解全貌了。

为了完成新的工作,Git 为你提供了 Git 所谓的工作树或工作树这是一个目录(或文件夹,如果您更喜欢该术语),其中包含计算机普通文件格式的普通文件。这些文件不在 Git 中。可以肯定的是,其中一些来自Git:git checkoutgit switch进程填充了您的工作树。 但它通过这个过程做到这一点:

  • 首先,如果您签出了某些现有提交,Git 需要删除该提交产生的所有文件。
  • 然后,由于您要移动到其他提交,Git 现在需要创建(新)存储在提交中的文件。

因此,Git 根据两次提交之间的差异删除旧文件并放入新文件。

但是您的工作树是一个普通的目录/文件夹。 这意味着您可以在此处创建文件,或在此处更改文件的内容,而无需 Git 对此过程进行任何控制或影响。 你创建的一些文件将是全新的:它们不在 Git 中,它们不是来自 Git,Git 从未见过它们。 其他文件实际上可能在很久以前的一些旧提交中,但没有来自提交。 一些文件确实来自此提交。

当你使用git status时,Git 需要将工作树中的内容与某些内容进行比较。 现在这个过程变得有点复杂,因为 Git 实际上并没有从工作树中的文件进行新的提交。3相反,Git 保留了所有文件的另一个副本

请记住,提交的文件(当前或HEAD提交的文件)是只读的,并且采用 Git 化、删除重复的格式,只有 Git 本身才能读取。 因此,Git 将这些文件提取为普通文件,每个文件有两个副本:

  • 提交中的 Git 只读,以及
  • 工作树中的那个。

但实际上,Git 偷偷地在这两个副本之间粘贴了一个副本,这样你就可以为每个文件提供三个副本:

  • HEAD中有 Git 化的那个,无法更改;
  • 在中间位置有一个 Git 化的准备提交副本;和
  • 您的工作树中有一个可用的副本。

因此,如果您有一些文件,例如README.mdmain.py,您实际上每个文件都有三个副本。 中间的那个位于 Git 调用的位置,各种索引、暂存区域缓存。 这个东西有三个名字,也许是因为索引是一个很糟糕的名字,缓存也不好。术语暂存区域可能是最好的术语,但我在这里使用索引,因为它更短且无意义,有时无意义是好的。

然后,我们的三个文件副本是:

HEAD        index       work-tree
---------    ---------    ---------
README.md    README.md    README.md
main.py      main.py      main.py

Git索引中的文件是 Git 将提交的文件。 因此,我想说的是 Git 的索引是你提议的下一个提交

当 Git 第一次提取提交时,Git 会同时填充它的索引你的工作树。Git 索引中的文件经过预压缩和重复数据删除。 由于它们来自提交,因此它们都是自动重复的,因此不占用空间。工作树中的那些确实占用了空间,但你需要它们,因为你必须让它们去 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 字节,以保存文件名、内部 Git 哈希 ID 和其他使 Git 快速的有用内容。


现在我们看看git commit是如何工作的

当你运行git commit时,Git :

  • 收集任何需要的元数据,例如git configuser.nameuser.email,以及进入新提交的日志消息;
  • 当前提交的哈希 ID 是新提交的父级;
  • Git索引中的任何内容都是快照,因此 Git 将索引冻结为新快照;
  • Git 写出快照和元数据,从而获取新提交的哈希 ID。

在您运行git commit之前,我们不知道哈希 ID 是什么,因为进入元数据的部分内容是当时的当前日期和时间,我们不知道您何时会进行提交。 所以我们永远不知道任何未来的提交哈希ID会是什么。 但我们确实知道,因为它们都是一成不变的,所有过去的提交哈希ID是什么。

所以现在 Git 可以写出提交I

I
/
...--G--H   <-- develop (HEAD), main

一旦 Git 写出它并获得了哈希 ID,Git 就可以将该哈希 ID 填充到分支名称develop,因为那是附加HEAD的地方:

I   <-- develop (HEAD)
/
...--G--H   <-- main

这就是我们分支机构的发展方式。

索引暂存区域确定下一次提交的内容。 您的工作树允许您编辑文件,以便您可以将它们git add到 Git 的索引中。 检出或切换命令从索引中删除当前提交的文件,并转到选定的提交,填写 Git 的索引和您的工作树,并选择哪个分支名称和提交将成为新的当前提交。 这些文件来自该提交并填充 Git 的索引和您的工作树,您就可以再次工作了。

但是,在您实际运行git commit之前,您的文件不在 Git。 一旦你运行git add,它们就会在 Git 的索引中,但这只是一个临时存储区域,会被下一个git checkoutgit switch覆盖。 这是真正拯救他们的git commit步骤。 这也会将新提交添加到当前分支

介绍其他 Git 存储库

现在,除了上述所有内容之外,您还在使用git fetch. 当至少有两个 Git 存储库时,可以使用此选项。 我们之前提到过,我们将两个 Git(使用两个存储库的两个 Git 软件实现)相互连接,并让它们传输提交。 一个 Git 可以通过显示哈希 ID 来判断另一个 Git 是否有一些提交:另一个 Git 要么在其所有提交的大数据库中该提交,要么没有。 如果缺少提交的 Git 说我没有那个,gimme,那么发送Git 必须打包该提交 - 加上任何必需的支持对象 - 并将它们发送过来,现在接收Git 也有那个提交。

我们在这里总是使用单向传输:我们运行git fetch从其他 Git获取提交,或者git push将提交发送到其他 Git。这两个操作 - fetch 和 push——与 Git 接近对立面一样接近,尽管这里存在某种根本的不匹配(我不会深入讨论,因为这已经很长了)。 我们只谈谈fetch.

当我们将 Git 连接到其他一些 Git 时——让我们使用 GitHub 的 Git 软件和存储库作为我们这里的例子,尽管任何说正确的 Git 软件协议的东西都可以工作——使用git fetch时,我们:

  1. 要求其他 Git 列出其所有分支(和标签)名称以及与这些分支名称一起使用的提交哈希 ID(标签使事情变得更加复杂,因此我们在此处忽略它们)。

  2. 对于我们没有但感兴趣的每个提交哈希 ID - 我们可以限制我们在这里打扰的分支名称,但默认设置是所有分支名称很有趣 - 我们要求他们发送该提交!他们现在有义务提供这些提交的提交。 我们检查是否有这些提交,如果没有,也要求这些提交。 这种情况一直持续到他们得到我们确实拥有的提交,或者完全用完提交。

  3. 这样,我们将从他们那里得到他们没有的每一次提交。 然后,他们将这些内容与任何所需的支持内部对象一起打包,并将它们全部发送出去。 现在我们有他们所有的提交!

  4. 但是还记得我们如何在存储库中使用分支名称查找提交吗? 我们现在有一个问题。

假设我们在仓库中有这些提交:

...--G--H--I   <-- main (HEAD)

也就是说,我们只有一个分支名称,main. 我们之前通过H从他们那里得到了提交,但后来我们自己I提交。

同时,当我们进行提交I时,他们进行了提交J并将其放在他们的主服务器上,因此他们有:

...--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 -rgit branch -a显示(它显示我们自己的分支名称和远程跟踪名称)。远程跟踪名称只是我们 Git 记住其分支名称的方式,而我们的 Git 通过在他们的分支名称前面粘贴origin/来弥补它。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 中。 ("Untracked"的意思是"甚至在 Git 的索引中也没有"。

检出命令会给你这个错误,让你在 Git 中保存文件(通过添加和提交文件)或其他位置。 但是你没有提到这样做:

$ git checkout -f A

这里的-f--force标志表示继续,覆盖这些文件。 所以创建的文件消失了:分支名称A选择了包含这些文件的提交,所以它们从提交中出来,进入 Git 的索引,并扩展到你的工作树中。

以前的工作树文件从未在 Git 中,因此 Git无法检索它们。 如果您有其他方法可以检索它们(例如,如果您的编辑器保存了备份),请使用它。 如果没有,你可能不走运。

相关内容

  • 没有找到相关文章

最新更新