如何找到Git分支的最近父级

601

假设我有以下本地仓库,其提交树如下:

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

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

使用 Git hooks,我希望能够拒绝将 feature 推送到远程仓库,除非提交 fdevelop HEAD 的直接子节点。也就是说,提交树看起来像这样,因为 feature 已经在 d 上进行了 git rebase

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

那么是否有可能:

  • 确定feature的父分支?
  • 确定f是哪个父分支的后代提交?

然后我会检查父分支的HEAD并查看f的前任提交是否与父分支的HEAD匹配,以确定是否需要对该功能进行变基。


这个问题应该重新表述为查找父级的父级。 - Tim Boland
17
通常我使用 git log --first-parent 命令,它会显示当前分支的所有提交记录,然后显示父分支及其提交记录。 - Vipul Patil
28个回答

404
假设远程仓库已经有了develop分支的一个副本(根据您最初的描述,它描述了在本地仓库中,但听起来它也存在于远程仓库),您应该能够实现我认为您想要的结果,但方法与您所设想的有些不同。
Git的历史基于提交的DAG。 分支(以及“refs”总体)只是指向不断增长的提交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 标签。
因此,您可以提出问题,例如“推送的提交是否是 develop 分支当前尖端的后代?”,但您不能可靠地询问“推送的提交的父分支是什么?”。
一个基本上可靠的问题似乎接近您想要的是:
对于所有推送的提交的祖先(不包括 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

上面的代码可以用于拒绝 HS,同时接受 H'JKN,但它也会接受 LP(它们涉及合并,但不合并 develop 的顶端)。
为了拒绝 LP,您可以改变问题并询问:
对于所有已推送的提交祖先(不包括当前的 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_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: 致命错误:模糊参数 '...':既有修订版本又有文件名。三个点的意图是什么? - Jack Ukleja
1
@Schneider 我非常确定 '...' 在这个例子中是一个占位符:如果你用要执行此检查的提交的SHA(比如,你当前所在分支的HEAD)替换它,一切都能正常工作。 - Daniel Brady
谢谢您提供这么详细的答案!非常有用。我想创建一个类似的钩子,但是我不想硬编码开发分支的名称。也就是说,我想要一个防止将衍合操作应用到除父分支以外的其他分支的钩子。如果我理解您的回答正确的话(我对bash等方面还很陌生),您的回答中没有涉及到这个问题,对吗?有什么方法可以实现这个功能吗? - Kemeia
122
我欣赏那张描绘霸王龙手持旗帜(或肉斧)的历史图表。 - Graham Russell
3
@GrahamRussell之前的评论中称它为迅猛龙,但是那个评论似乎已经被删除了一段时间。 - torek
显示剩余3条评论

376

改述

另一种表达这个问题的方式是“最接近的提交位于当前分支以外的哪个分支,并且是哪个分支?”

解决方案

你可以通过一点命令行魔法找到它。

git show-branch -a \
| 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 )}'

它是如何运作的:

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

结果如下:

在上述代码上运行。

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

如果你从 H 运行它,会得到 develop,如果你从 I 运行它,会得到 master

代码作为 gist 可用


