关于版本控制:Git分支从哪里开始,它的长度是多少?

Where does a Git branch start and what is its length?

不时有人问我,Git上的某个分支启动了什么,或者某个特定分支上是否创建了某个提交。分支的终点非常清楚:这就是分支标签所在的位置。但是-从哪里开始的?简单的答案是:在我们创建分支的提交上。但据我现在所知,这就是为什么我问这个问题,在第一次提交之后就失去了。

只要我们知道分支的提交位置,我们就可以绘制图表来清楚地说明:

1
2
3
4
5
A - B - C - - - - J     [master]
     \
      D - E - F - G     [branch-A]
           \
            H - - I     [branch-B]

我在commit E创建了branch-b,所以这就是"开始"。我知道,因为我做到了。但是其他人也能以同样的方式识别它吗?我们可以画同样的图:

1
2
3
4
5
6
7
A - B - C - - - - J     [master]
     \
      \       F - G     [branch-A]
       \     /
        D - E
             \
              H - I     [branch-B]

现在看图表,哪个分支从E开始,哪个分支从B开始?commit D是两个分支机构的成员,还是我们可以清楚地决定它是属于分支机构A还是分支机构B?

这听起来有些哲学,但实际上并非如此。管理者有时喜欢知道,当一个分支已经开始(它通常标志着一个任务的开始),并且一些变更属于哪个分支(为了得到一些变更的目的——这是工作所需要的),我想知道Git是否提供信息(工具、命令)或定义给正确回答这些问题。


在Git中,可以说每个分支都从根提交开始,这是完全正确的。但我想这对你没什么帮助。相反,您可以做的是定义与其他分支相关的"分支的开始"。一种方法是

1
git show-branch branch1 branch2 ... branchN

这将显示输出底部所有指定分支之间的公共提交(如果实际上有公共提交)。

下面是来自show-branch的Linux内核git文档的一个示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ git show-branch master fixes mhf
* [master] Add 'git show-branch'.
 ! [fixes] Introduce"reset type" flag to"git reset"
  ! [mhf] Allow"+remote:local" refspec to cause --force when fetching.
---
  + [mhf] Allow"+remote:local" refspec to cause --force when fetching.
  + [mhf~1] Use git-octopus when pulling more than one heads.
 +  [fixes] Introduce"reset type" flag to"git reset"
  + [mhf~2]"git fetch --force".
  + [mhf~3] Use .git/remote/origin, not .git/branches/origin.
  + [mhf~4] Make"git pull" and"git fetch" default to origin
  + [mhf~5] Infamous 'octopus merge'
  + [mhf~6] Retire git-parse-remote.
  + [mhf~7] Multi-head fetch.
  + [mhf~8] Start adding the $GIT_DIR/remotes/ support.
*++ [master] Add 'git show-branch'.

在这个例子中,将masterfixesmhf分支进行比较。把这个输出想象成一个表,每个分支由自己的列表示,每个提交都得到自己的行。包含commit的分支将在该commit行的列中显示+-

在输出的最底层,您将看到所有3个分支都有一个共同的祖先提交,实际上它是masterhead提交:

1
*++ [master] Add 'git show-branch'.

这意味着fixesmhf都是从master中的犯罪分支出来的。

替代解决方案

当然,这只是确定Git中公共基提交的1种可能的方法。其他方法包括:用git merge-base寻找共同祖先,用git log --all --decorate --graph --onelinegitk --all可视化树枝,看它们在哪里分叉(尽管如果有很多承诺很快就会变得困难)。

原海报其他问题

关于这些问题,你有:

Is commit D a member of both branches or can we clearly decide whether it belongs to branch-A or branch-B?

D是两个分支的成员,是两个分支的祖先。

Supervisors sometimes like to know, when a branch has been started (it usually marks the start of a task)...

在Git中,您可以重写整个提交树及其分支的历史记录,因此当分支"开始"时,并不像在诸如tfs或svn之类的东西中那样设置在石头中。您可以在Git树的任何时间点上执行cx1〔15〕分支,甚至可以在根提交之前执行!因此,您可以使用它在树中的任意时间点"启动"任务。

