如何找到Git分支最近的父节点

让我们假设我有以下本地存储库和一个像这样的提交树:

master --> a
\
\
develop c --> d
\
\
feature f --> g --> h

master是我的这是最新的稳定版本代码develop是我的这是“下一个”发布代码feature正在为develop准备一个新功能

使用钩子,我希望能够拒绝推送到feature到我的远程存储库,除非提交fdevelop HEAD的直接后代。也就是说,提交树看起来是这样的,因为特性已经在dgit rebase了。

master --> a
\
\
develop c --> d
\
\
feature f --> g --> h

那么是否有可能:

  • 确定feature?
  • 确定在父分支中f是其后代的提交?

从那里,我将检查父分支的HEAD是什么,并查看f前任是否与父分支HEAD匹配,以确定该特性是否需要重新基于。

613587 次浏览

请记住,正如在Git:查找提交来自哪个分支中所描述的,您不能轻易地确定提交的分支(分支可以重命名、移动、删除……),即使git branch --contains <commit>是一个开始。

  • 你可以从一个提交到另一个提交,直到git branch --contains <commit>没有列出feature分支和develop分支,
  • 比较提交SHA1到/refs/heads/develop

如果两个提交id匹配,就可以继续了(这意味着feature分支的起源是develop的HEAD)。

我对您的整体问题有一个解决方案(确定feature是否来自develop的尖端),但使用您概述的方法它不起作用。

您可以使用git branch --contains来列出从develop顶端开始的所有分支,然后使用grep来确保feature在其中。

git branch --contains develop | grep "^ *feature$"

如果它是其中之一,它将把" feature"打印到标准输出,并且返回代码为0。否则,它将不打印任何东西,并有一个返回代码1。

假设远程存储库有一个开发分支的副本(您最初的描述在本地存储库中描述了它,但听起来它也存在于远程存储库中),您应该能够实现我认为您想要的结果,但是方法与您预想的有点不同。

Git的历史是基于DAG的提交。分支(和一般的“引用”)只是在不断增长的提交DAG中指向特定提交的临时标签。因此,分支之间的关系可以随时间变化,但提交之间的关系不会。

    ---o---1                foo
\
2---3---o      bar
\
4
\
5---6  baz

看起来baz是基于bar的(旧版本)?但是如果我们删除bar呢?

    ---o---1                foo
\
2---3
\
4
\
5---6  baz

现在看起来baz是基于foo的。但是baz的祖先并没有改变。我们只是删除了一个标签(以及由此产生的悬空提交)。如果我们在4添加一个新标签呢?

    ---o---1                foo
\
2---3
\
4        quux
\
5---6  baz

现在看起来baz是基于quux的。尽管如此,祖先并没有改变,只是标签变了。

然而,如果我们问“提交6是提交3的后代吗?”(假设36是完整的SHA-1提交名称),那么无论是否存在barquux标签,答案都是“yes”。

因此,您可以问这样的问题:“被推送的提交是否是开发分支的当前尖端的后代?”,但你不能可靠地问“推送的提交的父分支是什么?”。

一个似乎接近你想要的最可靠的问题是:

对于所有被推送的提交的祖先(不包括开发的当前尖端及其祖先),它们以开发的当前尖端为父:

  • 是否至少存在一个这样的提交?
  • 所有这样的提交都是单亲提交吗?

可以实现为:

pushedrev=...
basename=develop
if ! baserev="$(git rev-parse --verify refs/heads/"$basename" 2>/dev/null)"; then
echo "'$basename' is missing, call for help!"
exit 1
fi
parents_of_children_of_base="$(
git rev-list --pretty=tformat:%P "$pushedrev" --not "$baserev" |
grep -F "$baserev"
)"
case ",$parents_of_children_of_base" in
,)     echo "must descend from tip of '$basename'"
exit 1 ;;
,*\ *) echo "must not merge tip of '$basename' (rebase instead)"
exit 1 ;;
,*)    exit 0 ;;
esac

这将涵盖您想要限制的一些内容,但可能不是所有内容。

作为参考,这里是一个扩展的例子历史:

    A                                   master
\
\                    o-----J
\                  /       \
\                | o---K---L
\               |/
C--------------D              develop
\             |\
F---G---H    | F'--G'--H'
|    |\
|    | o---o---o---N
\   \      \       \
\   \      o---o---P
\   \
R---S

上面的代码可以用来拒绝# eyz0和S,同时接受H'JK,或N,但它也会接受LP(它们涉及合并,但它们不合并开发的尖端)。

要拒绝LP,您可以更改问题并询问

对于所有推送提交的祖先(不包括开发及其祖先的当前提示):

  • 有双亲的提交吗?
  • 如果不是,至少有一个这样的提交有开发的当前提示它的(唯一)父?
pushedrev=...
basename=develop
if ! baserev="$(git rev-parse --verify refs/heads/"$basename" 2>/dev/null)"; then
echo "'$basename' is missing, call for help!"
exit 1
fi
parents_of_commits_beyond_base="$(
git rev-list --pretty=tformat:%P "$pushedrev" --not "$baserev" |
grep -v '^commit '
)"
case "$parents_of_commits_beyond_base" in
*\ *)          echo "must not push merge commits (rebase instead)"
exit 1 ;;
*"$baserev"*)  exit 0 ;;
*)             echo "must descend from tip of '$basename'"
exit 1 ;;
esac

一个rephrasal

这个问题的另一种表达方式是“驻留在当前分支之外的分支上的最近提交是什么?是哪个分支?”

一个解决方案

您可以使用一点命令行魔法找到它

git show-branch \
| sed "s/].*//" \
| grep "\*" \
| grep -v "$(git rev-parse --abbrev-ref HEAD)" \
| head -n1 \
| sed "s/^.*\[//"

与# EYZ0:

git show-branch -a \
| grep '\*' \
| grep -v `git rev-parse --abbrev-ref HEAD` \
| head -n1 \
| sed 's/[^\[]*//' \
| awk 'match($0, /\[[a-zA-Z0-9\/.-]+\]/) { print substr( $0, RSTART+1, RLENGTH-2 )}'

