65

时不时地有人问我,git 上的某个分支是从什么提交开始的,或者是否在特定分支上创建了某个提交。分支的终点很清楚:那是分支标签所在的位置。但是 - 它从哪里开始?简单的答案是:在我们创建该分支的那个提交上。但是,据我现在所知,这就是我问这个问题的原因,在第一次提交后就丢失了。

只要我们知道我们分支的提交,我们就可以绘制图表以使其清楚:

A - B - C - - - - J     [master]
     \
      D - E - F - G     [branch-A]
           \
            H - - I     [branch-B]

我在提交时创建了分支-B,E这就是“开始”。我知道,因为我做到了。但是其他人能以同样的方式识别它吗?我们可以像这样绘制相同的图表:

A - B - C - - - - J     [master]
     \
      \       F - G     [branch-A]
       \     /
        D - E
             \
              H - I     [branch-B]

那么,现在看一下图表,哪个分支开始于E,哪个分支开始于B?提交D是两个分支的成员还是我们可以清楚地决定它属于分支A还是分支B?

这听起来有些哲学,但事实并非如此。主管有时想知道,何时启动了一个分支(它通常标志着任务的开始)以及一些更改属于哪个分支(为了获得一些更改的目的 - 是否需要工作),我会想知道 git 是否提供信息(工具、命令)或定义来正确回答这些问题。

4

8 回答 8

62

在 Git 中,您可以说每个分支都从根提交开始,这确实是真的。但我想这对你不是很有帮助。相反,您可以做的是定义与其他分支相关的“分支的开始”。一种方法是使用

git show-branch branch1 branch2 ... branchN

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

这是Linux Kernel Git 文档中的一个示例show-branch

$ 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'.

在该示例中,master正在与fixesandmhf分支进行比较。将此输出视为一个表,每个分支由其自己的列表示,每个提交都有自己的行。包含提交的分支将在该提交的行中的列中显示+或显示。-

在输出的最底部,您会看到所有 3 个分支共享一个共同的祖先提交,并且它实际上是以下head提交master

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

这意味着fixesmhf都从master.

替代解决方案

当然,这只是在 Git 中确定公共基本提交的一种可能方法。其他方法包括git merge-base找到共同的祖先,git log --all --decorate --graph --oneline或者gitk --all可视化分支并查看它们的分歧(尽管如果有很多提交很快就会变得困难)。

来自原始海报的其他问题

至于你有的这些问题:

commitD是两个分支的成员还是我们可以清楚地决定它是否属于branch-Aor branch-B

D是两个分支的成员,它是它们两个分支的祖先提交。

主管有时想知道,何时启动了一个分支(它通常标志着任务的开始) ......

在 Git 中,您可以重写整个提交树及其分支的历史记录,因此一个分支“开始”时并不像 TFS 或 SVN 那样一成不变。您可以rebase分支到 Git 树中的任何时间点,甚至可以将其放在根提交之前!因此,您可以使用它在您想要的树中的任何时间点“启动”任务。

这是一个常见的用例git rebase,将分支与来自上游分支的最新更改同步,沿提交图及时将它们“向前”推送,就好像您“刚刚开始”在分支上工作一样,即使您'实际上已经研究了一段时间。如果您愿意,您甚至可以沿着提交图及时将分支推回(尽管您可能必须解决很多冲突,具体取决于分支内容......或者您可能不会)。您甚至可以在开发历史的中间插入或删除一个分支(尽管这样做可能会更改许多提交的提交 shas)。重写历史是 Git 的主要功能之一,它使它如此强大和灵活。

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

主管有时想知道......某些更改属于哪个分支(为了获得某些更改的目的 - 是否需要工作)。

同样,由于 Git 允许您重写历史记录,您可以(重新)基于您想要的提交图中的几乎任何分支/提交进行一组更改。 git rebase从字面上看,您可以自由移动整个分支(尽管您可能需要随时解决冲突,具体取决于您将分支移动到的位置以及它包含的内容)。

话虽如此,您可以在 Git 中使用来确定哪些分支或标签包含一组更改的工具之一是--contains

# Which branches contains commit X?
git branch --all --contains X