34
去掉末尾的反引号,就不会出错了。但运行该命令时,会收到大量警告,指出每个分支都说“无法处理超过25个参考”。 - Jon L.
60
抱歉,那是错误的。这是我用过的正确命令:git show-branch | grep '*' | grep -v "$(git rev-parse --abbrev-ref HEAD)" | head -n1 | sed 's/.*\[\(.*\)\].*/\1/' | sed 's/[\^~].*//' - droidbot
17
@droidbot 不错,但需要重新排序管道以避免在 grep -v 时捕捉到提交信息或分支名称包含另一个分支名称的情况下删除引用。 git show-branch | sed "s/].*//" | grep "\*" | grep -v "$(git rev-parse --abbrev-ref HEAD)" | head -n1 | sed "s/^.*\[//" - gaal
1
我遇到了这样一种情况,它无法正常工作——一个祖先不再存在于本地检出中的分支。该分支最初是从develop分支创建的,但随后有一个分叉的master合并到其中。然后删除了本地的develop(它仍然在远程仓库中)。这导致祖先分支不会出现在git show-branch输出中,并且它不是用通配符标记的第一个条目,因此解析器会忽略它而不捕获。也许只需记录这个过程依赖于在创建分支的git checkout上运行。 - Josip Rodin
4
@OlegAbrazhaev 我不知道你是否得到了问题的答案。使用以下Git别名:parent = "!git show-branch | grep '*' | grep -v \"$(git rev-parse --abbrev-ref HEAD)\" | head -n1 | sed 's/.*\\[\\(.*\\)\\].*/\\1/' | sed 's/[\\^~].*//' #" 对我有效。 - mduttondev
显示剩余15条评论

197

Git parent(Git 父提交)

您只需运行以下命令:

git parent

如果您将Joe Chrysler的答案作为Git别名添加,则可以找到分支的父级,这会简化使用。

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

vim  ~/.gitconfig
在文件中添加以下别名命令:
[alias]
    parent = "!git show-branch | grep '*' | grep -v \"$(git rev-parse --abbrev-ref HEAD)\" | head -n1 | sed 's/.*\\[\\(.*\\)\\].*/\\1/' | sed 's/[\\^~].*//' #"

保存并退出编辑器。

运行命令 git parent

就这样!


7
这是一个很好的解决方案。如果还能添加一些示例输出,就可以确保期望的结果了。当我运行它时,在最后一行之前,我得到了几个警告,我相信那是父分支的名称。 - ttemple
5
非常好用!对于 Windows 用户,.gitconfig 文件通常位于 c:\users\your-user.gitconfig。 - zion
46
收到“无法处理超过25个引用”的异常。 - shajin
7
git log --first-parent 这个命令有效了 - Ashok R
3
@shajin和其他人:如果你想要静音警告,你可以将git show-branch的stderr导向stdout,像这样:git show-branch 2>&1 | ...。下面的sed/grep将以这种方式过滤掉警告。或者你也可以通过2>/dev/null将错误导向/dev/null。 - Philippe Hebert
显示剩余9条评论

173

您也可以尝试:

git log --graph --decorate

12
git log --graph --decorate --simplify-by-decoration,其中 --graph 是可选的。 - user5494920
19
这是一个Git命令:git log --graph --decorate --simplify-by-decoration --oneline。它用于查看Git仓库的提交历史记录,并以图形化方式呈现。该命令会显示一条条简洁的提交信息,每行代表一个提交,并根据不同的分支进行归类和标记。 - anishtain4
1
git log --decorate=full --simplify-by-decoration --oneline --format="%D" - Dominique PERETTI

66

这对我来说很好用:

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

来自droidbot和@Jistanidiot的礼貌评论和回答


2
是的,但有时候你会从grep得到“broken pipe”的错误。 - Vladislav Rastrusny
3
*作为grep的正则表达式不正确。 应该使用grep -F '*'grep '\*'。 否则是一个好的解决方案。 - arielf
2
我没有输出。 - Sandip Subedi
用户“Jistanidiot”没有任何内容的痕迹。这是什么意思? - Peter Mortensen
7
我经常遇到“无法处理超过26个引用”的错误提示。 - SuperSandro2000

62
我有一个解决方案可以解决你的整个问题(确定feature是否由develop分支的最新版本派生而来),但是它并不使用你提出的方法。
您可以使用git branch --contains列出所有派生自develop分支最新版本的分支,然后使用grep确保feature也在其中。
git branch --contains develop | grep "^ *feature$"
如果它属于其中之一,将在标准输出中打印"feature"并返回代码0。否则,不会打印任何内容,返回代码为1。