对于git rebase来说,这是一个常见的用例,可以将分支与上游分支的最新更改同步起来,并在提交图中及时地将它们"向前"推送,就好像您"刚开始"处理分支一样,即使实际上已经处理了一段时间。如果您愿意的话,甚至可以沿着提交图及时地将分支推回来(尽管您可能需要解决很多冲突,这取决于分支内容……或者您可能不会)。甚至可以在开发历史的中间插入或删除分支(尽管这样做可能会改变许多提交的提交shas)。重写历史是Git的主要特性之一,这使得它如此强大和灵活。

这就是为什么提交同时具有编写日期(最初编写提交时)和提交日期(最后提交到提交树时)。您可以将它们视为类似于创建时间日期和上次修改的时间日期。

Supervisors sometimes like to know...to which branch some changes belong to (to get the purpose of some change - was it required for the work).

同样,因为Git允许您重写历史,所以您可以(重新)在提交图中几乎任何分支/提交上建立一组更改。git rebase字面上允许您自由地移动整个分支(尽管您可能需要在移动过程中解决冲突,这取决于您将分支移动到何处以及它包含的内容)。

也就是说,您可以在Git中使用的一个工具是--contains,用于确定哪些分支或标记包含一组更改:

1
2
3
4
5
# Which branches contains commit X?
git branch --all --contains X

# Which tags contains commit X?
git tag --contains X


关于这个问题的赏金通知要求,

I'd be interested in knowing whether or not thinking about Git branches as having a defined"beginning" commit other than the root commit even makes sense?

除了:

  • 根提交是"可以从分支头访问的第一个提交"(不要忘记,可以有多个带有孤立分支的根提交,例如在用于gh-pages的github中使用)。
  • 我更愿意考虑一个分支的开始是另一个分支的提交(Tobib的回答没有~1),或者(更简单的)共同祖先。(也在"使用git查找分支点"中),即使OP提到对共同祖先不感兴趣):

    1
    git merge-base A master

这意味着:

  • 第一个定义给了您一个固定的承诺(除非在大量filter-branch)的情况下,它可能永远不会改变)
  • 第二个定义为您提供了一个相对提交(相对于另一个分支),它可以随时更改(可以删除另一个分支)。

第二个对git来说更有意义,它是关于分支之间的合并和重新平衡。

Supervisors sometimes like to know, when a branch has been started (it usually marks the start of a task) and to which branch some changes belong to (to get the purpose of some change - was it required for the work)

分支只是一个错误的标记:由于分支的短暂性(可以重命名/移动/重新平衡/删除/…),您不能用分支模仿"更改集"或"活动"来表示"任务"。

这是一个X Y问题:操作要求一个尝试的解决方案(分支从哪里开始),而不是实际的问题(在Git中什么可以被视为任务)。

要执行此操作(表示任务),可以使用:

  • 标记:它们是不可变的(一旦与一个提交相关联,该提交就不再被认为是移动/重新调整基),并且两个命名良好的标记之间的任何提交都可以表示一个活动。
  • 一些git notes对已创建"工作项"的commit进行记忆(与标记相反,如果commit被修改或重新设置,则可以重写注释)。
  • 挂钩(根据提交消息将提交与一些"外部"对象(如"工作项")关联)。这就是Bridge git rtc——IBMRationalTeamConcert——使用预接收钩子所做的工作)重点是:分支的开始并不总是反映任务的开始,而仅仅是可以更改的历史的延续,以及哪些序列应该表示一组逻辑更改。


也许你问错了问题。在我看来,询问一个分支从哪里开始是没有意义的,因为一个给定的分支包含了对每个文件所做的所有更改(即自初始提交以来)。

另一方面,问两个分支在哪里分叉肯定是一个有效的问题。事实上,这似乎正是你想要知道的。换句话说,你不想知道关于一个分支的信息。相反,您希望了解有关比较两个分支的一些信息。

有一点研究显示了GitRevisions手册页,其中描述了引用特定提交和提交范围的详细信息。特别地,

To exclude commits reachable from a commit, a prefix ^ notation is used. E.g. ^r1 r2 means commits reachable from r2 but exclude the ones reachable from r1.

This set operation appears so often that there is a shorthand for it. When you have two commits r1 and r2 (named according to the syntax explained in SPECIFYING REVISIONS above), you can ask for commits that are reachable from r2 excluding those that are reachable from r1 by ^r1 r2 and it can be written as r1..r2.

因此,使用您问题中的示例,您可以得到branch-Amaster的不同之处

1
git log master..branch-A


这里有两个不同的关注点。从你的例子开始,

