>我有一个来自原始存储库 A 的分叉 B。
我的桌面(B)上也有我的本地(克隆?)签出版本。
在我的分支 B 上,在 Git 存储库网站上它说
This branch is 2 commits ahead of A/master
因此,如果我尝试执行任何新的拉取请求,它总是尝试添加它们。我不想要它认为领先的 2 个(其中一个已经被拉进来了,所以我觉得这有点乱了):)>
我只想让 B 恢复与 A 同步,我的桌面也随之同步。
在我的桌面上,我尝试过类似的事情。
git remote add original A
git fetch original
git checkout original
... uploads some stuff
git checkout original
error: pathspec 'original' did not match any file(s) known to git
我之前也尝试过类似的东西
git reset --hard origin/master
git push --force origin master
但似乎没有任何区别。要么我得到一个错误,要么一切看起来都一样。我的分叉存储库比主数据库早 2 次提交,我的本地桌面说一切都是最新的。
我该如何解决这个问题,所以我的远程 B 与 A 同步,我的桌面与 B 同步。
共 2 部分(转到此处查看第 1 部分)
git push --force
因此,我们现在知道如何在本地移动分支名称。 现在让我们再看一下git push
,特别是它的--force
或-f
选项。 我们知道git push
,我们通常使用它来将我们的新提交发送到其他一些 Git 存储库。 然后,我们通常会要求其他 Git 存储库将提交添加到其分支名称之一。 如果我们所做的只是正确添加提交,并且我们有权限,7 其他 Git 通常会接受该推送请求。
但问题是,当我们向他们发送提交时,我们通过哈希 ID 向他们发送提交,通过哈希ID 将它们串在一起到其他提交。 它们在内部不使用名称,只使用哈希 ID。 如果我们有这个:
...--G--H <-- main, origin/main
I--J <-- feature1 (HEAD)
那么我们的origin/main
意味着我们的 Git 最后一次与他们的 Git 交谈时,他们的最后一次main
提交是提交H
。 这可能仍然是真的,但也许 - 特别是如果这个GitHub存储库与运行git push
的其他人共享-只是也许其他人已经将新的提交添加到他们的主提交,因此在GitHub上,他们有:
...--G--H--N--O--P <-- main
我们会向他们发送我们的I-J
,他们会将其放入他们的大数据库中,8他们将拥有:
...--G--H--N--O--P <-- main
I--J [proposed update]
每当我们告诉其他 Git移动分支名称时,他们都会检查是否可以。 如果我们告诉他们feature1
起一个新名字,那可能没问题,但是假设我们决定在这里要求他们设置main
。 他们会回答我们:不! 如果我把我的名字main
指向J
,我将失去我的N-O-P
承诺!这可是天大的!请记住,它们与每个 Git 一样,通过使用分支名称查找最后一个提交然后向后工作来查找提交。J
导致I
导致H
,这不会导致向前N
,只会向后导致G
。
这通常是我们喜欢这样的事情的工作方式。 我们不是直接推送到他们的main
,而是推送我们的feature1
提交并要求他们创建一个名为feature1
的新分支,这一切都没问题。
但。。。假设 GitHub 上的 Git 存储库是你的,并且你有:
...--G--H <-- main (HEAD), origin/main
然后你在你的main
中添加了一个错误的提交I
或一对提交I-J
并运行git push origin main
,他们接受了它们?现在您有:
...--G--H--I--J <-- main (HEAD), origin/main
表示他们的main
(你的origin/main
)指向提交J
,就像你自己的main
一样。
您现在意识到I-J
不好,并且您运行git reset --hard HEAD~2
来删除这两个:
...--G--H <-- main (HEAD)
I--J <-- origin/main
如果你现在运行git push origin main
,你的 Git 会向他们的 Git 发送他们没有的任何新提交——即没有——然后要求他们将main
设置为指向H
,他们会拒绝请求,因为这会丢失提交I-J
他们的main
。
但这正是你想要的。您希望他们删除两个错误的提交。 因此,您实现这一目标的方式是使用--force
或更高级的--force-with-lease
选项:
git push --force origin main
这会发送新的提交(none),然后,而不是礼貌地要求他们提出main
点提交H
,命令他们main
点提交H
。 他们仍然可以拒绝,但同样,只要你有权限,他们这次就会服从:先生,是的,先生!main
更新,提交被弹出!您的 Git 存储库现在将具有:
...--G--H <-- main (HEAD), origin/main
I--J [abandoned]
7请注意,"base"Git 没有任何修改(推送到)分支的权限概念,但大多数托管服务器(包括 GitHub)也会添加它。
8传入的提交实际上进入"隔离区",在被接受之前不会迁移到真正的数据库中。 这个功能来自GitHub,因为GitHub过去常常接受所有内容进入他们的数据库,然后才拒绝它们,这给GitHub带来了很大的混乱。 所以现在有这个花哨的隔离功能。
<小时 />多个遥控器
最后,我们有足够的时间来修复这一切。
不过,第一个技巧是,您必须在笔记本电脑上或任何具有本地克隆的地方进行设置,以便您有两个遥控器,而不仅仅是一个。你跑了:
git clone <url>
最初,网址是您的分叉的网址。 您的前叉是您要调整的叉子。 我们现在必须为您分叉的存储库添加一个远程。
请记住,远程只是保存 URL 的短名称,Git 将使用此短名称组成远程跟踪名称。 所以你可以在这里编出任何你喜欢的名字。 标准的名字是origin
,你已经有了这个名字。 有些人喜欢使用upstream
作为他们的标准第二个名字。 我不是这个的忠实粉丝,因为 Git 已经有其他叫做上游的东西。 我会用另一个名字;在这里,我将使用一个愚蠢的,但您应该编造一些明智的内容:
git remote add lexluthor <url>
插入您分叉的存储库的 URL。 然后运行git fetch
到该遥控器:
git fetch lexluthor
您现在在笔记本电脑的存储库中拥有它们的所有提交(您可能已经拥有所有这些提交,在这种情况下,这部分运行得很快)。 您还具有每个分支名称的远程跟踪名称。
现在你只需要说服你的 GitHub 分支,它的分支bran
,或main
,或master
,或其他什么,应该指向相同的提交,即bran
或main
或master
或lexluthor
上的任何提交:
git push --force lexluthor/master origin/master
就是这样——这就是全部。 我们向origin
发送任何我们缺少的提交,origin
缺少他们需要更新他们的(origin
)分支:这根本不算什么,因为我们"领先两个",根本没有落后。 然后我们在 GitHub 上命令Git 使我们的origin
master
识别我们的lexluthor/master
标识的相同提交,即master
在您最初分叉的存储库中标识的提交。
您可能还希望自己的master
放弃您领先的两个提交。 您可能出于其他原因希望保留这些提交/放在另一个分支上/无论如何;为此:
git switch master
git status
# make sure it says "nothing to commit, working tree clean"
# if not, make a new commit now
git branch keep-extras
git reset --hard lexluthor/master
现在,您的master
与lexluthor
和origin
同步。 请注意,您可能在git reset
行中使用了origin/master
。
我们所做的非常简单。 我们只需要兜兜转转,走很长的路。 那是给你的Git!
转到此处查看第 2 部分)
您已经进入了一个有点高级的设置。 这里有三个Git 存储库需要担心,而不是两个,GitHub"forks"是具有一些特殊属性的克隆。 (请注意,纯 Git 没有分叉和拉取请求——这些是 GitHub 附加组件。 其他托管站点也有分叉附加组件和/或拉取请求和/或合并请求:它们作为附加组件很常见。 但它们都不在基础 Git 中。
入门所需了解的内容
Git 是一个分布式版本控制系统或 DVCS。 Git 通过拥有多个存储库来实现其"分布式"效果,Git 称之为克隆。 因此,您需要了解几件事:
- 存储库到底是什么?
- 克隆存储库有什么作用?
- GitHub 分叉有什么特殊的东西是克隆人没有的?
在稍微扩展第一个之后,我们将回到其他两个。我们可以而且应该说的还有很多,但我的空间已经用完了,无论如何都必须把它分开......
一个存储库主要是两个数据库
Git 存储库由两个大型数据库以及许多较小的辅助项组成。 这两个数据库是重要的东西,其中一个通常要大得多,并且总是更重要:
更大/更重要的数据库是 Git 的对象数据库。这将保存 Git提交和其他内部 Git 对象。 这个数据库中的所有内容都有一个OID或对象 ID,我更喜欢称之为哈希 ID(你会看到这两个术语,以及现在已经过时的术语 SHA-1,指的是 Git 用来获取其哈希 ID 的一种特定类型的哈希算法)。
在这个大数据库中,对你来说重要的实体是提交。 Git 存储库可能是——而且可能是,除了我们将在下面看到的烦恼之外——这个充满提交(及其支持对象)的数据库没有任何内容。 因此,您需要确切地知道什么是提交,但我们将在下一节中讨论。
每个对象(因此每次提交)都会获得一个 ID。 特别是提交会获得一个唯一的ID:当您进行新提交时,您将获得一个以前从未在任何地方、宇宙中任何地方的任何 Git存储库中使用过的 ID。 当我进行新提交时,我会得到一个唯一的 ID。 每个人的新提交始终获得一个新ID。 这部分是 Git 的真正魔力,并实现了它的分布式特性,而且在数学上也是不可能的,当然也注定要失败。1幸运的是,提交哈希ID的庞大规模是如此之大,以至于厄运之日可能还有数万亿年,不仅你和我都死了,而且宇宙本身或多或少已经过期。
为了从数据库中捞出提交,Git需要这个哈希 ID。 如果该数据库是存储库中的所有数据库,我们都必须一直记住哈希ID。 所以。。。
另一个通常小得多的数据库保存名称:分支名称、标记名称和所有其他类型的名称。 每个名称都有一个哈希 ID,由于提交的巧妙设计,这就是所需要的,我们稍后会谈到。
Git 将它需要的某些哈希 ID 存储在我们(人类)选择的名称下的名称数据库中。 然后我们(人类)只是给 Git 一个名字,比如一个分支名称,Git 用它来捞出Git需要的丑陋的随机哈希 ID 来获取提交。
因此,存储库由这两个数据库组成:一个充满提交和其他支持对象,另一个包含名称,这样人类就不必记住哈希 ID。
1详见鸽子洞原理。 简单来说,哈希 ID 已经相当均匀地分布在 160 位空间中,这一事实将冲突几率降低到无穷小,但可惜的是,生日问题反过来又出现了丑陋的头,所以一旦你有足够的千万亿次提交,它更像是让您的计算机爆炸的机会,这实际上可能发生。 (好吧,"有点"爆炸。 但在实践中我们是安全的,特别是因为我们可以在大多数时候稍微放松"完全唯一"的约束。 Git 也正在转向 256 位哈希,这将使我们更加安全。
提交中的内容
归根结底,提交是我们使用 Git 的原因。 我们不使用 Git 是因为分支——尽管我们使用称为分支的东西来组织我们的提交,如上所述,我们使用称为分支名称的东西来查找特定的提交(Git 通过混淆地将这个词用于至少三个不同的目的,几乎击败了糟糕的单词"branch",这就是为什么尝试避免裸词分支通常是一个好主意)。 Git 也不是关于文件的,但每个提交都会存储文件,因为虽然提交是 Git 的存储单元,但人类真的很关心单个文件。 我们也喜欢 Git 的各种功能,如合并和挑选等等;但这些功能都取决于提交。 大型数据库存储提交,重要的是提交,至少对 Git 来说是这样。
因此,您需要确切地知道提交是什么以及为您做什么。 您已经知道(像所有 Git 对象一样)它有一个哈希 ID。 还值得一提的是,为了使分布式事物工作,这些哈希 ID永远不会改变,而为了让它工作,Git 说提交中的任何内容都不能改变。如果我们不喜欢某些提交,我们可以改用其他(新的和改进的)提交,但我们实际上无法修复错误的提交。 幸运的是,提交本身便宜得离谱,尽管它们持有:
每个提交都有每个源文件的完整快照(也就是说,Git 在您或任何人进行提交时就知道)。 提交中的文件以特殊的只读、压缩和重复数据删除格式存储,只有 Git 可以读取,实际上没有任何内容(甚至 Git 本身)可以覆盖。
每个提交都会存储一些元数据或有关此特定提交的信息。 例如,这包括提交者的姓名和电子邮件地址,以及一些日期和时间戳。
重复数据消除意味着每个新提交都不需要存储所有文件,即使它存储了所有文件。 特别是,假设您稍微更改了一个文件,并进行了新的提交。 新提交必须存储更新的文件,但可以引用所有未更改的文件。 然后,将同一文件更改回来并再次提交。 第二个新提交是新的,所以它得到了另一个ID,但这次每个文件都是重复的,所以它实际上不需要空间来存储它们。
不过,元数据每次至少略有不同。 例如,每个提交都会获得"now"的时间戳(有一些方法可以调整这一点,但我们在这里不必担心这一点),因此由于时间总是在增加,因此每次提交都会获得不同的时间戳,例如,即使其他所有内容都相同(作者和提交者,快照等)。 这些东西也会被压缩——就像文件一样——所以它可能占用很少的实际空间,这就是为什么提交如此便宜的原因:它们大多只是那些丑陋的大哈希 ID 之一,再加上几个字节用于该特定提交的任何其他独特内容(包括我们将要看到的另一个哈希 ID)。
对于此元数据,Git 添加了自己的内容:每个提交在其元数据中存储以前的提交哈希 ID 列表。 大多数提交只存储一个哈希 ID。 Git 将其称为新提交的父级,而该父级是我们在进行新提交时使用的提交。
当某物存储提交的哈希 ID 时,我们说某物指向提交。 我们可以将其绘制为指向提交的箭头。 因此,假设我们有一个很小的三提交存储库。 三个提交中的每一个都有一些丑陋的唯一哈希ID,我们不会尝试记住或发音或任何东西:相反,我们将第一个称为"提交A
",第二个"提交B
",第三个"提交C
"。 让我们画出来:
A <-B <-C
在这里,提交C
指向其父提交B
,这是我们进行C
时的当前提交。B
依次指向其父A
。 但是A
是第一次提交:在A
之前没有提交,所以它的父哈希ID列表是空的,它没有指向任何地方。
现在,Git需要C
的哈希 ID 才能找到提交C
。 但正如我们之前所说,Git 将有一个分支名称来保存该哈希 ID。 名称将指向C
,如下所示:
A--B--C <-- main
(假设分支名称为main
)。
名称main
字面上包含最新的提交哈希 ID。 这让 Git 可以快速找到C
。 提交C
保存上一个提交的哈希 ID(指向其父级)B
,而A
又指向 ,它不指向任何地方,并且 -C
,然后B
,然后按该顺序A
-是存储库中的历史记录。
换句话说,历史就是提交;提交就是历史。 提交本身也是完全不可变的,但是我们发现使用分支名称提交,并且这些是可变的,因此,如果我们决定出于某种原因真的讨厌提交C
,我们可以弹出它并进行一个新的改进提交D
,直接指向B
:
C [ejected]
/
A--B--D <-- main
由于没有名称来查找提交C
,它似乎已经消失了;如果我们没有记住哈希ID,提交C
似乎已经改变,人类不记住哈希ID,所以我们在这一点上成功地"重写了历史"。
工作树、索引、当前分支和添加提交
在我们继续讨论分布式版本控制之前,让我们提一下其他一些事情:
提交是完全只读的。 此外,只有 Git 可以读取这些文件。 为了完成任何工作,我们需要普通的、可读的和可写的文件。 为此,我们将通过切换到某个分支名称来检查提交。 这将选择分支名称指向的最新提交,并将文件从提交复制到工作区。
工作区包含文件的可用版本,是您在 Git 中工作时看到的内容。 Git 称之为你的工作树或简称为你的工作树。这些文件实际上并不在 Git中!它们是从 Git中提取的,但当你处理它们时,它们会偏离 Git 所拥有的东西。
Git 在另一个区域中保留每个文件的额外"副本"(以重复数据删除的形式),Git 调用索引或暂存区域,或者(现在很少)缓存。您必须一直
git add
文件的原因是让 Git 更新其分阶段的"副本"。 当我们在下面进行新的提交时,我们将回到这个问题。由于我们可以有多个分支名称,因此 Git 需要一种方法来知道您正在使用哪个名称。 因为 Git 总是有一个当前提交,2Git 还需要一种方法来知道你正在使用哪个提交。 Git 将这两种需求结合为一件事:一个特殊名称,
HEAD
.
特殊名称HEAD
附加到当前分支名称,分支名称又指向当前提交。因此,如果我们有:
A--B--C <-- main (HEAD)
这意味着git status
会说on branch main
,因为HEAD
附加到main
:我们当前的分支名称是main
。 同时我们当前的提交是 commitC
,因为main
指向C
。
让我们做第二个名字,develop
. Git 中的分支名称必须指向某个现有提交。 我们只有三个,所以我们必须从这三个中选择一个来develop
指向它。 Git 的默认值是指向当前提交,根据定义,该提交也是当前分支的最新提交。所以我们会得到这个:
A--B--C <-- develop, main (HEAD)
我们现在有两个提交C
的名称。 提交A-B-C
位于两个分支上(同时提交)。 我们现在的名字仍然是main
.
如果我们现在运行git switch develop
或git checkout develop
,Git 将从我们的工作区(及其索引)中删除main
找到的所有已提交C
文件,并从我们要移动到的提交中交换所有文件,如develop
找到的那样,即提交C
。3最终结果是这样的:
A--B--C <-- develop (HEAD), main
我们现在可以做一些工作,像往常一样git add
和git commit
。 提交命令将:
- 收集任何必要的元数据(例如
user.name
和user.email
); - 找出当前提交的哈希 ID(无论
C
到底是什么); - 获取日志消息以放入新提交;
- 从 Git 的索引/暂存区中的任何内容制作快照(这就是您必须
git add
的原因);和 - 将所有这些转换为新的提交,我们将其称为提交
D
。
新提交将指向当前提交C
:
A--B--C
D
但现在我们有了git commit
的聪明部分:Git 将新提交的哈希 ID塞进当前名称中。 所以HEAD
所附的分支名称develop
现在指向D
。 分支名称main
是单独保留的,因此它仍然指向C
:
A--B--C <-- main
D <-- develop (HEAD)
如果我们切换回main
,我们会得到:
A--B--C <-- main (HEAD)
D <-- develop
Git 从我们的工作树及其索引/暂存区中删除了提交D
文件,改为放入提交C
文件,让我们开始工作。 如果我们现在进行新的提交E
我们会得到这个:
E <-- main (HEAD)
/
A--B--C
D <-- develop
现在您可以看到"分支"的实际效果。 提交A-B-C
仍然在两个分支上,但提交D
仅在develop
上,E
仅在main
上,至少目前是这样。
由于分支名称会移动,因此任何给定分支的提交集都会随着名称的移动而更改。 而且,由于我们可以随意创建和销毁分支名称,因此包含任何给定提交的分支集也会更改。永远不会改变的是提交本身。 真正改变的是我们发现的一组提交,从最后一个提交开始,如名称所示,并向后工作。
我们一会儿要做的是操纵名字。
2在一个新的完全空的存储库中,此规则有一个例外:这里根本没有提交,因此当前也不能提交。 这个例外是我们最初在没有父级的情况下获得提交A
的方式。 您可以重新创建特殊情况以进行其他根提交,但我们不会在这里介绍这一点。
3这看起来真的很愚蠢:为什么要删除并用它们替换文件? 而且它是愚蠢的,在这种情况下 Git不会这样做。也就是说,Git 很聪明地知道哪些文件需要删除和替换,并且只做需要它的文件。 如果你像这样"从C
移动到C
",没有文件需要它,Git 根本不会费心做任何事情。 这在以后变得很重要,但是如果您开始将其视为"将旧提交的每个文件换成新提交中的每个文件",那么您的状态会好得多:您可以稍后在脑海中添加优化。
克隆和分叉
Git 有克隆的概念:我们运行:
git clone <url>
并得到一些东西的副本。 但是我们到底复制了什么? 整个过程首先让 Git 为我们制作一个新的空存储库,所以我们有一个新的空 Git 对象数据库和一个新的空名称数据库(还有一个空的工作树和索引/暂存区)。 但是 Git 立即使用提供的 URL 联系,向应该更多的 Git 软件拨打"互联网电话"。 该软件应答"调用"并查找一些现有的Git存储库:一个充满提交和其他对象的数据库,以及一个名称数据库。
git clone
命令让其他Git 软件列出其名称。 因此,我们的 Git 可以看到他们的分支、标签和其他名称。 现在我们的 Git 对这些名字做了一些有趣的事情,我们稍后会回到这里;但是这些名称中的每一个也带有一个哈希 ID,至少对于分支名称,这些表示最新的提交,如在其他 Git 存储库对象数据库中找到的那样。
在这一点上,我们的 Git 将从他们的 Git 中获得他们拥有的我们没有的每一次提交。 (这里涉及一堆协议,允许我们制作单分支或浅克隆,我们将忽略这些克隆,以及我们也会忽略的其他一些特殊情况,以保持简单。 我们当然有一个完全空的数据库,所以这就是每次提交。 因此,他们打包了每个提交(以及所有必要的支持对象)并将它们交付过来,我们的 Git 将它们解压缩到我们的大数据库中。
我们现在有他们所有的提交,但没有分支。 现在我们的 Git 做了一件有趣的事情:对于他们的每个分支名称,我们的 Git 将此名称更改为远程跟踪名称。 (我们的 Git 保持他们的标签名称不变,所以如果他们有一个v1.0
,我们也得到一个v1.0
标签,至少默认情况下是这样;同样,有一些控制旋钮,但我们在这里忽略它们。 这些远程跟踪名称的工作方式很像分支名称,但它们是我们的 Git对其 Git分支名称的记忆。 它们实际上根本不是分支名称。
所以,如果他们有一个main
和一个develop
,例如,我们会得到一个origin/main
和origin/develop
. 我们的 Git 通过粘贴origin
(远程或另一个 Git 存储库的短名称,保存 URL)以及在其每个分支名称前面的斜杠来制作远程跟踪名称。四
在这个特定过程结束时,我们有这个:
- 我们的提交和对象数据库包含它们拥有的每个提交;
- 我们的名称数据库没有分支,只有一堆远程跟踪名称。
Git 完全能够以这种方式运行——Git并不真正需要分支名称——但对于普通人来说,以这种方式工作太烦人了,所以现在git clone
采取最后两个步骤:
- 它创建一个分支名称,然后
- 它检查出该分支,以便这是我们当前的分支并提交。
git clone
在此处创建的分支的名称是我们在命令行上为它命名的名称,当我们运行git clone -bbranchurl
. 但我们可能根本没有和-b
一起跑。 在这种情况下,我们的 Git 软件会询问他们的 Git 软件他们推荐哪个分支名称,这必须是他们的分支名称之一,因此也是我们的远程跟踪名称之一。 然后我们的 Git 假装这就是我们对-b
的要求。
我们的 Git 现在将在我们的存储库中创建一个分支名称,从-b
或隐含的-b
. 此分支将选择的提交与我们相应的远程跟踪名称选择的提交相同,这是其同名分支选择的提交。 如果这似乎是做一件简单事情的一种非常迂回的方式,那么,这就是适合你的 Git。
我们最终得到一个分支名称,但它是我们的分支,而不是他们的分支。 它只是拼写与他们的分支名称之一相同。 这整个概念——仅仅因为两个分支名称拼写相同,并不意味着它们相同——在一瞬间变得非常重要。
但是GitHub的绿色大FORK按钮呢? 这是做什么的? 好吧,分叉只是一个 GitHub 端克隆,有两个区别和一些附加功能:
- GitHub 上没有索引和工作树。 您将不得不克隆您的分叉,以便您可以完成工作。
- 他们将所有原始仓库的分支名称复制到分支中的分支名称:没有远程跟踪名称这样的东西。
添加的功能包括发出拉取请求的能力(以及问题和代码审查等所有常见的 GitHub 功能)。 您在 GitHub 上的新克隆大部分永久链接到 GitHub 上的原始存储库。5GitHub 还在幕后做了一些偷偷摸摸/聪明的技巧,在这里为自己节省了大量的磁盘空间:这使得 GitHub 的分叉操作相对便宜6
。4从技术上讲,远程跟踪名称位于单独的命名空间中,因此即使我们在其名称中创建一个带有origin/
的本地分支,Git也不会混淆。 但我们可能会,所以不要那样做。
5这里主要是因为拥有原始存储库的人都可以删除它。 当这种情况发生时,GitHub 会在内部通过链接存储库链传递"分叉所有权":这一切都有点复杂,但用户不必担心,因为 GitHub 在内部处理这一切。
6这也是用户通常不需要关心的事情,但它禁用了 Git 的git gc
机制,最终删除了未使用的提交。 这意味着,如果您不小心将包含敏感数据的提交推送到 GitHub,您必须让 GitHub管理员帮助删除它:您无法自己解决此问题。 即使他们改变了这一点,与他们联系仍然是一个好主意:gc 删除不需要/不需要的 Git 对象不可避免地会有延迟,像 GitHub 这样的大型托管网站会安排这种情况不经常发生以保持自己的负载更轻。
更新克隆
现在我们有了所有这些克隆,我们需要看看 Git 提供的更新它们的机制。 实际上只有两个:
git fetch
让你的 Git 调用另一个 Git 并从他们那里获取东西。git push
让你的 Git 调用另一个 Git 并向他们提供东西。
git pull
命令,我建议新手最初避免使用,只是意味着运行git fetch
,然后运行第二个 Git 命令来利用我们得到的东西。 最初避免这种情况的原因是精确地学习如何使用各种第二命令选项,包括可能出错的内容以及如何从中恢复。 (之后,您可能会发现想要在两者之间插入命令的情况,并且仍然避免git pull
,和/或者您可能会发现二合一命令的便利性对您来说很方便git pull
然后您可以安全地使用它。 因此,即使有了git pull
我们仍然只有两个操作,获取和推送。
这两个操作是不同的。 这不仅仅是转移的方向,尽管显然这很重要:
git fetch
获取内容并将其添加到您的存储库中。 但它也适用于那些远程跟踪名称。 当您从调用origin
的 Git 获取内容时,您的 Git 软件更新其存储库分支名称的存储库内存。 因此,您的远程跟踪名称会更新。成功
git fetch
后,您通常需要做一些事情来利用获取的提交。 (这就是git pull
存在的原因:Linus Torvalds最初似乎认为每个人都想立即这样做。git pull
命令是唯一面向用户的"获取内容"命令,并且没有远程跟踪名称。 没有遥控器! 这一切都被证明是一个坏主意,远程和远程跟踪名称被发明出来,但现在我们有一个尴尬的局面,fetch
是push
的对立面,pull
是压倒性的两件事命令。git push
发送内容并(尝试)将其添加到他们的存储库中。 但:- 他们可能会拒绝添加它。
- 如果他们确实添加了它,则没有等效的远程跟踪名称:您告诉他们将提交添加到他们的分支或分支,他们这样做了,现在他们的分支添加了新的提交。 没有"现在将它们组合起来"的步骤。您必须预先组合所有内容。
这里还有很多需要了解的内容,但现在我们将在这一点上停止,因为我们终于有足够的资源来解决您的特定问题。 不过,让我们先做一些回顾,并特别记下我们之前快速完成的一些事情。
"重置"分支名称
假设您的仓库中有以下分支名称和提交:
I--J <-- feature1
/
...--G--H <-- main
K--L <-- feature2 (HEAD)
也就是说,您一直在开发两个功能。 你为feature1
做了两次提交,两次提交都是在main
最后一次提交之后进行的。 假设它们都很好,你想保留它们。 但是后来你在feature2
上做了两次提交,也是在main
的最后一次提交之后,你一直在测试提交L
,发现它很糟糕。 所以你想摆脱它。
我们之前提到过,我们可以从链的末端启动提交,但当时我们展示了它的替代品。 现在让我们看看启动提交背后的机制。 我们需要做的是将名称feature2
指向提交K
而不是提交L
。 这将"放弃"提交L
:它仍将在大型全对象数据库中,但由于我们通过从末尾开始并向后工作来找到提交,因此feature2
K
而不是L
的"最后一次"提交使其看起来好像L
实际上已经消失了:
I--J <-- feature1
/
...--G--H <-- main
K <-- feature2 (HEAD)
L [abandoned]
我们如何做到这一点? 在 Git 中,我们使用git reset
来调整当前的分支名称。
git reset
命令又大又复杂:它做了太多的事情。 但是对于我们的特定情况,我们可以在一个简单的模式下使用它,运行:
git reset --hard HEAD~1
--hard
告诉git reset
清除 Git 索引/暂存区域中的内容以及工作树中的内容,即使它移动了分支名称。 这里的HEAD~1
意味着:找到当前提交,然后后退一跳。 我们可以运行git log
并用鼠标获取提交K
的原始哈希 ID,而不是HEAD~1
:
git reset --hard a123456
什么的。 有时使用带有git log
的复制粘贴是在这里的方法;有时相对表达式(如HEAD~1
或HEAD^
)更容易;但无论哪种方式,关键概念是这样的:git reset
使当前分支名称指向我们选择的任何提交。我们只需选择一些提交,通过找到它的任何名称,并将其交给git reset
,git reset
使当前名称(HEAD
附加到的名称)指向该提交。
(要撤消"错误"git reset
,我们只需在此处运行git reset --hardhash-of-L
,但是这样做,我们必须能够找到提交L
的哈希值。 如果它在您的屏幕上,您可以使用复制粘贴。 如果没有,你会从哪里得到它? Git 有很多方法可以让这些恢复一段时间,所以这并非不可能,只是很难而且很烦人。 请注意,--hard
意味着通过用新选择的提交中的文件覆盖所有文件来清除我的工作树。 由于您的工作树文件不在 Git 中,Git将无法帮助您取回它们。 在使用--hard
之前,请非常确定这一点:运行git status
很多!
我们还可以移动不是当前分支的分支名称。 假设,在弹出L
后,我们意识到提交J
也是不好的。 我们可以运行:
git checkout feature1
git reset --hard HEAD~1
弹出J
,但我们也可以运行:
git branch -f feature1 feature1~1
git branch
命令与-f
或--force
选项一起使用时,可以将当前分支以外的任何分支名称移动到任何提交,就像git reset
将当前分支移动到任何提交一样。 (--force
选项是必需的,这样当您创建新分支名称,但有拼写错误或 brain-o 或其他什么时,您不会意外移动现有分支名称。