1
这个方法是可行的,但需要注意的是,在一个有很多引用的代码库上可能需要很长时间。因此,在例如预接收钩子中运行它并不是最理想的选择。 - ebneter
我正在寻找一个名为<branch>的分支,我在那里执行了以下操作:git checkout -b <branch-2>这就是答案!真的不需要使用grep。git branch --contains <branch> - Bicameral Mind
这个答案是用于查找子元素。 - CervEd

39

为什么家里没有父母?

以下内容的预期

要点:

为什么大家想看这篇长文? 因为虽然之前的答案清楚地理解了原问题, 但它们并没有给出正确/有意义的结果; 或者准确地解决了一个不同的问题。

可以只查看第一部分; 它解决了“查找某些东西”的问题, 并应该突显出问题的范围。 对于某些人来说,这可能已经足够了。

本文将向您展示一种方法, 从 git 中提取正确和有意义的结果 (您可能不喜欢它们), 并演示一种应用您的约定知识的方式, 以从这些结果中提取您真正需要的东西。

以下各节内容包括:

  • 一个公正的问题和解决方案
    • 使用git show-branch找出最近的 git 分支。
    • 预期结果应该是什么样子
  • 示例图表和结果
  • 分批处理分支:解决 git show-branch 的限制
  • 有偏见的问题和解决方案: 引入(命名)约定以改进结果

这个问题的问题

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

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

让我们改变图表的形式和含有等级暗示的名称,以展示一个等效的图形:

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

图表(因此也包括git)并不能告诉我们哪个分支是最先创建的(因此也无法确定哪个是从另一个分支分离出来的)。

在第一个图表中,masterwork 的父节点只是一种惯例。

因此

  • 简单的工具会生成忽略偏见的结果。
  • 更复杂的工具会将惯例(偏见)考虑在内。

一个公正的问题

首先,我必须承认Joe Chrysler的回答,这里的其他回答以及各处的许多评论/建议;他们激励和指引了我!

请允许我重新表达Joe的表述,考虑到与那个最近提交相关的多个分支(这种情况确实存在!):

“最近的提交位于当前分支以外的哪个分支上,并且是哪些分支?”

换句话说:

Q1

给定一个分支B: 考虑到最接近B'HEAD的提交CC可以是B'HEAD), 这个提交在哪些除了B以外的其他分支中出现过?

一个公正的解决方案

提前道歉;似乎大家更喜欢一行代码。请随意建议(可读/可维护的)改进!

