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

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,以确定该特性是否需要重基。


当前回答

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

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 log --graph --decorate

基于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}';:我们只关心分支的名称,而不关心距离,因此提取第一个字段,这是父节点的名称。在这里!:)

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来输出。

一个rephrasal

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

一个解决方案

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

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

AWK:

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 )}'

下面是它的工作原理:

Display a textual history of all commits, including remote branches. Ancestors of the current commit are indicated by a star. Filter out everything else. Ignore all the commits in the current branch. The first result will be the nearest ancestor branch. Ignore the other results. Branch names are displayed [in brackets]. Ignore everything outside the brackets, and the brackets. Sometimes the branch name will include a ~# or ^# to indicate how many commits are between the referenced commit and the branch tip. We don't care. Ignore them.

结果是

运行上面的代码

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

如果你从H运行它会给你发展,如果你从I运行它会给你掌握。

代码可以作为要点提供。

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

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