1
2
3
4
5
6
7
A - B - C - - - - J     [master]
     \
      \       F - G     [branch-A]
       \     /
        D - E
             \
              H - I     [branch-B]

[...] Supervisors sometimes like to know, when a branch has been started (it usually marks the start of a task) and to which branch some changes belong (to get the purpose of some change - was it required for the work)

在我们到达肉食之前,有两个事实观察:

第一个观察:您的主管想要知道的是提交和一些外部工作订单记录之间的映射:什么提交解决bug-43289或featureb?为什么我们要更改longmsg.c中的strcat用法?谁来支付你前一次和这次之间20小时的费用?分支名称本身在这里并不重要,重要的是提交与外部管理记录的关系。

第二个观察:无论是branch-A还是branch-B首先发布(通过合并、重新平衡或推送),提交d和e中的工作都必须与之同步进行,不得被任何后续操作复制。在作出这些承诺时,当前的情况完全没有区别。这里的分支名称也不重要。重要的是通过祖传图彼此承诺关系。

所以我的答案是,就任何历史而言,分支名称都无关紧要。它们是方便标签,显示哪个提交对于特定于该回购的某个目的是最新的,仅此而已。如果您希望在默认的合并提交消息主题行中使用一些有用的名字对象,那么在合并之前,git branch some-useful-name会给出提示,并将其合并。无论哪种方式,他们都是相同的承诺。

将开发人员在提交时签出的任何分支名称与一些外部记录(或任何其他记录)绑定在一起,都将深入到"只要一切正常"领域。别这样。即使在大多数VCS中常见的限制使用情况下,您的D-E-{F-G,H-I}也会发生得更快,而不是更晚,然后您的分支命名约定将必须进行调整以处理这一问题,然后会出现更复杂的情况。…

为什么要麻烦?将提示工作的报告编号放在提交消息底部的一个标记行中,然后完成它。git log --grep(通常是Git)的速度非常快,这是有原因的。

即使是一个相当灵活的预处理钩子来插入这样的标语也很简单:

1
2
3
4
branch=`git symbolic-ref -q --short HEAD`                     # branch name if any
workorder=`git config branch.${branch:+$branch.}x-workorder`  # specific or default config
tagline="Acme-workorder-id: ${workorder:-***no workorder supplied***}"
sed -i"/^ *Acme-workorder-id:/d; \$a$tagline""$1"

下面是当您需要检查每个提交时的基本预接收钩子循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
while read old new ref; do              # for each pushed ref
        while read commit junk; do      # check for bad commits

                # test here, e.g.
                git show -s $commit | grep -q '^ *Acme-workorder-id: ' \
                || { rc=1; echo commit $commit has no workorder associated; }
                # end of this test

        done <<EOD
        $(git rev-list $old..$new)
EOD
done
exit $rc

内核项目使用类似这样的标记行进行版权签署和代码审查记录。它真的不能变得更简单或更健壮。

注意,我在C&P之后做了一些手工操作,以取消对真实脚本的专门化。键盘到编辑框警告


我认为这可能是一个很好的教育机会。git并没有真正记录任何分支的起点。除非该分支的reflog仍然包含创建记录,否则无法确定它从何处开始,并且如果分支已在任何地方合并,则它实际上可能有多个根提交,以及可能创建并开始偏离其原始源的多个不同的可能点。

在这种情况下,问一个相反的问题也许是个好主意——为什么你需要知道它从哪里分支,或者它从哪里分支以任何有用的方式重要?这一点可能很重要,也可能不是很好的原因——许多原因可能与您的团队采用并试图实施的特定工作流有关,并且可能表明您的工作流在某些方面可能得到改进。也许一个改进是找出"正确"的问题——例如,不是"branch-B分支从哪里来",而是"哪些分支可以或不包含branch-B引入的修复/新功能"……

我不确定这个问题是否真的有一个完全令人满意的答案…


从哲学的角度来看,一个分支机构的历史问题不能从全球的角度来回答。然而,reflog确实跟踪了该特定存储库中每个分支的历史。

因此,如果您有一个每个人都需要的中央存储库,您可以使用它的reflog来跟踪这些信息(本问题中的一些更详细的信息)。首先,在该中央存储库上,确保记录了reflog,并且不会被清除:

1
2
$ git config core.logAllRefUpdates true
$ git config gc.reflogExpire never

然后您可以运行git reflog 来检查分支的历史。

