590

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.

Using hooks, I want to be able to refuse pushes to feature to my remote repository, 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 the parent branch is, and see if f predecessor matches the parent branch HEAD, to determine if the feature needs to be rebased.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Peter Farmer
  • 9,020
  • 5
  • 27
  • 29

28 Answers28

402

Assuming that the remote repository has a copy of the develop branch (your initial description describes it in a local repository, but it sounds like it also exists in the remote), you should be able to achieve what I think you want, but the approach is a bit different from what you have envisioned.

Git’s history is based on a DAG of commits. Branches (and “refs” in general) are just transient labels that point to specific commits in the continually growing commit DAG. As such, the relationship between branches can vary over time, but the relationship between commits does not.

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

It looks like baz is based on (an old version of) bar? But what if we delete bar?

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

Now it looks like baz is based on foo. But the ancestry of baz did not change. We just removed a label (and the resulting dangling commit). And what if we add a new label at 4?

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

Now it looks like baz is based on quux. Still, the ancestry did not change, only the labels changed.

If, however, we were asking “is commit 6 a descendent of commit 3?” (assuming 3 and 6 are full SHA-1 commit names), then the answer would be “yes”, whether the bar and quux labels are present or not.

So, you could ask questions like “is the pushed commit a descendent of the current tip of the develop branch?”, but you can not reliably ask “what is the parent branch of the pushed commit?”.

A mostly reliable question that seems to get close to what you want is:

For all the pushed commit’s ancestors (excluding the current tip of develop and its ancestors), that have the current tip of develop as a parent:

  • does at least one such commit exist?
  • are all such commits single-parent commits?

Which could be implemented as:

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

This will cover some of what you want restricted, but maybe not everything.

For reference, here is an extended example history:

    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

The above code could be used to reject Hand S while accepting H', J, K, or N, but it would also accept L and P (they involve merges, but they do not merge the tip of develop).

To also reject L and P, you can change the question and ask

For all the pushed commit’s ancestors (excluding the current tip of develop and its ancestors):

  • are there any commits with two parents?
  • if not, does at least one such commit have the current tip of develop its (only) 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_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
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Chris Johnsen
  • 214,407
  • 26
  • 209
  • 186
  • I get this: git : fatal: ambiguous argument '...': both revision and filename. what is the triple dots intention? – Jack Ukleja Oct 13 '15 at 11:02
  • 1
    @Schneider I'm pretty sure the '...' is intended to be a placeholder in this example: if you replace it with the SHA of the commit you're trying to perform this check against (say, the HEAD of the branch you're currently on), everything works just fine. – Daniel Brady Aug 05 '17 at 23:04
  • Thanks for this elaborate answer! It is super useful. I want to make a similar hook, but I don't want to hard-code the name of the development branch. Meaning I want a hook for preventing rebase to a branch other than the parent branch. If I understand your answer well (I'm new to bash and stuff) this is not covered in your answer, right? Is there a way to do this? – Kemeia Dec 20 '17 at 15:02
  • Are you willing to answer a related question? I was not able to get your code to work on a REMOTE repository. Here is the link to the follow-up question about how to adapt your approach to work with a REMOTE repository: https://stackoverflow.com/questions/49619492/find-the-remote-parent-branch-of-a-local-git-branch – CodeMed Apr 02 '18 at 22:19
  • This did not work for me when I had `develop > release > feature`, I would get develop back and it requires knowing the parent. The solution to my issue was https://stackoverflow.com/a/56673640/2366390 – verdverm Jun 19 '19 at 18:18
  • 115
    I appreciate the T-Rex holding a flag (or meat cleaver) history graph – Graham Russell Apr 14 '20 at 12:29
  • 3
    @GrahamRussell: it was called a velociraptor in an earlier comment from some years ago, but that comment seems to have been deleted for a while now. – torek Mar 11 '21 at 23:48
  • Chris and @torek you should check out https://stackoverflow.com/questions/68207171/git-diff-between-current-branch-and-branch-creation/68207523?noredirect=1#comment120707957_68207523. I referenced this answer in a comment there. – Inigo Jul 10 '21 at 10:28
372

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 -a \
| 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.

tonymontana
  • 5,728
  • 4
  • 34
  • 53
