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


419

假设我有以下带有提交树的本地存储库:

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

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

我希望能够使用钩子在远程回购上feature执行的操作是拒绝推送,除非commit fdevelopHEAD 的直接后代。即提交树看起来像这样,因为功能已经git rebase打开d

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

因此有可能:

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

从那里,我将检查父分支的HEAD是什么,并查看其f前身是否与父分支HEAD匹配,以确定是否需要重新设置功能。


这个问题应该改写为寻找父母的父母。
蒂姆·波兰德

Answers:


347

假设远程存储库具有开发的副本分支(您的初始描述是在本地存储库中描述的,但听起来它也存在于远程存储库中),那么您应该能够实现我认为想要的功能,但是该方法与您的设想有些不同。

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。尽管如此,血统并没有改变,只是标签改变了。

但是,如果我们要问“是commit 6的后代3吗?” (假设3并且6是完整的SHA-1提交名称),那么答案将是“是”,无论是否barquux标签。

因此,您可能会问诸如“被推动的提交是开发当前提示的后代吗?的分支?”,但你不能可靠地问:“什么是推的父分支提交吗?”。

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

对于所有推送的提交的祖先(不包括当前的development和其祖先),以parent 为开发者的当前提示:

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

可以实现为:

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

我得到了:git:致命的:模棱两可的参数'...':修订版和文件名。三点意图是什么?
Jack Ukleja 2015年

1
@Schneider我很确定在此示例中,'...'打算用作占位符:如果将其替换为提交的SHA,则您要针对该提交执行该检查(例如,分支的HEAD)您当前正在使用),一切正常。
丹尼尔·布雷迪

感谢您的详尽回答!这是超级有用的。我想做一个类似的勾子,但是我不想硬编码开发分支的名称。意思是我想要一个钩子,以防止变基到父分支以外的其他分支。如果我很好地理解了您的答案(我对bash和bash还是陌生的),这不在您的答案中,对吗?有没有办法做到这一点?
Kemeia

您愿意回答一个相关的问题吗?我无法让您的代码在REMOTE存储库上工作。以下是有关后续问题的链接,该问题有关如何使您的方法适应远程存储库的使用: stackoverflow.com/questions/49619492/…–
CodeMed

当我有这个时,这对我不起作用develop > release > feature,我将重新获得发展,这需要了解父母。解决我的问题的方法是stackoverflow.com/a/56673640/2366390
verdverm

240

改写

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

一个解法

您可以通过一点命令​​行魔术来找到它

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

运作方式如下:

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

和结果

在上面运行代码

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

会给你develop,如果你自H运行它,master如果你从一运行

该代码可作为要点


24
删除了引起错误的结尾反引号。但是,当运行此命令时,我收到大量警告,抱怨每个分支都说cannot handle more than 25 refs
Jon L.

