Git fork 总是提前提交我不想要
Git fork always commits ahead I don't want
我有一个来自原始仓库 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 同步。很高兴丢失任何本地工作等。
第 1 部分(共 2 部分)()
您已经进入了 somewhat-advanced 设置。这里有 三个 Git 个存储库,而不是 两个 ,GitHub“forks”是具有某些特殊属性的克隆。 (请注意,普通 Git 没有分叉和拉取请求——这些是 GitHub add-ons。其他托管站点也有分叉 add-ons and/or 拉取请求 and/or 合并请求:它们和 add-ons 一样很常见。但是 none 是在基础 Git 中。)
您需要了解的入门知识
Git 是一个 分布式版本控制系统 或 DVCS。 Git 通过多个 存储库 实现其“分布式”效果,Git 调用 克隆 。所以你需要知道几件事:
- 存储库到底是什么?
- 克隆存储库有什么作用?
- GitHub 分支有哪些克隆没有的特殊之处?
我们将在稍微扩展第一个之后再回到其他两个。我们 可以而且应该 说的还有很多,但我 运行 超出了 space 并且无论如何都必须将其分开...
一个存储库主要是两个数据库
一个Git 存储库 由两个大数据库以及许多较小的辅助项目组成。两个数据库是最重要的东西,其中一个通常更大,而且总是更重要:
更大的/more-important数据库是Git的object数据库。这包含 Git 提交 和其他内部 Git objects。此数据库中的所有内容都有一个 OID 或 Object ID,我更喜欢将其称为 hash ID(您会看到这两个术语,加上 now-outdated 术语 SHA-1,指的是 Git 用于获取其哈希 ID 的一种特定哈希算法)。
在这个大数据库中你的重要实体是提交。 Git 存储库可能是——而且可能是,除了我们将在下面看到的烦人之处——与这个充满提交的数据库(加上它们的支持 objects)无关。因此,您需要确切地知道什么是提交,但我们将把它留到下一节。
每个 object——因此每次提交——都会获得一个 ID。特别是提交会得到一个 unique ID:当你进行新的提交时,你会得到一个以前从未在任何地方使用过的 ID,在 any Git 宇宙中任何地方的存储库。当我进行新提交时,I 获得一个唯一 ID。每个人的新提交总是获得一个 new ID。这部分是 Git 的真正魔力并启用了它的分布式特性,它在数学上也是不可能的,而且肯定是注定要失败的。1 幸运的是,庞大的 size 的提交哈希 ID 如此之大,以至于世界末日可能会在数万亿年后才到来,很久之后不仅你我都死了,而且宇宙本身也或多或少地过期了。
为了获取数据库的提交out,Git需要这个哈希ID。如果那个数据库就是存储库中的全部,那么我们都必须一直记住哈希 ID。所以...
另一个通常小得多的数据库包含 names:b运行ch 名称、标签名称和所有其他类型的名称。每个名称都包含 一个 哈希 ID,这就是提交的巧妙设计所需要的全部内容,我们稍后会讲到。
Git 将它需要的某些哈希 ID 存储到 names 数据库中,使用我们(人类)选择的名称。然后我们(人类)只要提供Git一个名字,比如一个b运行ch名字,然后Git用它来捞出大丑random-looking hash ID Git需要,获取commit.
所以一个存储库由这两个数据库组成:一个完整的提交和其他支持 objects,一个有名字,这样人们就不必记住哈希 ID。
1见pigeonhole principle for details. On a simple basis, the fact that the hash ID is already spread pretty evenly across a 160-bit space reduces the collision chance to infinitesimal, but alas, the birthday problem rears its ugly head in turn, so once you have enough quadrillions of commits, it's more like the chance of having your computer explode, which actually can happen。 (好的,“有点”爆炸。)但在实践中我们是安全的,特别是因为我们可以在大多数时候稍微放松“完全独特”的约束。 Git 也正在转向 256 位哈希,这将使我们更加安全。
提交内容
提交 最终是我们使用 Git 的原因。我们不使用 Git 因为 b运行ching——尽管我们使用称为 b运行ches 的东西来 组织 我们的提交,并且如前所述上面,我们使用名为 b运行ch names 的东西来 找到一个特定的提交 (并且 Git 打败了这个可怜的词"b运行ch" 混淆地使用单词 f 几乎要死r 至少三个不同的目的,这就是为什么尽量避免使用裸词 b运行ch 通常是个好主意。 Git 也与 files 无关,但每次提交都会 store 文件,因为 while 提交是 Git的 unit-of-storage,人类真的很关心单个文件。我们也喜欢 Git 的各种功能,如合并和 cherry-picking 等等;但这些功能都取决于提交。大数据库 stores 提交,而 commits 才是最重要的,至少对 Git.
因此,您需要确切地了解提交是什么以及对您有何作用。您已经知道(像所有 Git object 一样)它有一个哈希 ID。还值得一提的是,为了使分布式事物正常工作,这些哈希 ID 可以 永远不会改变 ,并且要使 that 正常工作,Git 说 in 提交也不会 be 改变。如果我们不喜欢某些提交,我们可以使用代替进行其他(新的和改进的)提交,但实际上我们不能修复坏的。幸运的是,提交本身便宜得离谱,尽管它们持有:
每个提交都有每个源文件的完整快照(Git 在您或任何人进行该提交时知道, 那是)。 在提交中的文件以特殊的read-only压缩和de-duplicated格式存储,只有Git 可以读取,实际上没有任何东西——甚至 Git 本身也不能——可以覆盖。
每个提交都会存储一些元数据,或者关于这个特定提交的信息。例如,这包括提交人的姓名和电子邮件地址,以及一些 date-and-time 邮票。
de-duplication意味着每次新提交不需要存储所有文件,即使它存储了所有文件。特别是,假设您稍微更改了一个文件,并进行了新的提交。新的提交必须存储更新的文件,但可以引用所有未更改的文件。然后您将同一个文件 改回 并再次提交。第二个新提交是新的,所以它有另一个 ID,但是这次 每个文件都是重复的 所以它确实需要 no space 来存储它们。
元数据至少每次都略有不同。例如,每个提交都有一个“现在”的时间戳(有一些方法可以调整这个但我们不要在这里担心这个),所以由于时间总是在增加,每个提交都会得到一个不同的 time-stamp 例如,即使如果其他一切都相同(作者和提交者、快照等)。这些东西也被压缩了——就像文件一样——所以它可能需要很少的实际 space,这就是为什么提交如此便宜的原因:它们 大部分 只是其中之一丑陋的大哈希 ID,加上一些字节,用于表示该特定提交的其他独特之处(包括我们即将看到的另一个哈希 ID)。
对于这个元数据,Git 添加了它自己的东西:每个提交在其元数据中存储一个以前提交哈希 ID 的列表。大多数提交只存储一个哈希 ID。 Git 将此称为新提交的 parent,那个 parent 是我们 制作时使用的提交 那个新提交。
当 something 存储提交的哈希 ID 时,我们说 something 指向 提交。我们可以把它画成一个箭头,指向提交。假设我们有一个很小的 three-commit 存储库。这三个提交中的每一个都有一些丑陋的唯一哈希 ID,我们不会尝试记住或发音或任何其他内容:相反,我们将第一个称为“commit A
”,第二个称为“commit [=28” =]”,第三个“commit C
”。让我们把它们画出来:
A <-B <-C
这里,commit C
指向它的parent commit B
,也就是我们做[=29=时的当前commit ]. B
又指向其 parent A
。但是 A
是第一个提交:在 A
之前没有提交,所以它的 parent 哈希 ID 列表是空的,它没有指向任何地方。
现在,Git 需要 C
的哈希 ID,以便 找到 提交 C
.但正如我们之前所说,Git 将有一个 b运行ch name 保存该哈希 ID。名字会指向C
,像这样:
A--B--C <-- main
(假设b运行ch名称是main
).
名称main
字面上包含最新提交哈希ID。这让 Git 可以快速找到 C
。 Commit C
持有前一次提交的哈希 ID(指向它的 parent)B
,后者又指向 A
,它没有指向任何地方,并且—按顺序依次为 C
、B
、A
— 是 存储库中的历史记录瑞.
历史,换句话说,就是提交;提交是历史。提交本身也是完全不可变的,但是我们 find 使用 b运行ch name 的提交,那些 是可变的,所以如果我们决定因为某种原因我们真的讨厌提交C
,我们可以弹出它并做一个new-improved提交D
直接指向后面到 B
:
C [ejected]
/
A--B--D <-- main
没有 name find 提交 C
,它似乎已经消失了;如果我们没有记住哈希 ID,提交 C
将 看起来 已经改变,人类不会记住哈希 ID,所以我们已经成功地“重写了历史”点.
工作树、索引、当前 b运行ch 和添加提交
在我们继续讨论分布式版本控制之前,让我们提一下其他一些事情:
提交 完全是read-only。而且,只有Git可以读取这些文件。要完成任何工作,我们需要普通的、可读的 和可写的 文件。为此,我们将通过 切换到 一些 b运行ch 名称来 检查 提交。这 select 是 最近的 提交,因为 pointed-to 通过 b运行ch 名称,并且 将文件复制出提交到工作区。
工作区域,其中包含文件的可用版本,是您在 Git 中工作时所看到的。 Git 将其称为您的 工作树 或简称 work-tree。 这些文件实际上并不是 在 Git! 它们是从 Git,但是当你处理它们时,它们会偏离 Git 的内容。
Git 在 Git 以不同方式调用 index,或暂存区,或者——现在很少见——缓存。您必须一直 git add
文件的原因是让 Git 更新其暂存的“副本”。当我们在下面进行新的提交时,我们会回到这个问题。
因为我们可以有多个 b运行ch name, Git 需要一种方法来知道哪个 name 你正在使用。因为 Git 总是有一个 当前提交 ,2 Git 也需要一种方法来知道哪个 commit 你正在使用。 Git 将这两个需求合二为一:一个特殊的名字,HEAD
.
特殊名称 HEAD
附加到 当前 b运行ch 名称,并且 b 运行ch 名称依次指向当前提交。所以如果我们有:
A--B--C <-- main (HEAD)
这意味着git status
会说on branch main
,因为HEAD
附加到main
:我们当前的b运行ch姓名 是main
。同时我们当前的 commit 是 commit C
,因为 main
指向 C
.
让我们起第二个名字,develop
。 Git 中的 b运行ch 名称必须指向某个现有提交。我们只有这三个,所以我们必须从这三个中选择一个让 develop
指向它。 Git 的默认值是指向 当前 提交,根据定义 ,它也是 最新的 提交当前 b运行ch。所以我们会得到这个:
A--B--C <-- develop, main (HEAD)
我们现在有两个提交名称 C
。提交 A-B-C
在 both b运行ches 上(同时)。我们的现在的名字还是main
.
如果我们现在 运行 git switch develop
或 git checkout develop
,Git 将从我们的工作区(及其索引)中删除来自 C
由 main
找到,并交换我们要移动到由 develop
找到的提交的所有文件,即提交 C
.3 最终结果是这样的:
A--B--C <-- develop (HEAD), main
我们现在可以做一些工作了,git add
和 git commit
和往常一样。提交命令将:
- 收集任何必要的元数据(例如
user.name
和 user.email
);
- 找出当前提交的哈希 ID(无论
C
是什么);
- 获取要放入新提交的日志消息;
- 从 Git 的索引 / staging-area 中的任何内容制作快照(这就是为什么你必须
git add
);和
- 将所有这些变成一个新的提交,我们称之为提交
D
。
新提交将指向当前提交C
:
A--B--C
\
D
但现在我们有了 git commit
的 聪明 部分:Git 推送新提交的 哈希 ID进入当前名称。因此 HEAD
所附加的 b运行ch 名称 develop
现在指向 D
。 B运行ch name main
被保留,所以它仍然指向 C
:
A--B--C <-- main
\
D <-- develop (HEAD)
如果我们切换回 main
,我们得到:
A--B--C <-- main (HEAD)
\
D <-- develop
Git 删除提交来自我们工作树的 D
文件及其 index/staging-area,改为放入 commit-C
文件,让我们开始工作。如果我们现在进行新的提交 E
我们会得到这个:
E <-- main (HEAD)
/
A--B--C
\
D <-- develop
现在您可以看到正在运行的“b运行ching”。提交 A-B-C
仍然在两个 b运行 上,但是提交 D
仅在 develop
上并且 E
仅在 main
上,至少目前
自从 b运行ch 名字移动后,任何给定 b运行ch 上的提交集合随着名字移动而改变。而且,由于我们可以随意创建和销毁 b运行ch 名称,b运行ches 的集合 contain 任何给定的提交更改也是如此。 永远不变的是提交本身。真正改变的是 我们发现 的一组提交,从 最后一个 开始,如名称所示,并向后工作。
我们稍后要做的是处理名称。
2这个规则有一个例外,在一个新的完全空的存储库中:这里根本没有提交,所以也不可能有当前提交。这个异常是我们如何在没有 parent 的情况下获得提交 A
的。您可以 re-create 在特殊情况下进行额外的 root 提交 但我们不打算在这里介绍。
3这看起来真的很愚蠢:为什么要用自己删除和替换文件?它是愚蠢的,Git 在这种情况下 不这样做 。也就是说,Git 很聪明地知道哪些文件需要删除和替换,并且只执行那些 需要 的文件。如果你像这样“从 C
移动到 C
”, 没有 文件需要它,并且 Git 不会费心做任何事情全部。这在以后变得很重要,但是如果您开始时将其视为“将旧提交中的每个文件换成新提交中的文件”,您的状态就会好得多:您稍后可以在脑海中添加优化。
克隆和分支
Git有克隆的概念:我们运行:
git clone <url>
并得到一份 副本 的东西。但我们究竟复制了什么?整个过程从 Git 为我们创建一个新的空存储库开始,所以我们有一个新的空 Git-objects 数据库和一个新的空 names-database (还有一个空的工作树和索引/staging-area)。但是 Git 立即伸出手,使用提供的 URL,对应该更 Git 的软件进行“Internet phone 呼叫”。该软件响应“呼叫”并查找一些现有的 Git 存储库:一个充满 commits-and-other-objects 的数据库以及一个名称数据库。
git clone
命令让其他Git软件列出了他们的名字。因此,我们的 Git 可以看到他们的 b运行ch 和标签以及其他名称。现在我们的 Git 对这些名字做了一些有趣的事情,我们稍后会回过头来;但是这些名称中的每一个都带有一个哈希 ID,至少对于 b运行ch 名称,它们代表 latest 提交,在另一个 Git 存储库 object 的数据库中找到。
在这一点上,我们的 Git 将从他们的 Git 中获得我们没有的每一次提交。 (这里涉及到一堆协议,允许我们制作一个 single-branch 或浅层克隆,我们将忽略这些,以及我们也将忽略的一些其他特殊情况,以保持简单。)我们的当然有一个 totally-empty 数据库,所以这是 每次提交 。所以他们打包每个提交(加上所有必要的支持 objects)并将它们发送出去,我们的 Git 将它们解压缩到我们的大数据库中。
我们现在有他们所有的提交,但是没有b运行ches。现在我们的 Git 做了一件有趣的事:对于他们的每个 b运行ch 名字,我们的 Git 改变 这个名字变成了remote-tracking名字。 (我们的 Git 保持他们的标签名称不变,所以如果他们有一个 v1.0
,我们也会得到一个 v1.0
标签,至少在默认情况下是这样;同样,有一些控制旋钮,但我们将在这里忽略它们。)这些 remote-tracking 名称与 b运行ch 名称非常相似,但它们是我们 Git 的 memory 他们 Git 的 b运行ch 名字。它们实际上根本不是 b运行ch 名称。
因此,例如,如果他们有 main
和 develop
,我们将得到 origin/main
和 origin/develop
。我们的 Git 通过粘贴 origin
— 远程 或其他 Git 存储库的短名称来创建 remote-tracking 名称,节省URL——以及他们每个名字前的斜线运行ch。4
在这个特定过程结束时,我们有这个:
- 我们的 commits-and-objects 数据库有他们所有的提交;
- 我们的姓名数据库没有运行ches,只是一堆remote-tracking名字。
Git 完全有能力以这种方式运行——Git 并不真的需要 b 运行ch names——但是对于普通人来说这样工作太烦人了,所以现在 git clone
采取最后两个步骤:
- 它创建一个b运行ch名称,然后
- 它检查出 b运行ch,所以这是我们当前的 b运行ch 并提交。
这里git clone
创建的b运行ch的名字就是我们在命令行给它取的名字,当我们运行git克隆-b<em>b运行ch</em><em>url</em>
。但我们可能根本没有 运行 和 -b
。在这种情况下,我们的Git软件询问他们的Git软件哪个b运行ch名称他们推荐,这必须是他们的b运行ch 名称,因此是我们的 remote-tracking 名称之一。我们的 Git 然后假装那是我们用 -b
.
要求的
我们的 Git 现在将在我们的存储库中创建 one b运行ch 名称,来自 -b
或隐含的 -b
。这个b运行ch会select的commit是samecommit我们对应的remote-trackingnameselects,也就是commit他们 同名 select 的 b运行ch。如果这看起来像是做一件简单事情的非常迂回的方式,那么,那是 Git 适合你的方式。
我们最终得到 one b运行ch name,但它是 our b运行ch,不是他们的。它只是 拼写相同 作为他们的一个 b运行ch 名字。这整个概念——仅仅因为两个 b运行ch 名字拼写相同,并不意味着它们是 同样——一瞬间变得很重要
但是GitHub 的绿色大FORK 按钮呢?那有什么作用?好吧,fork 只是 GitHub 端的克隆,有两个区别和一些附加功能:
- GitHub 上没有索引和工作树。您将不得不克隆您的分叉,以便完成工作。
- 他们复制 all 原始存储库的 b运行ch 名称到 b运行ch 名称fork: 没有 remote-tracking 名称这样的东西。
添加的功能包括提出拉取请求的能力(加上所有常见的 Git问题中心功能和代码审查等)。 GitHub 上的新克隆 mostly-permanently 链接到 GitHub 上的原始存储库。5 GitHub 也可以一些偷偷摸摸的/聪明的幕后技巧在这里节省了大量磁盘 space:这使得 GitHub 分叉操作对于 GitHub 来说相对便宜。 6
4从技术上讲,remote-tracking 名称在一个单独的 namespace 中,所以即使我们使用本地 b运行ch origin/
在它的名字里,Git不会混淆。但是我们可能会,所以不要那样做。
5这里的主要只是因为拥有原始存储库的人都有可能删除它。发生这种情况时,GitHub 在内部将“分叉所有权”传递给链接存储库链:这一切都有点复杂,但用户不必担心,因为 GitHub 会处理这一切内部。
6这也是用户通常不需要关心的事情,但它禁用了 Git 的 git gc
机制,最终删除未使用的提交。这意味着,如果您不小心将包含敏感数据的提交推送到 GitHub,您 必须 让 GitHub 管理员帮助删除它:您无法修复此问题你自己。即使他们改变了这一点,联系他们仍然是个好主意:在 gc 删除不需要的/不需要的 Git objects 和像 [=890 这样的大型托管网站时,不可避免地会有延迟=]Hub 会安排这种情况不经常发生,以减轻他们自己的负担。
正在更新克隆
现在我们有了所有这些克隆,我们需要看看 Git 提供的更新它们的机制。真的只有两个:
git fetch
让你的 Git 打电话给另一个 Git 并 从他们那里得到东西.
git push
让你的 Git 召集另一个 Git 并 向他们提供东西。
git pull
命令,我建议新手一开始避免使用,只是表示运行git fetch
,然后运行秒Git命令来使用我们得到的东西。最初避免这种情况的原因是为了准确学习如何使用各种 second-command 选项,包括可能出错的地方以及如何从中恢复。 (在那之后,您可能会发现需要在 之间插入命令的情况二,并且仍然避免git pull
,and/or你可能会发现方便的git pull
two-in-one命令对你来说很方便,你可以安全地使用它。)所以即使有git pull
我们仍然只有两个操作,获取和推送。
这两个操作是不同的。这不仅仅是 t运行sfer 的方向,尽管显然这很重要:
git fetch
获取 内容并将其添加到您的存储库。但它也适用于那些 remote-tracking 名称。当你从你调用 origin
的 Git 获取东西时,你的 Git 软件更新你的存储库内存 他们的 存储库的 b运行ch 名称。所以你的 remote-tracking 名字得到更新。
在成功 git fetch
之后,您通常会想做一些事情来 利用 获取的提交。 (这就是 git pull
存在的原因:Linus Torvalds 最初似乎假设每个人都会 总是 想要 马上 这样做。git pull
命令是唯一的 user-oriented“get stuff”命令,没有 remote-tracking 名称。没有遥控器!结果证明这是个坏主意,遥控器和 remote-tracking 名字被发明了,但现在我们有一个尴尬的情况,即 fetch
与 push
相反,pull
是被压倒的 does-two-things 命令。)
git push
发送 内容并(尝试)将其添加到他们的 存储库。但是:
- 他们可能会拒绝添加。
- 如果他们 添加它,则没有 remote-tracking 名称的等效项:您告诉他们将提交添加到他们的 b运行ch 或 b 运行ches,他们这样做了,现在 他们的 b运行ch 添加了新的提交。没有“现在合并它们”的步骤。 你一定拥有一切pre-combined.
这里还有很多东西需要了解,但现在我们就此打住,因为我们终于有足够的知识来解决您的特定问题。不过,让我们先回顾一下,并特别记下我们之前快速完成的事情。
“重置”a b运行频道名称
假设您的存储库中有这些 b运行ch 名称和提交:
I--J <-- feature1
/
...--G--H <-- main
\
K--L <-- feature2 (HEAD)
也就是说,您一直在研究两个功能。您为 feature1
做了两次提交,这两次提交都紧接在 main
的最后一次提交之后。假设它们都很好并且您想保留它们。但是随后您在 feature2
上进行了两次提交,也是在 main
上最后一次提交之后,并且您一直在测试提交 L
并发现它很糟糕。所以你想摆脱它。
我们之前提到我们可以从链的末尾启动提交,但我们当时显示了一个 替换 。现在让我们看看启动提交背后的机制。我们需要做的是让 name feature2
指向 commit K
而不是 commit L
.这将“放弃”提交 L
:它仍然在大 all-objects 数据库中,但是因为我们 find 从 开始提交结束并向后工作,使 feature2
上的“最后”提交成为 K
而不是 L
使它看起来好像 L
实际上已经消失了:
I--J <-- feature1
/
...--G--H <-- main
\
K <-- feature2 (HEAD)
\
L [abandoned]
我们如何做到这一点?在Git中,我们使用git reset
调整current b运行ch name.
git reset
命令又大又复杂:它做的事情太多了。但是对于我们的特殊情况,我们可以在简单模式下使用它,运行ning:
git reset --hard HEAD~1
--hard
告诉 git reset
清除 Git 索引中的内容 / staging-area 以及我们的工作树中的内容,即使它移动了 b运行通道名称。这里的HEAD~1
的意思是:找到当前提交,然后后退一步。我们可以 运行 git log
代替 HEAD~1
并使用鼠标获取提交 K
的原始哈希 ID:
git reset --hard a123456
或其他什么。有时使用 copy-paste 和 git log
是去这里的方法;有时像 HEAD~1
或 HEAD^
这样的相对表达式更容易;但无论哪种方式,关键概念是:git reset
使 current b运行ch name 指向我们选择的任何提交。 我们只是选择一些提交,通过找到它的任何名称,然后将其交给 git reset
和 git reset
使 当前名称 — HEAD
附加到——指向该提交。
(要撤消“错误”git reset
,我们只需 运行 git reset --hard <em>hash-of-L </em>
这里,但是这样做,我们必须能够 找到 提交 L
的散列。如果它在你的屏幕上,你可以使用 copy-paste。如果不能,你会从哪里得到它?Git 有很多方法可以暂时取回这些东西,所以这并非不可能,只是很难而且很烦人。请注意 Git =149=] 表示 通过覆盖 al 来清除我的工作树来自新 selected commit 的文件。由于您的工作树文件 不在 Git 中,Git 将无法帮助您找回它们。在使用 --hard
之前,非常确定:运行 git status
很多!)
我们还可以移动一个 而不是 当前 b运行ch 的 b运行ch 名称。假设在弹出 L
之后,我们意识到提交 J
也很糟糕。我们可以 运行:
git checkout feature1
git reset --hard HEAD~1
弹出J
,但我们也可以运行:
git branch -f feature1 feature1~1
git branch
命令可以——当与 -f
或 --force
选项一起使用时——移动 任何 b运行ch 名称而不是当前b运行ch 到任何提交,与 git reset
将 current b运行ch 移动到任何提交的方式相同。 (--force
选项是必需的,这样当您创建新的 b运行ch 名称时,您不会不小心移动了现有的 b运行ch 名称,但有错字或brain-o 或其他。)
第 2 部分(共 2 部分)()
git push --force
所以我们现在知道如何在本地移动 b运行ch 名称。现在让我们再看看 git push
,特别是它的 --force
或 -f
选项。我们知道使用 git push
,我们通常使用它将我们的新提交发送到其他一些 Git 存储库。然后我们通常要求其他 Git 存储库 添加提交到他们的 b运行ch 名称之一 。如果我们所做的只是正确地添加提交,并且我们有权限,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 移动 一个 b运行ch 名称,他们都会检查是否可以。如果我们告诉他们起一个 新名称 feature1
,那可能没问题,但假设我们在这里决定让他们设置 main
.他们会回答我们:不!如果我让我的名字 main
指向 J
,我将失去我的 N-O-P
提交!这是一个很大的 NOPe! 记住,他们像每个 Git、find 一样使用 b运行ch 名称提交 找到 last 提交,然后向后工作。 J
导致 I
导致 H
,它不会向前导致 N
,只会向后导致 G
。
这通常是我们喜欢这样的事情的工作方式。我们不会直接推送到他们的 main
,而是推送我们的 feature1
提交并要求他们创建一个名为 new b运行ch 33=] 这样就没问题了。
但是...假设 Git Hub 上的 Git 存储库是 你的 ,并且你有:
...--G--H <-- main (HEAD), origin/main
然后您向 main
和 运行 git push origin main
和 添加了一个错误的提交 I
或一对提交 I-J
他们拿走了它们?现在你有:
...--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 任何他们没有的新提交,即 none—然后要求他们将 main
设置为指向 H
,他们将 拒绝请求 因为那样会 输 从他们的 main
.
提交 I-J
但这正是您想要的。您希望 他们放弃这两个错误的提交。所以实现这一点的方法是使用 --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 没有任何修改(推送到)a b运行ch 的权限概念,但大多数托管服务器——包括 GitHub——也添加 that。
8传入的提交实际上进入了“qua运行tine zone”并且在被接受之前不会迁移到真实的数据库中。此功能来自 来自 GitHub,因为 GitHub 曾经将所有内容都接受到他们的数据库中,然后才拒绝它们,这给 [=234 造成了很大的混乱=]中心。所以现在有了这个奇特的 qua运行tine 功能。
不止一台遥控器
终于,我们有足够的办法解决所有问题。
但第一个技巧是,您必须在笔记本电脑或任何具有本地克隆的地方进行设置,以便拥有 两个 遥控器,而不是只有一个。你运行:
git clone <url>
最初,URL 是给你的叉子的。您的叉子就是您要调整的叉子。我们现在必须为您分叉的存储库添加一个远程。
请记住,远程只是一个包含 URL 的短名称,Git 将使用此短名称组成 remote-tracking 个名称。所以你开始做在这里输入任何你喜欢的名字。标准的 first 名称是 origin
,您已经有了这个。有些人喜欢使用 upstream
作为他们的标准名字。我不太喜欢这个,因为 Git 已经有其他东西叫做 upstream。我会用另一个名字;在这里我会使用一个愚蠢的,但你应该弥补一些明智的:
git remote add lexluthor <url>
为您分叉的存储库插入 URL。然后 运行 git fetch
到那个遥控器:
git fetch lexluthor
现在,在您笔记本电脑的存储库中,他们的所有提交(您可能已经拥有所有这些,在这种情况下,这部分进行得很快)。他们的每个 b运行ch 名称也有 remote-tracking 名称。
现在您只需要说服您的 GitHub 分支 its b运行ch bran
,或 main
或 master
或其他任何内容,应指向 相同的提交 即 bran
或 main
或 master
或任何提交在 lexluthor
:
git push --force lexluthor/master origin/master
就是这样——就是这样。我们向 origin
发送任何我们拥有的 origin
缺少的提交,他们需要更新他们的 (origin
's) b运行ch:那根本不是什么,因为我们是“两个领先”和 none 完全落后。然后我们 命令 Git 在 Git 集线器上让我们的 origin
的 master
识别相同的 commit that our lexluthor/master
identifies,这是 master
identified in the repository originally fork 的 commit。
您可能 也 想要您自己的 master
放弃您前面的两个提交。您可能出于其他原因想要保留这些提交/放置另一个 b运行ch / 其他;为此:
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 给你的!
我有一个来自原始仓库 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 同步。很高兴丢失任何本地工作等。
第 1 部分(共 2 部分)()
您已经进入了 somewhat-advanced 设置。这里有 三个 Git 个存储库,而不是 两个 ,GitHub“forks”是具有某些特殊属性的克隆。 (请注意,普通 Git 没有分叉和拉取请求——这些是 GitHub add-ons。其他托管站点也有分叉 add-ons and/or 拉取请求 and/or 合并请求:它们和 add-ons 一样很常见。但是 none 是在基础 Git 中。)
您需要了解的入门知识
Git 是一个 分布式版本控制系统 或 DVCS。 Git 通过多个 存储库 实现其“分布式”效果,Git 调用 克隆 。所以你需要知道几件事:
- 存储库到底是什么?
- 克隆存储库有什么作用?
- GitHub 分支有哪些克隆没有的特殊之处?
我们将在稍微扩展第一个之后再回到其他两个。我们 可以而且应该 说的还有很多,但我 运行 超出了 space 并且无论如何都必须将其分开...
一个存储库主要是两个数据库
一个Git 存储库 由两个大数据库以及许多较小的辅助项目组成。两个数据库是最重要的东西,其中一个通常更大,而且总是更重要:
更大的/more-important数据库是Git的object数据库。这包含 Git 提交 和其他内部 Git objects。此数据库中的所有内容都有一个 OID 或 Object ID,我更喜欢将其称为 hash ID(您会看到这两个术语,加上 now-outdated 术语 SHA-1,指的是 Git 用于获取其哈希 ID 的一种特定哈希算法)。
在这个大数据库中你的重要实体是提交。 Git 存储库可能是——而且可能是,除了我们将在下面看到的烦人之处——与这个充满提交的数据库(加上它们的支持 objects)无关。因此,您需要确切地知道什么是提交,但我们将把它留到下一节。
每个 object——因此每次提交——都会获得一个 ID。特别是提交会得到一个 unique ID:当你进行新的提交时,你会得到一个以前从未在任何地方使用过的 ID,在 any Git 宇宙中任何地方的存储库。当我进行新提交时,I 获得一个唯一 ID。每个人的新提交总是获得一个 new ID。这部分是 Git 的真正魔力并启用了它的分布式特性,它在数学上也是不可能的,而且肯定是注定要失败的。1 幸运的是,庞大的 size 的提交哈希 ID 如此之大,以至于世界末日可能会在数万亿年后才到来,很久之后不仅你我都死了,而且宇宙本身也或多或少地过期了。
为了获取数据库的提交out,Git需要这个哈希ID。如果那个数据库就是存储库中的全部,那么我们都必须一直记住哈希 ID。所以...
另一个通常小得多的数据库包含 names:b运行ch 名称、标签名称和所有其他类型的名称。每个名称都包含 一个 哈希 ID,这就是提交的巧妙设计所需要的全部内容,我们稍后会讲到。
Git 将它需要的某些哈希 ID 存储到 names 数据库中,使用我们(人类)选择的名称。然后我们(人类)只要提供Git一个名字,比如一个b运行ch名字,然后Git用它来捞出大丑random-looking hash ID Git需要,获取commit.
所以一个存储库由这两个数据库组成:一个完整的提交和其他支持 objects,一个有名字,这样人们就不必记住哈希 ID。
1见pigeonhole principle for details. On a simple basis, the fact that the hash ID is already spread pretty evenly across a 160-bit space reduces the collision chance to infinitesimal, but alas, the birthday problem rears its ugly head in turn, so once you have enough quadrillions of commits, it's more like the chance of having your computer explode, which actually can happen。 (好的,“有点”爆炸。)但在实践中我们是安全的,特别是因为我们可以在大多数时候稍微放松“完全独特”的约束。 Git 也正在转向 256 位哈希,这将使我们更加安全。
提交内容
提交 最终是我们使用 Git 的原因。我们不使用 Git 因为 b运行ching——尽管我们使用称为 b运行ches 的东西来 组织 我们的提交,并且如前所述上面,我们使用名为 b运行ch names 的东西来 找到一个特定的提交 (并且 Git 打败了这个可怜的词"b运行ch" 混淆地使用单词 f 几乎要死r 至少三个不同的目的,这就是为什么尽量避免使用裸词 b运行ch 通常是个好主意。 Git 也与 files 无关,但每次提交都会 store 文件,因为 while 提交是 Git的 unit-of-storage,人类真的很关心单个文件。我们也喜欢 Git 的各种功能,如合并和 cherry-picking 等等;但这些功能都取决于提交。大数据库 stores 提交,而 commits 才是最重要的,至少对 Git.
因此,您需要确切地了解提交是什么以及对您有何作用。您已经知道(像所有 Git object 一样)它有一个哈希 ID。还值得一提的是,为了使分布式事物正常工作,这些哈希 ID 可以 永远不会改变 ,并且要使 that 正常工作,Git 说 in 提交也不会 be 改变。如果我们不喜欢某些提交,我们可以使用代替进行其他(新的和改进的)提交,但实际上我们不能修复坏的。幸运的是,提交本身便宜得离谱,尽管它们持有:
每个提交都有每个源文件的完整快照(Git 在您或任何人进行该提交时知道, 那是)。 在提交中的文件以特殊的read-only压缩和de-duplicated格式存储,只有Git 可以读取,实际上没有任何东西——甚至 Git 本身也不能——可以覆盖。
每个提交都会存储一些元数据,或者关于这个特定提交的信息。例如,这包括提交人的姓名和电子邮件地址,以及一些 date-and-time 邮票。
de-duplication意味着每次新提交不需要存储所有文件,即使它存储了所有文件。特别是,假设您稍微更改了一个文件,并进行了新的提交。新的提交必须存储更新的文件,但可以引用所有未更改的文件。然后您将同一个文件 改回 并再次提交。第二个新提交是新的,所以它有另一个 ID,但是这次 每个文件都是重复的 所以它确实需要 no space 来存储它们。
元数据至少每次都略有不同。例如,每个提交都有一个“现在”的时间戳(有一些方法可以调整这个但我们不要在这里担心这个),所以由于时间总是在增加,每个提交都会得到一个不同的 time-stamp 例如,即使如果其他一切都相同(作者和提交者、快照等)。这些东西也被压缩了——就像文件一样——所以它可能需要很少的实际 space,这就是为什么提交如此便宜的原因:它们 大部分 只是其中之一丑陋的大哈希 ID,加上一些字节,用于表示该特定提交的其他独特之处(包括我们即将看到的另一个哈希 ID)。
对于这个元数据,Git 添加了它自己的东西:每个提交在其元数据中存储一个以前提交哈希 ID 的列表。大多数提交只存储一个哈希 ID。 Git 将此称为新提交的 parent,那个 parent 是我们 制作时使用的提交 那个新提交。
当 something 存储提交的哈希 ID 时,我们说 something 指向 提交。我们可以把它画成一个箭头,指向提交。假设我们有一个很小的 three-commit 存储库。这三个提交中的每一个都有一些丑陋的唯一哈希 ID,我们不会尝试记住或发音或任何其他内容:相反,我们将第一个称为“commit A
”,第二个称为“commit [=28” =]”,第三个“commit C
”。让我们把它们画出来:
A <-B <-C
这里,commit C
指向它的parent commit B
,也就是我们做[=29=时的当前commit ]. B
又指向其 parent A
。但是 A
是第一个提交:在 A
之前没有提交,所以它的 parent 哈希 ID 列表是空的,它没有指向任何地方。
现在,Git 需要 C
的哈希 ID,以便 找到 提交 C
.但正如我们之前所说,Git 将有一个 b运行ch name 保存该哈希 ID。名字会指向C
,像这样:
A--B--C <-- main
(假设b运行ch名称是main
).
名称main
字面上包含最新提交哈希ID。这让 Git 可以快速找到 C
。 Commit C
持有前一次提交的哈希 ID(指向它的 parent)B
,后者又指向 A
,它没有指向任何地方,并且—按顺序依次为 C
、B
、A
— 是 存储库中的历史记录瑞.
历史,换句话说,就是提交;提交是历史。提交本身也是完全不可变的,但是我们 find 使用 b运行ch name 的提交,那些 是可变的,所以如果我们决定因为某种原因我们真的讨厌提交C
,我们可以弹出它并做一个new-improved提交D
直接指向后面到 B
:
C [ejected]
/
A--B--D <-- main
没有 name find 提交 C
,它似乎已经消失了;如果我们没有记住哈希 ID,提交 C
将 看起来 已经改变,人类不会记住哈希 ID,所以我们已经成功地“重写了历史”点.
工作树、索引、当前 b运行ch 和添加提交
在我们继续讨论分布式版本控制之前,让我们提一下其他一些事情:
提交 完全是read-only。而且,只有Git可以读取这些文件。要完成任何工作,我们需要普通的、可读的 和可写的 文件。为此,我们将通过 切换到 一些 b运行ch 名称来 检查 提交。这 select 是 最近的 提交,因为 pointed-to 通过 b运行ch 名称,并且 将文件复制出提交到工作区。
工作区域,其中包含文件的可用版本,是您在 Git 中工作时所看到的。 Git 将其称为您的 工作树 或简称 work-tree。 这些文件实际上并不是 在 Git! 它们是从 Git,但是当你处理它们时,它们会偏离 Git 的内容。
Git 在 Git 以不同方式调用 index,或暂存区,或者——现在很少见——缓存。您必须一直
git add
文件的原因是让 Git 更新其暂存的“副本”。当我们在下面进行新的提交时,我们会回到这个问题。因为我们可以有多个 b运行ch name, Git 需要一种方法来知道哪个 name 你正在使用。因为 Git 总是有一个 当前提交 ,2 Git 也需要一种方法来知道哪个 commit 你正在使用。 Git 将这两个需求合二为一:一个特殊的名字,
HEAD
.
特殊名称 HEAD
附加到 当前 b运行ch 名称,并且 b 运行ch 名称依次指向当前提交。所以如果我们有:
A--B--C <-- main (HEAD)
这意味着git status
会说on branch main
,因为HEAD
附加到main
:我们当前的b运行ch姓名 是main
。同时我们当前的 commit 是 commit C
,因为 main
指向 C
.
让我们起第二个名字,develop
。 Git 中的 b运行ch 名称必须指向某个现有提交。我们只有这三个,所以我们必须从这三个中选择一个让 develop
指向它。 Git 的默认值是指向 当前 提交,根据定义 ,它也是 最新的 提交当前 b运行ch。所以我们会得到这个:
A--B--C <-- develop, main (HEAD)
我们现在有两个提交名称 C
。提交 A-B-C
在 both b运行ches 上(同时)。我们的现在的名字还是main
.
如果我们现在 运行 git switch develop
或 git checkout develop
,Git 将从我们的工作区(及其索引)中删除来自 C
由 main
找到,并交换我们要移动到由 develop
找到的提交的所有文件,即提交 C
.3 最终结果是这样的:
A--B--C <-- develop (HEAD), main
我们现在可以做一些工作了,git add
和 git commit
和往常一样。提交命令将:
- 收集任何必要的元数据(例如
user.name
和user.email
); - 找出当前提交的哈希 ID(无论
C
是什么); - 获取要放入新提交的日志消息;
- 从 Git 的索引 / staging-area 中的任何内容制作快照(这就是为什么你必须
git add
);和 - 将所有这些变成一个新的提交,我们称之为提交
D
。
新提交将指向当前提交C
:
A--B--C
\
D
但现在我们有了 git commit
的 聪明 部分:Git 推送新提交的 哈希 ID进入当前名称。因此 HEAD
所附加的 b运行ch 名称 develop
现在指向 D
。 B运行ch name main
被保留,所以它仍然指向 C
:
A--B--C <-- main
\
D <-- develop (HEAD)
如果我们切换回 main
,我们得到:
A--B--C <-- main (HEAD)
\
D <-- develop
Git 删除提交来自我们工作树的 D
文件及其 index/staging-area,改为放入 commit-C
文件,让我们开始工作。如果我们现在进行新的提交 E
我们会得到这个:
E <-- main (HEAD)
/
A--B--C
\
D <-- develop
现在您可以看到正在运行的“b运行ching”。提交 A-B-C
仍然在两个 b运行 上,但是提交 D
仅在 develop
上并且 E
仅在 main
上,至少目前
自从 b运行ch 名字移动后,任何给定 b运行ch 上的提交集合随着名字移动而改变。而且,由于我们可以随意创建和销毁 b运行ch 名称,b运行ches 的集合 contain 任何给定的提交更改也是如此。 永远不变的是提交本身。真正改变的是 我们发现 的一组提交,从 最后一个 开始,如名称所示,并向后工作。
我们稍后要做的是处理名称。
2这个规则有一个例外,在一个新的完全空的存储库中:这里根本没有提交,所以也不可能有当前提交。这个异常是我们如何在没有 parent 的情况下获得提交 A
的。您可以 re-create 在特殊情况下进行额外的 root 提交 但我们不打算在这里介绍。
3这看起来真的很愚蠢:为什么要用自己删除和替换文件?它是愚蠢的,Git 在这种情况下 不这样做 。也就是说,Git 很聪明地知道哪些文件需要删除和替换,并且只执行那些 需要 的文件。如果你像这样“从 C
移动到 C
”, 没有 文件需要它,并且 Git 不会费心做任何事情全部。这在以后变得很重要,但是如果您开始时将其视为“将旧提交中的每个文件换成新提交中的文件”,您的状态就会好得多:您稍后可以在脑海中添加优化。
克隆和分支
Git有克隆的概念:我们运行:
git clone <url>
并得到一份 副本 的东西。但我们究竟复制了什么?整个过程从 Git 为我们创建一个新的空存储库开始,所以我们有一个新的空 Git-objects 数据库和一个新的空 names-database (还有一个空的工作树和索引/staging-area)。但是 Git 立即伸出手,使用提供的 URL,对应该更 Git 的软件进行“Internet phone 呼叫”。该软件响应“呼叫”并查找一些现有的 Git 存储库:一个充满 commits-and-other-objects 的数据库以及一个名称数据库。
git clone
命令让其他Git软件列出了他们的名字。因此,我们的 Git 可以看到他们的 b运行ch 和标签以及其他名称。现在我们的 Git 对这些名字做了一些有趣的事情,我们稍后会回过头来;但是这些名称中的每一个都带有一个哈希 ID,至少对于 b运行ch 名称,它们代表 latest 提交,在另一个 Git 存储库 object 的数据库中找到。
在这一点上,我们的 Git 将从他们的 Git 中获得我们没有的每一次提交。 (这里涉及到一堆协议,允许我们制作一个 single-branch 或浅层克隆,我们将忽略这些,以及我们也将忽略的一些其他特殊情况,以保持简单。)我们的当然有一个 totally-empty 数据库,所以这是 每次提交 。所以他们打包每个提交(加上所有必要的支持 objects)并将它们发送出去,我们的 Git 将它们解压缩到我们的大数据库中。
我们现在有他们所有的提交,但是没有b运行ches。现在我们的 Git 做了一件有趣的事:对于他们的每个 b运行ch 名字,我们的 Git 改变 这个名字变成了remote-tracking名字。 (我们的 Git 保持他们的标签名称不变,所以如果他们有一个 v1.0
,我们也会得到一个 v1.0
标签,至少在默认情况下是这样;同样,有一些控制旋钮,但我们将在这里忽略它们。)这些 remote-tracking 名称与 b运行ch 名称非常相似,但它们是我们 Git 的 memory 他们 Git 的 b运行ch 名字。它们实际上根本不是 b运行ch 名称。
因此,例如,如果他们有 main
和 develop
,我们将得到 origin/main
和 origin/develop
。我们的 Git 通过粘贴 origin
— 远程 或其他 Git 存储库的短名称来创建 remote-tracking 名称,节省URL——以及他们每个名字前的斜线运行ch。4
在这个特定过程结束时,我们有这个:
- 我们的 commits-and-objects 数据库有他们所有的提交;
- 我们的姓名数据库没有运行ches,只是一堆remote-tracking名字。
Git 完全有能力以这种方式运行——Git 并不真的需要 b 运行ch names——但是对于普通人来说这样工作太烦人了,所以现在 git clone
采取最后两个步骤:
- 它创建一个b运行ch名称,然后
- 它检查出 b运行ch,所以这是我们当前的 b运行ch 并提交。
这里git clone
创建的b运行ch的名字就是我们在命令行给它取的名字,当我们运行git克隆-b<em>b运行ch</em><em>url</em>
。但我们可能根本没有 运行 和 -b
。在这种情况下,我们的Git软件询问他们的Git软件哪个b运行ch名称他们推荐,这必须是他们的b运行ch 名称,因此是我们的 remote-tracking 名称之一。我们的 Git 然后假装那是我们用 -b
.
我们的 Git 现在将在我们的存储库中创建 one b运行ch 名称,来自 -b
或隐含的 -b
。这个b运行ch会select的commit是samecommit我们对应的remote-trackingnameselects,也就是commit他们 同名 select 的 b运行ch。如果这看起来像是做一件简单事情的非常迂回的方式,那么,那是 Git 适合你的方式。
我们最终得到 one b运行ch name,但它是 our b运行ch,不是他们的。它只是 拼写相同 作为他们的一个 b运行ch 名字。这整个概念——仅仅因为两个 b运行ch 名字拼写相同,并不意味着它们是 同样——一瞬间变得很重要
但是GitHub 的绿色大FORK 按钮呢?那有什么作用?好吧,fork 只是 GitHub 端的克隆,有两个区别和一些附加功能:
- GitHub 上没有索引和工作树。您将不得不克隆您的分叉,以便完成工作。
- 他们复制 all 原始存储库的 b运行ch 名称到 b运行ch 名称fork: 没有 remote-tracking 名称这样的东西。
添加的功能包括提出拉取请求的能力(加上所有常见的 Git问题中心功能和代码审查等)。 GitHub 上的新克隆 mostly-permanently 链接到 GitHub 上的原始存储库。5 GitHub 也可以一些偷偷摸摸的/聪明的幕后技巧在这里节省了大量磁盘 space:这使得 GitHub 分叉操作对于 GitHub 来说相对便宜。 6
4从技术上讲,remote-tracking 名称在一个单独的 namespace 中,所以即使我们使用本地 b运行ch origin/
在它的名字里,Git不会混淆。但是我们可能会,所以不要那样做。
5这里的主要只是因为拥有原始存储库的人都有可能删除它。发生这种情况时,GitHub 在内部将“分叉所有权”传递给链接存储库链:这一切都有点复杂,但用户不必担心,因为 GitHub 会处理这一切内部。
6这也是用户通常不需要关心的事情,但它禁用了 Git 的 git gc
机制,最终删除未使用的提交。这意味着,如果您不小心将包含敏感数据的提交推送到 GitHub,您 必须 让 GitHub 管理员帮助删除它:您无法修复此问题你自己。即使他们改变了这一点,联系他们仍然是个好主意:在 gc 删除不需要的/不需要的 Git objects 和像 [=890 这样的大型托管网站时,不可避免地会有延迟=]Hub 会安排这种情况不经常发生,以减轻他们自己的负担。
正在更新克隆
现在我们有了所有这些克隆,我们需要看看 Git 提供的更新它们的机制。真的只有两个:
git fetch
让你的 Git 打电话给另一个 Git 并 从他们那里得到东西.git push
让你的 Git 召集另一个 Git 并 向他们提供东西。
git pull
命令,我建议新手一开始避免使用,只是表示运行git fetch
,然后运行秒Git命令来使用我们得到的东西。最初避免这种情况的原因是为了准确学习如何使用各种 second-command 选项,包括可能出错的地方以及如何从中恢复。 (在那之后,您可能会发现需要在 之间插入命令的情况二,并且仍然避免git pull
,and/or你可能会发现方便的git pull
two-in-one命令对你来说很方便,你可以安全地使用它。)所以即使有git pull
我们仍然只有两个操作,获取和推送。
这两个操作是不同的。这不仅仅是 t运行sfer 的方向,尽管显然这很重要:
git fetch
获取 内容并将其添加到您的存储库。但它也适用于那些 remote-tracking 名称。当你从你调用origin
的 Git 获取东西时,你的 Git 软件更新你的存储库内存 他们的 存储库的 b运行ch 名称。所以你的 remote-tracking 名字得到更新。在成功
git fetch
之后,您通常会想做一些事情来 利用 获取的提交。 (这就是git pull
存在的原因:Linus Torvalds 最初似乎假设每个人都会 总是 想要 马上 这样做。git pull
命令是唯一的 user-oriented“get stuff”命令,没有 remote-tracking 名称。没有遥控器!结果证明这是个坏主意,遥控器和 remote-tracking 名字被发明了,但现在我们有一个尴尬的情况,即fetch
与push
相反,pull
是被压倒的 does-two-things 命令。)git push
发送 内容并(尝试)将其添加到他们的 存储库。但是:- 他们可能会拒绝添加。
- 如果他们 添加它,则没有 remote-tracking 名称的等效项:您告诉他们将提交添加到他们的 b运行ch 或 b 运行ches,他们这样做了,现在 他们的 b运行ch 添加了新的提交。没有“现在合并它们”的步骤。 你一定拥有一切pre-combined.
这里还有很多东西需要了解,但现在我们就此打住,因为我们终于有足够的知识来解决您的特定问题。不过,让我们先回顾一下,并特别记下我们之前快速完成的事情。
“重置”a b运行频道名称
假设您的存储库中有这些 b运行ch 名称和提交:
I--J <-- feature1
/
...--G--H <-- main
\
K--L <-- feature2 (HEAD)
也就是说,您一直在研究两个功能。您为 feature1
做了两次提交,这两次提交都紧接在 main
的最后一次提交之后。假设它们都很好并且您想保留它们。但是随后您在 feature2
上进行了两次提交,也是在 main
上最后一次提交之后,并且您一直在测试提交 L
并发现它很糟糕。所以你想摆脱它。
我们之前提到我们可以从链的末尾启动提交,但我们当时显示了一个 替换 。现在让我们看看启动提交背后的机制。我们需要做的是让 name feature2
指向 commit K
而不是 commit L
.这将“放弃”提交 L
:它仍然在大 all-objects 数据库中,但是因为我们 find 从 开始提交结束并向后工作,使 feature2
上的“最后”提交成为 K
而不是 L
使它看起来好像 L
实际上已经消失了:
I--J <-- feature1
/
...--G--H <-- main
\
K <-- feature2 (HEAD)
\
L [abandoned]
我们如何做到这一点?在Git中,我们使用git reset
调整current b运行ch name.
git reset
命令又大又复杂:它做的事情太多了。但是对于我们的特殊情况,我们可以在简单模式下使用它,运行ning:
git reset --hard HEAD~1
--hard
告诉 git reset
清除 Git 索引中的内容 / staging-area 以及我们的工作树中的内容,即使它移动了 b运行通道名称。这里的HEAD~1
的意思是:找到当前提交,然后后退一步。我们可以 运行 git log
代替 HEAD~1
并使用鼠标获取提交 K
的原始哈希 ID:
git reset --hard a123456
或其他什么。有时使用 copy-paste 和 git log
是去这里的方法;有时像 HEAD~1
或 HEAD^
这样的相对表达式更容易;但无论哪种方式,关键概念是:git reset
使 current b运行ch name 指向我们选择的任何提交。 我们只是选择一些提交,通过找到它的任何名称,然后将其交给 git reset
和 git reset
使 当前名称 — HEAD
附加到——指向该提交。
(要撤消“错误”git reset
,我们只需 运行 git reset --hard <em>hash-of-L </em>
这里,但是这样做,我们必须能够 找到 提交 L
的散列。如果它在你的屏幕上,你可以使用 copy-paste。如果不能,你会从哪里得到它?Git 有很多方法可以暂时取回这些东西,所以这并非不可能,只是很难而且很烦人。请注意 Git =149=] 表示 通过覆盖 al 来清除我的工作树来自新 selected commit 的文件。由于您的工作树文件 不在 Git 中,Git 将无法帮助您找回它们。在使用 --hard
之前,非常确定:运行 git status
很多!)
我们还可以移动一个 而不是 当前 b运行ch 的 b运行ch 名称。假设在弹出 L
之后,我们意识到提交 J
也很糟糕。我们可以 运行:
git checkout feature1
git reset --hard HEAD~1
弹出J
,但我们也可以运行:
git branch -f feature1 feature1~1
git branch
命令可以——当与 -f
或 --force
选项一起使用时——移动 任何 b运行ch 名称而不是当前b运行ch 到任何提交,与 git reset
将 current b运行ch 移动到任何提交的方式相同。 (--force
选项是必需的,这样当您创建新的 b运行ch 名称时,您不会不小心移动了现有的 b运行ch 名称,但有错字或brain-o 或其他。)
第 2 部分(共 2 部分)()
git push --force
所以我们现在知道如何在本地移动 b运行ch 名称。现在让我们再看看 git push
,特别是它的 --force
或 -f
选项。我们知道使用 git push
,我们通常使用它将我们的新提交发送到其他一些 Git 存储库。然后我们通常要求其他 Git 存储库 添加提交到他们的 b运行ch 名称之一 。如果我们所做的只是正确地添加提交,并且我们有权限,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 移动 一个 b运行ch 名称,他们都会检查是否可以。如果我们告诉他们起一个 新名称 feature1
,那可能没问题,但假设我们在这里决定让他们设置 main
.他们会回答我们:不!如果我让我的名字 main
指向 J
,我将失去我的 N-O-P
提交!这是一个很大的 NOPe! 记住,他们像每个 Git、find 一样使用 b运行ch 名称提交 找到 last 提交,然后向后工作。 J
导致 I
导致 H
,它不会向前导致 N
,只会向后导致 G
。
这通常是我们喜欢这样的事情的工作方式。我们不会直接推送到他们的 main
,而是推送我们的 feature1
提交并要求他们创建一个名为 new b运行ch 33=] 这样就没问题了。
但是...假设 Git Hub 上的 Git 存储库是 你的 ,并且你有:
...--G--H <-- main (HEAD), origin/main
然后您向 main
和 运行 git push origin main
和 添加了一个错误的提交 I
或一对提交 I-J
他们拿走了它们?现在你有:
...--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 任何他们没有的新提交,即 none—然后要求他们将 main
设置为指向 H
,他们将 拒绝请求 因为那样会 输 从他们的 main
.
I-J
但这正是您想要的。您希望 他们放弃这两个错误的提交。所以实现这一点的方法是使用 --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 没有任何修改(推送到)a b运行ch 的权限概念,但大多数托管服务器——包括 GitHub——也添加 that。
8传入的提交实际上进入了“qua运行tine zone”并且在被接受之前不会迁移到真实的数据库中。此功能来自 来自 GitHub,因为 GitHub 曾经将所有内容都接受到他们的数据库中,然后才拒绝它们,这给 [=234 造成了很大的混乱=]中心。所以现在有了这个奇特的 qua运行tine 功能。
不止一台遥控器
终于,我们有足够的办法解决所有问题。
但第一个技巧是,您必须在笔记本电脑或任何具有本地克隆的地方进行设置,以便拥有 两个 遥控器,而不是只有一个。你运行:
git clone <url>
最初,URL 是给你的叉子的。您的叉子就是您要调整的叉子。我们现在必须为您分叉的存储库添加一个远程。
请记住,远程只是一个包含 URL 的短名称,Git 将使用此短名称组成 remote-tracking 个名称。所以你开始做在这里输入任何你喜欢的名字。标准的 first 名称是 origin
,您已经有了这个。有些人喜欢使用 upstream
作为他们的标准名字。我不太喜欢这个,因为 Git 已经有其他东西叫做 upstream。我会用另一个名字;在这里我会使用一个愚蠢的,但你应该弥补一些明智的:
git remote add lexluthor <url>
为您分叉的存储库插入 URL。然后 运行 git fetch
到那个遥控器:
git fetch lexluthor
现在,在您笔记本电脑的存储库中,他们的所有提交(您可能已经拥有所有这些,在这种情况下,这部分进行得很快)。他们的每个 b运行ch 名称也有 remote-tracking 名称。
现在您只需要说服您的 GitHub 分支 its b运行ch bran
,或 main
或 master
或其他任何内容,应指向 相同的提交 即 bran
或 main
或 master
或任何提交在 lexluthor
:
git push --force lexluthor/master origin/master
就是这样——就是这样。我们向 origin
发送任何我们拥有的 origin
缺少的提交,他们需要更新他们的 (origin
's) b运行ch:那根本不是什么,因为我们是“两个领先”和 none 完全落后。然后我们 命令 Git 在 Git 集线器上让我们的 origin
的 master
识别相同的 commit that our lexluthor/master
identifies,这是 master
identified in the repository originally fork 的 commit。
您可能 也 想要您自己的 master
放弃您前面的两个提交。您可能出于其他原因想要保留这些提交/放置另一个 b运行ch / 其他;为此:
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 给你的!