Joe Chrysler
  • 3,920
  • 1
  • 15
  • 9
  • 34
    Removed a trailing backtick that caused error. When running this command though, I get a large amount of warnings, complaining about each branch saying `cannot handle more than 25 refs` – Jon L. Mar 25 '14 at 13:00
  • 1
    @JoeChrysler do you think you can make it one line instead of 2, and possibly make it work on Mac as `ack` is not available on Mac (somebody suggested replacing `ack` with `grep`) – nonopolarity Apr 18 '14 at 17:19
  • Sounds like an idea. Works in simplest cases. Here is the version with grep: `git show-branch | grep '*' | grep -v '$branch' | head -n1 | sed 's/.*\[\(.*\)\].*/\1/' | sed 's/[\^~].*//'` – droidbot Apr 25 '14 at 16:44
  • 59
    Sorry, thats a wrong one. Here is the correct one that worked for me: `git show-branch | grep '*' | grep -v "$(git rev-parse --abbrev-ref HEAD)" | head -n1 | sed 's/.*\[\(.*\)\].*/\1/' | sed 's/[\^~].*//'` – droidbot Apr 25 '14 at 16:54
  • I added a git alias configuration as comments on the gist. Hope it helps someone. – jpbochi Aug 25 '14 at 13:27
  • 17
    @droidbot Nice, but needs piping reordering to avoid removal of refs when grep -v catch commit message or your branch name is part of another branch name. `git show-branch | sed "s/].*//" | grep "\*" | grep -v "$(git rev-parse --abbrev-ref HEAD)" | head -n1 | sed "s/^.*\[//"` – gaal Jul 27 '15 at 13:12
  • 1
    I encountered a situation where this doesn't work - a branch with an ancestor that is no longer present in the local checkout. The branch was first created from the `develop` branch, but then had a divergent `master` merged into it. Then the local `develop` was deleted (it remained on origin). This causes that ancestor branch to not turn up in `git show-branch` output, and it's not the first entry marked with a wildcard, and this parser flies by and doesn't catch that. Maybe just document the fact that this process depends on being run on the git checkout where the branch was created. – Josip Rodin Apr 20 '16 at 10:28
  • ack is not a standard *nix command. (Works with grep instead). – Ed Randall Jul 11 '16 at 13:44
  • 1
    How to add this long command as an alias? Doesn't work in this way `git config --global alias.parent "show-branch | sed "s/].*//" | grep "\*" | grep -v "$(git rev-parse --abbrev-ref HEAD)" | head -n1 | sed "s/^.*\[//""`. Error about | character. – Oleg Abrazhaev Oct 05 '17 at 07:40
  • 4
    @OlegAbrazhaev I dont know if you ever got your question answered. using the git alias of: `parent = "!git show-branch | grep '*' | grep -v \"$(git rev-parse --abbrev-ref HEAD)\" | head -n1 | sed 's/.*\\[\\(.*\\)\\].*/\\1/' | sed 's/[\\^~].*//' #"` works for me – mduttondev Mar 29 '18 at 20:13
  • Are you willing to answer a related question? I was not able to get your code to work on a REMOTE repository. Here is the link to the follow-up question about how to adapt your approach to work with a REMOTE repository: https://stackoverflow.com/questions/49619492/find-the-remote-parent-branch-of-a-local-git-branch – CodeMed Apr 02 '18 at 22:20
  • @JoeChrysler The comments are confusing. Can you please flag those that have been integrated into the answer in some way? – Raphael Jan 31 '19 at 17:44
  • I like this one because it's very clear how to find e.g. the "best matching" release branch `release-*`, or some other pattern. – Raphael Jan 31 '19 at 17:46
  • One problem is when the nearest labeled ancestor has two labels. For example, we just shipped r/4.1, which is still on the same commit as master. Which is the nearest parent of a subsequent feature branch? Thus, this strategy can be helpful for the question above, but in general it is impossible to get a single, reliable answer regarding the nearest ancestor. – michael Oct 04 '19 at 20:55
  • The sed version doesn't include the -a option to show-branch and so probably doesn't behave the same as the awk version or the way the description says it will. – Britton Kerin Nov 28 '20 at 19:29
  • 1
    The rephrasal is wrong for some reasons: "the nearest commit that resides on a branch other than the current branch" 1. The original question is what was the branch *at the time of the creation of the current branch*. Branches are pointers - they move, and they get deleted, thus the rephrasal answers another question. 2. The commit can reside on more than one branch, thus even this is not really answerable. – Gulzar Apr 22 '21 at 12:34
  • 1
    This used to work for me for checking that I was off master (not further removed) but it broke when my repo got too big. Its probably related to this warning that git now gives: warning: ignoring master; cannot handle more than 26 refs – Britton Kerin Apr 27 '21 at 19:01
  • +1 here, but i think the approach is a little overkill and would be great something that does work even if the branch is not checked out, so wondering if someone thought on filing a ticket for such a feature to be bundled directly into the git program, using a single straight command. (?) – diegocr Jun 20 '21 at 11:09
  • Its not working for me on windows 10 system – Vivek Nuna Sep 21 '22 at 09:09