#!/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. 给定一个有序集合(唯一的)BRANCHES,不包括BASE_BRANCH
  3. 为简洁起见,设NBRANCH_COUNT,它是BRANCHES的大小;它不包括BASE_BRANCH
  4. git show-branch --topo-order $BRANCHES $BASE_BRANCH
    • 由于BRANCHES仅包含唯一的名称(假定为有效),名称将与输出的标题行一一映射,并对应于分支/提交矩阵的前 N 列。
    • 由于BASE_BRANCH不在BRANCHES中,它将成为标题行的最后一行,并对应于分支/提交矩阵的最后一列。
  5. tail:从第N+3行开始;丢弃前N+2行:N个分支 + 基础分支 + 分隔符行---..
  6. sed:这些可以合并为一个...但为了清晰起见,它们被分开了。
    • 删除分支/提交矩阵之后的所有内容
    • 用下划线“_”替换空格;我的主要原因是避免可能出现的IFS解析麻烦以及为了调试/可读性。
    • 用“+”替换“*”;基础分支总是在最后一列,这足以满足要求。另外,如果不进行替换,它将通过bash路径名扩展,这总是有趣的。
  7. egrep:grep筛选至少映射到一个分支([^_])并且映射到BASE_BRANCH[^_]$)的提交。也许基础分支模式应该是\+$ 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,我们得到了以下结果:

    GIVEN B共享提交C历史中包含C的分支!B?
    feature/aHfeature/b
    feature/bHfeature/a
    feature/cPfeature/d
    feature/dPfeature/c
    feature/eOfeature/c,feature/d
    feature/fCfeature/a,feature/b,feature/g,hotfix,master,release/2,release/3
    feature/gCfeature/a,feature/b,feature/f,hotfix,master,release/2,release/3
    hotfixDfeature/a,feature/b,master
    masterDfeature/a,feature/b,hotfix
    release/2Cfeature/a,feature/b,feature/f,feature/g,hotfix,master,release/3
    release/3Cfeature/a,feature/b,feature/f,feature/g,hotfix,master,release/2
    release/4Lfeature/c,feature/d,feature/e,support/1
    support/1Lfeature/c,feature/d,feature/e,release/4

    批量分支

    [由于它最适合在此时呈现,因此呈现在最终脚本中。 此部分不是必需的,请随意跳过。]

    git show-branch 限制了29个分支。这可能会成为某些人(没有评判,只是说!)的阻碍。

    在某些情况下,我们可以通过将分支分组成批次来改进结果。

    • 每个分支都必须提交BASE_BRANCH
    • 如果一个存储库中有大量分支,则这可能具有有限的价值。
    • 如果您发现其他限制分支(将被分批的分支)的方法,则可以提供更多价值。
    • 上一个观点适用于我的用例,所以继续前进!

    此机制不是完美的,随着结果大小接近最大值(29),请预期它会失败。详见下文

    批处理解决方案

    #
    # 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. 调用nearestCommonBranches BASE_BRANCH BATCH
    3. 将结果聚合成一个新的(更小的?)分支集

    可能会出现的问题

    如果聚合分支的数量超过了最大值SIZE, 且进一步的拆分/处理无法减少该数量,则可能:

    • 已经聚合的分支是解决方案, 但git show-branch无法验证,或者
    • 每个批处理未减少; 可能需要从一个批处理中提取分支来帮助减少另一个 (diff merge base);当前算法放弃并失败。

    考虑替代方案

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

    我从后见之明的角度提出这个方案。 这可能真的是正确的方法。 我正在前进; 也许在当前主题之外有价值。

    一个有偏见的问题

    您可能已经注意到,早期脚本中的核心函数nearestCommonBranches 回答了不止一个问题Q1。 实际上,该函数回答了一个更普遍的问题:

    Q2

    给定一个分支B和 一组有序的(无重复)分支PB不在P中): 考虑最接近B'HEADC可以是B'HEAD) 且被P中的分支共享的提交C: 按照P的顺序,P中哪些分支在其提交历史中具有C?

    选择P会带来偏见或描述(有限的)惯例。 为了匹配您的所有偏见/惯例特征,可能需要额外的工具,超出了此讨论的范围。

    建模简单的偏见/惯例

    偏见因不同的组织和惯例而异, 以下内容可能不适合于您的组织。 如果没有其他,也许这里的一些想法可以帮助 您找到适合您需求的解决方案。

    有偏见的解决方案;基于分支命名约定的偏见

    可能可以将偏见映射到使用的命名约定中并从中提取偏见。

    基于P的偏见(其他分支名称)

    我们将需要它用于下一步, 因此让我们看看通过正则表达式过滤分支名称可以做什么。

    上面的代码和下面的新代码组合起来可在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 "$@"
    

    示例图表的偏向结果

    让我们考虑有序集合

    P = { ^release/.*$ ^support/.*$ ^master$ }

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

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

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

    GIVEN BShared Commit CBranches P with C in their history (in order)
    feature/aDmaster
    feature/bDmaster
    feature/cLrelease/4、support/1
    feature/dLrelease/4、support/1
    feature/eLrelease/4、support/1
    feature/fCrelease/2、release/3、master
    feature/gCrelease/2、release/3、master
    hotfixDmaster
    masterCrelease/2、release/3
    release/2Crelease/3、master
    release/3Crelease/2、master
    release/4Lsupport/1
    support/1Lrelease/4

    这让我们离得出一个明确的答案更近了一步。对于发布分支的响应还不理想。让我们再深入一步。

    BASE_NAMEP为偏好

    可以采取的一种方法是为不同的基本名称使用不同的P。 让我们设计一个方案。

    约定

    免责声明:我不是git流派的纯粹主义者,请谅解

    • 支持分支应从master分支中分支出。
      • 不会有两个共享一个公共提交的支持分支。
    • 热修复分支应从支持分支或master分支分支出。
    • 发布分支应从支持分支或master分支分支出。
      • 可能会有多个发布分支共用一个公共提交;也就是在相同的时间从master分支分支出来。
    • 错误修复分支应从发布分支中分支出。
    • 特性分支可以从特性、发布、支持或master分支中分支出:
      • 为了“父级”这一目的,一个特性分支不能被确定为另一个特性分支的父级(请参见初始讨论)。
      • 因此:跳过特性分支,寻找“父级”在发布、支持和/或master分支中。
    • 将任何其他分支名称视为工作分支,其约定与特性分支相同。

    让我们看看我们用git能走多远:

    基础分支模式 父分支,按顺序排列 注释
    ^master$ n/a 没有父分支
    ^support/.*$ ^master$
    ^hotfix/.*$ ^support/.*$ ^master$ 优先使用支持分支而不是主分支(排序)
    ^release/.*$ ^support/.*$ ^master$ 优先使用支持分支而不是主分支(排序)
    ^bugfix/.*$ ^release/.*$
    ^feature/.*$ ^release/.*$ ^support/.*$ ^master$
    ^.*$ ^release/.*$ ^support/.*$ ^master$ 冗余的,但保持设计上的独立性

    脚本

    组合先前的代码和下面的新代码可在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,我们得到了以下结果:

    GIVEN BShared Commit CBranches P with C in their history (in order)
    feature/aDmaster
    feature/bDmaster
    feature/cLrelease/4, support/1
    feature/dLrelease/4, support/1
    feature/eLrelease/4, support/1
    feature/fCrelease/2, release/3, master
    feature/gCrelease/2, release/3, master
    hotfixDmaster
    master(空白,没有值)
    release/2Cmaster
    release/3Cmaster
    release/4Lsupport/1
    support/1Lmaster
    为胜利进行重构!

    机遇!

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

    让我们重新评估已使用的惯例,并稍微收紧它们。

    考虑以下git使用惯例:

    创建新的发布分支时:立即创建新的提交;也许更新一个版本或README文件。这可以确保发布的特征/工作分支(从发布中分支出)将与发布分支共享提交,而不是支持或主分支的提交之前。

    例如:

            G---H   <- feature/z
           /
          E         <- release/1
         /
    A---B---C---D   <- master
         \
          F         <- release/2
    

    从发布/1分支创建的功能分支不能有一个包含发布/1(其父级)和master或release/2的公共提交。

    按照这些约定,对于每个分支都提供了一个结果,即父级。

    使用工具和约定完成后,我可以生活在一个OCF友好的结构化git世界中。

    你的情况可能会有所不同!

    别离的思考

    1. 要点
    1. 首先:我得出结论, 除了这里介绍的内容之外, 某个时间点上可能需要接受可能存在多个分支的现实。

      • 也许所有潜在分支都可以执行验证; 可以应用“至少一个”或“全部”或其他规则。
    2. 就像这样的一周,我真的认为是时候学习Python了。


