希望将子分支的更改合并到分支中,但两者都包含相同的提交

Looking to incorporate changes from sub-branch into branch but both contain same commits

在 git 方面绝对是新手,因此这里可能会提出一个非常愚蠢的问题:

我有一个 master 分支,我开始在其中进行增强。 20 多岁左右的提交我意识到我应该创建一个单独的分支并完成我的工作。

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

$ git reset --hard <commit hash>

在我这样做之后,我立即想到了以下工作流程(不知道从哪里来的 --lol):

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

所以在这个好主意之后,我立即想到 "hmmm...let me just create another separate specific "增强" 主分支。

我想我可能错误地创建了 "sub" 增强分支。 在 "main" 上,我只是使用了

$ git checkout -b enhancement

没有

$ git checkout -b enhancement main

现在当我在分支 "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 中的 "branches",据我所知,分支只是指向特定提交的指针,而 HEAD 指向的分支就是你的分支当前正在工作等提交时只会在 HEAD 之上添加连续提交,同时将 HEAD 推进到该分支中的最新提交。

所以我的增强分支的 HEAD 应该远远领先于我的主分支的 HEAD 包含提交 main 没有。

我不明白的是,如果我在增强分支上工作了最后 17 次提交,为什么当我切换到任一分支时 HEAD 指向同一个提交。

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

拜托,有人怜悯并指出我在这里做错了什么,以阐明一些问题。我已经为此苦苦思索了很久。

非常感谢任何帮助!

问题的根源在于分支没有分层。

提交 具有图形结构。特别是,几乎每个提交都有一个 parent,有些有两个 parent。一些古怪的提交可能有三个或更多,并且至少有一个——存储库中有史以来的第一个提交——必然有 noparents.

正是这种 parent/child 关系形成了图形结构。我们可以从一个只有三个提交的小型存储库开始:

A <-B <-C

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

要向此存储库添加新提交,我们将保存新的源代码快照,添加我们的姓名和电子邮件地址以及 date-and-time-stamp,保存日志消息,然后保存 C' s 的哈希 ID,全部放入一个新的提交中,自动分配给它自己的新的、唯一的哈希 ID,我们称之为 D,现在我们有:

A <-B <-C <-D

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

7c20df84bd21ec0215358381844274fa10515017
14fe4af084071803ab4f16e6841ff64ba7351071
c62bc49139f1d18e922fc98e35bb08b1aadbcafc
9b274e28871b3e4a4109582a34625df5fddc91c8

我应该将其中哪一个称为提交 A,我应该将哪个称为 B,等等?如果我想从 end 开始——在 latest 提交,就像 Git 那样,我是否从 14fe...,或者用 9b27...,或者什么?

我们可以查看这四个提交中的每一个,以查看它们存储的 parent 哈希 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

告诉我提交 9b274e28871b3e4a4109582a34625df5fddc91c8两个 parent、e46249f73ebddca06cf16c01e8de1f310360c856f3eda90ffc10f9152e7492a34408a9f5e4c28b0f,两者都不是一个我列出的四个。如果我查看存储库中的每个提交,并收集它们所有的 parent 行,我可以——最终,经过大量工作——找出哪些提交在结束。但这很慢。

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

$ git rev-parse master
7c20df84bd21ec0215358381844274fa10515017

啊哈,这是我上面列出的四个提交之一!该提交是 last 提交,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>

我们看到这个提交恰好有一个parent,14fe4af084071803ab4f16e6841ff64ba7351071,这是我的四个哈希ID中的另一个。所以 master 是链的末端,另一个以 14fe... 开头的丑陋的大哈希 ID 是下一个:

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

H7c20... 提交,G14fe... 提交。让我们看看 G 的 parents,这次使用 git rev-parse 及其特殊的“打印所有 parent 哈希 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

我们可以调用c62bc49139f1d18e922fc98e35bb08b1aadbcafc提交F。对于 d61e6ce1dda7f4b11601a0de549feefbcec55779,我们可能需要 E 以外的其他字母;让我们使用 I:

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

现在让我们放入分支 name master。名字本身标识commitH,而且只是直接意味着commitH,所以:

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

我们可以从 G 的日志消息中看到 Junio Hamano made G by 运行ning:

git merge sg/fsck-config-in-doc

所以让我们也绘制那个名称,指向提交 I:

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

有趣的是,提交 FGH 都在 master 上……但是 意味着 提交“在分支上”是我们可以从末尾开始——在这种情况下是提交H,然后向后工作,达到[=471] =] 提交。所以从 H 开始,我们走回唯一的一步,然后看到 G。从 G,我们可以退回到 FI。因此,不仅 master 上的提交 FI.

上的提交也是如此

同时,sg/fsck-config-in-doc是一个分支名称。它标识提交 I。所以提交 I 不仅在 master 上,它也在 sg/fsck-config-in-doc.

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

要在某个分支上进行新提交,我们首先将Gitselect那个特定的分支名称作为当前 支线。要创建一个新的分支名称,我们有 Git select 一些提交——默认是当前提交——并创建一个新名称,指向那个特定的提交。所以如果我们有:

...--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 的提交,现在在 both master and[=471] =] topic。如果我们 select topic 作为 当前分支 ,Git 在需要时提取提交 H 并附加名称 HEAD到名字 topic:

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