195

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 the 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!

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
NIKHIL C M
  • 3,873
  • 2
  • 28
  • 35
169

You can also try:

git log --graph --decorate
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
rcde0
  • 4,192
  • 3
  • 21
  • 31
65

This is working fine for me:

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

Courtesy comment and answers from droidbot and @Jistanidiot.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Murali Mopuru
  • 6,086
  • 5
  • 33
  • 51
62

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.

Daniel Stutzbach
  • 74,198
  • 17
  • 88
  • 77
  • 1
    This works but it should be noted that it can take a long time on a repository that has a lot of refs. That makes it a bit less than ideal for running in, e.g., a pre-receive hook. – ebneter Jan 19 '16 at 00:24
  • I was looking for the branch, we'll call it ``, where i performed: `git checkout -b ` from... THIS is the answer! No need for grep, really. `git branch --contains ` – Bicameral Mind Jul 17 '19 at 20:29
  • this is answer is for finding children – CervEd Sep 30 '21 at 08:51
38

Why isn't there a Parent in the House?

What to Expect Below

gists:

Why would anyone want to read this long post? Because while previous answers clearly understand the problem with the original question, they fall short of correct/meaningful results; or accurately solve a different problem.

Feel free to just review the first section; it solves the "find something" problem and should highlight scope of the problem. For some, that may be sufficient.

This will show you a way to extract correct & meaningful results from git (you may not like them), and demonstrate one way to apply your knowledge of your conventions to those results to extract what you are really looking for.

Sections below cover:

  • An Unbiased Question & Solution:
    • nearest git branches using git show-branch.
    • what expected results should look like
  • Example Graph & Results
  • Batching Branches: work around limits of git show-branch
  • A Biased Question & Solution: introducing (naming) conventions to improve results

The Problem with The Question

As has been mentioned, git does not track relationships between branches; branches are simply names referencing a commit. In official git documentation and other sources we'll often encounter somewhat misleading diagrams such as:

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

Let's change the form of the diagram and the hierarchically suggestive names to show an equivalent graph:

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

The graph (and hence git) tells us absolutely nothing about which branch was created first (hence, which was branched off the other).

That master is a parent of work in the first graph is a matter of convention.

Therefore

  • simple tooling will produce responses that ignore the bias
  • more complex tooling incorporates conventions (biases).

An Unbiased Question

First, I must acknowledge primarily Joe Chrysler's response, other responses here, and the many comments/suggestions all around; they inspired and pointed the way for me!

Allow me to rephrase Joe's rephrasal, giving consideration to multiple branches related to that nearest commit (it happens!):

"What is the nearest commit that resides on a branch other than the current branch, and which branches is that?"

Or, in other words:

Q1

Given a branch B: consider the commit C nearest to B'HEAD (C could be B'HEAD) that is shared by other branches: what branches, other than B, have C in their commit history?

An Unbiased Solution

Apologies up front; it seems folks prefer one-liners. Feel free to suggest (readable/maintainable) improvements!

#!/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 "${@}"

How it Works

Considering Output of: git show-branch

For git show-branch --topo-order feature/g hotfix master release/2 release/3 feature/d, the output would look similar to:

! [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

A few points:

  • the original command listed N (6) branch names on the command line
  • those branch names appear, in order, as the first N lines of the output
  • the lines following the header represent commits
  • the first N columns of the commit lines represent (as a whole) a "branch/commit matrix", where a single character in column X indicates the relationship (or lack of) between a branch (header row X) and the current commit.

Primary Steps

  1. Given a BASE_BRANCH
  2. Given an ordered set (unique) BRANCHES that does not include BASE_BRANCH
  3. For brevity, let N be BRANCH_COUNT, which is the size of BRANCHES; it does not include BASE_BRANCH
  4. git show-branch --topo-order $BRANCHES $BASE_BRANCH:
    • Since BRANCHES contains only unique names (presumed valid) the names will map 1-1 with the header lines of the output, and correspond to the first N columns of the branch/commit matrix.
    • Since BASE_BRANCH is not in BRANCHES it will be the last of the header lines, and corresponds to the last column branch/commit matrix.
  5. tail: start with line N+3; throw away the first N+2 lines: N branches + base branch + separator row ---...
  6. sed: these could be combined in one... but are separated for clarity
    • remove everything after the branch/commit matrix
    • replace spaces with underscores '_'; my primary reason was to avoid potential IFS parsing hassles and for debugging/readability.
    • replace * with +; base branch is always in last column, and that's sufficient. Also, if left alone it goes through bash pathname expansion, and that's always fun with *
  7. egrep: grep for commits that map to at least one branch ([^_]) AND to the BASE_BRANCH ([^_]$). Maybe that base branch pattern should be \+$?
  8. head -n1: take the first remaining commit
  9. sed: separate each character of the branch/commit matrix to separate lines.
  10. Capture the lines in an array MAP, at which point we have two arrays:
    • BRANCHES: length N
    • MAP: length N+1: first N elements 1-1 with BRANCHES, and the last element corresponding to the BASE_BRANCH.
  11. Iterate over BRANCHES (that's all we want, and it's shorter) and check corresponding element in MAP: output BRANCH[$idx] if MAP[$idx] is +.

Example Graph & Results

Consider the following somewhat contrived example graph:

  • Biased names will be used, as they help (me) weigh and consider results.
  • Presume merges exist and are being ignored.
  • The graph generally attempts to highlight branches as such (forking), without visually suggesting a preference/hierarchy; ironically master stands out after I was done with this thing.
                         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

Unbiased Results for Example Graph

Assuming the script is in executable file gitr, then run:

gitr <baseBranch>

For different branches B we obtain the following results:

GIVEN B Shared Commit C Branches !B with C in their history?
feature/a H feature/b
feature/b H feature/a
feature/c P feature/d
feature/d P feature/c
feature/e O feature/c, feature/d
feature/f C feature/a, feature/b, feature/g, hotfix, master, release/2, release/3
feature/g C feature/a, feature/b, feature/f, hotfix, master, release/2, release/3
hotfix D feature/a, feature/b, master
master D feature/a, feature/b, hotfix
release/2 C feature/a, feature/b, feature/f, feature/g, hotfix, master, release/3
release/3 C feature/a, feature/b, feature/f, feature/g, hotfix, master, release/2
release/4 L feature/c, feature/d, feature/e, support/1
support/1 L feature/c, feature/d, feature/e, release/4

Batching Branches

[Presented at this stage because it fits best into final script at this point. This section is not required, feel free to skip forward.]

git show-branch limits itself to 29 branches. That maybe a blocker for some (no judgement, just sayin!).

We can improve results, in some situations, by grouping branches into batches.

  • BASE_BRANCH must be submitted with each branch.
  • If there are a large number of branches in a repo this may have limited value, by itself.
  • May provide more value if you find other ways to limit the branches (that would be batched).
  • The previous point fits my use case, so charging ahead!

This mechanism is NOT perfect, as the result size approaches the max (29), expect it to fail. Details below

Batch Solution

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

How it Works

Repeat until results stabilize

  1. Break BRANCHES into batches of GIT_SHOW_BRANCH_MAX (aka SIZE) elements
  2. call nearestCommonBranches BASE_BRANCH BATCH
  3. Aggregating results into a new (smaller?) set of branches

How it can fail

If the number of aggregated branches exceeds the max SIZE and further batching/processing cannot reduce that number then either:

  • the aggregated branches IS the solution, but that can't be verified by git show-branch, or
  • each batch doesn't reduce; possibly a branch from one batch would help reduce another (diff merge base); the current algo admits defeat and fails.

Consider Alternative

Individually pairing a base branch with every other branch of interest, determine a commit node (merge base) for each pair; sorting the set of merge bases in commit history order, taking the nearest node, determining all branches associated with that node.

I present that from a position of hindsight. It's probably really the right way to go. I'm moving forward; perhaps there is value outside of the current topic.

A Biased Question

You may have noted that the core function nearestCommonBranches in the earlier script answers more than question Q1 asks. In fact, the function answers a more general question:

Q2

Given a branch B and an ordered set (no duplicates) P of branches (B not in P): consider the commit C nearest to B'HEAD (C could be B'HEAD) that is shared by branches in P: in order per order-of-P, what branches in P have C in their commit history?