1
@JoeChrysler你认为你可以把它一条线而不是2,并可能使其在Mac上工作作为ack不可用在Mac(有人建议用ackgrep
nonopolarity

53
对不起,那是错误的。这是为我工作的正确方法:git show-branch | grep '*' | grep -v "$(git rev-parse --abbrev-ref HEAD)" | head -n1 | sed 's/.*\[\(.*\)\].*/\1/' | sed 's/[\^~].*//'
droidbot 2014年

15
@droidbot不错,但是当grep -v catch提交消息或您的分支名称是另一个分支名称的一部分时,需要重新排序以免删除引用。git show-branch | sed "s/].*//" | grep "\*" | grep -v "$(git rev-parse --abbrev-ref HEAD)" | head -n1 | sed "s/^.*\[//"
gaal 2015年

3
@OlegAbrazhaev我不知道您是否回答过您的问题。使用git别名:parent = "!git show-branch | grep '*' | grep -v \"$(git rev-parse --abbrev-ref HEAD)\" | head -n1 | sed 's/.*\\[\\(.*\\)\\].*/\\1/' | sed 's/[\\^~].*//' #"对我
有用

111

您也可以尝试:

git log --graph --decorate

5
git log --graph --decorate --simplify-by-decoration在哪里--graph是可选的。
Na13-c

1
git log --graph --decorate --simplify-by-decoration --oneline
anishtain4

106

git父

您可以运行命令

git parent

如果您将@Joe Chrysler的答案添加为git别名,则可以找到分支的父级。它将简化用法。

"~/.gitconfig"使用任何文本编辑器打开位于的gitconfig文件。(对于linux)。对于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

而已!


5
这是一个很好的解决方案。添加一些示例输出以确保获得预期结果将很有帮助。当我运行它时,我在最后一行之前收到一些警告,我相信这是父分支的名称。
ttemple

4
奇迹般有效!对于Windows用户的.gitconfig通常位于C:\用户\你的用户\的.gitconfig
锡安

12
获取cannot handle more than 25 refs异常。
沙金

有人可以编辑它来处理警告吗?@ttemple,可以吗?
NIKHIL CM

@NIKHILCM像冠军一样工作。但是我在这里有一个问题,父母是否指示分支是从哪里创建的?
Hariprasath

52

对于您的整体问题,我有一个解决方案(确定是否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>
Poopy McFartnoise

44

这对我来说很好。

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

礼貌的答案来自:@droidbot和@Jistanidiot


是的,但有时它会使您从grep中“断管”。
弗拉迪斯拉夫·拉斯特鲁尼(Fladislav Rastrusny),

1
*不是传递给grep的适当正则表达式。应该使用grep -F '*'grep '\*'代替。否则,很好的解决方案。
arielf

我没有输出。
Sandip Subedi

为我工作....
roottraveller

11

由于以上答案均不适用于我们的存储库,因此我想使用最新的合并方式分享自己的方式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

谢谢。尽管如果您rebase经常使用(合并fast-forward经常被编辑),这可能无法很好地工作。如果找到更好的解决方案,我将编辑答案。
saeedgnu

1
唯一的?到目前为止,在当前分支是主分支的情况下,这对我来说是明智的。在没有实际的父分支的这种公认的边缘情况下,大多数其他解决方案给出了一个随机(显然是错误的)结果。
arielf

这是唯一对我有用的答案。要获取第一个父代而不是前10个父代的列表,可以使用以下命令:git log --oneline --merges "$@" | grep into | sed 's/.* into //g' | uniq --count | head -n 1 | cut -d ' ' -f 8
lot0logs

10

一个解法

基于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分支和Local分支必须都当前HEAD的同等水平或“之上”
  • 一定不能有任何标记(我想;我尚未在子分支和父分支之间带有标记的提交上测试脚本)
  • 该脚本依赖于事实 :该命令始终“ HEAD” 列为第一个装饰log
  • 运行脚本masterdevelop结果(大部分)在<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直接检出提交),脚本应按以下方式打印:

  • 对于提交GHI(分公司hotfix)→master
  • 对于提交MNO(分公司feature/a)→develop
  • 对于提交STU(分公司feature/c)→develop
  • 对于提交PQR(分公司feature/b)→feature/a
  • 对于提交JKL(支develop)→ <sha> Initial commit*
  • 对于提交BDEF(分公司master)→<sha> Initial commit

*-或者master如果develop的提交位于master的HEAD之上(〜master将是可快速转发的)


为什么不为我分行工作

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

  • 分离头包括分离式头套意味着替换grep '\*' \'grep'!' \ –那只是所有麻烦的开始
  • 运行脚本masterdevelop导致develop和``分别
  • master分支(hotfix/分支)上的分支develop以父作为结尾,因为其最近的master分支父标记有!而不是*原因。

2
只有有效的答案-作为git别名:"!git log --decorate --simplify-by-decoration --oneline | grep -v '(HEAD' | head -n1 | sed 's/.* (\\(.*\\)) .*/\\1/' | sed 's/\\(.*\\), .*/\\1/' | sed 's/origin\\///'"
伊恩·肯普

8

请记住,如“ Git:查找提交来自哪个分支”中所述,即使git branch --contains <commit>只是开始,也无法轻松地确定提交该提交的分支(可以重命名,移动,删除分支...)。

  • 您可以从提交返回到提交,直到git branch --contains <commit>不列出feature分支和列表develop分支,
  • 比较提交SHA1与 /refs/heads/develop

如果两个commits id匹配,那么您就可以进行(这意味着该feature分支的起源位于的HEAD develop)。


6

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

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

然后,这是乔的管道:

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


3
请注意,由于引用过多,输出中可能会缺少一些分支。它们在stderr上显示为警告。
Zitrax '16

5

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

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

5

我并不是说这是解决此问题的好方法,但是这似乎对我有用。

git branch --contains $(cat .git/ORIG_HEAD) 问题在于,正在接收文件的文件正在窥探git的内部工作,因此这不一定是向前兼容(或向后兼容)的。


3

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

2

@Mark Reed:您应该补充一点,提交行不仅应包含一个星号,而且应以一个星号开头!否则,匹配的行中还会包含包含星号的提交消息。因此应该是:

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

或长版:

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`

2
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. 提交消息包含 *

0

这几天想要做的任何人-Atlassian的SourceTree应用程序向您展示了分支之间如何相互关联的出色可视化表示,即分支的开始位置和当前在提交顺序中的位置(例如HEAD或后面的4个提交等)。 。


0

如果您使用“源树”,请查看提交详细信息>“父级”>,然后会看到带下划线的提交编号(链接)


0

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

获取最后一个提交,它既是我的分支又是master(或您要指定的任何分支)


0

当我做类似的事情时develop > release-v1.0.0 > feature-foo,这对我不起作用,它会一直进行下去,请注意其中涉及到重新设置,不确定这是否使我的问题更加复杂...

以下确实为我提供了正确的提交哈希

git log --decorate \
  | grep 'commit' \
  | grep 'origin/' \
  | head -n 2 \
  | tail -n 1 \
  | awk '{ print $2 }' \
  | tr -d "\n"
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.