# Which tags contains commit X?
git tag --contains X
于 2013-07-10T21:24:17.583 回答
14

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

我想知道是否将 Git 分支视为具有除根提交之外的已定义“开始”提交是否有意义?

除了:

这意味着:

  • 第一个定义为您提供了一个固定的提交(除非是大规模的,否则它可能永远不会改变filter-branch
  • 第二个定义为您提供了一个相对提交(相对于另一个分支),它可以随时更改(可以删除另一个分支)

第二个对 git 更有意义,它是关于分支之间的合并和变基。

主管有时想知道一个分支何时启动(它通常标志着任务的开始)以及某些更改属于哪个分支(为了获得某些更改的目的 - 是否需要工作)

分支只是错误的标记:由于分支的瞬态特性(可以重命名/移动/重新定位/删除/...),您不能模仿带有分支的“更改集”或“活动”,代表一个“任务”。

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

为此(代表一项任务),您可以使用:

  • 标签:它们是不可变的(一旦与提交相关联,该提交就不再应该移动/重新定位),并且两个命名良好的标签之间的任何提交都可以代表一个活动。
  • 一些git notes提交以记住该提交已创建到哪个“工作项”(与标签相反,如果提交被修改或重新设置,则可以重写注释)。
  • 挂钩(根据提交消息将提交关联到一些“外部”对象,如“工作项”)。这就是Git-RTC 的桥梁——IBM Rational Team Concert——使用预接收钩子所做的事情)关键是:分支的开始并不总是反映任务的开始,而仅仅是历史的延续哪些可以更改,以及谁的顺序应该代表一组逻辑更改。
于 2015-10-10T05:09:20.663 回答
10

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

另一方面,询问两个分支在哪里分歧绝对是一个有效的问题。事实上,这似乎正是你想知道的。换句话说,您并不真的想知道有关单个分支的信息。相反,您想了解一些有关比较两个分支的信息。

一些研究发现了gitrevisions 手册页,其中描述了引用特定提交和提交范围的详细信息。尤其是,

要从提交中排除可访问的提交,使用前缀 ^ 表示法。例如 ^r1 r2 表示可从 r2 访问的提交,但排除可从 r1 访问的提交。

这个集合操作经常出现,以至于有一个简写形式。当您有两个提交 r1 和 r2(根据上面指定修订中解释的语法命名)时,您可以请求可从 r2 访问的提交,不包括可通过 ^r1 r2 从 r1 访问的提交,并且可以写为 r1。 .r2。

因此,使用您问题中的示例,您可以获得branch-Amaster

git log master..branch-A
于 2013-07-10T22:24:40.810 回答
10

这里有两个不同的问题。从你的例子开始,

A - B - C - - - - J     [master]
     \
      \       F - G     [branch-A]
       \     /
        D - E
             \
              H - I     [branch-B]

[...] 主管有时想知道,何时启动了一个分支(它通常标志着任务的开始)以及某些更改属于哪个分支(为了获得某些更改的目的 - 是否需要工作)

在我们谈肉之前,有两个事实观察:

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

第二个观察:无论是先发布还是先发布(通过说合并branch-Abranch-B变基或推送),提交 D 和 E 中的工作必须立即进行,并且不会被任何后续操作复制。进行这些提交时,当前的内容完全没有区别。分支名称在这里也无关紧要。重要的是通过祖先图提交的相互关系。


所以我的回答是,就任何历史而言,分支名称根本不重要。它们是方便标签,显示哪个提交是针对特定于该仓库的特定目的的当前提交,仅此而已。如果您想在默认的合并提交消息主题行中使用一些有用的名字,合并git branch some-useful-name前的提示,然后合并它。无论哪种方式,它们都是相同的提交。

将开发人员在提交时签出的任何分支名称与一些外部记录 - 或任何东西 - 绑定在“只要一切正常,一切都很好”的领域。不要这样做。即使在大多数 VCS 中常见的受限用法,您D-E-{F-G,H-I}迟早会发生,然后您的分支命名约定必须适应处理它,然后会出现更复杂的东西,. . .

何必?将提示工作的报告编号放在提交消息底部的标语中并完成。 git log --grep(和一般的 git)有充分的理由非常快。

