我一直在使用 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。
我们将从一个只有三个提交的存储库开始,我们称之为A
、B
和C
。C
将是最新的提交。 让我们把它画进去:
<-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 将C
和D
提取到临时区域(在内存中)并检查包含的文件。 当他们匹配时,它什么也没说。 Git 执行的重复数据消除使此测试变得容易,Git 实际上可以完全跳过这些文件的提取。 只有对于不同的文件,Git 才真正需要提取它们。 然后它比较它们,玩一种发现差异的游戏,并告诉我们这些更改文件中的不同之处。 这是一个git diff
,这也是我们从git log -p
或git 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 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
或git switch
进程填充了您的工作树。 但它通过这个过程做到这一点:
- 首先,如果您签出了某些现有提交,Git 需要删除该提交产生的所有文件。
- 然后,由于您要移动到其他提交,Git 现在需要创建(新)存储在该提交中的文件。
因此,Git 根据两次提交之间的差异删除旧文件并放入新文件。
但是您的工作树是一个普通的目录/文件夹。 这意味着您可以在此处创建文件,或在此处更改文件的内容,而无需 Git 对此过程进行任何控制或影响。 你创建的一些文件将是全新的:它们不在 Git 中,它们不是来自 Git,Git 从未见过它们。 其他文件实际上可能在很久以前的一些旧提交中,但没有来自此提交。 一些文件确实来自此提交。
当你使用git status
时,Git 需要将工作树中的内容与某些内容进行比较。 现在这个过程变得有点复杂,因为 Git 实际上并没有从工作树中的文件进行新的提交。3相反,Git 保留了所有文件的另一个副本。
请记住,提交的文件(当前或HEAD
提交的文件)是只读的,并且采用 Git 化、删除重复的格式,只有 Git 本身才能读取。 因此,Git 将这些文件提取为普通文件,每个文件有两个副本:
- 提交中的 Git 只读,以及
- 工作树中的那个。
但实际上,Git 偷偷地在这两个副本之间粘贴了一个副本,这样你就可以为每个文件提供三个副本:
HEAD
中有 Git 化的那个,无法更改;- 在中间位置有一个 Git 化的准备提交副本;和
- 您的工作树中有一个可用的副本。
因此,如果您有一些文件,例如README.md
和main.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 config
的user.name
和user.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 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。这两个操作 - fetch 和 push——与 Git 接近对立面一样接近,尽管这里存在某种根本的不匹配(我不会深入讨论,因为这已经很长了)。 我们只谈谈fetch
.
当我们将 Git 连接到其他一些 Git 时——让我们使用 GitHub 的 Git 软件和存储库作为我们这里的例子,尽管任何说正确的 Git 软件协议的东西都可以工作——使用git fetch
时,我们:
要求其他 Git 列出其所有分支(和标签)名称以及与这些分支名称一起使用的提交哈希 ID(标签使事情变得更加复杂,因此我们在此处忽略它们)。
对于我们没有但感兴趣的每个提交哈希 ID - 我们可以限制我们在这里打扰的分支名称,但默认设置是所有分支名称都很有趣 - 我们要求他们发送该提交!他们现在有义务提供这些提交的父提交。 我们检查是否有这些提交,如果没有,也要求这些提交。 这种情况一直持续到他们得到我们确实拥有的提交,或者完全用完提交。
这样,我们将从他们那里得到他们没有的每一次提交。 然后,他们将这些内容与任何所需的支持内部对象一起打包,并将它们全部发送出去。 现在我们有他们所有的提交!
但是还记得我们如何在存储库中使用分支名称查找提交吗? 我们现在有一个问题。
假设我们在仓库中有这些提交:
...--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 -r
或git 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无法检索它们。 如果您有其他方法可以检索它们(例如,如果您的编辑器保存了备份),请使用它。 如果没有,你可能不走运。