3
被大量低估的答案。 - New Alexandria
1
如何在 Windows 计算机上运行这个程序?https://gist.github.com/rsitze/6a9d0ac280bca850e2471634a3f83204 - Vivek Nuna
@viveknuna:这些是依赖于各种Unix命令行工具的Bash脚本。你可以尝试使用Cygwin。我听说新版Windows有一个Linux子系统,你也可以尝试一下。 - Richard Sitze
如果我有机会,我可能会在PowerShell(跨平台的核心版本)中重写它。 - Tatiana Racheva
可以确认这里列出的大多数解决方案适用于WSL Ubuntu。Joe Chrysler的答案也适用于Windows的Git Bash。@VivekNuna - DiskJunky

26

一个解决方案

基于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工具这样做以确保它们在给定分支中构建正确的提交),但是{{origin branch和local branch}}必须同时处于当前HEAD的相同或更高级别。
  • 必须没有标签阻碍(我推测;我没有在子父分支之间有标签的提交上测试过脚本)
  • 脚本依赖于“HEAD”始终由log命令作为第一个装饰列出
  • 在master和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

尽管存在本地分支(例如,自直接使用其SHA检出提交O以来,仅存在origin/topic),脚本应打印如下内容:
  • 对于提交GHI(分支hotfix)→master
  • 对于提交MNO(分支feature/a)→develop
  • 对于提交STU(分支feature/c)→develop
  • 对于提交PQR(分支feature/b)→feature/a
  • 对于提交JKL(分支develop)→<sha> 初始提交*
  • 对于提交BDEF(分支master)→<sha> 初始提交

