Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Git: How to list all branches that were created from a specific branch?

Tags:

git

I need to generate a report with all branch names that were created from develop branch (which was branched off master sometime in the past). My approach was to get the first and last commits in develop using git log --pretty=format:"%h" master..develop, then use the range of commits with git branch --contains <hash>.

However, the above approach has a few problems:

  1. I need to run to commands to know the range of commits to consider in my query.
  2. I'll have to repeat the git branch --contains command each hash in the range.
  3. The output from git branch --contains commands will contain redundant results for most cases because of the fact that most commits are common between branches.

I'm hoping there is a better and more direct way of doing this. Below I'll try to illustrate the case of interest:

  master
    ^
    |                      develop
a---b                        ^
     \                       |
      --c--d---e---f---g--h--i
           \      \    \      \
            k-l    m    n      o--p
              ^    ^    ^         ^
              |    |    |         |
         branch_1  | branch_3     |
                 branch_2       branch_4

// Desired output:
branch_1
branch_2
branch_3
branch_4
like image 248
mbadawi23 Avatar asked Oct 29 '25 01:10

mbadawi23


2 Answers

A branch name simply points to one commit. All earlier commits that are part of that branch are found by walking backwards through the graph from that point. That's how the master..develop part works, for instance: it selects commits that are ancestors of develop, excluding any commits that are ancestors of master.

(Note that in, e.g., this situation:

          o--o   <-- master
         /
...--o--o
         \
          *--*--*   <-- develop

the two-dot master..develop syntax includes the three starred commits.)

I think what you are asking for here is:

for each branch name $branch:
    if $branch identifies a commit that is a descendant of any
               of the commits in the set `master..develop`:
        print $branch

But any commit that's a descendant of, say, commit i is by definition a descendant of commit c. So, as tkruse notes in a comment:

git branch --contains <hash-of-c>

should suffice. To find the commit to use here, you can run git rev-list --topo-order master..develop | tail -1. (If you use --reverse and head -1 you can get a broken-pipe failure, so the tail -1 method is better. Annoyingly, you cannot combine --reverse with -n 1.)

You may be worried about a glitch with this sort of case:

          o--o   <-- master
         /
...--o--o--1--o--o   <-- branch-X
         \  \
          2--*--*   <-- develop
           \
            o   <-- branch-Y

You may want branch-X and branch-Y included, and they are not going to be found by using one single git branch --contains operation as they're descendants of two different commits in the master..develop range, denoted here as 1 and 2. Here, you really do need multiple git branch --contains-es, or, alternatively:

set -- $(git rev-list master..develop)
git for-each-ref --format='%(refname)' refs/heads | while read ref; do
    branch=${ref#refs/heads/}
    tip=$(git rev-parse $ref)
    take=false
    for i do
        if git merge-base --is-ancestor $i $tip; then
            take=true
            break
        fi
    done
    $take && echo $branch
done

(untested and at least one bug fixed so far). But this will be much slower than the simple git branch --contains test. The logic here is to determine whether any of the commit hash IDs in the rev-list output is an ancestor of the tip of the branch in question.

(Note that this will print developgit merge-base --is-ancestor considers a commit to be its own ancestor—so you might want to explicitly skip that one.)

(You can speed this up a lot by finding just the appropriate base commits. In the example above, for instance, there are just two. You can then use --contains on those and union the named branches. The number of bases to use is a function of the number and structure of any merge commits in master..develop. It may be possible to use git rev-list --boundary to find just the boundary commits; I have not experimented with this.)

Addendum

I realized during lunch that there's an easy way to find the minimal set of commits for --contains or --is-ancestor testing. Start with the complete list of commits in the range, sorted into topological order with parents first, e.g.:

set -- $(git rev-list --reverse --topo-order master..develop)

Then start with an empty list of "commits that we must inspect":

hashes=

Now, for each hash ID in the list of all possible hashes, test each one, e.g.:

if ! is_descendant $i $hashes; then
    hashes="$hashes $i"
fi

where is_descendant is:

# return true (i.e., 0) if $1 is a descendant of any of
# $2, $3, ..., $n
is_descendant() {
    local i c=$1
    shift
    for i do
        # $i \ancestor $c => $c \descendant $i
        git merge-base --is-ancestor $i $commit_to_test && return 0
    done
    return 1
}

After the loop is done, $hashes contains the minimal set of commits that can be used for --contains or --is-ancestor testing.

like image 131
torek Avatar answered Oct 30 '25 18:10

torek


You can get awfully close with one command:

git log --ancestry-path  --branches --not $(git merge-base --all master develop) \
        --pretty=%D  --simplify-by-decoration --decorate-refs=refs/heads

That's "show me just the branch decorations in all branch-tip histories that trace back to the current merge base of master and develop".

You can get it prettier by piping the result through anything like awk '{print $NF}' RS=', |\n'.

Note that this (like @torek's answer) assumes you're doing this in a repo administered for archival-level stability like your production master, there's no necessary relation between branch names and commits at all, let alone a permanent, or global or unique one. If you're trying to permanently tie commits to some external administrative record, do it in the commit message, not the branch name.

Edit: if you want a quick overview of the current branch structure since the master-develop split,

git log --graph --decorate-refs=refs/heads --oneline \
         --branches --simplify-by-decoration \
         --ancestry-path --boundary --not `git merge-base master develop`
like image 39
jthill Avatar answered Oct 30 '25 16:10

jthill



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!