GIT : 如何在两个不同的分支中维护相同的提交历史
GIT : How to maintain same history of commits in two different branches
使用 gitbash 合并和提交。
先解释一下基本结构。所以我们有了 origin/dev,我们开始研究它。更改完成后,我们将更改推送到 origin/dev.
然后使用gitbash将dev合并到qa,我在下面做
git checkout qa
# for all recent changes in origin/qa(similar have parallel origing/dev and uat as well.)
git pull
# for checking out changes in dev to my local qa space which will be merged
# to origin/qa by the below commands
git checkout dev -- directorynameToCheckoutCodeFrom
git commit
git push
所以这是合并发生时任何 2 个不同环境之间通常遵循的过程。
所以我的问题是我在 DEV 中对 5 个问题进行了 5 次提交,所有提交 ID 都不同。因此,当我在 1 中提交所有五个更改时,当我从 DEV 合并到 QA 时,我得到 1 个提交 ID,并且所有更改都将合并到 1 中。在 UAT 中合并时也会发生同样的情况。
有什么方法可以在不同环境之间保持相同的历史记录。真正的问题出现在 QA 中,我们可能会在 10 天内合并 4-5 次,而在 UAT 中,我们希望保持完整并且每月只合并一次。在那种情况下,如果我们将所有从 QA 到 UAT 的更改作为一次提交提交,则 QA 中不同的历史记录将丢失。有什么办法解决这个问题吗?
看了一些网上的帖子,但无法理解,我理解的唯一方法是像我们在 DEV env 中所做的那样频繁提交。对于 1 issue merge in dev>then qa>the uat 这是保留相同历史的唯一方法,我的理解是正确的。
你可以试试
git checkout qa
git merge dev --no-ff
git push
git merge dev --no-ff
主要用于将所有开发分支提交到 qa 及其历史。
没有提交历史。只有 次提交; 次提交 是 历史。
每个提交都由哈希 ID 唯一标识。可以说,哈希 ID 是 提交的真实名称。如果您有 that 提交,则您有 that 哈希 ID。如果您有 that 哈希 ID,则您有 that 提交。读出丑陋的大哈希 ID,看看它是否在 "all the commits that I have in this repository" 的数据库中:即,看看 Git 是否知道它。如果是,则您有 that 提交。例如,b5101f929789889c2e536d915698f58d5c5c6b7a
是一个有效的哈希 ID:它是 Git 存储库中 Git 的提交。如果您的 Git 存储库中有该哈希 ID,则您有 that 提交。
人们通常根本不输入或使用这些哈希 ID。 Git 使用它们,但 Git 是计算机程序,而不是人类。人类在这些事情上做得不好——我必须剪切并粘贴上面的哈希 ID,否则我会弄错——所以人类使用不同的方式开始。人类使用 b运行ch 名称。但是许多不同的 Git 存储库都有 master
并且这个 master
并不总是(或永远!)意味着我在上面输入的大丑陋的哈希 ID。因此 name 像 master
特定于一个特定的 Git 存储库,而哈希 ID 则不是。
现在,每个提交都会存储一些东西。提交存储的内容包括 和 该提交的所有文件的快照,以便您稍后可以将其取回。它还包括 做出 承诺的人的姓名和电子邮件地址,以便您可以判断该表扬谁或责备谁。它包括一条日志消息:why 提交的人说他们做了那个提交。但是 - 这是第一个棘手的部分 - 几乎每个提交还包括至少一个 哈希 ID,这是 在 此特定提交之前的提交。
所以,如果你有 b5101f929789889c2e536d915698f58d5c5c6b7a
,那么你有这个:
$ git cat-file -p b5101f929789889c2e536d915698f58d5c5c6b7a | sed 's/@/ /'
tree 3f109f9d1abd310a06dc7409176a4380f16aa5f2
parent a562a119833b7202d5c9b9069d1abb40c1f9b59a
author Junio C Hamano <gitster pobox.com> 1548795295 -0800
committer Junio C Hamano <gitster pobox.com> 1548795295 -0800
Fourth batch after 2.20
Signed-off-by: Junio C Hamano <gitster pobox.com>
(tree
行表示与此提交一起保存的快照。您可以在此处忽略它。) parent
行给出了提交的哈希 ID 之前 b5101f929789889c2e536d915698f58d5c5c6b7a
.
如果你有 b5101f929789889c2e536d915698f58d5c5c6b7a
,你几乎肯定也有 a562a119833b7202d5c9b9069d1abb40c1f9b59a
。较晚提交的历史 是 较早提交。
如果我们用一个大写字母替换这些丑陋的大哈希 ID,1 我们可以更轻松地绘制此类历史记录:
... <-F <-G <-H
其中 H
是一长串提交中的 last 提交。由于H
持有G
的哈希ID,我们不需要写下G
的大丑哈希ID,我们可以直接写下H
的哈希.我们用它来让 Git 在 H
本身中找到 G
的 ID。如果我们想要 F
,我们使用 H
查找 G
来查找 F
的 ID,这让 Git 检索 F
。
但是我们还是要记下那个last哈希ID。这就是 b运行ch 名称的用武之地。像 master
这样的 B运行ch 名称作为我们保存 last 的哈希 ID 的方式提交。
要进行 new 提交,我们 Git 将 H
的哈希 ID 保存在 我们的新提交。我们 Git 保存了一张快照、我们的姓名和电子邮件地址以及所有其他信息——"the rest" 包括一个时间戳,即我们 时的精确秒数Git 做了这一切。现在 Git 计算所有这些数据的实际哈希 ID,包括时间戳。提交现在保存在我们所有提交的数据库中,并且 Git 给了我们一个新的哈希 ID I
:
...--F--G--H <-- master
\
I
我们Git自动将I
的哈希ID写入我们的名字master
:
...--F--G--H--I <-- master
并且我们添加了新的历史记录,它保留了所有现有的历史记录。
1当然,如果我们像这样只使用一个大写字母,我们将 运行 无法在世界任何地方创建提交,在仅创建 26 次提交之后。这就是 Git 的哈希 ID 如此之大的原因。它们拥有 160 位,因此可能的提交数或其他 objects 是 2160 或 1,461,501,637,330,902,918,203,684,832,716,283,019,655,932,542,976。事实证明,这还不够,Git 可能会移动到更大的散列,可以容纳 objects 的 79,228,162,514,264,337,593,543,950,336 倍。虽然第一个数字大到足以枚举宇宙中的所有原子,但有一些特定的攻击很麻烦,所以 256 位散列是个好主意。参见 How does the newly found SHA-1 collision affect Git?
这告诉你如何拥有相同的历史
历史是 提交。要在两个 b运行 中具有 相同的 历史记录,您需要两个 b运行ch 名称指向相同的提交:
...--F--G--H--I <-- master, dev
现在 master
中的历史是:开始于I
,显示 I
,然后返回 H
并显示 H
,然后返回 G
... 同样, dev
中的历史是:从 I
开始,显示 I
,然后返回 H
...
当然,这不是您想要的。您想要的是 发散 ,然后 再次收敛 的历史记录。这就是 b运行ches 真正的意义所在:
...--F--G--H <-- master
\
I <-- dev
此处 dev
中的历史记录从 I
开始(结束?),然后回到 H
,然后是 G
,依此类推。 master
中的历史从 H
开始(结束?),回到 G
,依此类推。当我们添加更多提交时,我们会添加更多历史记录,如果我们这样做:
K--L <-- master
/
...--F--G--H
\
I--J <-- dev
然后两个b运行的历史分歧。现在 master
从 L
开始并向后工作,而 dev
从 J
开始并向后工作。在 dev
上有两个提交在 master
上 not,在 master
上有两个提交not 在 dev
上,然后 H
后面的所有内容都在 both b运行ches.
这种分歧——在某些 b运行ch 上 不是 的提交——是工作线分歧的地方。 b运行ch names 仍然只记得 one commit each,特别是 tip 或者last 每条开发线的提交。 Git 将从这次提交开始,通过保存的哈希 ID,并使用该提交保存的 parent 哈希 ID 向后走,一次一个提交。线条重新连接的地方,历史重新连接。这就是存储库中的全部内容,下一节除外。
合并组合历史(和快照)
您现在可以做的是合并提交。进行合并提交的主要方法是使用 git merge
命令。这有两个部分:
- 组合工作,其中Git找出每条开发线发生了什么变化;和
- 制作一个合并提交,这是一个只有一个特殊功能的提交。
要进行合并,您首先要选择一个 b运行ch 尖端。你运行git checkout master
或git checkout dev
这里。无论你选择哪一个,这就是你现在的提交,并且 Git 将特殊名称 HEAD
附加到那个 b运行ch 名称以记住你选择的是哪个:
K--L <-- master (HEAD)
/
...--F--G--H
\
I--J <-- dev
现在你 运行 git merge
并给它一个标识符来选择提交到 merge。如果你在 master
= L
,你会想要使用 dev
= J
作为合并的提交:
git merge dev # or git merge --no-ff dev
Git 现在将像往常一样遍历图表以找到最佳的 shared 提交——在 both 上的最佳提交b运行ches,用作此合并的起点。在这里,那是提交 H
,其中两个 b运行ches 首先分歧。
现在 Git 会将使用提交 H
保存的快照(合并基础)与当前提交 L
中的快照进行比较。无论 有何不同,您都必须在 master
上进行更改。 Git 将这些更改放入一个列表中:
git diff --find-renames <hash-of-H> <hash-of-L> # what we changed
Git 重复这个但是 他们的 提交 J
:
git diff --find-renames <hash-of-H> <hash-of-J> # what they changed
现在Git合并了两组变化。无论我们改变了什么,我们都希望保持改变。无论他们改变了什么,我们也想使用这些改变。如果他们更改了 README.md
而我们没有,我们将接受他们的更改。如果我们更改了一个文件而他们没有,我们将接受我们的更改。如果我们都更改了 同一个 文件,Git 将尝试合并这些更改。如果 Git 成功,我们将对该文件进行合并更改。
无论如何,Git 现在采用所有组合更改并将它们应用到 H
中的快照。如果没有冲突,Git 会自动根据结果进行新的提交。如果存在 和 冲突,Git 仍将组合更改应用到 H
,但给我们留下了混乱的结果,我们必须修复它并执行最后的承诺;但我们假设没有冲突。
Git 现在进行具有一项特殊功能的新提交。而不是只是记住我们之前的提交L
,Git有这个合并提交记住两个 之前的提交,L
和 J
:
K--L <-- master (HEAD)
/ \
...--F--G--H M
\ /
I--J <-- dev
然后,一如既往,Git 更新我们的 current b运行ch 以记住新提交的哈希 ID:
K--L
/ \
...--F--G--H M <-- master (HEAD)
\ /
I--J <-- dev
请注意,如果我们通过 运行ning git checkout dev; git merge master
进行合并,Git 将执行 相同的两个差异 并获得 same merge commit M
(好吧,只要我们在同一秒完成,以便时间戳匹配)。但是 Git 会将 M
的哈希 ID 写入 dev
而不是写入 master
.
无论如何,如果我们现在询问master
的历史,Git将从M
开始。然后我将返回 both L
and J
并显示 both他们。 (它必须选择一个先显示,并且 git log
有很多标志可以帮助您选择 哪个 先显示。)然后它会从任何一个返回它首先选择,因此它现在必须显示 both K
和 J
,或 both L
和 I
。然后它将从它选择显示的任何一个返回。
在大多数情况下,Git 会在任何 parent 之前显示所有 children,也就是说,最终,它将显示所有 I
的四个, J
、K
和 L
并且只有 H
可以显示。因此,从这里开始,Git 将显示 H
,然后是 G
,依此类推 — 现在只有一个链可以返回,一次提交一个。但请注意,当您从合并中返回时,您 运行 进入 哪个提交显示下一个 问题。
git merge
并不总是进行合并提交
假设你有这样的历史:
...--F--G--H <-- master
\
I--J <-- dev
也就是说,没有分歧,dev
只是严格领先 master
。你 git checkout master
到 select 提交 H
:
...--F--G--H <-- master (HEAD)
\
I--J <-- dev
然后 git merge dev
将您自合并基础以来所做的工作与 他们 自合并基础以来所做的工作相结合。
合并基础是最好的共享提交。也就是说,我们从 H
开始并根据需要继续返回,也从 dev
开始并根据需要继续返回,直到我们到达一个共同的起点。所以从J
我们回到I
然后回到H
,从H
我们就静静地坐在H
直到 J
回到这里。
合并基础,换句话说,是当前提交。如果Git 运行:
git diff --find-renames <hash-of-H> <hash-of-H>
没有变化。 无变化(从H
到H
,通过master
)与一些变化(从 H
到 J
通过 dev
),然后将这些更改应用到 H
,将成为 J
中的任何内容。 Git 说:好吧,那太简单了 而不是进行 new 提交,它只是 移动 名称 master
forwards,与通常的向后方向相反。 (事实上 , Git 确实确实向后工作 - 从 J
到 I
再到 H
- 为了弄清楚这一点。它只记得它从 J
.) 所以你在这里得到的,默认情况下,是这样的:
...--F--G--H
\
I--J <-- dev, master (HEAD)
当Git能够像这样向前滑动master
这样的标签时,它称该操作为fast-forward。当您使用 git merge
本身执行此操作时,Git 将其称为 fast-forward 合并 ,但它根本不是真正的合并。 Git 真正做的是 签出 提交 J
,并使 master
指向 J
。
在很多情况下,这是可以的!现在的历史是:对于master
,从J
开始,然后往回走。对于 dev
,从 J
开始,然后往回走。 如果这就是您所需要和关心的,那很好。但是如果你想要一个 真正的 合并提交——这样你可以稍后区分 master
和 dev
,例如——你可以告诉 Git: 即使您可以进行 fast-forward 而不是合并,无论如何也要进行真正的合并。 Git 将继续比较 H
与 H
,然后将 H
与 J
进行比较,合并更改并进行 new 提交:
...--F--G--H------K <-- master (HEAD)
\ /
I--J <-- dev
现在你得到了一个真正的合并提交 K
,其中 两个 parent 是合并提交所必需的。第一个 parent 像往常一样是 H
,第二个是 J
,对于合并提交来说也是如此。 master
的历史现在 包括 dev
的历史,但仍然 不同于 dev
的历史], 因为 dev
的历史不包括提交 K
.
请注意,如果您现在切换回 dev
并进行更多提交,结果如下所示:
...--F--G--H------K <-- master
\ /
I--J--L--M--N <-- dev (HEAD)
您现在可以再次 git checkout master
和 git merge dev
。这次你不需要 --no-ff
,因为 master
上有一个提交 而不是 dev
上的 ,即 K
,当然还有 dev that are not on
master, namely
L-M-N. The *merge base* this time is shared commit
J(not
H—
His also shared, but
J 上的提交是 更好)。所以 Git 将通过以下方式合并更改:
git diff --find-renames <hash-of-J> <hash-of-K> # what did we change?
git diff --find-renames <hash-of-J> <hash-of-N> # what did they change?
我们从 J
到 K
做了什么改变? (这对你来说是一个练习,reader。)
假设 Git 能够自行合并更改,此合并操作将成功,生成:
...--F--G--H------K--------O <-- master (HEAD)
\ / /
I--J--L--M--N <-- dev
其中新合并提交 O
将 J
-vs-K
更改与 J
-vs-N
更改结合在一起。 master
的历史将从 O
开始,包括 N
和 M
以及 L
和 K
以及 J
和 I
和 H
等等。 dev
的历史将从 N
开始,包括 M
和L
和 J
(不是 K
!)和 I
和 H
等等。 Git 总是 向后 ,从 child 到 parent。合并 let / make Git 沿 两条 行 同时 向后工作(但一次向您展示一个,在某些情况下顺序取决于您提供给 git log
).
的参数
在您描述的过程中,您想要 'merge' 从存储库中的单个目录进行更改。这与 git 的工作方式相反,这就是为什么您无法保持良好的历史记录。
重要的是要了解您所做的并不是真正的合并[1]。合并提交有两个(或更多)父提交,并且以这种方式保留了完整的历史记录。公平地说,git 在使用某些术语时倾向于 "flexible" 不一致;有一些它称为“合并”的操作不会导致合并提交。但即使使用这些操作,您也会合并整个内容 - 而不是单个目录。
如果您有不同的模块 - 或者,无论您如何描述它们,不同目录中的不同内容 - 独立更改(如果您在 branches/environments 之间单独提升它们,这当然适用),它们应该分开回购。我想如果它对您有帮助,您可以将它们收集为 'parent' 存储库的子模块,以便能够从单个 url 或其他任何地方克隆。但除此之外,如果由于某种原因这种类型的分离不可接受,您可能需要考虑 git 是否是满足您特定源代码控制要求的最佳工具。
[1] 我还可以争论关于合并的语义,因为如果 dev 和 qa 都有更改,那么来自 qa 的更改将被覆盖和丢失——这通常不是合并所需要的。但是你可能会争辩说变化总是从开发流向质量保证,所以它不适用;无论如何,git 有时确实将一个分支与另一个分支的破坏描述为合并(即“我们的合并策略”)。
使用 gitbash 合并和提交。
先解释一下基本结构。所以我们有了 origin/dev,我们开始研究它。更改完成后,我们将更改推送到 origin/dev.
然后使用gitbash将dev合并到qa,我在下面做
git checkout qa
# for all recent changes in origin/qa(similar have parallel origing/dev and uat as well.)
git pull
# for checking out changes in dev to my local qa space which will be merged
# to origin/qa by the below commands
git checkout dev -- directorynameToCheckoutCodeFrom
git commit
git push
所以这是合并发生时任何 2 个不同环境之间通常遵循的过程。
所以我的问题是我在 DEV 中对 5 个问题进行了 5 次提交,所有提交 ID 都不同。因此,当我在 1 中提交所有五个更改时,当我从 DEV 合并到 QA 时,我得到 1 个提交 ID,并且所有更改都将合并到 1 中。在 UAT 中合并时也会发生同样的情况。
有什么方法可以在不同环境之间保持相同的历史记录。真正的问题出现在 QA 中,我们可能会在 10 天内合并 4-5 次,而在 UAT 中,我们希望保持完整并且每月只合并一次。在那种情况下,如果我们将所有从 QA 到 UAT 的更改作为一次提交提交,则 QA 中不同的历史记录将丢失。有什么办法解决这个问题吗?
看了一些网上的帖子,但无法理解,我理解的唯一方法是像我们在 DEV env 中所做的那样频繁提交。对于 1 issue merge in dev>then qa>the uat 这是保留相同历史的唯一方法,我的理解是正确的。
你可以试试
git checkout qa
git merge dev --no-ff
git push
git merge dev --no-ff
主要用于将所有开发分支提交到 qa 及其历史。
没有提交历史。只有 次提交; 次提交 是 历史。
每个提交都由哈希 ID 唯一标识。可以说,哈希 ID 是 提交的真实名称。如果您有 that 提交,则您有 that 哈希 ID。如果您有 that 哈希 ID,则您有 that 提交。读出丑陋的大哈希 ID,看看它是否在 "all the commits that I have in this repository" 的数据库中:即,看看 Git 是否知道它。如果是,则您有 that 提交。例如,b5101f929789889c2e536d915698f58d5c5c6b7a
是一个有效的哈希 ID:它是 Git 存储库中 Git 的提交。如果您的 Git 存储库中有该哈希 ID,则您有 that 提交。
人们通常根本不输入或使用这些哈希 ID。 Git 使用它们,但 Git 是计算机程序,而不是人类。人类在这些事情上做得不好——我必须剪切并粘贴上面的哈希 ID,否则我会弄错——所以人类使用不同的方式开始。人类使用 b运行ch 名称。但是许多不同的 Git 存储库都有 master
并且这个 master
并不总是(或永远!)意味着我在上面输入的大丑陋的哈希 ID。因此 name 像 master
特定于一个特定的 Git 存储库,而哈希 ID 则不是。
现在,每个提交都会存储一些东西。提交存储的内容包括 和 该提交的所有文件的快照,以便您稍后可以将其取回。它还包括 做出 承诺的人的姓名和电子邮件地址,以便您可以判断该表扬谁或责备谁。它包括一条日志消息:why 提交的人说他们做了那个提交。但是 - 这是第一个棘手的部分 - 几乎每个提交还包括至少一个 哈希 ID,这是 在 此特定提交之前的提交。
所以,如果你有 b5101f929789889c2e536d915698f58d5c5c6b7a
,那么你有这个:
$ git cat-file -p b5101f929789889c2e536d915698f58d5c5c6b7a | sed 's/@/ /'
tree 3f109f9d1abd310a06dc7409176a4380f16aa5f2
parent a562a119833b7202d5c9b9069d1abb40c1f9b59a
author Junio C Hamano <gitster pobox.com> 1548795295 -0800
committer Junio C Hamano <gitster pobox.com> 1548795295 -0800
Fourth batch after 2.20
Signed-off-by: Junio C Hamano <gitster pobox.com>
(tree
行表示与此提交一起保存的快照。您可以在此处忽略它。) parent
行给出了提交的哈希 ID 之前 b5101f929789889c2e536d915698f58d5c5c6b7a
.
如果你有 b5101f929789889c2e536d915698f58d5c5c6b7a
,你几乎肯定也有 a562a119833b7202d5c9b9069d1abb40c1f9b59a
。较晚提交的历史 是 较早提交。
如果我们用一个大写字母替换这些丑陋的大哈希 ID,1 我们可以更轻松地绘制此类历史记录:
... <-F <-G <-H
其中 H
是一长串提交中的 last 提交。由于H
持有G
的哈希ID,我们不需要写下G
的大丑哈希ID,我们可以直接写下H
的哈希.我们用它来让 Git 在 H
本身中找到 G
的 ID。如果我们想要 F
,我们使用 H
查找 G
来查找 F
的 ID,这让 Git 检索 F
。
但是我们还是要记下那个last哈希ID。这就是 b运行ch 名称的用武之地。像 master
这样的 B运行ch 名称作为我们保存 last 的哈希 ID 的方式提交。
要进行 new 提交,我们 Git 将 H
的哈希 ID 保存在 我们的新提交。我们 Git 保存了一张快照、我们的姓名和电子邮件地址以及所有其他信息——"the rest" 包括一个时间戳,即我们 时的精确秒数Git 做了这一切。现在 Git 计算所有这些数据的实际哈希 ID,包括时间戳。提交现在保存在我们所有提交的数据库中,并且 Git 给了我们一个新的哈希 ID I
:
...--F--G--H <-- master
\
I
我们Git自动将I
的哈希ID写入我们的名字master
:
...--F--G--H--I <-- master
并且我们添加了新的历史记录,它保留了所有现有的历史记录。
1当然,如果我们像这样只使用一个大写字母,我们将 运行 无法在世界任何地方创建提交,在仅创建 26 次提交之后。这就是 Git 的哈希 ID 如此之大的原因。它们拥有 160 位,因此可能的提交数或其他 objects 是 2160 或 1,461,501,637,330,902,918,203,684,832,716,283,019,655,932,542,976。事实证明,这还不够,Git 可能会移动到更大的散列,可以容纳 objects 的 79,228,162,514,264,337,593,543,950,336 倍。虽然第一个数字大到足以枚举宇宙中的所有原子,但有一些特定的攻击很麻烦,所以 256 位散列是个好主意。参见 How does the newly found SHA-1 collision affect Git?
这告诉你如何拥有相同的历史
历史是 提交。要在两个 b运行 中具有 相同的 历史记录,您需要两个 b运行ch 名称指向相同的提交:
...--F--G--H--I <-- master, dev
现在 master
中的历史是:开始于I
,显示 I
,然后返回 H
并显示 H
,然后返回 G
... 同样, dev
中的历史是:从 I
开始,显示 I
,然后返回 H
...
当然,这不是您想要的。您想要的是 发散 ,然后 再次收敛 的历史记录。这就是 b运行ches 真正的意义所在:
...--F--G--H <-- master
\
I <-- dev
此处 dev
中的历史记录从 I
开始(结束?),然后回到 H
,然后是 G
,依此类推。 master
中的历史从 H
开始(结束?),回到 G
,依此类推。当我们添加更多提交时,我们会添加更多历史记录,如果我们这样做:
K--L <-- master
/
...--F--G--H
\
I--J <-- dev
然后两个b运行的历史分歧。现在 master
从 L
开始并向后工作,而 dev
从 J
开始并向后工作。在 dev
上有两个提交在 master
上 not,在 master
上有两个提交not 在 dev
上,然后 H
后面的所有内容都在 both b运行ches.
这种分歧——在某些 b运行ch 上 不是 的提交——是工作线分歧的地方。 b运行ch names 仍然只记得 one commit each,特别是 tip 或者last 每条开发线的提交。 Git 将从这次提交开始,通过保存的哈希 ID,并使用该提交保存的 parent 哈希 ID 向后走,一次一个提交。线条重新连接的地方,历史重新连接。这就是存储库中的全部内容,下一节除外。
合并组合历史(和快照)
您现在可以做的是合并提交。进行合并提交的主要方法是使用 git merge
命令。这有两个部分:
- 组合工作,其中Git找出每条开发线发生了什么变化;和
- 制作一个合并提交,这是一个只有一个特殊功能的提交。
要进行合并,您首先要选择一个 b运行ch 尖端。你运行git checkout master
或git checkout dev
这里。无论你选择哪一个,这就是你现在的提交,并且 Git 将特殊名称 HEAD
附加到那个 b运行ch 名称以记住你选择的是哪个:
K--L <-- master (HEAD)
/
...--F--G--H
\
I--J <-- dev
现在你 运行 git merge
并给它一个标识符来选择提交到 merge。如果你在 master
= L
,你会想要使用 dev
= J
作为合并的提交:
git merge dev # or git merge --no-ff dev
Git 现在将像往常一样遍历图表以找到最佳的 shared 提交——在 both 上的最佳提交b运行ches,用作此合并的起点。在这里,那是提交 H
,其中两个 b运行ches 首先分歧。
现在 Git 会将使用提交 H
保存的快照(合并基础)与当前提交 L
中的快照进行比较。无论 有何不同,您都必须在 master
上进行更改。 Git 将这些更改放入一个列表中:
git diff --find-renames <hash-of-H> <hash-of-L> # what we changed
Git 重复这个但是 他们的 提交 J
:
git diff --find-renames <hash-of-H> <hash-of-J> # what they changed
现在Git合并了两组变化。无论我们改变了什么,我们都希望保持改变。无论他们改变了什么,我们也想使用这些改变。如果他们更改了 README.md
而我们没有,我们将接受他们的更改。如果我们更改了一个文件而他们没有,我们将接受我们的更改。如果我们都更改了 同一个 文件,Git 将尝试合并这些更改。如果 Git 成功,我们将对该文件进行合并更改。
无论如何,Git 现在采用所有组合更改并将它们应用到 H
中的快照。如果没有冲突,Git 会自动根据结果进行新的提交。如果存在 和 冲突,Git 仍将组合更改应用到 H
,但给我们留下了混乱的结果,我们必须修复它并执行最后的承诺;但我们假设没有冲突。
Git 现在进行具有一项特殊功能的新提交。而不是只是记住我们之前的提交L
,Git有这个合并提交记住两个 之前的提交,L
和 J
:
K--L <-- master (HEAD)
/ \
...--F--G--H M
\ /
I--J <-- dev
然后,一如既往,Git 更新我们的 current b运行ch 以记住新提交的哈希 ID:
K--L
/ \
...--F--G--H M <-- master (HEAD)
\ /
I--J <-- dev
请注意,如果我们通过 运行ning git checkout dev; git merge master
进行合并,Git 将执行 相同的两个差异 并获得 same merge commit M
(好吧,只要我们在同一秒完成,以便时间戳匹配)。但是 Git 会将 M
的哈希 ID 写入 dev
而不是写入 master
.
无论如何,如果我们现在询问master
的历史,Git将从M
开始。然后我将返回 both L
and J
并显示 both他们。 (它必须选择一个先显示,并且 git log
有很多标志可以帮助您选择 哪个 先显示。)然后它会从任何一个返回它首先选择,因此它现在必须显示 both K
和 J
,或 both L
和 I
。然后它将从它选择显示的任何一个返回。
在大多数情况下,Git 会在任何 parent 之前显示所有 children,也就是说,最终,它将显示所有 I
的四个, J
、K
和 L
并且只有 H
可以显示。因此,从这里开始,Git 将显示 H
,然后是 G
,依此类推 — 现在只有一个链可以返回,一次提交一个。但请注意,当您从合并中返回时,您 运行 进入 哪个提交显示下一个 问题。
git merge
并不总是进行合并提交
假设你有这样的历史:
...--F--G--H <-- master
\
I--J <-- dev
也就是说,没有分歧,dev
只是严格领先 master
。你 git checkout master
到 select 提交 H
:
...--F--G--H <-- master (HEAD)
\
I--J <-- dev
然后 git merge dev
将您自合并基础以来所做的工作与 他们 自合并基础以来所做的工作相结合。
合并基础是最好的共享提交。也就是说,我们从 H
开始并根据需要继续返回,也从 dev
开始并根据需要继续返回,直到我们到达一个共同的起点。所以从J
我们回到I
然后回到H
,从H
我们就静静地坐在H
直到 J
回到这里。
合并基础,换句话说,是当前提交。如果Git 运行:
git diff --find-renames <hash-of-H> <hash-of-H>
没有变化。 无变化(从H
到H
,通过master
)与一些变化(从 H
到 J
通过 dev
),然后将这些更改应用到 H
,将成为 J
中的任何内容。 Git 说:好吧,那太简单了 而不是进行 new 提交,它只是 移动 名称 master
forwards,与通常的向后方向相反。 (事实上 , Git 确实确实向后工作 - 从 J
到 I
再到 H
- 为了弄清楚这一点。它只记得它从 J
.) 所以你在这里得到的,默认情况下,是这样的:
...--F--G--H
\
I--J <-- dev, master (HEAD)
当Git能够像这样向前滑动master
这样的标签时,它称该操作为fast-forward。当您使用 git merge
本身执行此操作时,Git 将其称为 fast-forward 合并 ,但它根本不是真正的合并。 Git 真正做的是 签出 提交 J
,并使 master
指向 J
。
在很多情况下,这是可以的!现在的历史是:对于master
,从J
开始,然后往回走。对于 dev
,从 J
开始,然后往回走。 如果这就是您所需要和关心的,那很好。但是如果你想要一个 真正的 合并提交——这样你可以稍后区分 master
和 dev
,例如——你可以告诉 Git: 即使您可以进行 fast-forward 而不是合并,无论如何也要进行真正的合并。 Git 将继续比较 H
与 H
,然后将 H
与 J
进行比较,合并更改并进行 new 提交:
...--F--G--H------K <-- master (HEAD)
\ /
I--J <-- dev
现在你得到了一个真正的合并提交 K
,其中 两个 parent 是合并提交所必需的。第一个 parent 像往常一样是 H
,第二个是 J
,对于合并提交来说也是如此。 master
的历史现在 包括 dev
的历史,但仍然 不同于 dev
的历史], 因为 dev
的历史不包括提交 K
.
请注意,如果您现在切换回 dev
并进行更多提交,结果如下所示:
...--F--G--H------K <-- master
\ /
I--J--L--M--N <-- dev (HEAD)
您现在可以再次 git checkout master
和 git merge dev
。这次你不需要 --no-ff
,因为 master
上有一个提交 而不是 dev
上的 ,即 K
,当然还有 dev that are not on
master, namely
L-M-N. The *merge base* this time is shared commit
J(not
H—
His also shared, but
J 上的提交是 更好)。所以 Git 将通过以下方式合并更改:
git diff --find-renames <hash-of-J> <hash-of-K> # what did we change?
git diff --find-renames <hash-of-J> <hash-of-N> # what did they change?
我们从 J
到 K
做了什么改变? (这对你来说是一个练习,reader。)
假设 Git 能够自行合并更改,此合并操作将成功,生成:
...--F--G--H------K--------O <-- master (HEAD)
\ / /
I--J--L--M--N <-- dev
其中新合并提交 O
将 J
-vs-K
更改与 J
-vs-N
更改结合在一起。 master
的历史将从 O
开始,包括 N
和 M
以及 L
和 K
以及 J
和 I
和 H
等等。 dev
的历史将从 N
开始,包括 M
和L
和 J
(不是 K
!)和 I
和 H
等等。 Git 总是 向后 ,从 child 到 parent。合并 let / make Git 沿 两条 行 同时 向后工作(但一次向您展示一个,在某些情况下顺序取决于您提供给 git log
).
在您描述的过程中,您想要 'merge' 从存储库中的单个目录进行更改。这与 git 的工作方式相反,这就是为什么您无法保持良好的历史记录。
重要的是要了解您所做的并不是真正的合并[1]。合并提交有两个(或更多)父提交,并且以这种方式保留了完整的历史记录。公平地说,git 在使用某些术语时倾向于 "flexible" 不一致;有一些它称为“合并”的操作不会导致合并提交。但即使使用这些操作,您也会合并整个内容 - 而不是单个目录。
如果您有不同的模块 - 或者,无论您如何描述它们,不同目录中的不同内容 - 独立更改(如果您在 branches/environments 之间单独提升它们,这当然适用),它们应该分开回购。我想如果它对您有帮助,您可以将它们收集为 'parent' 存储库的子模块,以便能够从单个 url 或其他任何地方克隆。但除此之外,如果由于某种原因这种类型的分离不可接受,您可能需要考虑 git 是否是满足您特定源代码控制要求的最佳工具。
[1] 我还可以争论关于合并的语义,因为如果 dev 和 qa 都有更改,那么来自 qa 的更改将被覆盖和丢失——这通常不是合并所需要的。但是你可能会争辩说变化总是从开发流向质量保证,所以它不适用;无论如何,git 有时确实将一个分支与另一个分支的破坏描述为合并(即“我们的合并策略”)。