首页 > 解决方案 > 希望将子分支的更改合并到分支中,但都包含相同的提交

问题描述

绝对是 git 的新手,因此这里可能是一个非常愚蠢的问题:

我有一个 master 分支,我开始对其进行增强。大约有 20 次提交,我意识到我应该创建一个单独的分支并完成我的工作。

因此,我从 master 创建了一个新的 main/dev 分支,然后我将 master 分支指针重置为指向初始 repo 提交,这是我开始工作之前的点:

$ git reset --hard <commit hash>

完成此操作后,我立即想到了以下工作流程(不确定从哪里来--lol):

我的想法是沿着我自己的“主”分支而不是在 master 上进行所有主要开发。然后我会从这个主分支创建新的功能/增强分支,我猜是开发分支?然后我最终会将新的增强工作重新合并到“主”分支中,然后当我自己的所有“主”工作完成后将其合并到主分支中(不确定这是否是一个好的工作流程......似乎不是所以)。

因此,在这个好主意之后,我立即想“嗯……让我从 main 中创建另一个单独的特定“增强”分支。

我想我可能错误地创建了“子”增强分支。在“主要”上,我刚刚使用

$ git checkout -b enhancement

不是

$ git checkout -b enhancement main

现在,当我在“主”分支上并运行时:

$ git merge enhancement

我得到以下信息:

Already up to date.

当我跑

$ git show-branch 

据我了解,它用于显示所有分支及其所有提交,我得到以下输出:

! [enhancement] Test enhancing, fixing, and cleaning.
 * [main] Test enhancing, fixing, and cleaning.
  ! [master] Grammar improvements
---
+*  [enhancement] Test enhancing, fixing, and cleaning.
+*  [enhancement^] Bug Fix: addition of path id to updateTodo method.
+*  [enhancement~2] Additon of tests to TodoTest.java and TodoResourceTest.java for ValidForUpdate Todo annotation.
+*  [enhancement~3] Refactoring: moved all custom validation messages to properties file and all associated changes. Also cleaned up imports here and there.
+*  [enhancement~4] Refactoring: moved PastOrPresentValidator to an inner class of the PresentOrPast annotation class.
+*  [enhancement~5] Addition of ValidForUpdate custom constraint Todo object annotation and freinds.
+*  [enhancement~6] Fixed problem of same four tests failing when run with all tests in class but passing individually. Changed init from 'BeforeClass' to 'BeforeEach' fixed it.
+*  [enhancement~7] Added properties to enable forked & threaded test processing in surefire plugin.
+*  [enhancement~8] Added propertey for maven surefire plugin version.
+*  [enhancement~9] Refactoring of tests. Move of mian Todo template test object and friends to central location of TestTodoCreater.
+*  [enhancement~10] Refactoring to get PresetntOrPast validation error message from properties file. Also some slight refactoring by replacing validation error message string literals in tests with string constant.
+*  [enhancement~11] Addition of new 'PresentOrPast' validator and accompanying test changes. Also a little bit of test clean-up/refactoring.
+*  [enhancement~12] Addition of 'updateTodo' api method and accompanying tests. Tightening up of validation annotations in TodoResource. Addition of Todo field validations and all accompanying tests. Major refactoring of TodoResourceTest.
+*  [enhancement~13] Backed out previous commit's changes and updated dropwizard version to 1.3.13. Changes backed out due to what appears to be lack of Hibernate Validator 6 support in the dropwizard-testing library.
+*  [enhancement~14] Moved ValidationMessages.properties file. Still not yet using.
+*  [enhancement~15] updated hibernate-validator and java validation-api to latest versions.
+*  [enhancement~16] Changes required to add 'update' method to TodoDAO and some code cleanup.
+*  [enhancement~17] Changes in order to get maven to run ALL (Junit5) tests with 'mvn test'. Specific changes to get TodoIntegrationSmokeTest to run by adding test-config.yml.
+*  [enhancement~18] Completion of TodoIntegratoinSmokeTest for pre-existing api methods.
+*  [enhancement~19] Initial commit of TodoIntegratoinTest with working testCreateTodo method.
+*  [enhancement~20] Some minor code cleanup up and refactoring.
+*  [enhancement~21] completed tests for pre-existing api methods.
+*  [enhancement~22] Cleaned up TodoResource test and TestUtils.
+*  [enhancement~23] All API methods test-covered 'except' for deleteTodo.
+*  [enhancement~24] Validation for TododResource#getTodos and accompanying test changes .
+*  [enhancement~25] Began shoring-up code with parameter validation and accompanying test changes.
+*  [enhancement~26] Completed bulk of TodoDaoTest but missing update implementation and some odds and ends.
+*  [enhancement~27] A little more clean up/refactoring of TodoDAO associated test classes.
+*  [enhancement~28] Clean up of TodoDAOTest2
+*  [enhancement~29] Slight modification to server rootPath value.
+*  [enhancement~30] Addition of beginning of Todo DAO test classes. Creates a PostgresSQL Docker container and laods the schema with flyway.
+*  [enhancement~31] Replaced TodoResource string literal test path values with static UriBuilders.
+*  [enhancement~32] Changes in this commit are in support of getting the TodoResourceTest completed by covering all original api methods(given with task).
+*  [enhancement~33] Small change. Renamed Todo JSON test class to 'TodoRepresentationTest' to use Dropwizard-specific terminology. Removed non-used import.
+*  [enhancement~34] Conversion of Todo class into an immutable 'value' class. Added tests for serializing and deserializing Todo objects to and from JSON.
+*+ [master] Grammar improvements