下面是它的工作原理:

  1. 显示所有提交的文本历史,包括远程分支。
  2. 当前提交的祖先由星号表示。过滤掉其他的东西。
  3. 忽略当前分支中的所有提交。
  4. 第一个结果将是最近的祖先分支。忽略其他结果。
  5. 分支名称显示在[括号中]。忽略括号以外的所有内容。
  6. 有时分支名称将包含~#^#,以指示在被引用的提交和分支提示之间有多少次提交。我们不在乎。忽略它们。

还有结果

运行上面的代码

 A---B---D <-master
\
\
C---E---I <-develop
\
\
F---G---H <-topic

如果你从H运行它会给你develop如果你从I运行它会给你master

# EYZ0。

Joe Chrysler的命令行魔法可以简化。下面是Joe的逻辑——为了简洁起见,我在两个版本中都引入了一个名为cur_branch的参数来代替命令替换`git rev-parse --abbrev-ref HEAD`;可以像这样初始化:

cur_branch=$(git rev-parse --abbrev-ref HEAD)

然后,这是Joe的管道:

git show-branch -a           |
grep '\*'                  | # we want only lines that contain an asterisk
grep -v "$cur_branch"      | # but also don't contain the current branch
head -n1                   | # and only the first such line
sed 's/.*\[\(.*\)\].*/\1/' | # really, just the part of the line between []
sed 's/[\^~].*//'            # and with any relative refs (^, ~n) removed

我们可以在一个相对简单的awk命令中完成与所有这五个单独的命令过滤器相同的事情:

git show-branch -a |
awk -F'[]^~[]' '/\*/ && !/'"$cur_branch"'/ {print $2;exit}'

具体情况是这样的:

-F'[]^~[]'

