GIT: 如何unjoin_unlock 合并两个分支
GIT: How to unjoin_unlock a merge of two branches
我以某种方式设法生成了附加的合并图。提交的名称没有意义。这是一个游乐场 git。我最关心的是总体情况,尤其是本地主机(pc 图标)和源主机(有头像的那个)分支之间的关系。
我想要的是删除合并并获得一条直线路径,如:
[add] add ...
add git ...
[add] add .. (near duplicate)
add git ...(near duplicate)
[]1.added
etc.
基本上我有两个合并的分支,但我想做一些事情(我想是),比如解锁它们,然后做一个变基。最终,我可以压扁它们。
此外,事实上,我不需要两个分支。它们实际上是相同的。但我很好奇我如何以一种同时保留两者的方式来处理它,我想在这个过程中有可能删除其中一个。
我尝试了一些东西,但都是随机的,并不值得一提。我真的不知道如何进行。
[更新] 我正在寻找一个命令 line/git 最好是命令答案,因为我想了解发生了什么。但是,我使用 VScode 进行编码,并且有各种存储库管理器(Gitkracken、Fork、Gitx、Github、Sourcetree)供我使用,因此在这些上下文中的答案将是一个起点.
提前致谢...
Basically I have two branches that have merged, but I would like to do something (I imagine to be) like unlock them, and then do a rebase instead ...
这绝对是可以的(虽然"unlock"不是Git中的东西)。不过,您可能需要一些基本的 Git 说明(正如这个 "unlock" 概念所建议的)。
在使用任何分布式版本控制系统时,尤其是当所讨论的 DVCS 是 Git 时,需要牢记许多重要事项。第一件事是它是 分布式 ,因此部分或全部部分的副本不止一个。这使事情本质上变得复杂。我们需要一些方法来驯服和控制复杂性。
Git这里的选择是从commit的概念开始。提交是 Git 的 raison d'être。它们是它的基本存储单元。1 每个提交都有一个唯一的编号。如果这是一个简单的计数可能会很好:提交#1,提交#2,......但事实并非如此。相反,它是一个唯一的 哈希 ID。这些哈希 ID 看起来 运行dom,但实际上并不是 运行dom。事实上,如果我们可以提前预测您将进行新提交的确切秒数,并且知道您将在其提交消息中放入什么以及关于它的所有其他内容,我们就可以预测其哈希 ID。但是我们当然不会也不能。
每个提交包含两件事:
- 所有源文件的完整副本:快照,这是提交的主要数据;和
- 一些 元数据: 有关提交的信息,例如提交人、时间以及他们关于 为什么 他们提交的日志消息提交。
元数据的一个关键部分是每个提交都包含一些先前的哈希 ID,或 parent,提交。也就是说,以后的每个提交都会说 "my earlier parent commit is _____"(用哈希 ID 填写空白)。 links 一起提交,但仅向后指向。
一旦提交,任何提交都不能更改,甚至一位都不能更改,因为它的哈希 ID 是其所有位的加密哈希。也就是说,您 可以 从存储库中取出一个现有的提交,对它大惊小怪,然后保存一个 新的 提交,但对它的任何更改都会导致在保存 新的和不同的 提交时,只是 添加到存储库 。现有提交仍然存在,并且在其原始哈希 ID 下仍未更改。换句话说,提交一出生就被永久冻结。这意味着无法修改 parent 提交以保存其 children 的哈希 ID。 Children 知道他们的 parents(在你创建 child 时存在),但是 parents 永远不知道他们的 children(它们不存在parent 出生时还未出生。
最后,这也意味着要记住一个链的提交,我们只需要记住链中的最后一个link 。也就是说,如果我们绘制一系列提交,使用大写字母代表真正的哈希 ID,我们会得到如下所示的内容:
A <-B <-C <--master
name master
记住 last 提交的哈希 ID,C
。我们说名字master
指向C
。 Commit C
包含快照加元数据,在元数据中,C
记住了commit B
的哈希ID,所以我们说 C
指向 B
.同样,B
记住提交的哈希 ID A
。
提交 A
有点特别,因为它是第一次提交。它没有更早的提交要记住,所以它没有保存 parent。 Git 称之为 root commit 这意味着我们可以停止向后看。
要添加 new 提交,我们从最后一个提交开始——在本例中为 C
并提取其文件。提交中的文件采用特殊的 read-only、Git-only、冻结和压缩格式,2 以便进行任何实际的 工作 有一个提交,我们必须先提取它。提取提交 C
后,Git 知道当前提交是 C
。然后我们做我们平常的事情并进行新的提交:
A--B--C <-- master
\
D
新提交 D
指向 C
(这应该是一个箭头,但是箭头太难画了,所以我用连接线替换了大部分)。然后 git commit
施展魔法:将 D
的哈希 ID 写入 name master
,这样 master
现在指向到 D
:
A--B--C
\
D <-- master
(现在我们可以理顺线条:图中不再需要扭结)。
1承诺可以进一步分解,就像原子可以分解成质子、中子和电子一样,但是一旦你将它们分解,它们就不再是原子的,以一种微不足道的方式。
2我喜欢称这些冻结的 Git-ified 文件 freeze-dried”。因为它们 是 冻结的— 事实上,它们是散列的,就像提交一样 — new 提交可以只共享现有的冻结文件从以前的提交。这就是 Git 存储库不会很快膨胀的原因之一:大多数新提交主要是 re-use 以前提交的所有文件。
因为散列的 Git object 永远不会改变,所以保留 re-using 现有的 object 是完全安全的。提交总是获得唯一的 ID,因为它们有 time-stamps 和 parent link 等等。 re-use 提交 ID 的唯一方法是制作 相同的 快照,使用 相同的 parents,在同一时间——精确到同一秒——与制作较早快照的时间相同。因此,如果您今天制作与昨天制作的快照相同的快照,将时间设置回昨天,re-using 昨天的日志消息和昨天的所有其他内容,您将再次获得相同的提交......这是你昨天做的,有什么问题吗?
有一种方法可以通过脚本在多个 b运行ches 上同时进行多个提交。如果你开始这些 b运行 指向相同的提交,这会使它们指向相同的最终提交——这起初令人惊讶,但并没有被破坏。
由于pigeonhole principle, but it never occurs in practice. See also How does the newly found SHA-1 collision affect Git?
,哈希冲突也存在理论上的问题
B运行ch 名称只是指向现有提交的指针
这意味着 b运行ch names 本身实际上做的很少。他们所做的一件事就是记住一些提交的 哈希 ID。由于哈希 ID 又大又丑,人类不可能记住,这实际上非常有用。 工作量.
在Git中,你可以有任意数量的b运行ch名称,它们都指向同一个提交。您还可以随时 移动 您的任何 b运行ch 名称,只要每个名称都指向您确实拥有的一个提交。所以如果我们有:
A--B--C--D <-- master
我们可以通过 运行ning 为 D
添加更多名称,例如:
git branch dev
让我现在这样画:
A--B--C--D <-- master (HEAD), dev
我在这里的 parentheses 中添加了特殊名称 HEAD
,附加到名称 master
。这是 Git 在现实中所做的图:Git 存储 b运行ch 的 名称,即 master
, 在它用于 HEAD
,3 到 "attach" 的特殊名称到 b运行ch 名称的文件中。这就是 Git 知道你在哪个 b运行ch 的方式——然后 b运行ch 名称本身,在本例中 master
,就是 Git 知道你也在提交。
现在让我们做一个新的提交,并将其命名为E
。 Git 将像往常一样写出快照和元数据。由于当前提交是 D
,E
的 parent 将是 D
。然后,当 Git 将提交 E
保存到 all-commits 数据库中时,Git 会将 E
的哈希 ID 写入任何 b运行ch名称 HEAD
附加到,在本例中是 master
,给我们:
E <-- master (HEAD)
/
A--B--C--D <-- dev
HEAD
仍然附加到 master
,但现在 master
指向链的最后一次提交,即 E
。名称 dev
仍然指向 D
;提交 A
到 D
现在在 both b运行ches 上;并且提交 E
仅在 master
.
这是Git中普通的日常开发:
- pick a b运行ch to attach
HEAD
to, which picks its tip commit
- 从该提交中提取所有文件,以便我们可以使用/处理它们
- 做我们平常做的事
- 进行新的提交:将Git的index4中的内容打包进行新的提交, whose parent is the current commit, then update the current b运行ch name to point to the new commit.
通过这样做,随着时间的推移,b运行ches 增长——一次一个提交。
3Git 实际上至少在今天使用了一个文件。没有人保证它有一天不会改变方法,但是:一般来说,你应该使用提供的程序读写 HEAD
:git rev-parse
、git symbolic-ref
, git update-ref
, 等等,如果你正在编写 low-level 脚本;或 git branch
之类的,用于更正常的日常使用。
4 Git 也称为 暂存区 的索引在这个答案中没有得到正确解决,但是git commit
就是这样运作的。虽然索引在冲突合并期间发挥了扩展作用,但它的主要功能是充当要放入 next 提交中的文件的保存区域。它开始匹配 当前 提交的文件副本。
从技术上讲,索引保存的是散列 ID,而不是实际的文件副本。但是除非并且直到您开始使用 git update-index
和 git ls-files --stage
,您可以将索引视为保存每个文件的 pre-freeze-dried 副本。
合并(真正的合并)
最终,我们可能会有这样的东西
I--J <-- master (HEAD)
/
...--G--H
\
K--L <-- feature
我们现在想要合并 feature
b运行ch——这是真正的提交L
,加上我们开始工作的历史向后 L
、K
、H
、G
等——进入当前的 master
b运行ch,即 J
,然后是I
,然后是H
和G
等等。
要完成此合并,我们将 运行 git merge feature
。 Git 将定位不是一个,不是两个,而是 三个 提交:
- 提交#1 将是合并基础,但在我们到达那里之前,让我们找到#2 和#3。
- 提交 #2 是当前提交,这很简单:它是
HEAD
,即 J
。
- 提交 #3 也很简单:这是我们命名的那个。我们说
git merge feature
并且名称 feature
指向 L
,所以提交 #3 是提交 L
.
merge base 是最好的 shared (公共)提交,我们从两个提示开始并向后工作找到了.在这种情况下,很明显:both b运行ches 上的最佳提交是 H
.
合并现在通过比较所有三个提交的快照来进行。 (请记住,每个提交都有所有文件的完整快照。)比较 H
与 J
告诉 Git 我们在 我们的 上更改了什么(master
) b运行ch;比较 H
和 L
告诉 git 他们在 他们的 (feature
) b运行ch 上改变了什么。合并现在简单地——或 complicated-ly——合并这两个更改,将 combined 更改应用于合并基础 H
中的快照,如果一切顺利,创建Git 调用 合并提交 .
的新提交
新的合并提交几乎以通常的方式进行:索引内容的快照、日志消息和基于当前 b运行ch 的 parent。这个合并提交的特别之处在于它也有一个 second parent。合并的第二个 parent 是您合并的提交——在本例中,提交 L
。因此,如果一切顺利,Git 将自己进行新的合并提交 M
:
I--J
/ \
...--G--H M <-- master (HEAD)
\ /
K--L <-- feature
提交 M
指向 both J
and L
,但除此之外这与任何其他提交相同。请注意当前的 b运行ch 名称 master
现在如何指向最后一次提交 M
;但还要注意 M
如何返回到 both J
and L
,以便所有这些提交现在在 master
.
Fast-forward"merge"
git merge
命令可以,并且默认情况下,如果可以的话,将做一些根本不是合并的事情。假设我们有:
...--G--H <-- master (HEAD)
\
I--J <-- dev
如果我们 运行 git merge dev
, Git 像往常一样找到三个感兴趣的提交:#2 是 HEAD
即 H
, #3来自dev
,即J
,合并基础是在两个b运行ches上最好的shared提交,这是... H
再次.
如果我们 Git 将 H
中的快照与 H
中的快照进行比较,会有什么不同? (这是一个简单的练习。想一想。我们必须更改哪些文件才能将 H
中保存的文件更改为 H
中的文件?)
由于从 H
到 H
没有任何变化,我们将得到的唯一变化是从 H
到 J
的变化—— --theirs
设置——如果我们进行真正的合并。我们可以强制Git做真正的合并,如果我们这样做,Git将尽职尽责地合并no-changes 进行更改并进行新的合并提交 M
:
...--G--H------M <-- master (HEAD)
\ /
I--J <-- dev
如果我们 运行 git merge --no-ff dev
就会得到。但默认情况下,Git 会说:Combining nothing with something gives the something;将某物应用到 H
得到 J
中的快照;所以让我们 re-use 现有提交 J
! 运行 git merge dev
或 git merge --ff-only dev
将执行 fast-forward 而不是合并,给我们:
...--G--H
\
I--J <-- master (HEAD), dev
实际上,只需签出提交 J
并将 master
移动到指向 J
。 (像往常一样,特殊名称 HEAD
仍然附加。)
壁球合并
您还可以使用 git merge --squash
执行 "squash merge"。在这里,Git 经历了 大多数 的常规动作以进行完全合并。这意味着它适用于 fast-foward-like 情况,但也适用于 true-merge-like 情况:
I--J <-- master (HEAD)
/
...--G--H
\
K--L <-- feature
Git 将像往常一样执行 compare-and-combine — 如果我们有这个,结果和往常一样简单:
...--G--H <-- master (HEAD)
\
I--J <-- dev
——然后准备好进行新的提交以保存合并快照。但是,Git 没有将新提交作为合并提交,而是假装你告诉它 --no-commit
,抑制了提交。然后你必须 运行 git commit
自己,当你这样做时,Git 使用单个 parent 进行 普通提交 :
...--G--H--S <-- master (HEAD)
\
I--J <-- dev
例如,其中 S
是 "squash merge" 由 easy-merging 提交 J
产生的快照,或者:
I--J--S <-- master (HEAD)
/
...--G--H
\
K--L <-- feature
在哪里S
是由 true-merging J
和 L
使用 H
作为合并基础生成的 "squash merge" 快照。
请注意,在两种情况下,"squashed" 端的任何提交都不再有用。当我们 squash-merged feature
时,提交 K-L
做某事,但提交 S
做 相同的 某事,不管是什么,提交J
。我们不再需要提交 K-L
。
你得到的是合并 squash 或 rebase 的结果
我们还没有介绍变基——我们马上就会讲到——但让我们看看这个:
I--J--S <-- master (HEAD)
/
...--G--H
\
K--L <-- feature
我们现在可以 运行 git merge feature
,如果我们愿意的话(尽管这通常不是一个好主意)。 Git 将比较 H
与 S
以查看我们更改了什么,并比较 H
与 L
以查看它们更改了什么。 Git 然后将尽其所能合并两组更改。
由于S
已经包含了H
-vs-L
的变化,如果我们运气好(或者是运气不好?),没有冲突和Git 意识到它可以完全忽略 H
-vs-L
部分并仅使用 H
-vs-S
部分。或者,也许我们会发生一些冲突。我们何时以及是否发生冲突取决于 H
-vs-J
部分是什么,但不发生冲突是很常见的。也许我们手动解决一些冲突;无论哪种方式,我们继续进行新的合并提交,我将其称为 M
,即使 S
按字母顺序排在 M
之后:
...--G--H--I--J--S--M <-- master (HEAD)
\ /
K-------L <-- feature
我们现在在图中有这个合并气泡,冗余提交 K-L
作为合并 M
的第二个 parent。
我们马上就会看到如何完全摆脱 M
。
变基
git rebase
命令通过复制 提交来工作。我在开始时提到,不可能更改任何提交,但您可以取出一个提交(或比较两个提交),对文件大惊小怪,然后进行 new 提交。我们可以使用这个 属性 来 复制 提交到 new-and-improved 版本。
让我们开始:
...--G--H--K--L <-- master
\
I--J <-- feature (HEAD)
提交 I
和 J
非常好,但是如果我们有 Git 计算出 更改 从 H
到 I
,并将相同的 更改 应用到 L
中的快照?让我们 分离 HEAD
,使其直接指向 L
:
之后的新提交
I' <-- HEAD
/
...--G--H--K--L <-- master
\
I--J <-- feature
提交 I'
是我们对 I
的副本——这就是我们称之为 I'
的原因——因为我们已经 Git 复制了提交消息和所有内容。
原版I
和副本I'
的区别在于I'
的parent有L
,还有一个不同的快照,因此将 I'
与其parent L
进行比较得到的结果与将 I
进行比较的结果相同它 parent H
.
这个复制过程是由git cherry-pick
完成的。5 Cherry-pick是Git的一般"copy a commit"操作,并且在内部,它使用与完整 git merge
相同的引擎,但您通常可以将其视为 "copy commit"。6 将 I
复制到I'
,我们现在需要将J
复制到J'
:
I'-J' <-- HEAD
/
...--G--H--K--L <-- master
\
I--J <-- feature
现在,由于 I'-J'
是我们的 new-and-improved 提交,我们希望我们的 Git 放弃 支持这些新的原件.为了实现这一点,我们的 Git 将简单地从提交 J
中剥离标签 feature
并使其指向 J'
。完成后,我们的 Git 可以 re-attach HEAD
到 b运行ch name feature
:
I'-J' <-- feature (HEAD)
/
...--G--H--K--L <-- master
\
I--J [abandoned]
因为我们找到提交是从b运行ch名称开始,找到它存储的哈希ID,然后查找提交,当我们查看这个存储库时,它将 看起来像 我们以某种方式更改了两个提交。我们看到的不是 J
然后是 I
,而是 J'
然后是 I'
。但是如果我们仔细观察,我们会发现这些是不同的哈希 ID。
5某些形式的 git rebase
确实,实际上 运行 git cherry-pick
。其他人(主要是旧形式的变基)没有,但非常接近地模拟它。
6例外情况是在复制过程中遇到合并冲突,但我们不会在这里讨论。
分布式存储库
回到开头,我提到要记住的最重要的事情是 Git 是 分布式的 并且有不止一个副本存储库。
在我们的例子中,假设我们有本地 Git,在我们的机器上,另一个 Git 在 GitHub 上。 (在某种程度上,另一个 Git 在哪里并不重要——GitHub、Bitbucket、GitLab、公司服务器等等:它们的工作方式与他们都有一个Git在一些IP地址后面。最大的区别是托管公司通过网站添加在他们自己的用户界面上,web界面是不同的。)
无论如何,我们 Git 打电话给 他们的 Git——不管 "they" 是谁——通过 URL,其中 t运行 转化为一些 IP 地址和我们提供给服务器的路径名。 Git 将此 URL 存储在一个名称下,Git 调用 远程 。任何遥控器的标准名字都是 origin
,因此我们将在此处使用它作为名称。
由于 Git 在 origin
是 一个 Git 存储库,它有 自己的 b运行ch 名称。我们的 b运行ch 名称,在我们的 Git 中,是 我们的。他们的是 他们的。他们不需要匹配!特别是,当我们向我们的 b运行ches 添加提交时,我们将 "get ahead" 他们的 b运行ches.
让我们从在我们的机器上根本没有 Git 存储库开始(也许我们必须得到一台新笔记本电脑,或其他)。我们将 git clone
他们的 Git 存储库:
git clone <url>
我们的 Git 在我们的计算机上将创建一个新的 totally-empty 存储库,并添加名称 origin
来存储 URL。然后它将调用他们的 Git 并让他们列出他们的 b运行ch 名称,以及由这些 b运行ch 名称编辑的提交的哈希 ID select。他们将为这些 b运行ches 发送这些 tip 提交。
对于每个提交哈希 ID,我们的 Git 会说:是的,我想要那个提交。假设在 master
上提交 H
。他们有义务提供该提交的 parent、G
。我们的 Git 将检查:我是否已经提交了 parent? 当然,我们的 Git 的 object 数据库是空的,所以我们不这样做。所以我们也会要求 G
。他们的 Git 将提供 F
,我们将接受它,依此类推,最后,我们将获得 他们的每一次提交 (好吧,除了任何被遗弃的,如果他们有的话——有时他们有!)。
现在我们将有:
...--G--H
在我们的提交数据库中。但是我们还没有任何 names。我们已经完成了从他们那里获得的提交——他们只有 master
和提交 H
及其历史,我们得到了所有这些——所以我们的 Git 与他们的 Git 断开了连接.现在我们的 Git 获取了他们所有的 b运行ch 名称,也就是 master
,并且 通过将我们的远程名称 [=] 重命名为 每个236=],在前面,用斜线隔开:
...--G--H <-- origin/master
这些 origin/*
个名字是我们 Git 的 remote-tracking 个名字。他们为我们记住了他们Git的b运行ch名字
对于它的最后一招,我们的 git clone
运行s git checkout master
。我们实际上 没有 a master
b运行ch,但是如果你要求 Git 查看 b运行ch你没有,你的 Git 将尝试 creating b运行ch 来自相应的 remote-tracking 名称。我们确实有 origin/master
并且它 select 提交 H
,所以我们的 Git 创建 我们的 master
指向 H
,并在此处附上我们的 HEAD
:
...--G--H <-- master (HEAD), origin/master
我们的 git clone
现在完成了。
如果我们现在创建新的提交,它们会以通常的方式添加:
...--G--H <-- origin/master
\
I--J <-- master (HEAD)
我们现在可以使用git push
向他们发送提交。当我们这样做时,我们选择两件事:
- 要发送哪些提交,以及
- 要设置哪个 运行ch 名称
如果我们运行 git push origin master
,我们选择提交J
到发送(因为我们的名字master
selects 提交 J
) 和名称 master
到 set(因为我们说 master
)。
如果我们愿意,我们可以运行git push origin master:dev
,发送J
并要求他们设置 他们的 dev
而不是他们的 master
。你通常不会这样做——更典型的是,你会先创建自己的 dev
,这样你就可以在 dev
上创建 J
,然后再创建 git push origin dev
——但它是有用的例子。我们 发送 提交我们拥有的(大概他们没有),然后我们的 git push
要求他们设置 他们的 b运行ch 个名字。与我们的 Git 不同,他们在这里没有 remote-tracking 名字! Remote-tracking 名称是 git clone
和 git fetch
的 属性。
为了发送它们 J
,我们必须先发送它们 I
。我们也会提供给他们H
,但他们已经有了,所以他们说不,谢谢,我有那个。这让我们的 Git 压缩得非常好(我们知道他们也提交了 H
和 所有 更早的 提交!)我们向他们发送 I
和 J
。然后我们要求他们设置他们的 b运行ch name(s).
如果 server-side 存储库是 共享的 ——如果我们不是唯一使用它的人——他们的 master
可能已经获得了新的提交,因为我们最后和他们谈谈。例如,也许其他人 运行 git push origin master
。所以我们发送给他们I-J
,如果他们有:
...--G--H <-- master
\
I--J
然后我们要求他们将 master
设置为指向 J
,他们可能会说 ok,不要。他们现在有:
...--G--H--I--J <-- master
在 他们的 存储库中。我们的 Git 将相应地更新我们的 origin/master
。但如果他们有:
...--G--H--K <-- master
\
I--J
他们服从了我们的礼貌要求,他们最终会:
K [abandoned]
/
...--G--H--I--J <-- master
因为 any Git finds 提交的方式是从最后开始并向后工作。现在结尾是J
,谁的parent是I
,谁的parent是H
。从H
到K
是没办法的:箭头都是one-way,指向后面。所以在这种情况下他们会说 不,我不会设置我的 master
.
您的 Git 会将其显示为错误:
! rejected (non-fast-forward)
这意味着你必须得到他们的新提交来自他们,并将它们合并到你的工作中,例如,通过git merge
或 git rebase
.
或者,您可以向他们发送命令,而不是礼貌的请求:将您的 master
设置为 J
! 如果他们服从此命令,他们 将 失去提交 K
。您很有可能无法再从他们那里取回它。制作 K
的人可能会生气(但是——无论如何你可以希望——制作 K
的人仍然在 他们的 克隆中拥有它)。
拉取请求和 GitHub 的可点击按钮
拉取请求不是 Git 的东西,而是 GitHub 和其他托管服务提供商提供的东西。他们为您提供了一种跨他们所谓的 forked 存储库进行合并的方法。 (一个分支实际上只是一个添加了一些特殊功能的克隆,最重要的是这些拉取请求。)
GitHub 在合并 PR 时提供三个选项。一个是直接 git merge
,即使 fast-forward 是可能的,也进行真正的合并。一个名为 "rebase and merge",即使没有必要也会执行 git rebase
,总是 将所有提交复制到新链,然后执行 fast-forward-新链的样式合并。最后一个称为 "squash and merge",相当于 运行ning git merge --squash
.
由于GitHub 的压缩和变基风格合并总是产生新的散列 ID,您现在可以遇到我们之前观察到的相同问题,压缩后合并.
删除合并(或任何其他提交)
在您自己的 存储库中,您可以完全控制所有 b运行ch 名称。您可以使任何 b运行ch 名称指向 any 提交。
那么假设你有这个:
I--M <-- master (HEAD)
/ /
...--G--H--I' <-- origin/master
其中 I
是您之前在 master
上的原始提交,您将其发送到某个地方,然后将其复制到 I
并放在 他们的 master
。你的 origin/master
仍然指向这个副本 I'
;你的 master
指向你的合并 M
,第一个 parent 是 I
,第二个 parent 是 I'
.
如果您 git fetch origin; git merge origin/master
或者如果您只是 git pull
运行 和 git fetch origin master; git merge FETCH_HEAD
,您就会得到这个。同样,问题是 运行s origin
决定 复制 你的提交,无论出于何种原因。
如果您想放弃合并M
,您现在可以运行:
git reset --hard HEAD^ # or HEAD~1 or HEAD~
这将销毁所有未提交的工作,因此请确保您没有任何工作! reset
操作,除了它所做的所有其他事情(在这种情况下会破坏未提交的工作)之外,还说 移动当前 b运行ch 名称 。当前 b运行ch 名称(现在,master
)将 select 的新提交是您在命令行中命名的提交。
您可以使用原始哈希 ID,它始终有效:只需从 git log
输出中剪切它,并且您已经说过 我想要我当前的 b运行ch 名称到 select 提交 。或者,您可以使用名称:a b运行ch 名称,例如,selects 名称指向的提交。这里,我们使用HEAD
,意思是当前commit,然后加上一个后缀:^
,意思是第一个[=1057] =],或者~1
,意思是倒数一个first-parent,是一样的
这意味着Git会找到合并M
,然后看它的第一个parent,也就是I
。那就是我们说到 git reset --hard
的地方,所以我们最终会得到:
__M [abandoned]
/ /
I / <-- master (HEAD)
/ /
...--G--H--I' <-- origin/master
有点难画——提交M
仍然存在,但是没有人指向它,所以我们无法找到它。把它拿出来,结果更清楚:
I <-- master (HEAD)
/
...--G--H--I' <-- origin/master
请注意,这有效 因为 我们从未将提交 M
提交给任何其他 Git。只有 我们的 master
知道如何找到提交 M
。我们可以重置它,它不会再回来了。
如果我们做了发送M
给其他人Git,例如,通过git push origin master
,他们 会提交 M
。我们可以尝试将其重置为远离 our Git,这会工作一段时间,但 origin/master
在我们的存储库中,并且 thei master
在 他们的 克隆中,仍然会有合并提交 M
。要摆脱它,我们必须说服 他们 也改变 他们的 master
。
一般来说,一旦你分享了一个提交,你就会从其他人那里再次获得它 Git。 Git 是为 添加 提交而构建的,而不是将它们带走;默认共享操作是 添加到我的 collection,如果合适则合并 。
我以某种方式设法生成了附加的合并图。提交的名称没有意义。这是一个游乐场 git。我最关心的是总体情况,尤其是本地主机(pc 图标)和源主机(有头像的那个)分支之间的关系。
我想要的是删除合并并获得一条直线路径,如:
[add] add ...
add git ...
[add] add .. (near duplicate)
add git ...(near duplicate)
[]1.added
etc.
基本上我有两个合并的分支,但我想做一些事情(我想是),比如解锁它们,然后做一个变基。最终,我可以压扁它们。
此外,事实上,我不需要两个分支。它们实际上是相同的。但我很好奇我如何以一种同时保留两者的方式来处理它,我想在这个过程中有可能删除其中一个。
我尝试了一些东西,但都是随机的,并不值得一提。我真的不知道如何进行。
[更新] 我正在寻找一个命令 line/git 最好是命令答案,因为我想了解发生了什么。但是,我使用 VScode 进行编码,并且有各种存储库管理器(Gitkracken、Fork、Gitx、Github、Sourcetree)供我使用,因此在这些上下文中的答案将是一个起点.
提前致谢...
Basically I have two branches that have merged, but I would like to do something (I imagine to be) like unlock them, and then do a rebase instead ...
这绝对是可以的(虽然"unlock"不是Git中的东西)。不过,您可能需要一些基本的 Git 说明(正如这个 "unlock" 概念所建议的)。
在使用任何分布式版本控制系统时,尤其是当所讨论的 DVCS 是 Git 时,需要牢记许多重要事项。第一件事是它是 分布式 ,因此部分或全部部分的副本不止一个。这使事情本质上变得复杂。我们需要一些方法来驯服和控制复杂性。
Git这里的选择是从commit的概念开始。提交是 Git 的 raison d'être。它们是它的基本存储单元。1 每个提交都有一个唯一的编号。如果这是一个简单的计数可能会很好:提交#1,提交#2,......但事实并非如此。相反,它是一个唯一的 哈希 ID。这些哈希 ID 看起来 运行dom,但实际上并不是 运行dom。事实上,如果我们可以提前预测您将进行新提交的确切秒数,并且知道您将在其提交消息中放入什么以及关于它的所有其他内容,我们就可以预测其哈希 ID。但是我们当然不会也不能。
每个提交包含两件事:
- 所有源文件的完整副本:快照,这是提交的主要数据;和
- 一些 元数据: 有关提交的信息,例如提交人、时间以及他们关于 为什么 他们提交的日志消息提交。
元数据的一个关键部分是每个提交都包含一些先前的哈希 ID,或 parent,提交。也就是说,以后的每个提交都会说 "my earlier parent commit is _____"(用哈希 ID 填写空白)。 links 一起提交,但仅向后指向。
一旦提交,任何提交都不能更改,甚至一位都不能更改,因为它的哈希 ID 是其所有位的加密哈希。也就是说,您 可以 从存储库中取出一个现有的提交,对它大惊小怪,然后保存一个 新的 提交,但对它的任何更改都会导致在保存 新的和不同的 提交时,只是 添加到存储库 。现有提交仍然存在,并且在其原始哈希 ID 下仍未更改。换句话说,提交一出生就被永久冻结。这意味着无法修改 parent 提交以保存其 children 的哈希 ID。 Children 知道他们的 parents(在你创建 child 时存在),但是 parents 永远不知道他们的 children(它们不存在parent 出生时还未出生。
最后,这也意味着要记住一个链的提交,我们只需要记住链中的最后一个link 。也就是说,如果我们绘制一系列提交,使用大写字母代表真正的哈希 ID,我们会得到如下所示的内容:
A <-B <-C <--master
name master
记住 last 提交的哈希 ID,C
。我们说名字master
指向C
。 Commit C
包含快照加元数据,在元数据中,C
记住了commit B
的哈希ID,所以我们说 C
指向 B
.同样,B
记住提交的哈希 ID A
。
提交 A
有点特别,因为它是第一次提交。它没有更早的提交要记住,所以它没有保存 parent。 Git 称之为 root commit 这意味着我们可以停止向后看。
要添加 new 提交,我们从最后一个提交开始——在本例中为 C
并提取其文件。提交中的文件采用特殊的 read-only、Git-only、冻结和压缩格式,2 以便进行任何实际的 工作 有一个提交,我们必须先提取它。提取提交 C
后,Git 知道当前提交是 C
。然后我们做我们平常的事情并进行新的提交:
A--B--C <-- master
\
D
新提交 D
指向 C
(这应该是一个箭头,但是箭头太难画了,所以我用连接线替换了大部分)。然后 git commit
施展魔法:将 D
的哈希 ID 写入 name master
,这样 master
现在指向到 D
:
A--B--C
\
D <-- master
(现在我们可以理顺线条:图中不再需要扭结)。
1承诺可以进一步分解,就像原子可以分解成质子、中子和电子一样,但是一旦你将它们分解,它们就不再是原子的,以一种微不足道的方式。
2我喜欢称这些冻结的 Git-ified 文件 freeze-dried”。因为它们 是 冻结的— 事实上,它们是散列的,就像提交一样 — new 提交可以只共享现有的冻结文件从以前的提交。这就是 Git 存储库不会很快膨胀的原因之一:大多数新提交主要是 re-use 以前提交的所有文件。
因为散列的 Git object 永远不会改变,所以保留 re-using 现有的 object 是完全安全的。提交总是获得唯一的 ID,因为它们有 time-stamps 和 parent link 等等。 re-use 提交 ID 的唯一方法是制作 相同的 快照,使用 相同的 parents,在同一时间——精确到同一秒——与制作较早快照的时间相同。因此,如果您今天制作与昨天制作的快照相同的快照,将时间设置回昨天,re-using 昨天的日志消息和昨天的所有其他内容,您将再次获得相同的提交......这是你昨天做的,有什么问题吗?
有一种方法可以通过脚本在多个 b运行ches 上同时进行多个提交。如果你开始这些 b运行 指向相同的提交,这会使它们指向相同的最终提交——这起初令人惊讶,但并没有被破坏。
由于pigeonhole principle, but it never occurs in practice. See also How does the newly found SHA-1 collision affect Git?
,哈希冲突也存在理论上的问题B运行ch 名称只是指向现有提交的指针
这意味着 b运行ch names 本身实际上做的很少。他们所做的一件事就是记住一些提交的 哈希 ID。由于哈希 ID 又大又丑,人类不可能记住,这实际上非常有用。 工作量.
在Git中,你可以有任意数量的b运行ch名称,它们都指向同一个提交。您还可以随时 移动 您的任何 b运行ch 名称,只要每个名称都指向您确实拥有的一个提交。所以如果我们有:
A--B--C--D <-- master
我们可以通过 运行ning 为 D
添加更多名称,例如:
git branch dev
让我现在这样画:
A--B--C--D <-- master (HEAD), dev
我在这里的 parentheses 中添加了特殊名称 HEAD
,附加到名称 master
。这是 Git 在现实中所做的图:Git 存储 b运行ch 的 名称,即 master
, 在它用于 HEAD
,3 到 "attach" 的特殊名称到 b运行ch 名称的文件中。这就是 Git 知道你在哪个 b运行ch 的方式——然后 b运行ch 名称本身,在本例中 master
,就是 Git 知道你也在提交。
现在让我们做一个新的提交,并将其命名为E
。 Git 将像往常一样写出快照和元数据。由于当前提交是 D
,E
的 parent 将是 D
。然后,当 Git 将提交 E
保存到 all-commits 数据库中时,Git 会将 E
的哈希 ID 写入任何 b运行ch名称 HEAD
附加到,在本例中是 master
,给我们:
E <-- master (HEAD)
/
A--B--C--D <-- dev
HEAD
仍然附加到 master
,但现在 master
指向链的最后一次提交,即 E
。名称 dev
仍然指向 D
;提交 A
到 D
现在在 both b运行ches 上;并且提交 E
仅在 master
.
这是Git中普通的日常开发:
- pick a b运行ch to attach
HEAD
to, which picks its tip commit - 从该提交中提取所有文件,以便我们可以使用/处理它们
- 做我们平常做的事
- 进行新的提交:将Git的index4中的内容打包进行新的提交, whose parent is the current commit, then update the current b运行ch name to point to the new commit.
通过这样做,随着时间的推移,b运行ches 增长——一次一个提交。
3Git 实际上至少在今天使用了一个文件。没有人保证它有一天不会改变方法,但是:一般来说,你应该使用提供的程序读写 HEAD
:git rev-parse
、git symbolic-ref
, git update-ref
, 等等,如果你正在编写 low-level 脚本;或 git branch
之类的,用于更正常的日常使用。
4 Git 也称为 暂存区 的索引在这个答案中没有得到正确解决,但是git commit
就是这样运作的。虽然索引在冲突合并期间发挥了扩展作用,但它的主要功能是充当要放入 next 提交中的文件的保存区域。它开始匹配 当前 提交的文件副本。
从技术上讲,索引保存的是散列 ID,而不是实际的文件副本。但是除非并且直到您开始使用 git update-index
和 git ls-files --stage
,您可以将索引视为保存每个文件的 pre-freeze-dried 副本。
合并(真正的合并)
最终,我们可能会有这样的东西
I--J <-- master (HEAD)
/
...--G--H
\
K--L <-- feature
我们现在想要合并 feature
b运行ch——这是真正的提交L
,加上我们开始工作的历史向后 L
、K
、H
、G
等——进入当前的 master
b运行ch,即 J
,然后是I
,然后是H
和G
等等。
要完成此合并,我们将 运行 git merge feature
。 Git 将定位不是一个,不是两个,而是 三个 提交:
- 提交#1 将是合并基础,但在我们到达那里之前,让我们找到#2 和#3。
- 提交 #2 是当前提交,这很简单:它是
HEAD
,即J
。 - 提交 #3 也很简单:这是我们命名的那个。我们说
git merge feature
并且名称feature
指向L
,所以提交 #3 是提交L
.
merge base 是最好的 shared (公共)提交,我们从两个提示开始并向后工作找到了.在这种情况下,很明显:both b运行ches 上的最佳提交是 H
.
合并现在通过比较所有三个提交的快照来进行。 (请记住,每个提交都有所有文件的完整快照。)比较 H
与 J
告诉 Git 我们在 我们的 上更改了什么(master
) b运行ch;比较 H
和 L
告诉 git 他们在 他们的 (feature
) b运行ch 上改变了什么。合并现在简单地——或 complicated-ly——合并这两个更改,将 combined 更改应用于合并基础 H
中的快照,如果一切顺利,创建Git 调用 合并提交 .
新的合并提交几乎以通常的方式进行:索引内容的快照、日志消息和基于当前 b运行ch 的 parent。这个合并提交的特别之处在于它也有一个 second parent。合并的第二个 parent 是您合并的提交——在本例中,提交 L
。因此,如果一切顺利,Git 将自己进行新的合并提交 M
:
I--J
/ \
...--G--H M <-- master (HEAD)
\ /
K--L <-- feature
提交 M
指向 both J
and L
,但除此之外这与任何其他提交相同。请注意当前的 b运行ch 名称 master
现在如何指向最后一次提交 M
;但还要注意 M
如何返回到 both J
and L
,以便所有这些提交现在在 master
.
Fast-forward"merge"
git merge
命令可以,并且默认情况下,如果可以的话,将做一些根本不是合并的事情。假设我们有:
...--G--H <-- master (HEAD)
\
I--J <-- dev
如果我们 运行 git merge dev
, Git 像往常一样找到三个感兴趣的提交:#2 是 HEAD
即 H
, #3来自dev
,即J
,合并基础是在两个b运行ches上最好的shared提交,这是... H
再次.
如果我们 Git 将 H
中的快照与 H
中的快照进行比较,会有什么不同? (这是一个简单的练习。想一想。我们必须更改哪些文件才能将 H
中保存的文件更改为 H
中的文件?)
由于从 H
到 H
没有任何变化,我们将得到的唯一变化是从 H
到 J
的变化—— --theirs
设置——如果我们进行真正的合并。我们可以强制Git做真正的合并,如果我们这样做,Git将尽职尽责地合并no-changes 进行更改并进行新的合并提交 M
:
...--G--H------M <-- master (HEAD)
\ /
I--J <-- dev
如果我们 运行 git merge --no-ff dev
就会得到。但默认情况下,Git 会说:Combining nothing with something gives the something;将某物应用到 H
得到 J
中的快照;所以让我们 re-use 现有提交 J
! 运行 git merge dev
或 git merge --ff-only dev
将执行 fast-forward 而不是合并,给我们:
...--G--H
\
I--J <-- master (HEAD), dev
实际上,只需签出提交 J
并将 master
移动到指向 J
。 (像往常一样,特殊名称 HEAD
仍然附加。)
壁球合并
您还可以使用 git merge --squash
执行 "squash merge"。在这里,Git 经历了 大多数 的常规动作以进行完全合并。这意味着它适用于 fast-foward-like 情况,但也适用于 true-merge-like 情况:
I--J <-- master (HEAD)
/
...--G--H
\
K--L <-- feature
Git 将像往常一样执行 compare-and-combine — 如果我们有这个,结果和往常一样简单:
...--G--H <-- master (HEAD)
\
I--J <-- dev
——然后准备好进行新的提交以保存合并快照。但是,Git 没有将新提交作为合并提交,而是假装你告诉它 --no-commit
,抑制了提交。然后你必须 运行 git commit
自己,当你这样做时,Git 使用单个 parent 进行 普通提交 :
...--G--H--S <-- master (HEAD)
\
I--J <-- dev
例如,其中 S
是 "squash merge" 由 easy-merging 提交 J
产生的快照,或者:
I--J--S <-- master (HEAD)
/
...--G--H
\
K--L <-- feature
在哪里S
是由 true-merging J
和 L
使用 H
作为合并基础生成的 "squash merge" 快照。
请注意,在两种情况下,"squashed" 端的任何提交都不再有用。当我们 squash-merged feature
时,提交 K-L
做某事,但提交 S
做 相同的 某事,不管是什么,提交J
。我们不再需要提交 K-L
。
你得到的是合并 squash 或 rebase 的结果
我们还没有介绍变基——我们马上就会讲到——但让我们看看这个:
I--J--S <-- master (HEAD)
/
...--G--H
\
K--L <-- feature
我们现在可以 运行 git merge feature
,如果我们愿意的话(尽管这通常不是一个好主意)。 Git 将比较 H
与 S
以查看我们更改了什么,并比较 H
与 L
以查看它们更改了什么。 Git 然后将尽其所能合并两组更改。
由于S
已经包含了H
-vs-L
的变化,如果我们运气好(或者是运气不好?),没有冲突和Git 意识到它可以完全忽略 H
-vs-L
部分并仅使用 H
-vs-S
部分。或者,也许我们会发生一些冲突。我们何时以及是否发生冲突取决于 H
-vs-J
部分是什么,但不发生冲突是很常见的。也许我们手动解决一些冲突;无论哪种方式,我们继续进行新的合并提交,我将其称为 M
,即使 S
按字母顺序排在 M
之后:
...--G--H--I--J--S--M <-- master (HEAD)
\ /
K-------L <-- feature
我们现在在图中有这个合并气泡,冗余提交 K-L
作为合并 M
的第二个 parent。
我们马上就会看到如何完全摆脱 M
。
变基
git rebase
命令通过复制 提交来工作。我在开始时提到,不可能更改任何提交,但您可以取出一个提交(或比较两个提交),对文件大惊小怪,然后进行 new 提交。我们可以使用这个 属性 来 复制 提交到 new-and-improved 版本。
让我们开始:
...--G--H--K--L <-- master
\
I--J <-- feature (HEAD)
提交 I
和 J
非常好,但是如果我们有 Git 计算出 更改 从 H
到 I
,并将相同的 更改 应用到 L
中的快照?让我们 分离 HEAD
,使其直接指向 L
:
I' <-- HEAD
/
...--G--H--K--L <-- master
\
I--J <-- feature
提交 I'
是我们对 I
的副本——这就是我们称之为 I'
的原因——因为我们已经 Git 复制了提交消息和所有内容。
原版I
和副本I'
的区别在于I'
的parent有L
,还有一个不同的快照,因此将 I'
与其parent L
进行比较得到的结果与将 I
进行比较的结果相同它 parent H
.
这个复制过程是由git cherry-pick
完成的。5 Cherry-pick是Git的一般"copy a commit"操作,并且在内部,它使用与完整 git merge
相同的引擎,但您通常可以将其视为 "copy commit"。6 将 I
复制到I'
,我们现在需要将J
复制到J'
:
I'-J' <-- HEAD
/
...--G--H--K--L <-- master
\
I--J <-- feature
现在,由于 I'-J'
是我们的 new-and-improved 提交,我们希望我们的 Git 放弃 支持这些新的原件.为了实现这一点,我们的 Git 将简单地从提交 J
中剥离标签 feature
并使其指向 J'
。完成后,我们的 Git 可以 re-attach HEAD
到 b运行ch name feature
:
I'-J' <-- feature (HEAD)
/
...--G--H--K--L <-- master
\
I--J [abandoned]
因为我们找到提交是从b运行ch名称开始,找到它存储的哈希ID,然后查找提交,当我们查看这个存储库时,它将 看起来像 我们以某种方式更改了两个提交。我们看到的不是 J
然后是 I
,而是 J'
然后是 I'
。但是如果我们仔细观察,我们会发现这些是不同的哈希 ID。
5某些形式的 git rebase
确实,实际上 运行 git cherry-pick
。其他人(主要是旧形式的变基)没有,但非常接近地模拟它。
6例外情况是在复制过程中遇到合并冲突,但我们不会在这里讨论。
分布式存储库
回到开头,我提到要记住的最重要的事情是 Git 是 分布式的 并且有不止一个副本存储库。
在我们的例子中,假设我们有本地 Git,在我们的机器上,另一个 Git 在 GitHub 上。 (在某种程度上,另一个 Git 在哪里并不重要——GitHub、Bitbucket、GitLab、公司服务器等等:它们的工作方式与他们都有一个Git在一些IP地址后面。最大的区别是托管公司通过网站添加在他们自己的用户界面上,web界面是不同的。)
无论如何,我们 Git 打电话给 他们的 Git——不管 "they" 是谁——通过 URL,其中 t运行 转化为一些 IP 地址和我们提供给服务器的路径名。 Git 将此 URL 存储在一个名称下,Git 调用 远程 。任何遥控器的标准名字都是 origin
,因此我们将在此处使用它作为名称。
由于 Git 在 origin
是 一个 Git 存储库,它有 自己的 b运行ch 名称。我们的 b运行ch 名称,在我们的 Git 中,是 我们的。他们的是 他们的。他们不需要匹配!特别是,当我们向我们的 b运行ches 添加提交时,我们将 "get ahead" 他们的 b运行ches.
让我们从在我们的机器上根本没有 Git 存储库开始(也许我们必须得到一台新笔记本电脑,或其他)。我们将 git clone
他们的 Git 存储库:
git clone <url>
我们的 Git 在我们的计算机上将创建一个新的 totally-empty 存储库,并添加名称 origin
来存储 URL。然后它将调用他们的 Git 并让他们列出他们的 b运行ch 名称,以及由这些 b运行ch 名称编辑的提交的哈希 ID select。他们将为这些 b运行ches 发送这些 tip 提交。
对于每个提交哈希 ID,我们的 Git 会说:是的,我想要那个提交。假设在 master
上提交 H
。他们有义务提供该提交的 parent、G
。我们的 Git 将检查:我是否已经提交了 parent? 当然,我们的 Git 的 object 数据库是空的,所以我们不这样做。所以我们也会要求 G
。他们的 Git 将提供 F
,我们将接受它,依此类推,最后,我们将获得 他们的每一次提交 (好吧,除了任何被遗弃的,如果他们有的话——有时他们有!)。
现在我们将有:
...--G--H
在我们的提交数据库中。但是我们还没有任何 names。我们已经完成了从他们那里获得的提交——他们只有 master
和提交 H
及其历史,我们得到了所有这些——所以我们的 Git 与他们的 Git 断开了连接.现在我们的 Git 获取了他们所有的 b运行ch 名称,也就是 master
,并且 通过将我们的远程名称 [=] 重命名为 每个236=],在前面,用斜线隔开:
...--G--H <-- origin/master
这些 origin/*
个名字是我们 Git 的 remote-tracking 个名字。他们为我们记住了他们Git的b运行ch名字
对于它的最后一招,我们的 git clone
运行s git checkout master
。我们实际上 没有 a master
b运行ch,但是如果你要求 Git 查看 b运行ch你没有,你的 Git 将尝试 creating b运行ch 来自相应的 remote-tracking 名称。我们确实有 origin/master
并且它 select 提交 H
,所以我们的 Git 创建 我们的 master
指向 H
,并在此处附上我们的 HEAD
:
...--G--H <-- master (HEAD), origin/master
我们的 git clone
现在完成了。
如果我们现在创建新的提交,它们会以通常的方式添加:
...--G--H <-- origin/master
\
I--J <-- master (HEAD)
我们现在可以使用git push
向他们发送提交。当我们这样做时,我们选择两件事:
- 要发送哪些提交,以及
- 要设置哪个 运行ch 名称
如果我们运行 git push origin master
,我们选择提交J
到发送(因为我们的名字master
selects 提交 J
) 和名称 master
到 set(因为我们说 master
)。
如果我们愿意,我们可以运行git push origin master:dev
,发送J
并要求他们设置 他们的 dev
而不是他们的 master
。你通常不会这样做——更典型的是,你会先创建自己的 dev
,这样你就可以在 dev
上创建 J
,然后再创建 git push origin dev
——但它是有用的例子。我们 发送 提交我们拥有的(大概他们没有),然后我们的 git push
要求他们设置 他们的 b运行ch 个名字。与我们的 Git 不同,他们在这里没有 remote-tracking 名字! Remote-tracking 名称是 git clone
和 git fetch
的 属性。
为了发送它们 J
,我们必须先发送它们 I
。我们也会提供给他们H
,但他们已经有了,所以他们说不,谢谢,我有那个。这让我们的 Git 压缩得非常好(我们知道他们也提交了 H
和 所有 更早的 提交!)我们向他们发送 I
和 J
。然后我们要求他们设置他们的 b运行ch name(s).
如果 server-side 存储库是 共享的 ——如果我们不是唯一使用它的人——他们的 master
可能已经获得了新的提交,因为我们最后和他们谈谈。例如,也许其他人 运行 git push origin master
。所以我们发送给他们I-J
,如果他们有:
...--G--H <-- master
\
I--J
然后我们要求他们将 master
设置为指向 J
,他们可能会说 ok,不要。他们现在有:
...--G--H--I--J <-- master
在 他们的 存储库中。我们的 Git 将相应地更新我们的 origin/master
。但如果他们有:
...--G--H--K <-- master
\
I--J
他们服从了我们的礼貌要求,他们最终会:
K [abandoned]
/
...--G--H--I--J <-- master
因为 any Git finds 提交的方式是从最后开始并向后工作。现在结尾是J
,谁的parent是I
,谁的parent是H
。从H
到K
是没办法的:箭头都是one-way,指向后面。所以在这种情况下他们会说 不,我不会设置我的 master
.
您的 Git 会将其显示为错误:
! rejected (non-fast-forward)
这意味着你必须得到他们的新提交来自他们,并将它们合并到你的工作中,例如,通过git merge
或 git rebase
.
或者,您可以向他们发送命令,而不是礼貌的请求:将您的 master
设置为 J
! 如果他们服从此命令,他们 将 失去提交 K
。您很有可能无法再从他们那里取回它。制作 K
的人可能会生气(但是——无论如何你可以希望——制作 K
的人仍然在 他们的 克隆中拥有它)。
拉取请求和 GitHub 的可点击按钮
拉取请求不是 Git 的东西,而是 GitHub 和其他托管服务提供商提供的东西。他们为您提供了一种跨他们所谓的 forked 存储库进行合并的方法。 (一个分支实际上只是一个添加了一些特殊功能的克隆,最重要的是这些拉取请求。)
GitHub 在合并 PR 时提供三个选项。一个是直接 git merge
,即使 fast-forward 是可能的,也进行真正的合并。一个名为 "rebase and merge",即使没有必要也会执行 git rebase
,总是 将所有提交复制到新链,然后执行 fast-forward-新链的样式合并。最后一个称为 "squash and merge",相当于 运行ning git merge --squash
.
由于GitHub 的压缩和变基风格合并总是产生新的散列 ID,您现在可以遇到我们之前观察到的相同问题,压缩后合并.
删除合并(或任何其他提交)
在您自己的 存储库中,您可以完全控制所有 b运行ch 名称。您可以使任何 b运行ch 名称指向 any 提交。
那么假设你有这个:
I--M <-- master (HEAD)
/ /
...--G--H--I' <-- origin/master
其中 I
是您之前在 master
上的原始提交,您将其发送到某个地方,然后将其复制到 I
并放在 他们的 master
。你的 origin/master
仍然指向这个副本 I'
;你的 master
指向你的合并 M
,第一个 parent 是 I
,第二个 parent 是 I'
.
如果您 git fetch origin; git merge origin/master
或者如果您只是 git pull
运行 和 git fetch origin master; git merge FETCH_HEAD
,您就会得到这个。同样,问题是 运行s origin
决定 复制 你的提交,无论出于何种原因。
如果您想放弃合并M
,您现在可以运行:
git reset --hard HEAD^ # or HEAD~1 or HEAD~
这将销毁所有未提交的工作,因此请确保您没有任何工作! reset
操作,除了它所做的所有其他事情(在这种情况下会破坏未提交的工作)之外,还说 移动当前 b运行ch 名称 。当前 b运行ch 名称(现在,master
)将 select 的新提交是您在命令行中命名的提交。
您可以使用原始哈希 ID,它始终有效:只需从 git log
输出中剪切它,并且您已经说过 我想要我当前的 b运行ch 名称到 select 提交 。或者,您可以使用名称:a b运行ch 名称,例如,selects 名称指向的提交。这里,我们使用HEAD
,意思是当前commit,然后加上一个后缀:^
,意思是第一个[=1057] =],或者~1
,意思是倒数一个first-parent,是一样的
这意味着Git会找到合并M
,然后看它的第一个parent,也就是I
。那就是我们说到 git reset --hard
的地方,所以我们最终会得到:
__M [abandoned]
/ /
I / <-- master (HEAD)
/ /
...--G--H--I' <-- origin/master
有点难画——提交M
仍然存在,但是没有人指向它,所以我们无法找到它。把它拿出来,结果更清楚:
I <-- master (HEAD)
/
...--G--H--I' <-- origin/master
请注意,这有效 因为 我们从未将提交 M
提交给任何其他 Git。只有 我们的 master
知道如何找到提交 M
。我们可以重置它,它不会再回来了。
如果我们做了发送M
给其他人Git,例如,通过git push origin master
,他们 会提交 M
。我们可以尝试将其重置为远离 our Git,这会工作一段时间,但 origin/master
在我们的存储库中,并且 thei master
在 他们的 克隆中,仍然会有合并提交 M
。要摆脱它,我们必须说服 他们 也改变 他们的 master
。
一般来说,一旦你分享了一个提交,你就会从其他人那里再次获得它 Git。 Git 是为 添加 提交而构建的,而不是将它们带走;默认共享操作是 添加到我的 collection,如果合适则合并 。