[git] How to find the nearest parent of a Git branch?

Let's say I have the following local repository with a commit tree like this:

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

master is my this is the latest stable release code, develop is my this is the 'next' release code, and feature is a new feature being prepared for develop.

What I want to be able to do on my remote repo using hooks, is for pushes to feature to be refused unless commit f is a direct descendant of develop HEAD. i.e. the commit tree looks like this because feature has been git rebase on d.

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

So is it possible to:

  • Identify the parent branch of feature?
  • Identify the commit in parent branch which f is a descendant of?

From there I would check what HEAD of parent branch is, and see if f predecessor matches the parent branch HEAD, to determine if the feature needs to be rebased.

This question is related to git branch

The answer is


Since none of the answers above worked on our repository, I want to share my own way, using latest merges in git log:

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

Put it in a script named git-last-merges, which also accepts a branch name as argument (instead of current branch) as well as other git log arguments

From the output, we can manually detect the parent branch(es) based on own branching conventions and number of merges from each branch.

EDIT: If you use git rebase on child branches often (and merges are fast-forwarded often so there aren't too many merge commits), this answer won't work well, so I wrote a script to count ahead commits (normal and merge), and behind commits (there shouldn't be any behind merge in parent branch) on all branches comparing to the current branch. Just run this script and let me know if works for you or not

#!/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

I'm not saying this is a good way to solve this problem, however this does seem to work-for-me.

git branch --contains $(cat .git/ORIG_HEAD) The issue being that cat'ing a file is peeking into the inner working of git so this is not necessarily forwards-compatible (or backwards-compatible).


JoeChrysler's command-line magic can be simplified. Here's Joe's logic - for brevity I've introduced a parameter named cur_branch in place of the command substitution `git rev-parse --abbrev-ref HEAD` into both versions; that can be initialized like so:

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

Then, here's Joe's pipeline:

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

We can accomplish the same thing as all five of those individual command filters in a relatively simple awk command:

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

That breaks down like this:

-F'[]^~[]' 

split the line into fields at ], ^, ~, and [ characters.

/\*/                      

Find lines that contain an asterisk

&& !/'"$cur_branch"'/

...but not the current branch name

{ print $2;               

When you find such a line, print its second field (that is, the part between the first and second occurrences of our field separator characters). For simple branch names, that will be just what's between the brackets; for refs with relative jumps, it will be just the name without the modifier. So our set of field separators handles the intent of both sed commands.

  exit }

Then exit immediately. This means it only ever processes the first matching line, so we don't need to pipe the output through head -n 1.


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/[\^~].*//'

Achieves the same ends as Mark Reed's answer, but uses a much safer approach that doesn't misbehave in a number of scenarios:

  1. Parent branch's last commit is a merge, making the column show - not *
  2. Commit message contains branch name
  3. Commit message contains *

I have a solution to your overall problem (determine if feature is descended from the tip of develop), but it doesn't work using the method you outlined.

You can use git branch --contains to list all the branches descended from the tip of develop, then use grep to make sure feature is among them.

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

If it is among them, it will print " feature" to standard output and have a return code of 0. Otherwise, it will print nothing and have a return code of 1.


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

(origin/parent-name, parent-name)

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

origin/parent-name

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

parent-name


You can also try:

git log --graph --decorate

git parent

You can just run the command

git parent

to find the parent of the branch, if you add the @Joe Chrysler's answer as a git alias. It will simplify the usage.

Open gitconfig file located at "~/.gitconfig" by using any text editor. ( For linux). And for Windows the ".gitconfig" path is generally located at c:\users\your-user\.gitconfig

vim  ~/.gitconfig

Add the following alias command in the file:

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

Save and exit the editor.

Run the command git parent

That's it!


Anyone wanting to do this these days - Atlassian's SourceTree application shows you a great visual representation of how your branches relate to one another, i.e. Where they began and where they currently sit in the commit order (e.g. HEAD or 4 commits behind, etc.).


Cross-platform implementation with 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 comes with a couple of GUI clients that helps you visualize this. Open GitGUI and go to menu Repository > Visualize All Branch History


This working fine for me.

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

Courtesy answers from: @droidbot and @Jistanidiot


The solutions based on git show-branch -a plus some filters have one downside: git may consider a branch name of a short lived branch.

If you have a few possible parents which you care about, you can ask yourself this similar question (and probably the one the OP wanted to know about):

From a specific subset of all branches, which is the nearest parent of a git branch?

To simplify, I'll consider "a git branch" to refer to HEAD (i.e., the current branch).

Let's imagine that we have the following branches:

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

The solutions based on git show-branch -a + filters, may give that the nearest parent of HEAD is spam/a, but we don't care about that.

If we want to know which of important/a and important/b is the closest parent of HEAD, we could run the following:

for b in $(git branch -a -l "important/*"); 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}';

What it does:

1.) $(git branch -a -l "important/*"): Print a list of all branches with some pattern ("important/*").