将行分割为]^~[字符的字段。

/\*/

找出包含星号的行

&& !/'"$cur_branch"'/

...但不是当前的分支名称

{ print $2;

当您找到这样的一行时,打印它的第二个字段(即字段分隔符第一次和第二次出现之间的部分)。对于简单的分支名称,这将是括号之间的内容;对于具有相对跳转的引用,它将只是没有修饰符的名称。因此,我们的字段分隔符集处理了两个sed命令的意图。

  exit }

然后立即退出。这意味着它只处理第一个匹配的行,所以我们不需要通过head -n 1管道输出。

Mark Reed的解决方案基本上是正确的。但是,请注意,提交行不仅应该包含星号,而且应该以星号开头!否则,包含星号的提交消息也包含在匹配的行中。所以它应该是:

# EYZ0

或者是更长的版本:

git show-branch -a           |
awk '^\*'                  | # we want only lines that contain an asterisk
awk -v "$current_branch"   | # but also don't contain the current branch
head -n1                   | # and only the first such line
sed 's/.*\[\(.*\)\].*/\1/' | # really, just the part of the line between []
sed 's/[\^~].*//'            # and with any relative refs (^, ~n) removed`

下面是Mark Reed的解决方案的PowerShell实现:

git show-branch -a | where-object { $_.Contains('*') -eq $true} | Where-object {$_.Contains($branchName) -ne $true } | select -first 1 | % {$_ -replace('.*\[(.*)\].*','$1')} | % { $_ -replace('[\^~].*','') }

使用Ant进行跨平台实现

    <exec executable="git" outputproperty="currentBranch">
<arg value="rev-parse" />
<arg value="--abbrev-ref" />
<arg value="HEAD" />
</exec>


<exec executable="git" outputproperty="showBranchOutput">
<arg value="show-branch" />
<arg value="-a" />
</exec>


<loadresource property="baseBranch">
<propertyresource name="showBranchOutput"/>
<filterchain>
<linecontains>
<contains value="*"/>
</linecontains>
<linecontains negate="true">
<contains value="${currentBranch}"/>
</linecontains>
<headfilter lines="1"/>
<tokenfilter>
<replaceregex pattern=".*\[(.*)\].*" replace="\1"/>
<replaceregex pattern="[\^~].*" replace=""/>
</tokenfilter>
</filterchain>
</loadresource>


<echo message="${currentBranch} ${baseBranch}" />

由于之前的答案在我们的存储库中都不起作用,我想分享我自己的方法,使用git log中的最新合并:

#!/bin/bash
git log --oneline --merges "$@" | grep into | sed 's/.* into //g' | uniq --count | head -n 10

把它放在一个名为git-last-merges的脚本中,该脚本也接受一个分支名称作为参数(而不是当前分支)以及其他git log参数。

从输出中,我们可以根据自己的分支约定和每个分支的合并数量手动检测父分支。

如果你经常在子分支上使用git rebase(合并通常是快进的,所以没有太多的合并提交),这个答案就不能很好地工作,所以我写了一个脚本来计算所有分支与当前分支相比的前提交(正常和合并)和后提交(在父分支中不应该有任何后合并)。

#!/bin/bash
HEAD="`git rev-parse --abbrev-ref HEAD`"
echo "Comparing to $HEAD"
printf "%12s  %12s   %10s     %s\n" "Behind" "BehindMerge" "Ahead" "Branch"
git branch | grep -v '^*' | sed 's/^\* //g' | while read branch ; do
ahead_merge_count=`git log --oneline --merges $branch ^$HEAD | wc -l`
if [[ $ahead_merge_count != 0 ]] ; then
continue
fi
ahead_count=`git log --oneline --no-merges $branch ^$HEAD | wc -l`
behind_count=`git log --oneline --no-merges ^$branch $HEAD | wc -l`
behind_merge_count=`git log --oneline --merges ^$branch $HEAD | wc -l`
behind="-$behind_count"
behind_merge="-M$behind_merge_count"
ahead="+$ahead_count"
printf "%12s  %12s   %10s     %s\n" "$behind" "$behind_merge" "$ahead" "$branch"
done | sort -n

使用:

vbc=$(git rev-parse --abbrev-ref HEAD)
vbc_col=$(( $(git show-branch | grep '^[^\[]*\*' | head -1 | cut -d* -f1 | wc -c) - 1 ))
swimming_lane_start_row=$(( $(git show-branch | grep -n "^[\-]*$" | cut -d: -f1) + 1 ))
git show-branch | tail -n +$swimming_lane_start_row | grep -v "^[^\[]*\[$vbc" | grep "^.\{$vbc_col\}[^ ]" | head -n1 | sed 's/.*\[\(.*\)\].*/\1/' | sed 's/[\^~].*//'

它实现了与Mark Reed的回答相同的目的,但它使用了一种更安全的方法,在许多场景中都不会出错:

  1. 父分支的最后一次提交是一个合并,使列显示-,而不是*
  2. 提交消息包含一个分支名称
  3. 提交消息包含*

这对我来说很有效:

git show-branch | grep '*' | grep -v "$(git rev-parse --abbrev-ref HEAD)" | head -n1 | sed 's/.*\[\(.*\)\].*/\1/' | sed 's/[\^~].*//'

礼貌的评论和回答从droidbot和@ jstanidiot。

现在任何人都想这样做——Atlassian的Sourcetree应用程序向你展示了你的分支如何相互关联的一个很好的可视化表示,即它们开始的位置和它们目前在提交顺序中的位置(例如,HEAD或4个提交后等)。

你也可以试试:

git log --graph --decorate

如果你使用Sourcetree,查看你的提交细节→父母。然后您将看到带下划线的提交数字(链接)。

Git父

您可以直接执行命令

git parent

如果您将乔·克莱斯勒的回答添加为Git别名,则可以找到分支的父分支。它将简化使用。

使用任何文本编辑器打开位于"~/.gitconfig"gitconfig文件(适用于Linux)。对于Windows,“。gitconfig"路径通常位于C:\users\your-user\.gitconfig

vim  ~/.gitconfig

在文件中添加如下alias命令:

[alias]
parent = "!git show-branch | grep '*' | grep -v \"$(git rev-parse --abbrev-ref HEAD)\" | head -n1 | sed 's/.*\\[\\(.*\\)\\].*/\\1/' | sed 's/[\\^~].*//' #"

保存并退出编辑器。

执行命令git parent

就是这样!

我并不是说这是解决问题的好方法,但这似乎对我来说确实有效:

git branch --contains $(cat .git/ORIG_HEAD)

问题是,隐藏一个文件是窥探Git的内部工作,所以这并不一定是向前兼容(或向后兼容)。

另一个:

git rev-list master | grep "$(git rev-list HEAD)" | head -1

获得最后一个提交,它既是我的分支,也是master(或您想指定的任何分支)。

一个解决方案

解决方案基于git show-branch不太适合我(见下文),所以我把它与基于git log结合起来,最后得到了这个:

git log --decorate --simplify-by-decoration --oneline \ # selects only commits with a branch or tag
| grep -v "(HEAD" \                               # removes current head (and branch)
| head -n1 \                                      # selects only the closest decoration
| sed 's/.* (\(.*\)) .*/\1/' \                    # filters out everything but decorations
| sed 's/\(.*\), .*/\1/' \                        # picks only the first decoration
| sed 's/origin\///'                              # strips "origin/" from the decoration

限制和注意事项

  • 可以分离HEAD(许多CI工具这样做是为了确保它们在给定的分支中构建正确的提交),但是起源分支和本地分支必须同时是相当于或“高于”和当前HEAD。
  • 一定有没有标签在路上(我假设;我还没有测试在子分支和父分支之间使用标签提交的脚本)
  • 脚本依赖于“头”总是由log命令变成列为第一装饰这一事实
  • 脚本在#EYZ0和develop的结果(大部分)是<SHA> Initial commit

结果

 A---B---D---E---F <-origin/master, master
\      \
\      \
\      G---H---I <- origin/hotfix, hotfix
\
\
J---K---L <-origin/develop, develop
\
\
M---N---O <-origin/feature/a, feature/a
\   \
\   \
\   P---Q---R <-origin/feature/b, feature/b
\
\
S---T---U <-origin/feature/c, feature/c

尽管本地分支存在(例如,只有origin/topic存在,因为提交O是由它的SHA直接签出的),脚本应该打印如下:

  • 对于提交# eyz0, # eyz1, # eyz2(分支hotfix)→master
  • 对于提交# eyz0, # eyz1, # eyz2(分支feature/a)→develop
  • 对于提交# eyz0, # eyz1, # eyz2(分支feature/c)→develop
  • 对于提交# eyz0, # eyz1, # eyz2(分支feature/b)→feature/a
  • 对于提交JKL(分支develop)→<sha> Initial commit*
  • 对于提交BDEF(分支master)→<sha> Initial commit

* -或master如果develop的提交是在master的HEAD的顶部(~ master将是快速前进的开发)


为什么show-branch对我没用

在以下情况下,EYZ1解决方案对我来说并不可靠:

  • 分离的头 -包括分离的头部情况意味着取代grep '\*' \为“grep”!——而这只是所有麻烦的开始
  • 脚本在#EYZ0和develop的结果分别是develop和' '
  • 分支在master分支(hotfix/分支)最终以develop作为父分支,因为它们最近的master分支父分支被标记为!而不是*是有原因的。

当我做了开发release-v1.0.0feature-foo这样的事情时,这对我不起作用。它会一直往回发展。请注意,其中涉及到一个基数调整,我不确定这是否加剧了我的问题……

下面给出了正确的提交哈希值:

git log --decorate \
| grep 'commit' \
| grep 'origin/' \
| head -n 2 \
| tail -n 1 \
| awk '{ print $2 }' \
| tr -d "\n"

这是我的PowerShell版本:

function Get-GHAParentBranch {
[CmdletBinding()]
param(
$Name = (git branch --show-current)
)
git show-branch |
Select-String '^[^\[]*\*' |
Select-String -NotMatch -Pattern "\[$([Regex]::Escape($Name)).*?\]" |
Select-Object -First 1 |
Foreach-Object {$PSItem -replace '^.+?\[(.+)\].+$','$1'}
}
git log -2 --pretty=format:'%d' --abbrev-commit | tail -n 1 | sed 's/\s(//g; s/,/\n/g';

(来源/母体名,母体名)

git log -2 --pretty=format:'%d' --abbrev-commit | tail -n 1 | sed 's/\s(//g; s/,/\n/g';

来源/母体名

git log -2 --pretty=format:'%d' --abbrev-commit | tail -n 1 | sed 's/(.*,//g; s/)//';

母体名

基于git show-branch -a加上一些过滤器的解决方案有一个缺点:Git可能会考虑存在时间较短的分支的分支名称。

如果你有一些你关心的潜在父母,你可以问自己类似的问题(可能也是OP想知道的):

从所有分支的特定子集,哪个是最近的父git分支?< / em >

为了简化起见,我将考虑“一个git分支”;引用HEAD(即当前分支)。

让我们假设我们有以下分支:

HEAD
important/a
important/b
spam/a
spam/b

基于git show-branch -a +过滤器的解决方案可能会给出HEAD的最近父节点是spam/a,但我们不关心这一点。

如果我们想知道important/aimportant/b中哪一个是HEAD的最近父节点,我们可以运行以下命令:

for b in $(git branch -a -l "important/*" | sed -E -e "s/\*//"); do
d1=$(git rev-list --first-parent ^${b} HEAD | wc -l);
d2=$(git rev-list --first-parent ^HEAD ${b} | wc -l);
echo "${b} ${d1} ${d2}";
done \
| sort -n -k2 -k3 \
| head -n1 \
| awk '{print $1}';

它的作用:

1.) $(git branch -a -l "important/*" | sed -E -e "s/\*//"):打印带有一些模式("important/*")的所有分支的列表。(如果您恰好在important/*分支中的一个,git branch将包含一个*来指示您当前的分支。命令替换$()将把它展开到当前目录的内容中。sedgit branch的输出中删除*。)

2.) d=$(git rev-list --first-parent ^${b} HEAD | wc -l);:对于每个分支($b),计算从HEAD$b中最近的提交的距离($d1)(类似于计算从一点到直线的距离)。你可能需要考虑不同的距离在这里:你可能不想使用--first-parent,或者可能想要从尖端到分支尖端的距离("${b}"...HEAD),…

2.2) d2=$(git rev-list --first-parent ^HEAD ${b} | wc -l);:对于每个分支($b),计算从分支顶端到HEAD中最近的提交的距离($d2)。我们将使用这个距离在距离$d1相等的两个分支之间进行选择。

3.) echo "${b} ${d1} ${d2}";:打印每个分支的名称,后面跟着距离,以便稍后对它们进行排序(首先是$d1,然后是$d2)。

4.) | sort -n -k2 -k3:对之前的结果进行排序,因此我们得到了所有分支的排序(按距离)列表,然后是它们的距离(两者)。

5) | head -n1:上一步的第一个结果将是距离较小的分支,即最近的父分支。所以丢弃所有其他分支。

6) | awk '{print $1}';:我们只关心分支名称,而不关心距离,因此提取第一个字段,这是父名称。在这里!:)

Git附带了几个GUI客户端,可以帮助您可视化这些内容。打开GitGUI,进入菜单存储库可视化所有分支历史

为什么家里没有家长?

以下内容

总结:

为什么会有人想读这篇长文章? 因为之前的答案很清楚 用原来的问题来理解问题, 他们没有得到正确/有意义的结果; 或准确地解决不同的问题

请随意回顾第一部分; 它解决了“find something"问题, 应该突出问题的范围。 对某些人来说,这可能就足够了 这将告诉你一种方法 提取正确的&来自git的有意义的结果 (你可能不喜欢), 并演示一种应用方法 你对惯例的了解 对于这些结果

以下各节涵盖:

    <李> # EYZ0:
    • 最近的git分支使用git show-branch
    • 预期的结果应该是什么样的
  • 示例图&结果
  • 批处理分支:围绕git show-branch的限制工作
  • <李> # EYZ0: 引入(命名)约定以改善结果

问题的问题

如上所述,git不跟踪分支之间的关系; 分支只是引用提交的名称。 在官方git文档和其他来源中,我们经常会遇到一些误导性的图表,例如:

A---B---C---D    <- master branch
\
E---F      <- work branch

让我们改变图表的形式和层次暗示的名称,以显示一个等效的图表:

      E---F   <- jack
/
A---B
\
C---D   <- jill

这个图(以及git)完全没有告诉我们哪个分支是先创建的(因此,哪个分支是从另一个分支分支出来的)。

在第一个图中,masterwork的父级,这是一种惯例。

因此

  • 简单的工具将产生忽略偏差的响应
  • 更复杂的工具包含了约定(偏差)。

一个公正的问题

首先,我必须首先承认Joe Chrysler的回应,这里的其他回应,以及周围的许多评论/建议; 他们激励了我,为我指明了道路!< / p >

请允许我重新措辞Joe的措辞,考虑到与最近的提交相关的多个分支(它发生了!):

函数之外的分支上最近的提交是什么

.当前分支,是哪个分支

换句话说:

第一季度

给定分支B: 考虑提交C最接近B'HEAD (C可以是B'HEAD) 由其他分支共享的: 除了B,还有哪些分支的提交历史中有C ?< / p >

一个公正的解决方案

先道歉;人们似乎更喜欢说俏皮话。请随意提出(可读/可维护的)改进建议!

#!/usr/local/bin/bash


# git show-branch supports 29 branches; reserve 1 for current branch
GIT_SHOW_BRANCH_MAX=28


CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
if (( $? != 0 )); then
echo "Failed to determine git branch; is this a git repo?" >&2
exit 1
fi




##
# Given Params:
#   EXCEPT : $1
#   VALUES : $2..N
#
# Return all values except EXCEPT, in order.
#
function valuesExcept() {
local except=$1 ; shift
for value in "$@"; do
if [[ "$value" != "$except" ]]; then
echo $value
fi
done
}




##
# Given Params:
#   BASE_BRANCH : $1           : base branch; default is current branch
#   BRANCHES    : [ $2 .. $N ] : list of unique branch names (no duplicates);
#                                perhaps possible parents.
#                                Default is all branches except base branch.
#
# For the most recent commit in the commit history for BASE_BRANCH that is
# also in the commit history of at least one branch in BRANCHES: output all
# BRANCHES that share that commit in their commit history.
#
function nearestCommonBranches() {
local BASE_BRANCH
if [[ -z "${1+x}" || "$1" == '.' ]]; then
BASE_BRANCH="$CURRENT_BRANCH"
else
BASE_BRANCH="$1"
fi


shift
local -a CANDIDATES
if [[ -z "${1+x}" ]]; then
CANDIDATES=( $(git rev-parse --symbolic --branches) )
else
CANDIDATES=("$@")
fi
local BRANCHES=( $(valuesExcept "$BASE_BRANCH" "${CANDIDATES[@]}") )


local BRANCH_COUNT=${#BRANCHES[@]}
if (( $BRANCH_COUNT > $GIT_SHOW_BRANCH_MAX )); then
echo "Too many branches: limit $GIT_SHOW_BRANCH_MAX" >&2
exit 1
fi


local MAP=( $(git show-branch --topo-order "${BRANCHES[@]}" "$BASE_BRANCH" \
| tail -n +$(($BRANCH_COUNT+3)) \
| sed "s/ \[.*$//" \
| sed "s/ /_/g" \
| sed "s/*/+/g" \
| egrep '^_*[^_].*[^_]$' \
| head -n1 \
| sed 's/\(.\)/\1\n/g'
) )