除非我误解了这个命令的输出,否则看起来“main”包含与“enhancement”相同的所有提交,这使得看起来我对增强分支所做的任何提交也以某种方式被提交到主枝。

看起来我一直在同时向两个分支工作并提交相同的东西。

跑步后也是

git show-ref --head

我得到:

b43e3c2b3d19a4a19497cf78e3909727f25796a2 HEAD
b43e3c2b3d19a4a19497cf78e3909727f25796a2 refs/heads/enhancement
b43e3c2b3d19a4a19497cf78e3909727f25796a2 refs/heads/main
...

这表明 HEAD 同时指向两个分支?这怎么发生的???

此外,当我运行命令以显示两个分支之间的不同提交时:

$ git log --left-right --graph --cherry-pick --oneline main...enhancement

没有输出。所以这就是说这两个分支之间根本没有区别。所以我真的很想知道我在这里做了什么。

我喜欢 git,但她又让我感到困惑 :(。

我已经阅读了 git 中的“分支”,据我所知,分支只是指向特定提交的指针,而 HEAD 指向的任何分支都是您当前工作的分支,因此在提交时只会添加连续的在 HEAD 之上提交,同时将 HEAD 推进到该分支中的最新提交。

所以我的增强分支的 HEAD 应该在包含提交 main 的主分支的 HEAD 之前。

我不明白的是,如果我在过去 17 次提交中一直在增强分支中工作,那么当我切换到任一分支时,HEAD 会指向同一个提交。

所以我期望看到的是不同的提交历史,增强包含更多的提交,但我没有,他们有相同的提交!

请有人怜悯并通过向我指出我在这里做错了什么来阐明一些观点。我一直在努力解决这个问题。

任何和所有的帮助都无法估量!

标签: gitbranchbranching-strategy

解决方案


您的问题的根源是分支不是分层的。

提交具有图形结构。特别是,几乎每个提交都有一个父级,有些有两个父级。一些古怪的提交可能有三个或更多,并且至少有一个——在存储库中进行的第一个提交——必然没有父级。

正是这种父/子关系形成了图形结构。我们可能从一个很小的存储库开始,只有三个提交:

A <-B <-C

三个提交的实际名称是三个丑陋的哈希 ID,但是我们可以使用单个大写字母来代替这些哈希 ID,只要我们不进行超过几十个提交(我们会得到多少取决于你的字母表中有多少个字母:例如,你有 O 和 Ö 吗?)。最后一次提交,C,包含提交的实际哈希 ID B,所以我们说它C 指向 BBC的父母。提交B包含A的哈希 ID 作为B的父级,因此B指向A. A是第一次提交,所以它不能更早地指向任何东西,也没有;在这里,动作停止。

要向此存储库添加新的提交,我们将保存一个新的源代码快照,添加我们的姓名和电子邮件地址以及日期和时间戳,保存一条日志消息,并保存C的哈希 ID,所有这些都到一个新的提交,它被自动分配了它自己的新的、唯一的哈希 ID,我们将调用它D,现在我们有:

A <-B <-C <-D

但是分支名称在哪里出现?好吧,请记住,每个提交都有一些大而丑陋的随机散列 ID。以下是按某种顺序排列的四个实际哈希 ID:

7c20df84bd21ec0215358381844274fa10515017
14fe4af084071803ab4f16e6841ff64ba7351071
c62bc49139f1d18e922fc98e35bb08b1aadbcafc
9b274e28871b3e4a4109582a34625df5fddc91c8

其中哪一个我应该调用 commit A,我应该调用哪一个B,等等?如果我想从最后开始——最后一次提交时,就像 Git 所做的那样,我是从 开始,还是从 开始,还是什么?14fe...9b27...

我们可以查看这四个提交中的每一个,以查看它们存储的父哈希 ID。例如:

$ git cat-file -p 9b274e28871b3e4a4109582a34625df5fddc91c8 | sed 's/@/ /'
tree c921299d1381a3bd6486ef999e3cc432118d1d72
parent e46249f73ebddca06cf16c01e8de1f310360c856
parent f3eda90ffc10f9152e7492a34408a9f5e4c28b0f
author Junio C Hamano <gitster pobox.com> 1564776722 -0700
committer Junio C Hamano <gitster pobox.com> 1564776722 -0700

Merge branch 'jc/log-mailmap-flip-defaults'

Hotfix for making "git log" use the mailmap by default.

* jc/log-mailmap-flip-defaults:
  log: really flip the --mailmap default
  log: flip the --mailmap default unconditionally

告诉我 commit9b274e28871b3e4a4109582a34625df5fddc91c8两个父母,e46249f73ebddca06cf16c01e8de1f310360c856并且f3eda90ffc10f9152e7492a34408a9f5e4c28b0f,这两个都不是我列出的四个之一。如果我查看存储库中的每个提交,并收集它们的所有parent行,我可以——<em>最终,经过大量工作——找出哪些提交在末尾。但这很慢。

Git 对此的回答是分支名称。分支名称仅包含一 (1) 个提交的哈希 ID:

$ git rev-parse master
7c20df84bd21ec0215358381844274fa10515017

啊哈,这是我上面列出的四个提交之一!该提交是Git 应该考虑的最后一个master提交。如果我们看里面:

$ git cat-file -p 7c20df84bd21ec0215358381844274fa10515017 | sed 's/@/ /'
tree 8858576e734aa4f1cd9b45e207e7ee2937488d13
parent 14fe4af084071803ab4f16e6841ff64ba7351071
author Junio C Hamano <gitster pobox.com> 1564776744 -0700
committer Junio C Hamano <gitster pobox.com> 1564776744 -0700

Git 2.23-rc1

Signed-off-by: Junio C Hamano <gitster pobox.com>

我们看到这个提交只有一个父级,14fe4af084071803ab4f16e6841ff64ba7351071,这是我的四个哈希 ID 中的另一个。链的末端也是如此master,另一个开头的丑陋的大哈希 ID14fe...是下一个:

...--G--H   <-- master

H7c20...提交,G14fe...提交。让我们看看G's parents,这一次使用git rev-parse及其特殊的“打印所有父哈希 ID”语法:

$ git rev-parse 14fe4af084071803ab4f16e6841ff64ba7351071^@
c62bc49139f1d18e922fc98e35bb08b1aadbcafc
d61e6ce1dda7f4b11601a0de549feefbcec55779

c62b...一个是我四个列表中的第三个;还有一个不在我的列表中。这个提交是一个合并提交,我们可以看看它的其余部分:

$ git cat-file -p 14fe4af084071803ab4f16e6841ff64ba7351071 | sed 's/@/ /'
tree 06a0b1de4cb3857cdd23a939a857dc720240496b
parent c62bc49139f1d18e922fc98e35bb08b1aadbcafc
parent d61e6ce1dda7f4b11601a0de549feefbcec55779
author Junio C Hamano <gitster pobox.com> 1564776723 -0700
committer Junio C Hamano <gitster pobox.com> 1564776723 -0700

Merge branch 'sg/fsck-config-in-doc'

Doc update.

* sg/fsck-config-in-doc:
  Documentation/git-fsck.txt: include fsck.* config variables

我们可以调用c62bc49139f1d18e922fc98e35bb08b1aadbcafccommit F。我们可能想要一些其他的字母而不是Efor d61e6ce1dda7f4b11601a0de549feefbcec55779; 让我们使用I

...--F--G--H
       /
 ...--I

现在让我们输入分支名称 master。该名称本身标识了 commit H,并且仅直接 表示commit H,因此:

...--F--G--H   <-- master
       /
 ...--I

我们可以从G's log message 中看到 Junio Hamano通过 G运行:

git merge sg/fsck-config-in-doc

因此,让我们也使用该名称,指向 commit I

...--F--G--H   <-- master
       /
 ...--I   <-- sg/fsck-config-in-doc

有趣的是 commits F, G, and Hare all on master... 但是提交“在一个分支上”意味着我们可以从最后开始——在这种情况下是在 commit ——H然后向后工作并达到该承诺。所以从 开始H,我们走回我们能做到的唯一一步,然后看看G。从G,我们可以退回到FI。所以不仅是 commit Fon master,commit 也是I

同时,sg/fsck-config-in-doc是一个分支名称。它标识 commit I。所以 commitI不仅是 on master,它也是 on sg/fsck-config-in-doc

这是关于分支名称的第一件事。它们只是标签。它们只是作为让 Git 开始查看图表的方式。分支名称标识一个特定的提交,我们称之为该分支的提示提交。该提交标识了一些父提交或提交,并且这些提交也在分支上。通过移动或查看父母,我们会找到更多的父母;那些也在树枝上。

要在某个分支上进行新的提交,我们首先让 Git 选择该特定分支名称作为当前分支。为了创建一个新的分支名称,我们让 Git 选择一些提交(默认为当前提交)并创建一个新名称,指向那个特定的提交。所以如果我们有:

...--F--G--H   <-- master
       /
 ...--I   <-- sg/fsck-config-in-doc

并要求 Git 创建一个新名称topic,我们得到:

...--F--G--H   <-- master, topic
       /
 ...--I   <-- sg/fsck-config-in-doc

提交通过I,加上那些彻底I,现在都在 master topic上。如果我们选择topic作为当前分支,Git 会H根据需要提取提交并将名称附加HEAD到名称topic

...--F--G--H   <-- master, topic (HEAD)
       /
 ...--I   <-- sg/fsck-config-in-doc

当我们进行新的提交时,它的父级将是H——前一个提示topic——并且新的提交将成为名称所topic标识的那个:

             J   <-- topic (HEAD)
            /
...--F--G--H   <-- master
       /
 ...--I   <-- sg/fsck-config-in-doc

请注意,HEAD仍然附加到分支名称,但分支名称已移动

移动分支名称不会影响存储库中的任何提交!它们在您移动名称之前都存在,并且在您移动名称后它们继续存在。无论您将名称移动到何处,图形本身都保持不变(尽管为了使来自名称的箭头指向正确的提交,您有时可能希望以不同的方式绘制图形)。

假设我们决定topic指出I而不是J

             J
            /
...--F--G--H   <-- master
       /
 ...--I   <-- sg/fsck-config-in-doc, topic (HEAD)

提交J仍然存在,但现在很难找到。如果您从 开始master并向后工作,您会得到H,然后G,然后FI,依此类推。你无法到达J:箭头指向错误的方向。topic如果你从现在的尖端开始I,然后向后工作,你就无法到达J。提交J实际上被放弃了。

特定命令使用这些名称执行特定操作

正如您已经看到的,git checkout -b

  • 创建一个新的分支名称,指向一些现有的提交,然后
  • 执行git checkout新名称,以便HEAD附加到该名称,并且现有提交成为您当前的提交。

同时,git reset --hard

  • 接受一个可选的名称或哈希 ID,或者任何git rev-parse可以处理的东西,真的,然后把它变成一个哈希 ID,然后

  • 获取当前分支名称(附加HEAD到的分支名称)并使其指向该特定提交。

  • 因为这个是--hard,所以它也重新设置了索引和工作树。其他口味的git reset工作方式不同。事实上,有一些git resets根本不移动当前分支名称,永远,但是git reset --soft,git reset --mixedgit reset --harddo。

    但是请注意,如果您使用 name HEAD,或者git reset使用其默认值HEAD,则该git rev-parse步骤会出现当前的 commit。然后git reset将当前分支从其当前提交 ... 移动到其当前提交,毕竟它不会移动它。这就是为什么git reset --hard重置您对索引和工作树所做的任何更新但未提交的好方法的原因。

git merge命令变得有点复杂——不是git reset更好,因为它有许多不同的操作模式——但它所做的事情是从查看提交图开始的。你选择一些提交——可能是分支名称,分支名称标识提示提交——并git merge检查提交图,以确定如何从当前提交和你命名的提交向后工作,以达到一个共同的, 共享提交。

在这种情况下,您已经进行了如下设置:

...--F--G--H   <-- master
            \
             I--J--...--N   <-- main (HEAD), enhancement

然后运行git merge enhancement。该名称enhancement标识 commit N。当前分支名称也是如此main因此,两个分支上的共享提交是 commit N。因此没有什么可做的。

您现在可以将名称移动main到 commit H,使用:

git reset --hard master

和以前一样,--hard将重置索引和工作树,因此请确保您没有尚未保存的内容;最终结果可绘制为:

...--F--G--H   <-- master, main (HEAD)
            \
             I--J--...--N   <-- enhancement

如果您现在运行git merge enhancement,Git 将从 向后走H,并从 向后走N,以找到第一个共享提交。这就是提交H本身,它位于所有三个分支上。然后,如果您允许,Git 将裁定这种合并太微不足道而无法努力工作,并简单地移动名称main以便它标识 commit N,同时也git checkout提交N

Git将此称为快进操作,最后,图片为:

...--F--G--H   <-- master
            \
             I--J--...--N   <-- main (HEAD), enhancement

这就是你刚才所在的地方!

但是,您可以强制Git 进行真正的合并。在我们开始之前,让我们看一下 Git 本身必须进行真正合并的情况。

真正的合并

假设您不是从上述内容开始,而是从以下内容开始:

...--F--G--H   <-- master (HEAD)

然后你运行:

git checkout -b branch

它添加了一个新的分支名称branch,标识当前提交H,并附HEAD加到它:

...--F--G--H   <-- master, branch (HEAD)

现在你做出一个新的提交II的父母会H像往常一样。当前名称,br1被重写以指向新的提交I,作为此git commit操作的最后一步:

...--F--G--H   <-- master
            \
             I   <-- branch (HEAD)

为了好玩(或者是为了写出我想要的字母M,真的),让我们再做一个仅 on 的提交branch,而不是所有那些以 结尾的共享提交H

...--F--G--H   <-- master
            \
             I--J   <-- branch (HEAD)

现在让我们git checkout master检查提交H并重新附加HEAD

             I--J   <-- branch
            /
...--F--G--H   <-- master (HEAD)

然后在master. 出于某种难以理解的原因,我会将它们绘制在新的顶行上(这实际上不是必需的):

             K--L   <-- master (HEAD)
            /
...--F--G--H
            \
             I--J   <-- branch

现在我们运行git merge branch(或,除了提交日志消息之外,它将执行相同的操作)。Git 必须从开始并向后工作,从开始并向后工作,以找到最佳共享提交,即.git merge hash-of-JLJH

此共享提交称为合并基础。既然 Git 知道哪个提交是合并基础,它会检查合并基础是否是当前提交L和/或另一个提交J,因为这些是特殊的简单情况。不是,所以这不是一个简单的合并。

合并的目标是合并更改,但提交HLJ所有都只是有快照。因此,Git 首先必须转换L并转换J为更改。通过从最佳共同起点(合并基础)开始,两组更改都应适用该共同起点。为了获得更改,Git 实际上运行了两个 git diff --find-renames命令:

  • git diff --find-renames hash-of-H hash-of-L告诉 Git 我们改变了什么master
  • git diff --find-renames hash-of-H hash-of-J告诉 Git 他们(好吧,我们)在branch.

合并过程现在尝试合并更改,将合并后的更改从合并基础应用到快照H。如果一切顺利——如果变化结合得很好——Git 从结果中创建一个新的快照。这个新快照有两个父级。第一个是通常的。我们在master,这意味着 commit L,所以新合并的第一个父级是L。我们命名的另一个提交是J,因此新合并的第二个父提交是J。提交后,Git 像往常一样将其新的哈希 ID 写入当前分支名称。

那么最终结果——如果一切顺利的话——是:

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

我们已经产生了真正的合并。

强制与--no-ff

让我们回到这个设置:

...--F--G--H   <-- master, main (HEAD)
            \
             I--J--...--N   <-- enhancement

并运行git merge --no-ff enhancement--no-ff告诉 Git: 即使合并基础是无论如何都要进行真正的合并。H

Git 现在将继续进行这两个差异(内部)。第一个比较Hvs H。当然,这会产生一个空的变更集。第二个比较Hvs NH当然,这会产生将 in 中的快照转换为in 中的快照所需的所有更改N

然后,Git 将第一个变更集——说“什么都不做”的那个——与第二个结合起来。结果只是第二个变更集。将此应用于 中的快照H,Git 获取 中的快照N。不过,合并工作正常,所以现在 Git 进行了一个新的合并提交,我们将其称为O. O的第一个父级是H,它的第二个父级是N,我们有:

             ,------------------O   <-- main (HEAD)
            /                  /
...--F--G--H   <-- master     /
            \                /
             I--J----...----N   <-- enhancement

你应该使用哪个?

有时,快进是要走的路。有时,真正的合并是要走的路。让我们再看一下我之前使用的 Git 的实际 Git 存储库的图表:

...--F--G--H   <-- master
       /
 ...--I   <-- sg/fsck-config-in-doc

我们现在可以拿走这个名字了sg/fsck-config-in-doc。它所做的只是让我们直接访问提交I,但我们可以从H. 所以让我们删除名称:

...--F--G--H   <-- master
       /
 ...--I

这里的想法是分支名称无关紧要(除了查找最终提交)。只有提交很重要。快进操作通常使两个名称标识同一个提交。如果您打算在将来取消一个或两个名字,您将永远不会知道快进曾经发生过。

这是好事还是坏事?好吧,假设您将来真的想知道O,您的强制合并--no-ff是一个合并操作。这意味着通过提交N是一些副业,副业终于准备好迎接黄金时间并一次性添加。因此,您确实需要合并提交,即使git merge默认为快进。

另一方面,也许你未来不会在意​​提交N是某种副业。副业与主要工作都无关紧要:只有你在工作。也许之前的一些特定提交N也无关紧要。也许您希望将所有提交连续提交,以便于查看,或者您可能希望这样做:

...--F--G--H   <-- master, main (HEAD)
            \
             I--J--...--N   <-- enhancement

并将其变成:

...--F--G--H--O--P   <-- main

并且只是丢弃所有的I-through-N提交,保留两个新的提交O并且P紧凑地表示“正确地做第一件事”然后“正确地做第二件事”,而不是你在I, J,K等中所做的漫无边际的事情。在这种情况下,你根本不想要合并。

你未来将拥有的是一个带有一些指定提示提交的提交图。这就是你将拥有的一切。您今天可以确定提交图中的提交集;你明天可以决定保留哪些名字;和未来——你会祝福或诅咒今天——你和明天——基于你在这个计划中做得有多好……或者,也许,根本不在乎。由您决定在计划提交时要多小心。

但无论如何,名称只对查找提交很重要。真正重要的是提交及其结果图。


推荐阅读