Visualize git branch dependency
Asked Answered
C

2

2

Is there a tool that can show me what branches a specific branch has been merged into? For example, if "A" has been merged into "B" and "C" but not "D", how do I output "B" and "C"? I'd only need the branch names.

Collenecollet answered 4/6, 2019 at 23:46 Comment(0)
B
5

If you mean a graphic tool, gitk would seem to be a good starting point.

Alternatively, you might prefer the CLI which is very useful for some needs, give it a try :

git log --oneline --decorate --simplify-by-decoration --graph --all

And to adress more specifically the question "How to know which branches are merged into branch A?"

git branch --contains A
Bluetongue answered 4/6, 2019 at 23:57 Comment(4)
Thanks, though I only want to know very specific info about a branch "descendants", like I described. If parent branch is A, I want to quickly know what branches has this branch been merged into without having to scan the git tree. Do you think that's possible from the command line?Collenecollet
Oh boy that was so easy, thanks! While I got your attention, say A was merged into B, and then B was merged into C, can I get similar output to git branch --contains A but that shows A was first merged into B and then B into C? (currently it will show both B and C separately)Collenecollet
@Collenecollet Then it's maybe time to consider the use of a tool. gitk is actually pretty useful, if you can get passed the rather dated interface. Did you try it and rule it out for a specific reason?Bluetongue
Not gitk, tried several other clients, but haven't found one that lets me do this easily (and upon trying gitk it also is insufficient). Thanks again for the help.Collenecollet
G
4

It's important to realize here that branch names don't actually mean anything.

A branch name is simply a label, or pointer, pointing to a commit. Think of them as one of those yellow sticky notes, or even sticky arrows, that you can paste onto commits. It's the commits that matter. The identity of a commit is its hash ID. Commits are permanent1 and unchangeable, and each commit records the hash ID of its immediate parent (most commits) or parents (2 or more for a merge commit). This is what produces the commit graph, which—except for the fact that you can always add to it, is as permanent and unchangeable as the commits themselves.

It's the graph itself that determines the actual "branchiness" of the repository:

(older commits towards the left, newer ones towards the right)

          o--A
         /
...--o--o
         \
          o--o--B

This particular part of the graph shows two branches, ending in two tip commits A and B. Adding a new commit past A gives us C, which remembers A as its parent:

          o--A--C
         /
...--o--o
         \
          o--o--B

If we now add a merge commit—let's call it M for "merge"—whose parents are both C and B, we get:

          o--A--C
         /       \
...--o--o         M
         \       /
          o--o--B

It takes two labels—two sticky-note arrows—to remember both A and B, or both C and B, but now that we have M, we can dispose of one of those labels and just keep one sticky-note arrow pointing to M itself:

          o--A--C
         /       \
...--o--o         M   <-- branch
         \       /
          o--o--B

So this is what a branch name is and does: it's a pointer, pointing to one single commit, which Git will consider to be the tip commit of some backwards-looking chain of commits. As Git works backwards, starting from this tip commit, Git will traverse all legs of a merge, to find all commits that can be reached by working backwards. Since M has two parents C and B, Git will visit commit C and then A and so on, and also B and then all commits to the left of B.

We can, of course, keep labels point to A and/or B and/or C:

                .... <-- tag: v1.0
               .
              .
          o--A--C
         /       \
...--o--o         M   <-- branch
         \       /
          o--o--B   <-- feature

As in this illustration, the labels need not be branch names. The special feature of a branch name is that by getting "on" the branch, using git checkout name, any new commit we make will automatically update the name to point to the new commit. The new commit will point back to the previous commit.

This is the notion behind git branch --contains, too. Because commit B is reachable from commit M, by starting at M and going backwards one step, git branch --contains specifier-for-B will print the name branch. That name points to M and M reaches B. More generally, Git repeatedly asks and answers the question:

  • is commit X an ancestor of commit Y?

With git branch --contains anything that finds some commit, Git asks the question about each branch-tip commit for each branch name:

target=$(git rev-parse $argument) || exit 1

git for-each-ref --format='%(refname)' refs/heads |
    while read fullbranchname; do
        branchtip=$(git rev-parse $fullbranchname)
        if git merge-base --is-ancestor $branchtip $target; then
            echo "branch ${fullbranchname#refs/heads/} contains $argument"
        fi
    done

If $argument is a branch name like branch, the operation to turn it into a hash ID selects the hash ID of commit M. Then the operation to turn refs/heads/feature into a hash ID selects the hash ID of B, and git merge-base --is-ancestor answers the question is commit B an ancestor of commit M (and it is).2

What you're asking is a little more complicated: you want, for each branch name, to not only determine does the tip commit of this branch come before the tip commit of some selected branch (as git branch --contains does), but also for each such branch name, which ones are more-ancestor-y and which ones are less-ancestor-y. This question can be answered, but only in some circumstances.

Consider, for instance, this graph:

...--o--o--o--o---M1--M2-o   <-- master
      \     \    /   /
       \     o--o   /  <-- feature1
        \          /
         o--o--o--o   <-- feature2

I think you want feature2 to be mentioned as "after" feature1, because the merge commit that brings in the tip of feature2 comes after the merge commit that brings in the tip of feature1.3 But merges don't always just bring two commits together.4 Consider:

          o--o--o   <-- feature1
         /       \
...--o--o--o--o---M1--o   <-- master
         \       /
          o--o--o   <-- feature2

Now there's no obvious order: feature1 and feature2 were both brought in at the same time, by a single commit.

(Of course, if we delete one of the two feature names, that solves the problem. If we delete both names, it's even-more solved! 😀)


1You can remove a commit, by removing all access to the commit. Git starts from the known names—branch and tag names, and all other forms of reference—and works backwards through the graph. If this working-backwards reaches a commit, the commit will have to remain. If not, the commit is eligible for removal. There are additional names, other than the obvious ones, which generally keep commits alive for at least an extra 30 days.

2We can eliminate the git rev-parse calls because git merge-base accepts a branch name in place of a hash ID. When given a name, it just finds the commit hash, the same way git rev-parse would. I put them in primarily for illustration.

3You'll have to write your own code to produce this kind of ordering. Note that it's not as simple as comparing the tip commits of the various branches; we need to know which merges, if any, bring in those tips, and compare the merges. We must also account for branches formed by having their tip embedded in a non-merge-y subgraph:

             ..... <-- branch3
            .
...--o--o--o--o    <-- branch1
         .
          ........ <-- branch2

Here, I think we want to claim branch2 < branch3.

4Even in a pair-at-a-time merge situation, you can repeatedly merge some commit with some main-line, which makes for trouble with the question (there are possible orderings but you must choose some graph traversal algorithm to pick a total order here). Essentially, remember that when you work with a DAG, you're dealing with a poset.

Gomer answered 5/6, 2019 at 1:16 Comment(1)
I knew the basics of that, but your extended (and awesome) explanation helped me understand much better, particularly the point about branches being just pointers. Gonna have to rethink my workflow around this. Thanks!Collenecollet

© 2022 - 2024 — McMap. All rights reserved.