Choosing P provides bias, or describes a (limited) convention. To match all the characteristics of your biases/convention may require additional tools, which is out-of-scope for this discussion.

Modeling Simple Bias/Convention

Bias varies for different organization & practices, and the following may not be suitable for your organization. If nothing else, perhaps some of the ideas here might help you find a solution to your needs.

A Biased Solution; Bias by Branch Naming Convention

Perhaps the bias can be mapped into, and extracted from, the naming convention in use.

Bias by P (Those Other Branch Names)

We're going to need this for the next step, so let's see what we can do by filtering branch names by regex.

The combined previous code and the new code below is available as a 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 "$@"

Biased Results for Example Graph

Let's consider the ordered set

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

Assuming the script (all parts) is in executable file gitr, then run:

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

For different branches B we obtain the following results:

GIVEN B Shared Commit C Branches P with C in their history (in order)
feature/a D master
feature/b D master
feature/c L release/4, support/1
feature/d L release/4, support/1
feature/e L release/4, support/1
feature/f C release/2, release/3, master
feature/g C release/2, release/3, master
hotfix D master
master C release/2, release/3
release/2 C release/3, master
release/3 C release/2, master
release/4 L support/1
support/1 L release/4

That's getting closer to a definitive answer; the responses for release branches aren't ideal. Let's take this one step further.

Bias by BASE_NAME and P

One direction to take this could be to use different P for different base names. Let's work out a design for that.

Conventions

DISCLAIMER: A git flow purist I am not, make allowances for me please

  • A support branch shall branch off master.
    • There will NOT be two support branches sharing a common commit.
  • A hotfix branch shall branch off a support branch or master.
  • A release branch shall branch off a support branch or master.
    • There may be multiple release branches sharing a common commit; i.e. branched off master at the same time.
  • A bugfix branch shall branch off a release branch.
  • a feature branch may branch off a feature, release, support, or master:
    • for the purpose of "parent", one feature branch cannot be established as a parent over another (see initial discussion).
    • therefore: skip feature branches and look for "parent" among release, support, and/or master branches.
  • any other branch name to be considered a working branch, with same conventions as a feature branch.

Let's see how far we git with this:

Base Branch Pattern Parent Branches, Ordered Comment(s)
^master$ n/a no parent
^support/.*$ ^master$
^hotfix/.*$ ^support/.*$ ^master$ give preference to a support branch over master (ordering)
^release/.*$ ^support/.*$ ^master$ give preference to a support branch over master (ordering)
^bugfix/.*$ ^release/.*$
^feature/.*$ ^release/.*$ ^support/.*$ ^master$
^.*$ ^release/.*$ ^support/.*$ ^master$ Redundant, but keep design concerns separate

Script

The combined previous code and the new code below is available as a 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"
Biased Results for Example Graph

Assuming the script (all parts) is in executable file gitr, then run:

gitr <baseBranch>

For different branches B we obtain the following results:

GIVEN B Shared Commit C Branches P with C in their history (in order)
feature/a D master
feature/b D master
feature/c L release/4, support/1
feature/d L release/4, support/1
feature/e L release/4, support/1
feature/f C release/2, release/3, master
feature/g C release/2, release/3, master
hotfix D master
master (blank, no value)
release/2 C master
release/3 C master
release/4 L support/1
support/1 L master
Refactor for the Win!

Opportunities!

In this last example, the release branch shares a common commit with multiple others: release, support, or master branches.

Let's "refactor" or re-evaluate the conventions in used, and tight them a bit.

Consider this git usage convention:

When creating a new release branch: immediately create a new commit; perhaps update a version, or the README file. This ensures that feature/work branches for the release (branched off the release) will have the commit shared with the release branch prior to (and not shared by) the commit for the underlying support or master branch.

For example:

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

A feature branches off release/1 could not have a common commit that includes release/1 (it's parent) and master or release/2.

That provides one result, the parent, for every branch, with these conventions.

DONE! with tools and conventions, I can live in an OCD friendly structured git world.

Your mileage may vary!

Parting thoughts

  1. gists
  1. Foremost: I've come to the conclusion that, beyond what's been presented here, at some point one may need to accept that there may be multiple branches do deal with.

    • Perhaps validations might be done on all potential branches; "at-least-one" or "all" or ?? rules might be applied.
  2. It's weeks like this that I really think it's time I learn Python.

Richard Sitze
  • 8,262
  • 3
  • 36
  • 48
25

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.
Matt Stuvysant
  • 439
  • 4
  • 6
  • 3
    Only answer that worked - as a git alias: `"!git log --decorate --simplify-by-decoration --oneline | grep -v '(HEAD' | head -n1 | sed 's/.* (\\(.*\\)) .*/\\1/' | sed 's/\\(.*\\), .*/\\1/' | sed 's/origin\\///'"` – Ian Kemp May 02 '19 at 10:42
  • this command worked for me git log --decorate --simplify-by-decoration --oneline | grep -v "(HEAD" | head -n1 | sed 's/.* (\(.*\)) .*/\1/' | sed 's/\(.*\), .*/\1/' | sed 's/origin\///' – Bravo Dec 18 '20 at 04:35
  • 1
    This should be the accepted answer :/ – Meredith Dec 04 '21 at 08:05
  • 1
    This can be condensed to `git --no-pager log --simplify-by-decoration --format="format:%D%n" -n1 --decorate-refs-exclude=refs/tags HEAD~1` which may show more than one branch. Add `| sed 's/\(.*\), .+/\1/'` to get just the first branch. – six8 Mar 15 '22 at 19:46
  • This worked well for me, but it had a few problems. First, it wouldn't properly return a single decoration (e.g. when there were multiple common ancestors, such as `origin/main, origin/feature/foo`). Second, it didn't strip the `tag: ` prefix if the ancestor was a tag. Here's the updated version that handles both of these scenarios: `git log --decorate --simplify-by-decoration --oneline | grep -v "(HEAD" | head -n1 | sed 's/.* (\(.*\)) .*/\1/' | sed 's/\([^,]*\), .*/\1/' | sed 's/tag: //'` – Thomas M Nov 04 '22 at 14:01
  • Hello @Matt , How to learn about git from intermediate to advance so that i know everything about it? – Rajanboy Jan 10 '23 at 11:26
  • Instead of parsing remote refs, we can filter them out by means of git: `git --no-pager log --simplify-by-decoration --format="format:%D%n" -n1 --decorate-refs-exclude=refs/remotes HEAD~1` – olegrog May 20 '23 at 07:12
14

Since none of the previous answers 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.

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.

#!/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
alper
  • 2,919
  • 9
  • 53
  • 102
saeedgnu
  • 4,110
  • 2
  • 31
  • 48
  • 1
    Thanks. Although this may not work very well if you use `rebase` often (and merges are `fast-forward`ed often). I'll edit my answer if I found a better solution. – saeedgnu Sep 30 '16 at 06:44
  • 1
    The only? answer so far that worked sensibly for me in the case where current branch is master. Most other solutions gave a random (and clearly incorrect) result in this admittedly edge-case where there are no actual parent branches. – arielf May 14 '18 at 22:09
  • 2
    This is the only answer that worked for me. To get the first parent instead of a list of the first 10 you can use this: `git log --oneline --merges "$@" | grep into | sed 's/.* into //g' | uniq --count | head -n 1 | cut -d ' ' -f 8` – lots0logs Jul 11 '19 at 00:19
11
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

Ivan Kuznetsov
  • 119
  • 1
  • 2
8

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).

Community
  • 1
  • 1
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
8

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).

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
user1529413
  • 458
  • 8
  • 19
7

Joe Chrysler'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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Mark Reed
  • 91,912
  • 16
  • 138
  • 175
