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。因此 namemaster 特定于一个特定的 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运行的历史分歧。现在 masterL 开始并向后工作,而 devJ 开始并向后工作。在 dev 上有两个提交在 masternot,在 master 上有两个提交notdev 上,然后 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 mastergit 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有这个合并提交记住两个 之前的提交,LJ:

             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 KJ,或 both LI。然后它将从它选择显示的任何一个返回。

在大多数情况下,Git 会在任何 parent 之前显示所有 children,也就是说,最终,它将显示所有 I 的四个, JKL 并且只有 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>

没有变化无变化(从HH,通过master)与一些变化(从 HJ 通过 dev),然后将这些更改应用到 H,将成为 J 中的任何内容。 Git 说:好吧,那太简单了 而不是进行 new 提交,它只是 移动 名称 master forwards,与通常的向后方向相反。 (事实上​​ , Git 确实确实向后工作 - 从 JI 再到 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 开始,然后往回走。 如果这就是您所需要和关心的,那很好。但是如果你想要一个 真正的 合并提交——这样你可以稍后区分 masterdev,例如——你可以告诉 Git: 即使您可以进行 fast-forward 而不是合并,无论如何也要进行真正的合并。 Git 将继续比较 HH,然后将 HJ 进行比较,合并更改并进行 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 mastergit merge dev。这次你不需要 --no-ff,因为 master 上有一个提交 而不是 dev 上的 ,即 K,当然还有 dev that are not onmaster, namelyL-M-N. The *merge base* this time is shared commitJ(notHHis also shared, butJ 上的提交是 更好)。所以 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?

我们从 JK 做了什么改变? (这对你来说是一个练习,reader。)

假设 Git 能够自行合并更改,此合并操作将成功,生成:

...--F--G--H------K--------O   <-- master (HEAD)
            \    /        /
             I--J--L--M--N   <-- dev

其中新合并提交 OJ-vs-K 更改与 J-vs-N 更改结合在一起。 master 的历史将从 O 开始,包括 NM 以及 LK 以及 JIH 等等。 dev 的历史将从 N 开始,包括 MLJ(不是 K!)和 IH 等等。 Git 总是 向后 ,从 child 到 parent。合并 let / make Git 沿 两条 同时 向后工作(但一次向您展示一个,在某些情况下顺序取决于您提供给 git log).

的参数

在您描述的过程中,您想要 'merge' 从存储库中的单个目录进行更改。这与 git 的工作方式相反,这就是为什么您无法保持良好的历史记录。

重要的是要了解您所做的并不是真正的合并[1]。合并提交有两个(或更多)父提交,并且以这种方式保留了完整的历史记录。公平地说,git 在使用某些术语时倾向于 "flexible" 不一致;有一些它称为“合并”的操作不会导致合并提交。但即使使用这些操作,您也会合并整个内容 - 而不是单个目录。

如果您有不同的模块 - 或者,无论您如何描述它们,不同目录中的不同内容 - 独立更改(如果您在 branches/environments 之间单独提升它们,这当然适用),它们应该分开回购。我想如果它对您有帮助,您可以将它们收集为 'parent' 存储库的子模块,以便能够从单个 url 或其他任何地方克隆。但除此之外,如果由于某种原因这种类型的分离不可接受,您可能需要考虑 git 是否是满足您特定源代码控制要求的最佳工具。


[1] 我还可以争论关于合并的语义,因为如果 dev 和 qa 都有更改,那么来自 qa 的更改将被覆盖和丢失——这通常不是合并所需要的。但是你可能会争辩说变化总是从开发流向质量保证,所以它不适用;无论如何,git 有时确实将一个分支与另一个分支的破坏描述为合并(即“我们的合并策略”)。