for idx in "${!BRANCHES[@]}"; do
## to include "merge", symbolized by '-', use
## ALT: if [[ "${MAP[$idx]}" != "_" ]]
if [[ "${MAP[$idx]}" == "+" ]]; then
echo "${BRANCHES[$idx]}"
fi
done
}


# Usage: gitr [ baseBranch [branchToConsider]* ]
#   baseBranch: '.' (no quotes needed) corresponds to default current branch
#   branchToConsider* : list of unique branch names (no duplicates);
#                        perhaps possible (bias?) parents.
#                        Default is all branches except base branch.
nearestCommonBranches "${@}"

工作原理

考虑输出:git show-branch

对于git show-branch --topo-order feature/g hotfix master release/2 release/3 feature/d,输出看起来类似于:

! [feature/g] TEAM-12345: create X
* [hotfix] TEAM-12345: create G
! [master] TEAM-12345: create E
! [release/2] TEAM-12345: create C
! [release/3] TEAM-12345: create C
! [feature/d] TEAM-12345: create S
------
+      [feature/g] TEAM-12345: create X
+      [feature/g^] TEAM-12345: create W
+ [feature/d] TEAM-12345: create S
+ [feature/d^] TEAM-12345: create R
+ [feature/d~2] TEAM-12345: create Q
...
+    [master] TEAM-12345: create E
*     [hotfix] TEAM-12345: create G
*     [hotfix^] TEAM-12345: create F
*+    [master^] TEAM-12345: create D
+*+++  [release/2] TEAM-12345: create C
+*++++ [feature/d~8] TEAM-12345: create B