例子

我将您的示例提交图复制到一个测试存储库中,只需几次推送即可。现在我可以这样做了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ git log --graph --all --oneline --decorate
* 64c393b (branch-b) commit I
* feebd2f commit H
| * 3b9dbb6 (branch-a) commit G
| * 18835df commit F
|/  
* d3840ca commit E
* b25fd0b commit D
| * 8648b54 (master) commit J
| * 676e263 commit C
|/  
* 17af0d2 commit B
* bdbfd6a commit A

$ git reflog --date=local master branch-a branch-b
64c393b branch-b@{Sun Oct 11 21:45:03 2015}: push
3b9dbb6 branch-a@{Sun Oct 11 21:45:17 2015}: push
18835df branch-a@{Sun Oct 11 21:43:32 2015}: push
8648b54 master@{Sun Oct 11 21:42:09 2015}: push
17af0d2 master@{Sun Oct 11 21:41:29 2015}: push
bdbfd6a master@{Sun Oct 11 21:40:58 2015}: push

所以你可以看到在我的例子中,当branch-a第一次出现时,它被指向commit F,第二次推到中央服务器时,它向前移动以commit G。然而,当branch-b首次出现时,它被指向commit I,而且还没有看到任何更新。

注意事项

这只显示了它被推到中央回购的历史。例如,如果一个同事在commit A上启动了branch-a,但在推送之前将其重新调整为commit B,则该信息不会反映在中央存储库的回流中。

这也不能提供分支机构起源的确切记录。我们不能确定哪个分支机构"拥有"了最初从master分出的DE。他们是在branch-a上被创造出来,然后被branch-b接受,还是相反?

两个分支最初都出现在包含这些提交的中央存储库中,reflog确实告诉我们哪个分支首先出现在中央存储库中。但是,这些提交可能已经在多个最终用户存储库中"传递",通过format-patch等。因此,即使我们知道哪个分支指针首先负责将它们传递到中央服务器,我们也不知道它们的最终来源。


一些建筑细节

Git将对存储库的修订存储为一系列提交。这些提交包含一个链接,指向自上次提交以来对文件所做的更改的信息,重要的是,还包含一个指向上一次提交的链接。从广义上讲,分支的提交历史是一个单独链接的列表,从最近的修订版一直返回到存储库的根目录。在任何提交时,存储库的状态都是,提交与之前的所有提交结合在一起,一直返回到根目录。

那么头部是什么?什么是分支?

头部是当前活动分支中最新提交的特殊指针。每个分支,包括master1,也是指向其历史上最新修订的指针。

像泥一样清澈?让我们来看一个使用pro-git图书中的图像的例子,希望能稍微澄清一些问题。2

Simple Git Tree

在这个图中,我们有一个相对简单的具有4个提交的存储库。98ca9是根。有两个分支,master和testing。主分支在commit f30ab处,测试分支在87ab2处。我们目前在主分支机构工作,所以头指向主分支机构。示例存储库中分支的历史记录是(从最新到最旧):

1
2
testing:  87ab2 -> f30ab -> 34ac2 -> 98ca9
 master:           f30ab -> 34ac2 -> 98ca9

由此我们可以看出,从f30ab开始,这两个分支是相同的,因此我们也可以说测试是提交时的分支。

这本pro-git的书更详细,绝对值得一读。

现在我们可以解决--

具体问题

想象一下我们得到的图表:

氧化镁

Is commit D a member of both branches or can we clearly decide whether it belongs to branch-A or branch-B?

知道我们现在知道什么,我们可以看到commit d是从分支指针到根的两个链的成员。因此我们可以说D是两个分支的成员。

Which branch started at E, which one at B?

分支A和分支B都源于B的主分支,并且在E.Git上彼此分离。Git本身并不区分哪个分支拥有E。在它们的核心,分支只是从最新到最旧的提交链,以根结尾。

1事实:总分行只是一个普通分行。它与其他任何分支都没有区别。

2该pro-git图书获得了创作共享属性非商业共享类似3.0的许可证(未发布)。


正如@cupcake所解释的,分支没有起点。您只能检查一个分支首次接触另一个分支的位置。在大多数情况下,这可能是你想要的。@代码专家已经解释了引用提交范围的语法。

综上所述:此命令显示了在branch-A而不是master中的第一次提交之前的第一次提交:

江户十一〔七〕号