7

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('[\^~].*','') }
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
chrisevett
  • 611
  • 8
  • 15
  • Does not work, see comments in the original answer and also on ladiko's answer. The answer of Justin Grote works instead on Powershell. – daniol Feb 06 '23 at 06:32
4

Use:

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

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

  1. The parent branch's last commit is a merge, making the column show -, not *
  2. Commit message contains a branch name
  3. Commit message contains *
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Haoyang Feng
  • 170
  • 1
  • 8
4

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'}
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Justin Grote
  • 119
  • 2
  • 1
    Hello! While this code may solve the question, [including an explanation](https://meta.stackexchange.com/q/114762) of how and why this solves the problem would really help to improve the quality of your post, and probably result in more up-votes. Remember that you are answering the question for readers in the future, not just the person asking now. Please [edit] your answer to add explanations and give an indication of what limitations and assumptions apply. – Brian61354270 Jun 25 '20 at 18:40
3

Mark Reed's solution is essentially correct. However, note 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`
Cody Gray - on strike
  • 239,200
  • 50
  • 490
  • 574
ladiko
  • 87
  • 6
3

I didn't like the unsafe assumptions involved in parsing semi-structured text output, so I wanted a more robust solution with less assumptions:

# Search backwards in history for the first commit that is in a branch other than $1
# and output that branch's name.
parent_branch() {
    local result rev child_branch=$1
    rev=$(git rev-parse --revs-only $child_branch)
    while [[ -n $rev ]]; do
        result=$(git branch --contains $rev | grep -v " $child_branch$")
        if [[ -n $result ]]; then
            echo $result
            return 0
        fi
        rev=$(git rev-parse --revs-only $rev^)
    done
    return 1
}

Caveat: Since this iteratively walks backwards in history, looking at each commit to find the first that is in a different branch than $1, it gets more expensive the longer your branch gets. But since normally branches are supposed to be relatively short-lived anyways, that shouldn't be too much of a problem.

Also note that I'm using git branch --contains, so this will also find branches that share a common base but have already progressed beyond it. To only find branches that point exactly at the common base, use git branch --points-at.

Victor Mataré
  • 2,446
  • 2
  • 16
  • 20
2

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}" />
ENargit
  • 614
  • 1
  • 5
  • 9
