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

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

Master是我的,这是最新的稳定发布代码,develop是我的,这是“下一个”发布代码,feature是一个正在准备开发的新功能。

使用钩子,我希望能够拒绝推送功能到我的远程存储库,除非commit f是develop HEAD的直接后代。也就是说,提交树看起来是这样的,因为feature已经基于d。

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

那么是否有可能:

识别特征的父分支? 确定父分支中的提交f是哪个分支的后代?

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


当前回答

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

你可以使用git branch——contains列出所有来自develop提示的分支,然后使用grep确保feature在其中。

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

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

其他回答

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

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

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

为了简化起见,我将“一个git分支”表示HEAD(即当前分支)。

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

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

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

如果我们想知道important/a和important/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/*")的所有分支列表。(如果你碰巧在一个重要的/*分支上,git分支将包含一个*来指示你当前的分支。命令替换$()将把它展开到当前目录的内容中。sed从git分支的输出中删除*。)

2.) d=$(git revl -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的历史记录基于提交的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的。但是巴兹的祖先并没有改变。我们只是删除了一个标签(以及由此产生的悬空提交)。如果我们在4处添加一个新标签呢?

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

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

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

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

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

对于所有被推送的提交的祖先(不包括develop的当前尖端及其祖先),它们以develop的当前尖端为父: 是否至少存在一个这样的提交? 所有这样的提交都是单亲提交吗?

可以实现为:

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

上面的代码可以在接受H'、J、K或N的同时拒绝手牌S,但它也可以接受L和P(它们涉及合并,但它们不合并develop的尖端)。

要拒绝L和P,你可以改变问题,然后问

对于所有被推送的提交的祖先(不包括develop的当前尖端及其祖先): 有双亲的提交吗? 如果不是,是否至少有一个这样的提交有开发其父(唯一)的当前提示?

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

一个解决方案

基于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相同或“高于”当前HEAD。 不能有标签挡道(我想;我还没有测试在子分支和父分支之间使用标签提交的脚本) 脚本依赖于“HEAD”总是被log命令列为第一个装饰的事实 在master上运行脚本并开发结果(大部分)在<SHA>初始提交

结果

 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直接签出的),脚本应该打印如下:

对于提交G, H, I(分支热修复)→master 对于提交M, N, O(分支特征/a)→开发 对于提交S, T, U(分支功能/c)→开发 对于提交P, Q, R(分支特征/b)→特征/a 对于提交J, K, L(分支开发)→<sha>初次提交* 对于提交B, D, E, F (master分支)→<sha>初次提交

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


为什么show-branch对我没用

基于git show-branch的解决方案在以下情况下被证明是不可靠的:

分离头-包括分离头情况意味着替换grep '\*' \为' grep '!——而这只是所有麻烦的开始 在master和develop上运行脚本的结果分别为develop和' ' 主分支(hotfix/ Branches)上的分支最终以develop作为父分支,因为它们最近的主分支父分支被标记为!而不是*是有原因的。

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

git show-branch——| awk - f '[]^~[]' '/^\*/ && !/'"$ current_branch”/{打印2美元;退出}'

或者是更长的版本:

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`

使用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}" />