* - 如果develop的提交在master的HEAD之上(即,master可以快进到develop),则为master


为什么show-branch命令对我无效

基于git show-branch解决方案在以下情况下对我不可靠:

  • 游离HEAD - 包括游离HEAD的情况,需要将grep '\*'替换为grep '!' \ ,这只是所有麻烦的开始。
  • masterdevelop上运行脚本会分别导致develop和空结果。
  • master分支上的分支(即hotfix/分支)以*的原因而不是!标记其最近的master分支父级,因此它们的父级是develop

3
只有一个有效的答案 - 作为git别名:“!git log --decorate --simplify-by-decoration --oneline | grep -v '(HEAD' | head -n1 | sed 's/.* (\(.\)) ./\1/' | sed 's/\(.\), ./\1/' | sed 's/origin\///'" - Ian Kemp
这个命令对我很有用 git log --decorate --simplify-by-decoration --oneline | grep -v "(HEAD" | head -n1 | sed 's/.* ((.)) ./\1/' | sed 's/(.), ./\1/' | sed 's/origin///' - Bravo
1
这应该是被接受的答案:/ - Meredith
1
这可以简化为 git --no-pager log --simplify-by-decoration --format="format:%D%n" -n1 --decorate-refs-exclude=refs/tags HEAD~1 ,它可能会显示多个分支。添加 | sed 's/\(.*\), .+/\1/' 可以仅获取第一个分支。 - six8
这对我很有效,但它有一些问题。首先,它不能正确返回单个修饰(例如当存在多个共同祖先时,如 origin/main, origin/feature/foo)。其次,如果祖先是标签,它不会去掉 tag: 前缀。这是更新后的版本,可以处理这两种情况:git log --decorate --simplify-by-decoration --oneline | grep -v "(HEAD" | head -n1 | sed 's/.* (\(.*\)) .*/\1/' | sed 's/\([^,]*\), .*/\1/' | sed 's/tag: //' - Thomas M
显示剩余2条评论

15

由于之前的回答都没能解决我们仓库的问题,我想分享一下我的方法,使用 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

1
谢谢。虽然如果您经常使用rebase(并且合并通常是fast-forwarded),这可能不起作用。如果我找到更好的解决方案,我会编辑我的答案。 - saeedgnu
1
目前为止,唯一一个在当前分支为master的情况下对我有意义的答案。大多数其他解决方案在这种极端情况下给出随机(明显不正确)的结果,因为实际上没有父分支。 - arielf
2
这是唯一对我有效的答案。要获取第一个父级而不是前10个列表,您可以使用以下命令:git log --oneline --merges "$@" | grep into | sed 's/.* into //g' | uniq --count | head -n 1 | cut -d ' ' -f 8 - lots0logs

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

(origin/父级分支名称, 父级分支名称)

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

origin/父级分支名称

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

父级分支名称


1
那只是返回了远程分支的名称。 - Mavamaarten
返回最新的标签,而不是父级名称。 - DAlekperov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接