1

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/*" | 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}';

What it does:

1.) $(git branch -a -l "important/*" | sed -E -e "s/\*//"): Print a list of all branches with some pattern ("important/*"). (If you happen to be on one of the important/* branches, git branch will include a * to indicate your current branch. The command substitution $() will then expand it into the contents of your current directory. The sed removes the * from the output of git branch.)

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! :)

andronikus
  • 4,125
  • 2
  • 29
  • 46
1

Git comes with a couple of GUI clients that helps you visualize this. Open GitGUI and go to menu RepositoryVisualize All Branch History.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Rod
  • 14,529
  • 31
  • 118
  • 230
  • How exactly is "GitGUI" opened? What platform (operating system, etc.)? Can you add some references to your answer (but ***without*** "Edit:", "Update:", or similar - the answer should appear as if it was written today)? – Peter Mortensen Jul 06 '21 at 21:54
1

A shell function that seeks the first commit in more than one branch:

# Get the first commit hash of a given branch.
# Uses `git branch --contains` to backward (starts from HEAD) check each commits
# and output that branch's name.
first_commit_of_branch() {
    if [ $# -ne 1 ] || [ -z "${1}" ] ; then
        (>&2 echo "Error: Missing or empty branch name.")
        (>&2 echo "Usage: $0 branch_to_test")
        return 2
    fi
    local branch_to_test="${1}"; shift
    local commit_index_to_test
    local maximum_number_of_commit_to_test
    local branch_count_having_tested_commit

    git rev-parse --verify --quiet "${branch_to_test}" 2>&1 > /dev/null || {
        (>&2 echo "Error: Branch \"${branch_to_test}\" does not exists.")
        return 2
    }

    commit_index_to_test=0
    maximum_number_of_commit_to_test=$(git rev-list --count "${branch_to_test}")

    while [ ${commit_index_to_test} -le ${maximum_number_of_commit_to_test} ] ; do
        # Testing commit $branch_to_test~$commit_index_to_test…

        # If it fails, it means we tested all commits from the most recent of
        # branch $branch_to_test to the very first of the git DAG. So it must be it.
        git rev-parse --verify --quiet ${branch_to_test}~${commit_index_to_test} 2>&1 > /dev/null || {
            git rev-list --max-parents=0 "${branch_to_test}"
            return 0
        }

        branch_count_having_tested_commit="$( \
            git --no-pager branch --no-abbrev --verbose \
                --contains ${branch_to_test}~${commit_index_to_test} \
            | cut -c 3- \
            | cut -d ' ' -f 2 \
            | wc -l \
        )"

        # Tested commit found in more than one branch
        if [ ${branch_count_having_tested_commit} -gt 1 ] ; then
            if [ ${commit_index_to_test} -eq 0 ]; then
                (>&2 echo "Error: The most recent commit of branch \"${branch_to_test}\" (${branch_to_test}~${commit_index_to_test}) is already in more than one branch. This is likely a new branch without any commit (yet). Cannot continue.")
                return 1
            else
                # Commit $branch_to_test~$commit_index_to_test is in more than
                # one branch, stopping there…
                git rev-parse ${branch_to_test}~$((commit_index_to_test-1))
                return 0
            fi
        fi
        # else: Commit $branch_to_test~$commit_index_to_test is still only in
        #       branch ${branch_to_test} continuing…"
        commit_index_to_test=$((commit_index_to_test+1))
    done
}

Caveat: It fails when executed on a branch having a sub branch and no new commit since.

A---B---C---D      <- "main" branch
 \   \
  \   E---F        <- "work1" branch
   \       \
    \       G---H  <- "work1-b" branch
     \
      I---J        <- "work2" branch
first_commit_of_branch main # C
first_commit_of_branch work1 # (Fails)
first_commit_of_branch work1-b # G
first_commit_of_branch work2 # I
CDuv
  • 2,098
  • 3
  • 22
  • 28
0

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.).

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
jake
  • 91
  • 2
  • 6
0

If you use Sourcetree, look at your commit details → Parents. Then you'll see commit numbers underlined (links).

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Mike6679
  • 5,547
  • 19
  • 63
  • 108
0

This did not work for me when I had done something like developrelease-v1.0.0feature-foo. It would go all the way back to develop. Note there was a rebase involved and I am 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"
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
verdverm
  • 329
  • 4
  • 11
  • So far that seems to be the only answer that works for me. Small hint: use ripgrep instead of grep to make it almost instant. – SuperSandro2000 Nov 12 '21 at 17:32
  • We actually made this a lot easier by saving the exact answer to a `.json` file in the repo. Adds a bit of clutter, but it gives us enormous flexibility to define extra variables for storing information and controlling processes in CI. We made a script that creates a branch, creates this file, and then commits and pushes, setting up the origin at the same time. – verdverm Nov 12 '21 at 20:01
0

In my case, the "parent" of the "feature" branch, is a branch, that I explicitly marked as "base" in a pull request for the "feature" branch. It can be discovered with GitHub CLI:

gh pr list --head "feature" --json baseRefName --jq '.[].baseRefName'
Oleksandr Boiko
  • 182
  • 3
  • 9
0

This earlier answer helped me: https://stackoverflow.com/a/37159240/1009693 However, using git show-branch assumes the parent branch we're looking for is already cloned in the local repository. What if it isn't?

Suppose the local repository cloned a single branch and we want to find its parent -- most likely, it will be a remote branch, if there is one. This is where git show-branch --remotes becomes useful; however, we now need to handle the different output, there may not be any '*' characters! Finally, we also need to extract the name of the remote -- what if it is not origin?

The following does all that.


vbc=$(git rev-parse --abbrev-ref @{u})
remote=$(echo $vbc | sed 's|\(.*\)/.*|\1|')
vbc_col=$(( $(git show-branch --remotes | grep '^\s*\!' | wc -l) )) 
swimming_lane_start_row=$(( $(git show-branch --remotes | grep -n "\s\[$vbc" | tail -1 | cut -d: -f1) + 1 )) 
git show-branch --remotes | tail -n +$swimming_lane_start_row | head -n1 | sed 's/.*\[\(.*\)\].*/\1/' | sed 's/[\^~].*//' | sed -e "s|$remote/||"
-2

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).

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131