以下几点:

  • 原来的命令在命令行上列出了N(6)个分支名称
  • 这些分支名称按顺序出现在输出的前N行
  • 标题后面的行表示提交
  • 提交行的前N列表示(作为一个整体)分支/提交矩阵"其中X列中的单个字符表示分支(标题行X)和当前提交之间的关系(或缺乏关系)。

主要步骤

  1. 给定BASE_BRANCH
  2. 给定一个不包括BASE_BRANCH的有序集(唯一)BRANCHES
  3. 为简洁起见,设NBRANCH_COUNT, 大小为BRANCHES; 它不包括BASE_BRANCH <李> # EYZ0:
      因为BRANCHES只包含唯一的名称(假定有效) 名称将与输出的标题行映射为1-1, 并对应于分支/提交矩阵的前N列 因为BASE_BRANCH不在BRANCHES中 这将是标题的最后一行, 对应于最后一列分支/提交矩阵
  4. tail:从行N+3开始;丢掉第一行N+2: N个分支+基础分支+分隔行---..
  5. 这些可以合并成一个…但为了清晰起见,将它们分开
    • 删除分支/提交矩阵之后的所有内容
    • 将空格替换为下划线“_”; 我的主要原因是避免潜在的IFS解析麻烦 和调试/可读性。
    • +替换*;基本分支总是在最后一列, 这就足够了。此外,如果不加处理,它会经过bash 路径名展开,使用*
    • 总是很有趣的
  6. egrep: grep用于提交映射到至少一个分支([^_])和BASE_BRANCH ([^_]$)。也许这个基本分支模式应该是\+$?
  7. head -n1:执行剩下的第一个提交
  8. sed:将分支/提交矩阵的每个字符分开到单独的行。
  9. 捕获数组MAP中的行,此时我们有两个数组:
    • BRANCHES:长度N
    • MAP:长度N+1:第一个N元素1-1与BRANCHES,最后一个元素对应BASE_BRANCH
  10. 遍历BRANCHES(这就是我们想要的,它更短),并检查MAP中的相应元素:如果MAP[$idx]+,则输出BRANCH[$idx]

示例图&结果

请看下面这个有点做作的例子:

  • 将使用有偏见的名称,因为它们有助于(我)衡量和考虑结果。
  • 假设合并已经存在并且被忽略。
  • 图形通常试图突出显示这样的分支(分叉), 没有从视觉上暗示偏好/层次; 具有讽刺意味的是,master在我完成这个事情后脱颖而出
                         J                   <- feature/b