即使是一个相当灵活的准备钩子来插入这样的标语也是微不足道的:

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"

当您需要检查每个提交时,这是基本的预接收挂钩循环:

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 之后进行了一些手工修改,以使真实脚本不再专业化。键盘到编辑框警告

于 2015-10-11T20:24:00.687 回答
9

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

在这种情况下提出反问可能是一个好主意——为什么你需要知道它从哪里分支,或者它从哪里分支以任何有用的方式很重要?可能有也可能没有充分的理由表明这一点很重要 - 许多原因可能与您的团队采用并试图强制执行的特定工作流程有关,并且可能表明您的工作流程可能以某种方式改进的领域。也许一项改进是弄清楚要问什么“正确”的问题——例如,而不是“branch-B分支从哪里来”,也许是“哪些分支包含或不包含由branch-B“...引入的修复/新功能”。

我不确定是否真的存在对这个问题的完全令人满意的答案......

于 2013-07-10T21:42:29.343 回答
6

一些建筑细节

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

那么什么是HEAD?什么是分支?

HEAD 是指向当前活动分支中最新提交的特殊指针。每个分支,包括 master 1,也是指向其历史中最新版本的指针。

清如泥?让我们看一个使用Pro Git 书中的图像的示例,希望能在一定程度上澄清一些事情。2

简单的 Git 树

在此图中,我们有一个相对简单的存储库,其中包含 4 个提交。 98ca9是根。有两个分支,master 和 testing。主分支处于提交状态f30ab,而测试分支处于87ab2. 我们目前在 master 分支工作,所以 HEAD 指向 master 分支。我们示例存储库中分支的历史记录是(从最新到最旧):

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

从这里我们可以看出,两个分支从 开始是相同的f30ab,所以我们也可以说测试是那个提交的分支。

Pro Git 书更详细,绝对值得一读。

现在我们可以解决——

具体问题

幻想我们得到的图表:

喜欢喝茶的小指头。

提交 D 是两个分支的成员,还是我们可以清楚地确定它属于分支 A 还是分支 B?

知道我们现在所知道的,我们可以看到提交 D 是从分支指针指向根的两条链的成员。因此我们可以说 D 是两个分支的成员。

哪个分支从 E 开始,哪个分支在 B?

分支 A 和分支 B 都源自 B 的主分支,并在 E 处相互分歧。Git 本身不区分哪个分支拥有 E。在它们的核心,分支只是从最新到的提交链最老的在根处结束。


1有趣的事实:master 分支只是一个普通的分支。它与任何其他分支没有什么不同。

2 Pro Git 书籍使用 Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License 进行许可。

于 2015-10-17T00:59:20.090 回答
6

从哲学的角度来看,一个分支的历史问题不能在全球范围内得到回答。但是,reflog确实会跟踪该特定存储库中每个分支的历史记录。

因此,如果您有一个每个人都推送到的中央存储库,您可以使用它来跟踪此信息( this questionreflog中的更多详细信息)。首先,在该中央存储库上,确保记录 reflog 并且永远不会被清除:

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

然后您可以运行git reflog <branchname>以检查分支的历史记录。

例子

我复制了您的示例提交图,并在测试存储库中进行了几次推送。现在我可以做这样的事情:

$ 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,并且还没有看到任何更新。

注意事项

这仅显示了被推送到中央回购的历史记录。例如,如果一个同事从branch-Acommit开始A,但B在推送之前将其重新设置为 commit,则该信息不会反映在中央存储库的 reflog 中。

这也没有提供分支开始的确切记录。我们不能确定哪个分支“拥有”提交D,以及E最初是从 master 分叉出来的。它们是在 上创建branch-a然后被 拾取的branch-b,还是相反?

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

于 2015-10-12T02:02:48.733 回答
5

正如@cupcake 解释的那样,没有分支的起点。您只能检查一个分支第一次接触另一个分支的位置。在大多数情况下,这可能是您想要的。@code-guru 已经解释了引用提交范围的语法。

把它们放在一起:这个命令显示了第一个提交之前的第一个提交,它是 inbranch-A但不是 in master

git show `git rev-list branch-A ^master --topo-order | tail -n 1`~1

于 2014-06-04T10:43:44.030 回答