Git将选择的分支合并到一个新分支中,保留对分支的访问权限 [英] Git Merging selected branches into a new branch, retaining access to the branches

查看:250
本文介绍了Git将选择的分支合并到一个新分支中,保留对分支的访问权限的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我输入的内容是什么:
(我也从其他人那里得到了一些感觉,我的图画表明我不太了解git - 忍受着我。)


(b)
/
- $(b)
$ b $ p $ - <> - < ----- master-
\
- <> - <> - (A)

其中'<>'是提交。

对此:

 (将A和B合并到C中)

-------------- o-> (C,新的'干净的'分支主管)
/
- <> - <> - <> b // /
----- master- /
\ /
- <> - <> - /(A)

其中'o'是A和B合并为C.

能够 git check-out 分支(A)(B) code $?

或者我可以这样做:

 <$ c (c)
/ /
/ - <> - <<> (B)
// /
----- master-- /
\(<> /
- <> - / - <> - <> ;-( A)
pre>

如果可以的话,即使在某种程度上,你能解释一下吗?谢谢。

解决方案

让我们在这里备份一下,并从Git中简单的普通提交开始。首先,我们来定义一个commit 是什么。他们真的很简单。尝试一下,运行:

  $ git cat-file -p HEAD 

这会在你的终端上打印你当前的提交,它看起来很像这样,但是有着不同的丑陋哈希ID(当然还有名字):

 树142feb985388972de41ba56af8bc066f1e22ccf9 
父母62ebe03b9e8d5a6a37ea2b726d64b109aec0508c
作者AU Thor< thor@example.com> 1501864272 -0700
提交者A U Thor< thor@example.com> 1501864272 -0700

这是一些提交

它有一个提交消息。

就是这样 - 这就是你需要提交的一切!不过,这里隐藏着很多东西。特别是,有行,它们具有这些大丑陋的哈希ID。实际上,名称 HEAD 可以作为另一个的替身:

  $ git rev-parse HEAD 
4384e3cde2ce8ecd194202e171ae16333d241326

(同样,您的号码将会是不同的)。
$ b 这些哈希ID是每个提交的真实名称(或者 - - 其他一些Git对象)。这些哈希ID实际上是提交内容(或其他对象类型,如 tree )的加密校验和。如果你知道内容 - 构成对象的字节序列 - 以及它的类型和大小,你可以自己计算这个哈希ID,尽管没有真正的理由打扰。

< h3>提交中的内容

从上面可以看到,提交会存储相对较少的信息。实际的对象,这短文本行的字符串,进入Git数据库并获取唯一的哈希ID。这个哈希ID是它的真实名称:当Git想要看看提交中的内容时,你给Git一些产生ID的东西,Git从Git数据库中检索对象本身。在一个提交对象中,我们有:


  • 一棵。这保存了您保存的源代码树(由 git add ing并最终 git commit ing - 最终 git commit 步骤首先写出树,然后提交)。
  • 。这是一些其他提交的哈希ID。我们稍后再讨论这个问题。 作者提交者:代码(即作者)并作出承诺。如果有人给你发送电子邮件补丁,他们是分开的:那么另一个人是作者,但你是提交者。 (Git诞生于像GitHub这样的合作网站之前,因此通过电子邮件发送补丁程序的情况非常普遍)。它们还会存储电子邮件地址和时间戳,并在时间戳中以奇数对形式显示。

  • 日志消息。这只是自由形式的文本,无论你想提供什么。 Git唯一解释的是将日志消息的主题与日志消息的其余部分分隔开的空行(即使这样,仅用于格式化: git log --oneline vs git log ,例如)。

    $ b

    进行提交,从一个完全空的仓库



    假设我们有一个完全空的仓库,没有任何提交。如果我们要去提交提交,我们最终会得到一张空白图或空白白板。因此,让我们通过 git add 来完成第一个提交,例如 README ,并运行 git commit



    这个第一次提交会得到一些很大的丑陋哈希ID,但我们只是称它为提交A,并将其提取出来:

      A 

    这是 only 提交。所以...它的父母是什么?



    答案是,它没有任何父母。它是第一个提交,所以它不能。所以它毕竟没有父母行。这使得它成为一个 root commit



    让我们做一个有用的文件,而不是一个自述。然后我们将 git add 这个文件和 git commit 。新的提交会得到另一个大的丑陋哈希ID,但我们只需将它称为 B 。让我们把它画出来:

      A< -B 

    如果我们用查看 B > git cat-file -p< hash< B> ,我们将看到这次我们有一个父母行,它显示了 A 。我们说 B 指向 A ; A B 的父母。



    做第三次提交 C ,看看它,我们会看到 C 的父项是 B 的哈希值:

      A< -B< -C 

    现在 C 指向 B B 指向 A A 是一个根提交,并且不指向任何地方。这就是Git的提交工作方式:每一个指向向后的父项。当我们到达根提交时,向后指针的链结束。



    现在,所有这些内部指针都是固定的,就像提交的其他所有内容一样。你永远不能在任何提交中改变 ,因为它的丑陋哈希ID是该提交的内容的加密校验和。如果你以某种方式设法改变某种东西,那么加密校验和也会改变。你会有一个新的,不同的提交。



    由于所有内部指针都是固定的(并且总是向后指向),所以我们并不需要费心去绘制它们:

      A  -  B  -  C 

    就够了。但是,这里是分支名称和 HEAD 这个名称的来源 - 我们需要知道在哪里 start 。散列ID看起来非常随机,不像我们很好的简单的 A-B-C ,我们知道这些字母的顺序。如果您有两个ID:

      62ebe03b9e8d5a6a37ea2b726d64b109aec0508c 
    3e05c534314fd5933ff483e73f54567a20c94a69

    没有告诉他们进入的顺序,至少不是来自ID。因此,我们需要知道哪些是 latest 提交,即 master 等某些分支的 tip 提交。然后,我们可以从最新的提交开始,然后逐步完成这些父链接。如果我们可以找到commit C C 会让我们找到 B B 会让我们找到 A



    < h3>分支名称存储散列ID

    Git做的是将分支的 tip 提交的散列ID存储在(另一个)数据库。而不是使用散列ID作为键,这里的键是分支名称,它们的值不是实际的对象,而只是提示提交的哈希ID。



    < (这个数据库至少目前是 - 仅仅是一组文件: .git / refs / heads / master 是一个文件, code> master 。所以更新数据库只是意味着在文件中写入一个新的哈希ID。但是这种方法在Windows上不能很好地工作,因为这意味着 master MASTER ,它们应该是两个不同的分支,使用相同的文件导致所有类型的问题,现在不要使用仅在大小写方面不同的两个分支名称。)现在我们来看看如何添加一个新的提交 D 到我们的三个提交系列。首先,让我们画出名字 master

      A  -  B --C < -  master 

    名称 master 现在保存着 C 的哈希ID,这让我们(或Git)找到 C ,do无论我们想要什么,并使用 C 来找到 B 。然后,我们使用 B 来找到 A ,然后自 A 是一个根提交,我们完成了。我们说 master 指向 C



    现在我们添加或更改一些文件和 git commit 。 Git像往常一样写出一棵新树,然后写入一个新的提交 D D 的父母将是 C

    <$ p $ < - c $ c> A - B - C < - master
    \
    D

,最后Git只是将 D 的哈希值填入 master code>:

  A  -  B  -  C 
\
D < - master

现在 master 指向 D ,因此下次我们使用 master 时,我们将以commit D ,然后按照 D 的父箭头回到 C ,依此类推。通过指向 D ,分支名称 master 现在具有 D

我们保留带分支名称的箭头,因为与提交不同,分支名称​​移动。提交本身不能改变,但分支名称记录了我们想要调用最新的任何提交。



多个分支 h3>

现在让我们看看创建多个分支,以及为什么我们需要 HEAD



我们将继续使用我们的四次提交 - 至今:

  A  -  B- -C  -  D < -  master 

现在我们来创建一个 new 分支, develop ,使用 git branch develop git checkout -b develop 。由于分支名称只是包含散列ID的文件(或数据库条目),因此我们将使新名称 develop 也指向提交 D

  A  -  B  -  C  -  D < - 主,开发

但现在我们有两个或多个分支名称,我们需要知道:哪一个分支是我们的? 这是 HEAD 进来的地方。 HEAD 在Git中实际上只是另一个文件, .git / HEAD ,通常包含字符串 ref:后跟分支的全名。如果我们是 master .git / HEAD ref:refs / heads /主人在里面。如果我们在开发 .git / HEAD ref:refs / heads /在其中开发。这些 refs / heads / 是持有提示提交哈希的文件的名称,所以Git可以读取 READ ,获取分支的名称,然后阅读分支文件,并获得正确的哈希ID。



让我们在我们切换到分支开发

  A  -  B- -C  -  D < -  master(HEAD),开发

然后切换到 develop

  A  -  B  -  C  -  D < ;  -  master,develop(HEAD)

这就是这里发生的一切!当切换分支时,其他地方会发生更多的事情,但是为了处理,所有 git checkout 都会更改名称 HEAD 附加到。



现在让我们做一个新的提交 E 。新的提交像往常一样进行,它的新父节点是 HEAD ,它是 D ,所以:

  A  -  B  -  C  -  D < -  master,develop(HEAD)
\
E

现在我们必须更新一些分支。 当前分支是 develop ,所以这就是我们更新的那个。我们写了 E 的哈希ID,现在我们有:

  A-B-C -D < - 主
\
E < - develop(HEAD)

就是这样 - 这是所有使分支在Git中成长!我们只是将一个新提交添加到 HEAD 现在的任何位置,使新提交的父对象成为旧的 HEAD 提交。然后我们移动它指向我们刚刚创建的新提交的分支。
$ b

合并和合并提交



现在我们有多个分支,让我们对每个分支进行更多的提交。我们将不得不为每个分支 git checkout 做一些提交到这里,但是假设我们结束了这个图:

  A  -  B  -  C  -  D  -  G < -  master(HEAD)
\
E - F < - 开发

现在我们在 master (这是我们所在的分支),还有两个在 develop ,加上原来的四个 ABCD 在这两个 分支上提交。



(顺便说一下,这是Git的一个特殊功能,在许多其他版本控制系统,在大多数VCS中,提交是on的分支是在你提交的时候建立的,就像提交的父母在那个时候被设置一样,但是在Git中,分支名称是非常轻松的,到一个单一的提交:分支的尖端。因此,一些提交是on的分支的集合是通过查找所有分支名称,然后跟随所有分支e从指向哪个分支的提示开始向后指向箭头以查看哪些提交是可访问的。这个可达的概念很重要,但很快,尽管我们不会在此发布。另请参阅 http://think-like-a-git.net/ 。 )



现在让我们运行 git merge develop 来合并 develop 提交回 master 。请记住,我们目前 master - 只需查看图中的 HEAD 即可。因此,Git将使用名称 develop 来查找它的 tip 提交,它是 F ,和 HEAD 来找到我们的提示提交,它是 G



然后Git将使用我们绘制的这张图来寻找常见的 merge base 提交。在这里,这是提交 D 。 commit D 是这两个分支再次合并的地方。



Git的底层合并过程有点复杂和凌乱,但如果一切顺利,而且通常会这样,我们不必再深入研究它。我们可以知道Git比较commit D 来提交 G 来看看我们做了什么在 master 上,并比较commit D 来提交 F 到看看 开发上做了什么。然后,Git将两套更改结合在一起,确保 完成的任何操作都完成一次。



这个计算和组合变更集合是合并的过程。更具体地说,它是一个三路合并(可能被称为是因为有三个输入:合并基础和两个分支提示)。这就是我喜欢称之为合并的动词部分:合并,以完成三路合并。



这个合并过程的结果,这个合并为一个动词,是一个源代码树,你知道我们用树做什么,对吧?我们做一个 commit!这就是Git接下来要做的事情:它做了一个新的提交。新提交的工作和普通提交一样。它有一棵树,就是刚刚制作的一个Git。它有一个作者,提交者和提交信息。它有一个父母,它是我们的当前或 HEAD 提交...和另一个,第二个父母,这是我们合并的提交!

让我们用两个向后指向的父箭头绘制我们的合并提交 H

  A  -  B  -  C  -  D  -  G  -  H < -  master(HEAD)
\\ \\ /
E - F < - develop

(我们没有 - 因为这太难了,第一个父母是 G 而第二个 是<$ c $ )



与每次提交一样,新提交进入当前的 当前分支,并使分支名称前进。因此 master 现在指向新的合并提交 H 。它是 H ,它指向 G F

这种提交,这个合并提交,也使用单词merge。在这种情况下,合并是一个形容词,但我们(和Git)经常称这种合并,用合并这个词作为名词。所以合并,即名词,指的是合并提交,合并为形容词。合并提交简单地是至少有两个父母的提交

我们通过运行 git merge进行合并提交。然而,有一点麻烦: git merge 并不总是进行合并提交。它可以做动词类的合并而不用做形容词类,事实上,它甚至不总是做动词类。即使在可以跳过所有工作的情况下,我们也可以使用 git merge --no-ff 强制 Git进行合并提交。



目前,我们只是使用 - no-ff ,迫使Git做出真正的合并。但是我们首先会看到为什么我们需要 - no-ff ,然后第二,为什么我们不应该打扰!让我们按照我的方式重新绘制图表,因为我的方式更好一些。 :-)你有这个开始:

  B  -  C  -  D  -  E < -  branch- B 
/
- o - o - A - - - -
\
F - G < - 分支-A

(这里没有标记 HEAD ,因为我们不知道或现在关心哪一个是 HEAD ,如果它是这些的话。)



你现在想让一个新的分支 branch-C ,指向提交 A ,并使当前分支。假设一切已经干净,最快的方法是使用:

  $ git checkout -b分支-C主

移动到(检入到索引和工作树)由<$ c标识的提交$ c> master (commit A ),然后创建一个新的分支 branch-C 指向该提交,然后使 HEAD 名称分支分支-C

  B  -  C  -  D  -  E < - 分支-B 
/
--o - o - A< ; - master,branch-C(HEAD)
\
F - G < - branch -A

现在我们运行第一个 git merge 来获取 branch -A

  $ git merge --no-ff分支-A 

这将比较当前提交 A 与合并基础提交,它是 A 。 (这是我们需要 - no-ff :merge base 当前提交的原因!)然后它会将当前提交与提交 G 。 Git将结合这些变化,这意味着取 G ,并在我们当前的分支上创建新的合并提交。名称 master 将继续指向 A ,但现在我要停止完全绘制它,因为ASCII艺术的限制:

  B  -  C  -  D  -  E < -  branch-B 
/
--o - o - A ------ H < - branch-C(HEAD)
\ /
F - G < - - branch -A

接下来,我们将合并 branch-B

  $ git合并分支-B 

这将比较合并基提交 A 提交 H ,并将 A E 进行比较。 (这次合并的基础不是当前提交,所以我们不需要 - no-ff 。)Git将像往常一样,尝试将变化合并为动词 - 如果成功,Git将进行另一次合并提交(合并为名词或形容词),我们可以这样绘制:

  B  -  C  -  D  -  E < - 分支-B 
/ \
--o - - - A ------ H ----- I < - branch-C(HEAD)
\ /
F - G < - branch- A

请注意,没有其他名称完全移动。分支分支-A 分支-B 仍指向它们的原始提交。分支 master 仍然指向 A (如果这是一个白板或纸张或某些我们可以保留它的内容)。现在,名称 branch-C 指向我们使用的两个合并提交中的第二个,因为我们每个合并都只能指向两个提交 ,而不是一次三个。



Git确实有三种合并方式



如果,出于某种原因,你不喜欢进行两次合并,Git确实提供了一种称为章鱼合并的功能,可以同时合并两个以上的分支提示。但是从来没有任何要求做章鱼合并,所以我只是在这里提到它的完整性。



我们真的应该观察相反,这两个合并中的一个合并是不必要的。



我们不需要其中一个合并



我们从> git merge --no-ff branch -A 开始,我们必须使用 - no- ff 来防止Git执行Git所谓的快进合并。我们还注意到了为什么:这是因为合并基础在我们的绘图中提交 A 是同一个 提交 branch-C 指向当时。



我们让Git将从commit A 提交 A (所有这些更改为零) c> A 提交 G 是使用 - no-ff 好的,Git,我知道你可以做这个快速向前的非合并,但我最终需要一个真正的合并,所以假装你努力工作并进行合并提交。如果我们忽略了这个选项,Git会简单地向前滑动分支标签,违背内部提交箭头的方向。我们将从以下开始:

  B  -  C  -  D  -  E < - 分支-B 
/
--o - o - A< - master,branch-C(HEAD)
\
F - G < - branch-A

然后Git会执行此操作:

  B  -  C  -  D  -  E < -  branch-B 
/
--o - o - A < - master
\
F - G < - 分支-A,分支-C(HEAD)

然后,当我们进行了第二次合并时,我们没有这样做并且仍然不需要 - no-ff -Git将找到合并基 A ,比较 A vs G ,比较 A vs E ,结合更改以创建新的 object,并在结果中新建一个提交 H

  B-C -D ----- E < - 分支-B 
/ \
--o - o - A < - 主H < ; - 分支-C(HEAD)
\ /
F ----------- G < - 分支-A

与以前一样,其他标签都没有移动(这次我们可以绘制名称通过扩展图形来掌握)。我们只得到一个合并提交 H ,而不是两个合并提交 H - I



为什么你可能需要 - no-ff 无论如何



如果我们使用 git merge --no-ff ,我们将得到的源代码树进行两次合并,当Git将所有更改合并时,如果我们允许一个快进合并,我们会得到与源树相同的 。但是最终的是不同的。


$ b

Git中的提交图是 >历史。如果您想知道过去发生了什么,那么您可以看到的是提交图。该图由所有提交组成,并且提交存储作者和提交者名称以及日期和日志消息。它们链接到保存的源代码树,并提供组成该图的父链接。



这意味着未来,如果您想知道如果你做了两次合并,你现在必须进行两次合并。但是,如果将来你不关心有多少 git merge 命令你运行的时候,你可以让任何数量的 git merge 步骤都是快进(非合并)操作。它们在提交图中没有留下任何痕迹 - 它们只是将一个分支名称标签从一个提交移动到另一个提交到另一个 - 将来 您不能确定是否发生了这种情况。该图不存储名称动作;它只有提交。


What exactly do I type to go from: (I also get a sense from other people that my drawings suggest I don't quite understand git - bear with me.)

               -<>-<>-<>-<>- (B)
             /            
-----master-            
             \         
               --<>-<>- (A)

where '<>' is a commit.

to this:

                    (merge A and B into C)

               --------------o-> (C, new 'clean' branch off master)
              /             /
             /-<>-<>-<>-<>-/ (B)
            //            /
-----master--            /
              \         /
               --<>-<>-/ (A)

where 'o' is a merge of A and B into C.

And will I then still be able to git check-out the branches (A) and (B)?

And/or could I do this:

               --------------o-<>-(C)
              /             /
             /-<>-<>-<>-<>-/-<>-<>-(B)
            //            /
-----master--            /
              \         /
               --<>-<>-/-<>-<>-<>-(A)

If you can, even in some round about way, could you explain? Thanks.

解决方案

Let's back up a bit here and start with how simple, ordinary commits work in Git. First, let's define what a commit is. They really are pretty simple. Try, as an experiment, running:

$ git cat-file -p HEAD

This will print, on your terminal, your current commit, which will look much like this, but with different big ugly hash IDs (and of course names):

tree 142feb985388972de41ba56af8bc066f1e22ccf9
parent 62ebe03b9e8d5a6a37ea2b726d64b109aec0508c
author A U Thor <thor@example.com> 1501864272 -0700
committer A U Thor <thor@example.com> 1501864272 -0700

this is some commit

It has a commit message.

That's it—that's all you need to have a commit! There's a lot hiding away in plain sight here, though. In particular, there are the tree and parent lines, which have these big ugly hash IDs. In fact, the name HEAD acts as a stand-in for another one:

$ git rev-parse HEAD
4384e3cde2ce8ecd194202e171ae16333d241326

(again, your number will be different).

These hash IDs are the "true names" of each commit (or—as for the tree—some other Git object). These hash IDs are actually cryptographic checksums of the contents of the commit (or other object type like tree). If you know the contents—the sequence of bytes making up the object—and its type and size, you can compute this hash ID yourself, though there's no real reason to bother.

What's in a commit

As you can see from the above, a commit stores a relatively small amount of information. The actual object, this short string of text lines, goes into the Git database and gets a unique hash ID. That hash ID is its "true name": when Git wants to see what's in the commit, you give Git something that produces the ID, and Git retrieves the object itself from the Git database. Inside a commit object, we have:

  • A tree. This holds the source tree you saved (by git adding and eventually git commiting—the final git commit step writes out the tree first, then the commit).
  • A parent. This is the hash ID of some other commit. We'll come back to this in a moment.
  • An author and committer: these hold the name of the person who wrote the code (i.e., the author) and made the commit. They are separated in case someone sends you an email patch: then the other person is the author, but you are the committer. (Git was born in the days before collaboration sites like GitHub, so emailing patches was pretty common.) These store an email address and a time stamp too, with the time stamp in that odd numeric pair form.
  • A log message. This is just free-form text, whatever you want to provide. The only thing Git interprets here is the blank line separating the subject of the log message from the rest of the log message (and even then, only for formatting: git log --oneline vs git log, for instance).

Making commits, starting with a completely empty repository

Suppose we have a completely empty repository, with no commits in it. If we were to go to draw the commits, we'd just end up with a blank drawing, or blank whiteboard. So let's make the first commit, by git adding some files, such as a README, and running git commit.

This first commit gets some big ugly hash ID, but let's just call it "commit A", and draw it in:

A

That's the only commit. So ... what's its parent?

The answer is, it doesn't have any parent. It's the first commit, so it can't. So it doesn't have a parent line after all. This makes it a root commit.

Let's make a second commit, by making a useful file, not just a README. Then we'll git add that file and git commit. The new commit gets another big ugly hash ID, but we'll just call it B. Let's draw it in:

A <-B

If we look at B with git cat-file -p <hash for B>, we'll see that this time we have a parent line, and it shows the hash for A. We say that B "points to" A; A is B's parent.

If we make a third commit C, and look at it, we'll see that C's parent is B's hash:

A <-B <-C

So now C points to B, B points to A, and A is a root commit and doesn't point anywhere. This is how Git's commits work: each one points backwards, to its parent. The chain of backwards pointers ends when we reach the root commit.

Now, all of these internal pointers are fixed, just like everything else about a commit. You can't change anything in any commit, ever, because its big ugly hash ID is a cryptographic checksum of the contents of that commit. If you somehow managed to change something, the cryptographic checksum would change too. You'd have a new, different commit.

Since all the internal pointers are fixed (and always point backwards), we don't really have to bother drawing them:

A--B--C

suffices. But—here's where branch names and the name HEAD come in—we need to know where to start. The hash IDs look quite random, unlike our nice simple A-B-C where we know the order of the letters. If you have two IDs like:

62ebe03b9e8d5a6a37ea2b726d64b109aec0508c
3e05c534314fd5933ff483e73f54567a20c94a69

there's no telling what order they go in, at least not from the IDs. So we need to know which is the latest commit, i.e., the tip commit of some branch like master. Then we can start at the latest commit, and work backwards, following these parent links one at a time. If we can find commit C, C will let us find B, and B will let us find A.

Branch names store hash IDs

What Git does is to store the hash ID of the tip commit of a branch, in a (another) database. Instead of using hash IDs as the keys, the keys here are the branch names, and their values are not the actual objects, but rather just the hash IDs of the tip commits.

(This "database" is—at least currently—mostly just a set of files: .git/refs/heads/master is a file holding the hash ID for master. So "updating the database" just means "writing a new hash ID into the file". But this method does not work very well on Windows, since this means that master and MASTER, which are supposed to be two different branches, use the same file, which causes all kinds of problems. For now, never use two branch names that differ only in case.)

So now let's look at adding a new commit D to our series of three commits. First, let's draw in the name master:

A--B--C   <-- master

The name master holds the hash ID of C at the moment, which lets us (or Git) find C, do whatever we want with it, and use C to find B. Then we use B to find A, and then since A is a root commit, we are done. We say that master points to C.

Now we add or change some files and git commit. Git writes out a new tree as usual, and then writes a new commit D. D's parent will be C:

A--B--C   <-- master
       \
        D

and finally Git just stuffs D's hash, whatever it turns out to be, into master:

A--B--C
       \
        D   <-- master

Now master points to D, so the next time we work with master we will start with commit D, then follow D's parent arrow back to C, and so on. By pointing to D, the branch-name master now has D as its tip commit. (And of course, there's no longer a reason to draw the graph with a kink in it like this.)

We keep the arrows with the branch names, because unlike commits, the branch names move. The commits themselves can never be changed, but the branch names record whatever commit we want to call "the latest".

Multiple branches

Now let's look at making more than one branch, and why we need HEAD.

We'll keep going with our four commits-so-far:

A--B--C--D   <-- master

Now let's make a new branch, develop, using git branch develop or git checkout -b develop. Since branch names are just files (or database entries) holding hash IDs, we will make the new name develop also point to commit D:

A--B--C--D   <-- master, develop

But now that we have two or more branch names, we need to know: which branch are we on? This is where HEAD comes in.

The HEAD in Git is actually just another file, .git/HEAD, that normally contains the string ref: followed by the full name of the branch. If we're on master, .git/HEAD has ref: refs/heads/master in it. If we're on develop, .git/HEAD has ref: refs/heads/develop in it. These refs/heads/ things are the names of the files holding the tip commit hashes, so Git can read READ, get the name of the branch, then read the branch file, and get the right hash ID.

Let's draw this in, too, before we've switched to branch develop:

A--B--C--D   <-- master (HEAD), develop

and then after we switch to develop:

A--B--C--D   <-- master, develop (HEAD)

That's all that happens here! There's more stuff that happens elsewhere when switching branches, but for dealing with the graph, all that git checkout does is change the name HEAD is attached to.

Now let's make a new commit E. The new commit goes in as usual, and its new parent is whatever HEAD says, which is D, so:

A--B--C--D   <-- master, develop (HEAD)
          \
           E

Now we have to update some branch. The current branch is develop, so that's the one we update. We write E's hash ID in, and now we have:

A--B--C--D   <-- master
          \
           E   <-- develop (HEAD)

This is it—this is all there is to making branches grow in Git! We just add on a new commit to wherever HEAD is now, making the new commit's parent be the old HEAD commit. Then we move whichever branch it is to point to the new commit we just made.

Merging and merge commits

Now that we have multiple branches, let's make a few more commits on each. We'll have to git checkout each branch and make some commits to get here, but suppose we end up with this graph:

A--B--C--D--G   <-- master (HEAD)
          \
           E--F   <-- develop

We now have one extra commit on master (which is the branch we're on), and two on develop, plus the original four A-B-C-D commits that are on both branches.

(This, by the way, is a peculiar feature of Git, not found in many other version control systems. In most VCSes, the branch a commit is "on" is established when you make the commit, just like commits' parents are set in stone at that time. But in Git, the branch names are very light fluffy things that just point to one single commit: the tip of the branch. So the set of branches that some commit is "on" is determined by finding all branch names, and then following all the backwards-pointing arrows to see which commits are reachable by starting at which branch-tips. This concept of reachable matters a lot, soonish, though we won't get there in this posting. See also http://think-like-a-git.net/ for instance.)

Now let's run git merge develop to merge the develop commits back into master. Remember, we're currently on master—just look at HEAD in the drawing. So Git will use the name develop to find its tip commit, which is F, and the name HEAD to find our tip commit, which is G.

Then Git will use this graph we've been drawing to find the common merge base commit. Here, that's commit D. Commit D is where these two branches first join up again.

Git's underlying merge process is somewhat complicated and messy, but if everything goes well—and it usually does—we don't have to look any deeper into it. We can just know that Git compares commit D to commit G to see what we did on master, and compares commit D to commit F to see what they did on develop. Git then combines both sets of changes, making sure that anything done on both branches gets done exactly once.

This process, of computing and combining the change-sets, is the process of merging. More specifically it is a three-way merge (probably called that because there are three inputs: the merge base, and the two branch tips). This is what I like to call the "verb part" of merging: to merge, to do the work of a three-way merge.

The result of this merge process, this merge-as-a-verb, is a source-tree, and you know what we do with a tree, right? We make a commit! So that's what Git does next: it makes a new commit. The new commit works a whole lot like any ordinary commit. It has a tree, which is the one Git just made. It has an author, committer, and commit message. And it has a parent, which is our current or HEAD commit ... and another, second parent, which is the commit we merged-in!

Let's draw in our merge commit H, with its two backwards-pointing parent arrows:

A--B--C--D--G---H   <-- master (HEAD)
          \    /
           E--F   <-- develop

(We didn't—because it's too hard—draw in the fact that the first parent is G and the second is F, but that's a useful property later.)

As with every commit, the new commit goes into the current branch, and makes the branch name advance. So master now points to the new merge commit H. It's H that points back to both G and F.

This kind of commit, this merge commit, also uses the word "merge". In this case "merge" is an adjective, but we (and Git) often just call this "a merge", using the word "merge" as a noun. So a merge, the noun, refers to a merge commit, with merge as adjective. A merge commit is simply any commit with at least two parents.

We make a merge commit by running git merge. There is, however, a little bit of a hitch: git merge doesn't always make a merge commit. It can do the verb kind of merge without doing making the adjective kind, and in fact, it doesn't even always do the verb kind either. We can force Git to make a merge commit using git merge --no-ff, even in the case where it could skip all the work.

For the moment, we'll just use --no-ff, forcing Git to make a real merge. But we'll see first why we will need --no-ff, and then second, why we shouldn't have bothered!

Back to your problem from your question

Let's redraw your graphs my way, because my way is better. :-) You have this to start with:

          B--C--D--E   <-- branch-B
         /            
--o--o--A   <-- master
         \         
          F--G   <-- branch-A

(There's nothing labeled HEAD here because we don't know or care right now which one is HEAD, if it is even any of these.)

You now want to make a new branch, branch-C, pointing to commit A, and make that the current branch. The quickest way to do that, assuming everything is already clean, is to use:

$ git checkout -b branch-C master

which moves to (checks out into the index and work-tree) the commit identified by master (commit A), then makes a new branch branch-C pointing to that commit, then makes HEAD name branch branch-C.

          B--C--D--E   <-- branch-B
         /
--o--o--A   <-- master, branch-C (HEAD)
         \
          F--G   <-- branch-A

Now we'll run the first git merge to pick up branch-A:

$ git merge --no-ff branch-A

This will compare the current commit A to the merge-base commit, which is A again. (This is the reason we need --no-ff: the merge base is the current commit!) Then it will compare the current commit to commit G. Git will combine the changes, which means "take G", and make a new merge commit on our current branch. The name master will continue to point to A, but now I'm going to just stop drawing it altogether due to the limitations of ASCII art:

          B--C--D--E   <-- branch-B
         /
--o--o--A------H   <-- branch-C (HEAD)
         \    /
          F--G   <-- branch-A

Next, we'll merge branch-B:

$ git merge branch-B

This will compare the merge base commit A to commit H, and also compare A to E. (This time the merge base is not the current commit so we don't need --no-ff.) Git will, as usual, try to combine the changes—merge as a verb—and if it succeeds, Git will make another merge commit (merge as a noun or adjective), which we can draw like this:

          B--C--D--E   <-- branch-B
         /          \
--o--o--A------H-----I   <-- branch-C (HEAD)
         \    /
          F--G   <-- branch-A

Note that none of the other names have moved at all. Branches branch-A and branch-B still point to their original commits. Branch master still points to A (and if this were a whiteboard or paper or some such we could keep it drawn in). The name branch-C now points to the second of the two merge commits we used, since each of our merges can only point back to two commits, not to three at once.

Git does have a three-at-once kind of merge

If, for some reason, you don't like having two merges, Git does offer something called an octopus merge, that can merge more than two branch tips at once. But there's never any requirement to do an octopus merge, so I'm just mentioning it here for completeness.

What we really should be observing instead is that one of these two merges was unnecessary.

We didn't need one of the merges

We started out with git merge --no-ff branch-A, and we had to use --no-ff to prevent Git from doing what Git calls a fast forward merge. We also noted why: it's because the merge base, commit A in our drawing, was the same commit to which branch-C pointed at the time.

The way we made Git combine the "changes" going from commit A to commit A (all zero of these "changes") with the changes it found going from commit A to commit G was to use --no-ff: OK, Git, I know you can do this as a fast-forward non-merge, but I want a real merge in the end, so pretend you worked hard and make a merge commit. If we left out this option, Git would simply "slide the branch label forward", going against the direction of the internal commit arrows. We would start with:

          B--C--D--E   <-- branch-B
         /
--o--o--A   <-- master, branch-C (HEAD)
         \
          F--G   <-- branch-A

and then Git would do this:

          B--C--D--E   <-- branch-B
         /
--o--o--A   <-- master
         \
          F--G   <-- branch-A, branch-C (HEAD)

Then, when we did the second merge—for which we did not and still do not need --no-ff—Git would find the merge base A, compare A vs G, compare A vs E, combine the changes to make a new tree object, and make a new commit H out of the result:

          B--C--D-----E   <-- branch-B
         /             \
--o--o--A   <-- master  H   <-- branch-C (HEAD)
         \             /
          F-----------G   <-- branch-A

Just as before, none of the other labels move at all (and this time we can draw the name master in by stretching out the graph a bit). We get only the one merge commit H, instead of two merge commits H--I.

Why you might want --no-ff anyway

If we make two merges, using git merge --no-ff, the source tree we'll get, when Git combines all our changes, will be the same as the source tree we get if we allow the one fast-forward merge. But the final graph is different.

The commit graph, in Git, is the history. If you want to know what happened in the past, what you have—the thing you can look at—is the commit graph. The graph is made up of all the commits, and the commits store the author and committer names and dates and log messages. They link to the saved source trees, and provide the parent links that make up the graph.

This means that in the future, if you will want to know that you made two merges, you must make two merges now. But if in the future, you don't care how many git merge commands you ran, you can let any number of those git merge steps be fast-forward (non-merge) operations. They leave no trace in the commit graph—they just move one branch name label from one commit to another—so in the future you can't really tell if this ever happened. The graph does not store name motion; it has only the commits.

这篇关于Git将选择的分支合并到一个新分支中,保留对分支的访问权限的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