首页 > 解决方案 > 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从远程获取分支的版本localremote版本之间的差异A。那也是失败的。

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

“git pull”和“git fetch”有什么区别?

标签: gitgithubgitlab

解决方案


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,让我们使用单个大写字母来代表这些提交。

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

      <-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 向我们展示. DCD

为此,Git 将 和 提取 C 一个 D临时区域(在内存中)并检查包含的文件。当他们匹配时,它什么也没说。Git 执行的重复数据删除使这个测试变得容易,Git 实际上可以完全跳过这些文件的提取。只有对于不同的文件, Git 实际上才需要提取它们。然后它比较它们,玩一种Spot the Difference游戏,并告诉我们这些更改的文件有什么不同那是 a git diff,也是我们从git log -por看到的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 checkoutgit switch可以做到这一点,但现在是全面了解情况的时候了。

为了完成新工作,Git 为您提供了 Git 所谓的工作树工作树。这是一个目录(或文件夹,如果您更喜欢该术语),其中包含您计算机的普通文件格式的普通文件。 这些文件不在 Git 中。 可以肯定的是,其中一些来自Git:git checkoutorgit 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.mdand 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.nameuser.emailfrom git 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

这就是我们的分支的成长方式。

indexstaging area确定下一次提交的内容。您的工作树允许您编辑文件,以便您可以git add将它们放入 Git 的索引中。checkout 或 switch 命令从索引中删除当前提交的文件,然后转到所选提交,填写 Git 的索引和您的工作树,并选择哪个分支名称和提交作为新的当前提交。这些文件来自该提交并填写Git 的索引和您的工作树,您已准备好再次工作。

但是,在您实际运行之前git commit,您的文件不在Git中。一旦你运行git add,它们就在 Git 的index中,但这只是一个临时存储区域,将被 nextgit checkoutgit 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,我们:

  1. 让另一个 Git 列出它的所有分支(和标签)名称以及与这些分支名称相关的提交哈希 ID(标签使事情变得更加复杂,所以我们将在这里忽略它们)。

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

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

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

假设我们的存储库中有这些提交:

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

也就是说,我们只有一个分支名称,main. 我们早些时候从他们那里得到了提交,但后来我们自己做出了承诺。HI

同时,当我们进行 commitI时,他们做出了 commitJ并将其放在了他们的main 上,所以他们有:

...--G--H
         \
          J   <-- main (HEAD)

我把它画J了一条线,因为当我们结合我们的提交和他们的提交时,我们最终得到:

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

我们将附加什么名称来提交J以便能够找到它?(请记住,它的真实名称是一些看起来很丑的随机散列 ID。) 他们正在使用他们命名的分支main来查找它,但是如果我们将分支移动指向mainJ我们将失去自己的 I

所以我们不会更新任何分支名称。相反,我们的 Git 将为它们的每个分支名称创建或更新一个远程跟踪名称:

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

我们的远程跟踪名称显示为git branch -r, 或git branch -a(它显示了我们自己的分支名称我们的远程跟踪名称)。远程跟踪名称只是我们的 Git 记住他们的分支origin/名称的方式,我们的 Git 通过粘贴在他们的分支名称前面来弥补它。5

现在我们既有他们的提交,也有我们的提交,加上远程跟踪名称,如果他们不完全重叠我们的提交,可以帮助我们找到他们的提交,现在我们可以他们的提交做一些事情。我们所做的“某事”取决于我们想要完成的事情,而这里的事情实际上开始变得复杂——所以我会在这里停下来。


5从技术上讲,我们的远程跟踪名称位于单独的命名空间中,因此即使我们做一些疯狂的事情,比如创建一个名为的(本地)分支origin/helloGit也会保持这些名称不变。但是不要这样做:即使使用 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 无法检索它们。如果您有其他检索它们的方法(例如,如果您的编辑器保存备份),请使用它。如果没有,你可能不走运。


推荐阅读