/
H
/ \
/   I                   <- feature/a
/
D---E                     <- master
/ \
/   F---G                   <- hotfix
/
A---B---C                             <- feature/f, release/2, release/3
\   \
\   W--X                        <- feature/g
\
\       M                     <- support/1
\     /
K---L                       <- release/4
\
\       T---U---V     <- feature/e
\     /
N---O
\
P             <- feature/c
\
Q---R---S   <- feature/d

例子图的无偏结果

假设脚本在可执行文件gitr中,然后运行:

gitr <baseBranch>

对于不同的分支B,我们得到以下结果:

鉴于B 共享提交C 分支,B和C的历史?
特性/ H 特性/ b
特性/ b H 特性/
特性/ c P 特性/ d
特性/ d P 特性/ c
特性/ e O 特性/ c,功能/ d
特性/ f C 特性/a,特性/b,特性/g,热修复,master,版本/2,版本/3
特性/ g C 特性/a,特性/b,特性/f,热修复,master,版本/2,版本/3
热修复补丁 D Feature /a, Feature /b, master
D 功能/a,功能/b,热修复
发布/ 2 C Feature /a, Feature /b, Feature /f, Feature /g, hotfix, master, release/3
发布/ 3 C Feature /a, Feature /b, Feature /f, Feature /g, hotfix, master, release/2
发布/ 4 l Feature /c, Feature /d, Feature /e, support/1
支持/ 1 l Feature /c, Feature /d, Feature /e, release/4
< / div > # EYZ0

[在此阶段出现 因为在这一点上它最适合最终的脚本。 这部分不是必需的,请跳过。]

git show-branch将自己限制为29个分支。 这对某些人来说可能是一个阻碍(没有判断,只是说!) 在某些情况下,我们可以改善结果,

.

.

.
  • BASE_BRANCH必须与每个分支一起提交。
  • 如果一个repo中存在大量分支 这本身可能有有限的价值 如果你找到其他方法,可能会提供更大的价值
  • . . 前一点符合我的用例, 所以向前冲吧!李< / >

这种机制并不完美, 当结果大小接近最大值(29)时, 预计它会失败。下面的细节< /强> < / em > < / p >

批处理解决方案

#
# Remove/comment-out the function call at the end of script,
# and append this to the end.
##


