说,我有一个空的存储库,只有一个初始提交:
* - Initial commit (master)
然后我在本地创建一个develop
分支并对其进行一些提交。但在我将其推送到远程(尚未与master合并)之前,我使用master
更新了remote
,这也为master
分支提供了一些提交,我得到了这个:
* - Update something (master)
| * - Make some changes to the new feature (develop)
| |
* | - Make some changes (master)
| * - Add new feature (develop)
|/
* - Initial commit (master)
所以,我的问题是,在我将develop
分支推送到远程之后,我怎么能够而且应该如何将develop
与master
分支合并为一个提交而不复制任何develop
分支到master
的历史记录,但保留develop
分支上的所有历史记录。
答案 0 :(得分:3)
简短的回答是你不能 - 但你可以能够做你想要的,这里的主要问题是一堆Git做的事情(并谈论)奇怪和不同的事情,技术定义繁琐(人们经常出错,这只会增加混乱)。
让我水平重绘您的提交(这对于StackOverflow上的文本文章更有效),并给它们一个字母的名称:
A--C--E <-- master
\
B--D <-- develop
也就是说,A
是初始提交,C
和E
是当前&#34; on&#34;的另外两个提交。 (可从中找到)分支master
,B
和D
是您提交的两个提交,其中&#34; on&#34; (可以从develop
找到。
这是一个技巧问题:哪个分支提交A
? master
或develop
?
这是一个技巧问题,因为在Git中,它位于两个分支上。
事实上,名称 master
和develop
几乎无关紧要。重要的是提交。我向右侧绘制的名称仅指向一个特定提交。我们将一个特定的提交称为分支的 tip ,然后我们和Git使用每个提交中存储的父信息向后工作,以查找先前的提交。
这些父链接,E
返回C
返回A
,或D
返回B
返回A
,仅限单向:从孩子到父母。它们从我们最初期望的方式开始倒退,并且它们确定来自任何给定提交的可达哪些提交。从C
开始,我们可以自己(当然)和C
联系A
;这就是全部。从E
开始,我们可以到达E
,C
和A
- 这是分支master
的完整内容,因为master
指向提交E
。从D
开始,我们可以D
,B
和A
;由于develop
指向D
,因此该分支的完整内容为develop
。
但这意味着提交A
位于两个分支上。这就是Git的方式:提交在任意数量的分支上 - 有时甚至没有 - 并且分支集合基于 reachability ,通过每个提交中的父链接。 名称仅用于让我们从图中开始,并且除了分支名称之外还有其他名称,例如标记名称。让我们进行两次新的临时提交F
和G
,仅用于说明:
tag: T
|
v
F--G <-- tempbranch
/
A--C--E <-- master
\
B--D <-- develop
现在可以通过G
查找提交tempbranch
;提交F
可通过其标记T
和通过tempbranch
找到,提交A-C-E-F-G
全部位于tempbranch
。现在让我们完全删除名称 tempbranch
:
tag: T
|
v
F--G
/
A--C--E <-- master
\
B--D <-- develop
现在我们有一个有趣的案例:提交G
根本无法访问。仍可通过代码F
访问提交T
。 Git最终会垃圾收集提交G
,让它真正消失。在此之前,如果您已将哈希ID保存在某个地方,您仍然可以查看它,甚至可以“恢复”#34;给它一个分支或标签名称。 (Git还将这些ID保存在名为 reflogs 的内容中。每个引用都有一个 - 例如分支和标记名称 - 以及HEAD
的一个。这些保留了默认情况下30天,即使删除了他们的名字。)
如果我们也删除标记T
,那么提交F
也有资格进行垃圾回收,最终我们回到之前的五次提交。同时,由于F
和G
不是可见,你不会看到它们:Git会表现得好像他们不在那里,除非你挖掘以某种方式提高他们的哈希ID。
尽管如此,让我们来看看常规的普通合并。至少在Git中的合并提交就像任何其他提交一样,但有一个例外:它有两个或更多个父项,而不是一个。 (大多数合并只有两个,并且对于三个或更多的父合并没有什么特别有用。)
让我们看看在提交图方面将develop
合并到master
的常规合并会发生什么:
A--C--E--F <-- master
\ /
B--D-´ <-- develop
名称develop
继续指向提交D
,但名称master
现在指向新的合并提交 F
。提交F
点回两者提交E
和 D
。
当Git进行可达性计算时,它遵循 all 父链接(同时,就像它一样)。所以在这一点上,图中的每个提交都在分支master
上。提交A-B-D
(仍然)在develop
上,如果您git checkout develop
并编写新提交,则新提交将仅在develop
上, name develop
自动转移到指向新提交:
A--C--E--F <-- master
\ /
B--D-´--G <-- develop
这是处理这些事情的常用方法。与提交F
一起使用的树(工作树副本)将B
和D
的更改合并到master
中:Git接受提交{ {1}} vs提交A
以查找E
和master
与A
之间的更改,以查找D
的更改,并将它们组合在一起develop
。然后,它将F
和 E
记录为D
的父项,以便合并记住合并了哪个提交。
尽管如此,我们现在可以谈论Git所谓的&#34;壁球合并&#34;。你可以通过运行F
来获得这些,然后再做一些。具体来说,您之后还必须运行git merge --squash
。
壁球合并实际上根本不是合并。如果我们绘制后效应,我们得到这个图:
git commit
提交A--C--E--F <-- master
\
B--D <-- develop
的内容与真实合并的内容相同。像往常一样,新提交继续F
,即master
被移动到指向新提交。关键区别在于新提交不指向合并提交,但仅指向master
的上一个提示。
起初,这似乎正是你想要的。然而,它确实有一个非常大的缺点。让我们为master
添加一些新的提交:
develop
现在,当我们想要将A--C--E--F <-- master
\
B--D----G--H--I <-- develop
中的更改合并到G-H-I
时,我们需要再次合并。如果我们有正常合并,那么master
指向F
以及D
- Git会知道如何执行此操作。但是,如果没有真正的合并,Git将再次回到共同提交E
,并比较A
- vs - A
,然后F
-vs - A
,试图将两个变化结合起来。
但我们已经进行了I
更改!他们在B-D
。如果我们有一个真正的合并,Git会知道这一点,而不必解决这个问题。
如果我们很幸运 - 我们常常是这样--Git无论如何都会想到这一点。但是,提交链越长,随着越来越多的变化,Git会越来越不可能自己解决这个问题。
因此,对于squash合并的通常规则是,如果我们要放弃合并的分支,我们应该将它们设为 ,例如,删除名称完全F
。删除名称后,无法访问的提交最终会被垃圾收集。
当你使用真正的合并时,缺点是你看到了一切,因为Git同时跟随父母的方式。也就是说,如果你运行:
develop
您会看到所有git log master
,A
,B
,C
,D
和E
(按日期混合和排序) ,通常,虽然你可以控制这个)。但是,避免这种情况有一个非常重要的特征。你可以运行:
F
相反,要查看提交而不遵循任何&#34;额外&#34;来自合并的父母。此git log --first-parent master
视图会向您显示仅提交--first-parent
,并跳过A-C-E-F
。这是因为B-D
限制了图遍历以跳过每个合并提交的第二个父项。
答案 1 :(得分:2)
请注意,在更新{{1}之后,正确的工作流程(因为您尚未推送develop
)在develop
之上 rebase master
}}
master
现在您可以推送,因为git checkout master
git pull
git checkout develop
git rebase master
内容基于最新develop
内容(并经过验证)。
您可以首先在本地解决任何冲突(从master
之上重播develop
),然后再推送。
答案 2 :(得分:0)
您可以在master
上创建新的单个提交,其中包含develop
使用get merge --squash
引入的所有更改,然后提交结果。
merge --sqauash
会将develop
中的所有更改应用到索引,并准备一条提交消息,其中包含来自develop
的所有连接提交消息,但它会不创建提交。然后由git commit
决定编辑准备好的提交消息。