并且当我们进行新提交时,它的 parent 将是 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是attached-to——并使其指向那个特定的提交。

  • 因为这个是--hard,它还有re-sets索引和work-tree。 git reset 的其他风格的工作方式不同。事实上,有些 git reset 根本不会 移动当前分支名称,但是 git reset --softgit reset --mixedgit reset --hard做。

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

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

在这种情况下,您的设置如下:

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

然后是运行git merge enhancement。名称 enhancement 标识提交 N。当前分支名称 main 也是如此。 both 分支上的共享提交因此是提交 N。因此无事可做。

您现在可以移动名称main指向提交H,使用:

git reset --hard master

和以前一样,--hard 将重置索引和 work-tree,因此请确保您没有任何未保存的内容;最终结果是 draw-able 为:

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

如果你现在运行 git merge enhancement,Git会从H向后走,也会从N向后走,找到第一个共享犯罪。那是提交 H 本身,它在所有三个分支上。 Git 然后,如果你允许的话,会判定这种合并太微不足道,无法努力工作,并且只需移动名称 main 以便它标识提交 N,并且同时 git checkout 提交 N

Git 将此称为 fast-forward 操作,最后f吧,图片是:

...--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 的 parent 将照常变为 H。当前 namebr1 被重写为指向新提交 I,作为此 git commit 操作的最后一步:

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

为了好玩(或者为了制作我想要 M 的字母,真的),让我们进行第二次提交,它只在 branch 上进行,而不是所有那些以 [ 结尾的共享提交=72=]:

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

现在让我们 git checkout master,检查提交 H 和 re-attaches HEAD:

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

然后在 master 上进行两次新提交。由于一些难以理解的原因,我将把它们画在新的顶行(这实际上不是必需的):

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

现在我们运行git merge branch(或者git合并<em>hash-of-J</em> ,除了提交日志消息外,它会做同样的事情)。 Git 必须从 L 开始向后计算,从 J 开始向后计算,以找到最佳共享提交,即 H.

此共享提交称为合并基础。现在 Git 知道哪个提交是合并基础,它检查合并基础 是否是 当前提交 L and/or 另一个提交J,因为那些是特殊的简单情况。不是,所以这不是一个微不足道的合并。

合并的目标合并更改,但会提交HLJ 都只有 快照 。所以Git首先要把LJ转化为变化。通过从最佳公共起点(合并基础)开始,两组更改都应应用 该公共起点。要获得更改,Git 实际上 运行s 两个 git diff --find-renames 命令:

  • git差异--find-renames<em>hash-of-H</em><em>hash-of-L</em> 告诉 Git 我们在 master;
  • 上改变了什么
  • git差异--find-renames<em>hash-of-H</em><em>hash-of-J</em> 告诉 Git 他们(好吧,我们)在 branch.
  • 上改变了什么

合并进程现在尝试合并 更改,将合并的更改应用到来自合并基础H 的快照。如果一切顺利——如果变化很好地结合——Git 从结果中创建一个新的快照。这个新快照有两个 parents。第一个是通常的。我们在 master,这意味着提交 L,因此新合并的第一个 parent 是 L。我们命名的另一个提交是 J,因此新合并的第二个 parent 是 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 现在将继续进行两个差异(内部)。第一个比较 HH。这当然会产生一个空的 change-set。第二个比较 HN。当然,这会产生将 H 中的快照转换为 N.

中的快照所需的所有更改

Git 然后将第一个 change-set(表示“什么都不做”的那个)与第二个结合起来。结果正好是第二个change-set。将此应用到 H 中的快照,Git 得到 N 中的快照。不过,合并工作正常,所以现在 Git 进行了一个新的合并提交,我们将其称为 OO的第一个parent是H,第二个parent是N,我们有:

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

你应该使用哪个?

有时,fast-forwards 是必经之路。有时,真正的合并是可行的方法。让我们再看一下我之前使用的 Git 的实际 Git 存储库的图表:

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

我们现在可以带走名字sg/fsck-config-in-doc。它所做的只是让我们直接访问提交 I,但我们可以从 H 到达那里。所以让我们删除名称:

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

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

这是好事还是坏事?好吧,假设你 真的想知道 将来 O,你强制与 --no-ff 的合并是 merge行动。这意味着通过 N 提交是一些副业,而副业终于准备好迎接黄金时间并一次性添加。那么你 do 想要合并提交,即使 git merge 默认做 fast-forward.

另一方面,也许 you-in-the-future 不会关心 通过 N 提交的是某种 side-work。 Side-work 与 main-work 完全无关:只是你在工作。也许 N 之前的一些特定提交也无关紧要。也许你想把所有的提交都排成一排,以便于查看,或者你可能想这样做:

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

并把它变成:

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

并丢弃所有 I-through-N 提交,保留两个 new 提交 OP 紧凑地表示“正确地做第一件事”,然后“正确地做第二件事”,而不是你在 IJK 等中所做的杂乱无章的事情。在那种情况下,您根本不需要合并。

you-in-the-future 将拥有一个 提交图 ,其中包含一些指定的 提示提交 。那就是你将拥有的 all。 You-today 确定提交图中的提交集; you-tomorrow 可以决定保留哪些 names; future-you 会祝福或诅咒 today-you 和 tomorrow-you,这取决于你在这个计划中做得有多好......或者,也许根本不在乎。由您决定在计划提交时要多谨慎。

但在任何情况下,名称 仅对查找 提交有影响。真正重要的是提交及其结果图。