##
# Given:
#   BASE_BRANCH : $1           : first param on every batch
#   BRANCHES    : [ $2 .. $N ] : list of unique branch names (no duplicates);
#                                perhaps possible parents
#                                Default is all branches except base branch.
#
# Output all BRANCHES that share that commit in their commit history.
#
function repeatBatchingUntilStableResults() {
local BASE_BRANCH="$1"


shift
local -a CANDIDATES
if [[ -z "${1+x}" ]]; then
CANDIDATES=( $(git rev-parse --symbolic --branches) )
else
CANDIDATES=("$@")
fi
local BRANCHES=( $(valuesExcept "$BASE_BRANCH" "${CANDIDATES[@]}") )


local SIZE=$GIT_SHOW_BRANCH_MAX
local COUNT=${#BRANCHES[@]}
local LAST_COUNT=$(( $COUNT + 1 ))


local NOT_DONE=1
while (( $NOT_DONE && $COUNT < $LAST_COUNT )); do
NOT_DONE=$(( $SIZE < $COUNT ))
LAST_COUNT=$COUNT


local -a BRANCHES_TO_BATCH=( "${BRANCHES[@]}" )
local -a AGGREGATE=()
while (( ${#BRANCHES_TO_BATCH[@]} > 0 )); do
local -a BATCH=( "${BRANCHES_TO_BATCH[@]:0:$SIZE}" )
AGGREGATE+=( $(nearestCommonBranches "$BASE_BRANCH" "${BATCH[@]}") )
BRANCHES_TO_BATCH=( "${BRANCHES_TO_BATCH[@]:$SIZE}" )
done
BRANCHES=( "${AGGREGATE[@]}" )
COUNT=${#BRANCHES[@]}
done
if (( ${#BRANCHES[@]} > $SIZE )); then
echo "Unable to reduce candidate branches below MAX for git-show-branch" >&2
echo "  Base Branch : $BASE_BRANCH" >&2
echo "  MAX Branches: $SIZE" >&2
echo "  Candidates  : ${BRANCHES[@]}" >&2
exit 1
fi
echo "${BRANCHES[@]}"
}


repeatBatchingUntilStableResults "$@"
exit 0

工作原理

重复,直到结果稳定

  1. BRANCHES拆成批次 GIT_SHOW_BRANCH_MAX(也就是SIZE)元素
  2. 叫# EYZ0
  3. 将结果聚合到一个新的(更小的?)分支集

为什么会失败

如果聚合的分支数量超过最大SIZE 进一步的批处理/处理不能减少这个数字 然后:< / p >

  • 聚合的分支就是解, 但是无法通过git show-branch
  • 验证
  • 每批不减; 一个批次的分支可能有助于减少另一个批次的分支 (差异归并基);
  • .当前算法承认失败,失败

考虑替代

将一个基本分支与每个感兴趣的其他分支单独配对,为每对确定一个提交节点(合并基);按照提交历史顺序对归并基集进行排序,取最近的节点,确定与该节点关联的所有分支。

我是事后诸葛亮才说的。 这可能是正确的选择。 我在前进;

.

.

.

有偏见的问题

你可能已经注意到核心函数nearestCommonBranches . # 在前面的脚本中回答了比问题Q1问的更多的问题。 实际上,该函数回答了一个更普遍的问题:

第二季

给定一个分支B和 分支的有序集(没有重复)P (B不在P中): 考虑提交C最接近B'HEAD (C可以是B'HEAD) 由P中的分支共享: 按P的顺序,P中的哪些分支在它们的提交历史中有C ?< / p >

选择P提供了偏差,或者描述了一个(有限的)约定。 要匹配您的偏见/惯例的所有特征可能需要额外的工具,这超出了本文的讨论范围

简单偏见/惯例建模

偏差因组织不同而异&实践, 以下内容可能不适合您的组织。 不出意外的话,也许这里的一些想法会有所帮助 你找到了你需要的解决方案。

有偏解;分支命名惯例的偏差

也许偏差可以映射到,并从,

偏向P(那些其他分支名称)

我们将在下一步中需要这个, 因此,让我们看看通过regex过滤分支名称可以做什么

上面的代码和下面的新代码的组合可以作为gist: gitr

#
# Remove/comment-out the function call at the end of script,
# and append this to the end.
##


##
# Given Params:
#   BASE_BRANCH : $1           : base branch
#   REGEXs      : $2 [ .. $N ] : regex(s)
#
# Output:
#   - git branches matching at least one of the regex params
#   - base branch is excluded from result
#   - order: branches matching the Nth regex will appear before
#            branches matching the (N+1)th regex.
#   - no duplicates in output
#
function expandUniqGitBranches() {
local -A BSET[$1]=1
shift


local ALL_BRANCHES=$(git rev-parse --symbolic --branches)
for regex in "$@"; do
for branch in $ALL_BRANCHES; do
## RE: -z ${BSET[$branch]+x ...  ; presumes ENV 'x' is not defined
if [[ $branch =~ $regex && -z "${BSET[$branch]+x}" ]]; then
echo "$branch"
BSET[$branch]=1
fi
done
done
}




##
# Params:
#   BASE_BRANCH: $1    : "." equates to the current branch;
#   REGEXS     : $2..N : regex(es) corresponding to other to include
#
function findBranchesSharingFirstCommonCommit() {
if [[ -z "$1" ]]; then
echo "Usage: findBranchesSharingFirstCommonCommit ( . | baseBranch ) [ regex [ ... ] ]" >&2
exit 1
fi


local BASE_BRANCH
if [[ -z "${1+x}" || "$1" == '.' ]]; then
BASE_BRANCH="$CURRENT_BRANCH"
else
BASE_BRANCH="$1"
fi


shift
local REGEXS
if [[ -z "$1" ]]; then
REGEXS=(".*")
else
REGEXS=("$@")
fi


local BRANCHES=( $(expandUniqGitBranches "$BASE_BRANCH" "${REGEXS[@]}") )


## nearestCommonBranches  can also be used here, if batching not used.
repeatBatchingUntilStableResults "$BASE_BRANCH" "${BRANCHES[@]}"
}


findBranchesSharingFirstCommonCommit "$@"

偏倚结果示例图

我们考虑有序集

# EYZ0

假设脚本(所有部分)在可执行文件gitr中,然后运行:

gitr <baseBranch> '^release/.*$' '^support/.*$' '^master$'

对于不同的分支B,我们得到以下结果:

鉴于B 共享提交C 分支P和分支C的历史(按顺序)
特性/ D
特性/ b D
特性/ c l 发布/ 4,支持/ 1
特性/ d l 发布/ 4,支持/ 1
特性/ e l 发布/ 4,支持/ 1
特性/ f C Release /2, Release /3, master
特性/ g C Release /2, Release /3, master
热修复补丁 D
C / 2发布,发布/ 3
发布/ 2 C 发布/ 3,主人
发布/ 3 C 发布/ 2,主人
发布/ 4 l 支持/ 1
支持/ 1 l 发布/ 4

这离确定答案越来越近了;发布分支的响应并不理想。让我们更进一步。

偏向BASE_NAMEP

一个方向是使用不同的P来表示不同的 基地名称。我们来设计一下。

约定

免责声明:我不是一个纯粹的git流主义者,请体谅我

    支持分支应从主机分支。
    • 不会有两个支持分支共享一个共同的提交。
  • 热修复分支应该从支持分支或主分支分支出来。
  • 释放分支应从支持分支或主分支分支出来。
      可能有多个发布分支共享一个共同提交; . .
  • 一个bug修复分支应该从一个发布分支分支出来。
  • 一个特性分支可以从一个特性、发布、支持或master中分支出来:
    • for &; parents " 不能将一个特性分支建立为
    • . .
    • 因此:跳过特征分支和 寻找“父母”;在发布、支持和/或主分支之间
  • 任何其他被认为是工作分支的分支名称,

让我们看看我们git有多远:

基本分支图 父分支,已排序 评论(s)
^大师美元 N/A 没有父母
^ /支持。*美元 ^大师美元
/ . * $ ^热修复补丁 ^ /支持。* $ ^ $大师 优先使用支持分支而不是主分支(排序)
^ /发布。*美元 ^ /支持。* $ ^ $大师 优先使用支持分支而不是主分支(排序)
/ . * $ ^错误修复 ^ /发布。*美元
/ ^功能。*美元 ^ /发布。* $ ^支持/。* $ ^ $大师
^ * $。 ^ /发布。* $ ^支持/。* $ ^ $大师 冗余,但保持设计关注点分离
< / div > # EYZ0

上面的代码和下面的新代码的组合可以作为gist: gitp

#
# Remove/comment-out the function call at the end of script,
# and append this to the end.
##


# bash associative arrays maintain key/entry order.
# So, use two maps, values correlated by index:
declare -a MAP_BASE_BRANCH_REGEX=( "^master$" \
"^support/.*$" \
"^hotfix/.*$" \
"^release/.*$" \
"^bugfix/.*$" \
"^feature/.*$" \
"^.*$" )


declare -a MAP_BRANCHES_REGEXS=("" \
"^master$" \
"^support/.*$ ^master$" \
"^support/.*$ ^master$" \
"^release/.*$" \
"^release/.*$ ^support/.*$ ^master$" \
"^release/.*$ ^support/.*$ ^master$" )


function findBranchesByBaseBranch() {
local BASE_BRANCH
if [[ -z "${1+x}" || "$1" == '.' ]]; then
BASE_BRANCH="$CURRENT_BRANCH"
else
BASE_BRANCH="$1"
fi


for idx in "${!MAP_BASE_BRANCH_REGEX[@]}"; do
local BASE_BRANCH_REGEX=${MAP_BASE_BRANCH_REGEX[$idx]}
if [[ "$BASE_BRANCH" =~ $BASE_BRANCH_REGEX ]]; then
local BRANCHES_REGEXS=( ${MAP_BRANCHES_REGEXS[$idx]} )
if (( ${#BRANCHES_REGEXS[@]} > 0 )); then
findBranchesSharingFirstCommonCommit $BASE_BRANCH "${BRANCHES_REGEXS[@]}"
fi
break
fi
done
}


findBranchesByBaseBranch "$1"
偏倚结果示例图

假设脚本(所有部分)在可执行文件gitr中,然后运行:

gitr <baseBranch>

对于不同的分支B,我们得到以下结果:

鉴于B 共享提交C 分支P和分支C的历史(按顺序)
特性/ D
特性/ b D
特性/ c l 发布/ 4,支持/ 1
特性/ d l 发布/ 4,支持/ 1
特性/ e l 发布/ 4,支持/ 1
特性/ f C Release /2, Release /3, master
特性/ g C Release /2, Release /3, master
热修复补丁 D
(空白,无值)
发布/ 2 C
发布/ 3 C
发布/ 4 l 支持/ 1
支持/ 1 l
< / div > # EYZ0

机会!

在最后一个例子中,发布分支共享一个公共提交 与多个其他分支:发布、支持或主分支。

让我们“refactor"或者重新评估使用的约定,并收紧一点。

考虑git使用约定:

当创建一个新的发布分支时: 立即创建一个新的提交;可能更新一个版本,或者README文件。 这确保了特性/工作分支 对于发布(从发布分支出来) 是否将提交与发布分支共享 在底层的提交之前(并且不被共享)

.支持或主分支

例如:

        G---H   <- feature/z
/
E         <- release/1
/
A---B---C---D   <- master
\
F         <- release/2
一个特性分支从发布/1不能有一个公共提交 这包括release/1(它的父)和master或release/2.

为每个分支提供一个结果,parent,

完成了!有了工具和约定,我可以生活在一个OCD友好的结构化git世界。

你的里程可能会有所不同!

分开的想法

  1. 依据
  1. 最重要的:我已经得出结论, 除了这里展示的内容, 在某种程度上,人们可能需要接受可能有多个 分支处理。

    • 也许可以在所有潜在的分支上进行验证; “at-least-one"或“;all"还是? ?
  2. 像这样的几个星期,我真的觉得是时候学习Python了。

我不喜欢解析半结构化文本输出时涉及的不安全假设,所以我想要一个更健壮的解决方案,假设更少:

# Search backwards in history for the first commit that is in a branch other than $1
# and output that branch's name.
parent_branch() {
local result rev child_branch=$1
rev=$(git rev-parse --revs-only $child_branch)
while [[ -n $rev ]]; do
result=$(git branch --contains $rev | grep -v " $child_branch$")
if [[ -n $result ]]; then
echo $result
return 0
fi
rev=$(git rev-parse --revs-only $rev^)
done
return 1
}

警告:由于这是在历史上迭代地向后走,查看每个提交以找到第一个位于不同分支的提交,因此分支越长,代价就越高。但是,由于通常情况下分支应该是相对短暂的,所以这不应该是一个太大的问题。

还要注意,我使用的是git branch --contains,所以这也会找到共享公共基础但已经超越它的分支。若要仅查找恰好指向公共基础的分支,请使用git branch --points-at

在多个分支中寻找第一次提交的shell函数:

# Get the first commit hash of a given branch.
# Uses `git branch --contains` to backward (starts from HEAD) check each commits
# and output that branch's name.
first_commit_of_branch() {
if [ $# -ne 1 ] || [ -z "${1}" ] ; then
(>&2 echo "Error: Missing or empty branch name.")
(>&2 echo "Usage: $0 branch_to_test")
return 2
fi
local branch_to_test="${1}"; shift
local commit_index_to_test
local maximum_number_of_commit_to_test
local branch_count_having_tested_commit


git rev-parse --verify --quiet "${branch_to_test}" 2>&1 > /dev/null || {
(>&2 echo "Error: Branch \"${branch_to_test}\" does not exists.")
return 2
}


commit_index_to_test=0
maximum_number_of_commit_to_test=$(git rev-list --count "${branch_to_test}")


while [ ${commit_index_to_test} -le ${maximum_number_of_commit_to_test} ] ; do
# Testing commit $branch_to_test~$commit_index_to_test…


# If it fails, it means we tested all commits from the most recent of
# branch $branch_to_test to the very first of the git DAG. So it must be it.
git rev-parse --verify --quiet ${branch_to_test}~${commit_index_to_test} 2>&1 > /dev/null || {
git rev-list --max-parents=0 "${branch_to_test}"
return 0
}


branch_count_having_tested_commit="$( \
git --no-pager branch --no-abbrev --verbose \
--contains ${branch_to_test}~${commit_index_to_test} \
| cut -c 3- \
| cut -d ' ' -f 2 \
| wc -l \
)"


# Tested commit found in more than one branch
if [ ${branch_count_having_tested_commit} -gt 1 ] ; then
if [ ${commit_index_to_test} -eq 0 ]; then
(>&2 echo "Error: The most recent commit of branch \"${branch_to_test}\" (${branch_to_test}~${commit_index_to_test}) is already in more than one branch. This is likely a new branch without any commit (yet). Cannot continue.")
return 1
else
# Commit $branch_to_test~$commit_index_to_test is in more than
# one branch, stopping there…
git rev-parse ${branch_to_test}~$((commit_index_to_test-1))
return 0
fi
fi
# else: Commit $branch_to_test~$commit_index_to_test is still only in
#       branch ${branch_to_test} continuing…"
commit_index_to_test=$((commit_index_to_test+1))
done
}

警告:当在一个有子分支的分支上执行并且没有新的提交时,它会失败。

A---B---C---D      <- "main" branch
\   \
\   E---F        <- "work1" branch
\       \
\       G---H  <- "work1-b" branch
\
I---J        <- "work2" branch
first_commit_of_branch main # C
first_commit_of_branch work1 # (Fails)
first_commit_of_branch work1-b # G
first_commit_of_branch work2 # I