2.) d=$(git rev-list --first-parent ^${b} HEAD |wc -l);: For each of those branches ($b), calculate the distance ($d1) in number of commits, from HEAD to the nearest commit in $b (similar to when you calculate the distance from a point to a line). You may want to consider the distance differently here: you may not want to use --first-parent, or may want distance from tip to the tip of the branches ("${b}"...HEAD), ...

2.2) d2=$(git rev-list --first-parent ^HEAD ${b} |wc -l);: For each of those branches ($b), calculate the distance ($d2) in number of commits from the tip of the branch to the nearest commit in HEAD. We will use this distance to choose between two branches whose distance $d1 was equal.

3.) echo "${b} ${d1} ${d2}";: Print the name of each of the branches, followed by the distances to be able to sort them later (first $d1, and then $d2).

4.) |sort -n -k2 -k3: Sort the previous result, so we get a sorted (by distance) list of all of the branches, followed by their distances (both).

5.) |head -n1: The first result of the previous step will be the branch that has a smaller distance, i.e., the closest parent branch. So just discard all other branches.

6.) |awk '{print $1}';: We only care about the branch name, and not about the distance, so extract the first field, which was the parent's name. Here it is! :)


@Mark Reed: You should add that the commit line should not only contain an asterisk, but begin with an asterisk! Otherwise commit messages that contain an asterisk are also included in the matched lines. So it should be:

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

or the long version:

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`

A rephrasal

Another way to phrase the question is "What is the nearest commit that resides on a branch other than the current branch, and which branch is that?"

A solution

You can find it with a little bit of command line magic

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

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

Here's how it works:

  1. Display a textual history of all commits, including remote branches.
  2. Ancestors of the current commit are indicated by a star. Filter out everything else.
  3. Ignore all the commits in the current branch.
  4. The first result will be the nearest ancestor branch. Ignore the other results.
  5. Branch names are displayed [in brackets]. Ignore everything outside the brackets, and the brackets.
  6. 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.

And the Result

Running the above code on

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

Will give you develop if you run it from H and master if you run it from I.

The code is available as a gist


Here is a PowerShell implementation of Mark Reed's solution:

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

Remember that, as described in "Git: Finding what branch a commit came from", you cannot easily pinpoint the branch where that commit has been made (branches can be renamed, moved, deleted...), even though git branch --contains <commit> is a start.

  • You can go back from commit to commit until git branch --contains <commit> doesn't list the feature branch and list develop branch,
  • compare that commit SHA1 to /refs/heads/develop

If the two commits id match, you are good to go (that would mean the feature branch has its origin at the HEAD of develop).


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

Get the last commit that it's both my branch and master (or whatever branch you want to specify)


Here's my Powershell Version:

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

A solution

The solution based on git show-branch did not quite work for me (see below), so I've combined it with the one based on git log and ended up with this:

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

Limitations and Caveats

  • HEAD can be detached (many CI tools do so to ensure they build correct commit in a given branch), but origin branch and local branch have to be both at par or "above" the current HEAD.
  • There must be no tags in the way (I presume; I have not tested the script on commits with a tag between child and parent branch)
  • the script relies on the fact "HEAD" is always listed as the first decoration by the log command
  • running the script on master and develop results (mostly) in <SHA> Initial commit

The results

 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

Despite local branch existence (e.g. only origin/topic is present since the commit O was checked-out by directly by its SHA), the script should print as follows:

  • For commits G, H, I (branch hotfix) ? master
  • For commits M, N, O (branch feature/a) ? develop
  • For commits S, T, U (branch feature/c) ? develop
  • For commits P, Q, R (branch feature/b) ? feature/a
  • For commits J, K, L (branch develop) ? <sha> Initial commit*
  • For commits B, D, E, F (branch master) ? <sha> Initial commit

* - or master if develop's commits were on top of master's HEAD (~ the master would be fast-forwardable to develop)


Why did not show-branch work for me

The solution based on git show-branch proved unreliable for me in the following situations:

  • detached HEAD – including detached head case means replacing grep '\*' \ for `grep '!' \ – and that is just the beginning of all the troubles
  • running the script on master and develop results in develop and `` respectively
  • branches on master branch (hotfix/ branches) end up with the develop as a parent since their closest master branch parent was marked with ! instead of * for a reason.

This did not work for me when I had done something like develop > release-v1.0.0 > feature-foo, it would go all the way back to develop, note there was a rebase involved, not sure if that is compounding my issue...

The following did give the correct commit hash for me

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

If you use Source Tree look at your commit details > Parents > then you'll see commit numbers underlined (links)