498

I have a repository with branches master and A and lots of merge activity between the two. How can I find the commit in my repository when branch A was created based on master?

My repository basically looks like this:

-- X -- A -- B -- C -- D -- F  (master) 
          \     /   \     /
           \   /     \   /
             G -- H -- I -- J  (branch A)

I'm looking for revision A, which is not what git merge-base (--all) finds.

Matt Ball
  • 332,322
  • 92
  • 617
  • 683
Jochen
  • 6,399
  • 5
  • 21
  • 30
  • See also [Find the parent branch of a branch](http://stackoverflow.com/questions/3161204/find-the-parent-branch-of-a-branch) and [Branch length: where does a branch start in Git?](http://stackoverflow.com/questions/17581026/branch-length-where-does-a-branch-start-in-git). –  Aug 05 '13 at 15:13

22 Answers22

550

I was looking for the same thing, and I found this question. Thank you for asking it!

However, I found that the answers I see here don't seem to quite give the answer you asked for (or that I was looking for) -- they seem to give the G commit, instead of the A commit.

So, I've created the following tree (letters assigned in chronological order), so I could test things out:

A - B - D - F - G   <- "master" branch (at G)
     \   \     /
      C - E --'     <- "topic" branch (still at E)

This looks a little different than yours, because I wanted to make sure that I got (referring to this graph, not yours) B, but not A (and not D or E). Here are the letters attached to SHA prefixes and commit messages (my repo can be cloned from here, if that's interesting to anyone):

G: a9546a2 merge from topic back to master
F: e7c863d commit on master after master was merged to topic
E: 648ca35 merging master onto topic
D: 37ad159 post-branch commit on master
C: 132ee2a first commit on topic branch
B: 6aafd7f second commit on master before branching
A: 4112403 initial commit on master

So, the goal: find B. Here are three ways that I found, after a bit of tinkering:


1. visually, with gitk:

You should visually see a tree like this (as viewed from master):

gitk screen capture from master

or here (as viewed from topic):

gitk screen capture from topic

in both cases, I've selected the commit that is B in my graph. Once you click on it, its full SHA is presented in a text input field just below the graph.


2. visually, but from the terminal:

git log --graph --oneline --all

(Edit/side-note: adding --decorate can also be interesting; it adds an indication of branch names, tags, etc. Not adding this to the command-line above since the output below doesn't reflect its use.)

which shows (assuming git config --global color.ui auto):

output of git log --graph --oneline --all

Or, in straight text:

*   a9546a2 merge from topic back to master
|\  
| *   648ca35 merging master onto topic
| |\  
| * | 132ee2a first commit on topic branch
* | | e7c863d commit on master after master was merged to topic
| |/  
|/|   
* | 37ad159 post-branch commit on master
|/  
* 6aafd7f second commit on master before branching
* 4112403 initial commit on master

in either case, we see the 6aafd7f commit as the lowest common point, i.e. B in my graph, or A in yours.


3. With shell magic:

You don't specify in your question whether you wanted something like the above, or a single command that'll just get you the one revision, and nothing else. Well, here's the latter:

diff -u <(git rev-list --first-parent topic) \
             <(git rev-list --first-parent master) | \
     sed -ne 's/^ //p' | head -1
6aafd7ff98017c816033df18395c5c1e7829960d

Which you can also put into your ~/.gitconfig as (note: trailing dash is important; thanks Brian for bringing attention to that):

[alias]
    oldest-ancestor = !zsh -c 'diff -u <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | sed -ne \"s/^ //p\" | head -1' -

Which could be done via the following (convoluted with quoting) command-line:

git config --global alias.oldest-ancestor '!zsh -c '\''diff -u <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | sed -ne "s/^ //p" | head -1'\'' -'

Note: zsh could just as easily have been bash, but sh will not work -- the <() syntax doesn't exist in vanilla sh. (Thank you again, @conny, for making me aware of it in a comment on another answer on this page!)

Note: Alternate version of the above:

Thanks to liori for pointing out that the above could fall down when comparing identical branches, and coming up with an alternate diff form which removes the sed form from the mix, and makes this "safer" (i.e. it returns a result (namely, the most recent commit) even when you compare master to master):

As a .git-config line:

[alias]
    oldest-ancestor = !zsh -c 'diff --old-line-format='' --new-line-format='' <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | head -1' -

From the shell:

git config --global alias.oldest-ancestor '!zsh -c '\''diff --old-line-format='' --new-line-format='' <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | head -1'\'' -'

So, in my test tree (which was unavailable for a while, sorry; it's back), that now works on both master and topic (giving commits G and B, respectively). Thanks again, liori, for the alternate form.


So, that's what I [and liori] came up with. It seems to work for me. It also allows an additional couple of aliases that might prove handy:

git config --global alias.branchdiff '!sh -c "git diff `git oldest-ancestor`.."'
git config --global alias.branchlog '!sh -c "git log `git oldest-ancestor`.."'

Happy git-ing!

lindes
  • 8,329
  • 2
  • 28
  • 45
  • 5
    Thanks lindes, the shell option is great for situations where you want to find the branch point of a long running maintenance branch. When you are looking for a revision that might be a thousand commits in the past, the visual options really isn't going to cut it. *8') – Mark Booth Apr 02 '12 at 15:47
  • @MarkBooth: Heh, indeed. Glad to help! – lindes Apr 02 '12 at 17:02
  • 4
    In your third method you depend on that the context will show the first unchanged line. This won't happen in certain edge cases or if you happen to have slightly different requirements (e.g. I need only the one of the histories be --first-parent, and I am using this method in a script that might sometimes use the same branches on both sides). I found it safer to use `diff`'s if-then-else mode and erase changed/deleted lines from its output instead of counting on having big enough context., by: `diff --old-line-format='' --new-line-format='' – liori Oct 05 '12 at 14:17
  • 1
    Note the trailing dash at the end of the oldest-ancestor alias. Without it, the positional parameters will be wrong. – Brian White Dec 19 '12 at 15:56
  • Thanks, @BrianWhite, for pointing that out. Which also took me over the activity threshold to finally take in liori's comment... [mentioning in a separate comment, because only one _at-mention_ is allowed per comment. sigh.] – lindes Dec 20 '12 at 10:36
  • Much belated thanks, @liori, for pointing that out! I've added your version, and very thankful for it. That is indeed much better (presuming GNU diff is available, anyway). Thank you for helping to make a well-received answer even better. – lindes Dec 20 '12 at 10:37
  • Just curious, isn't it better to use `git merge-base`? – CharlesB Sep 26 '13 at 12:24
  • Well, in the stated question, it says that what's being looked for is something that's different than what `git merge-base` finds (the latter finds a recent ancestor, and the questioner is looking for the oldest ancestor). If there's a way to use `git merge-base` to get at what the questioner is looking for, please add your comments (or a new answer), describing that! – lindes Sep 30 '13 at 14:17
  • 4
    `git log -n1 --format=format:%H $(git log --reverse --format=format:%H master..topic | head -1)~` will work as well, I think – Tobias Schulte Jan 29 '14 at 08:36
  • Does the 3rd solution works for anyone with Fast Forward commits? I am trying to use this but I have some fast forward commits coming in from the branch in question to master. And thus, the oldest-ancestor returns revision of the last forwarded commit, rather than the branch point. Is this just me or the solution is unable to handle this scenario? – Salman A. Kagzi Mar 28 '14 at 10:26
  • @SalmanA.Kagzi: Are all of the merge commits fast-forwarded? If so, then you essentially don't have any branches recorded in the tree... and then no, this wouldn't work. If it's only some of them, I'd have to dig in to see what happens; in that case, maybe you can say more about what you know of how things happened? – lindes Apr 03 '14 at 08:47
  • 1
    I think using `--first-parent` can give incorrect results depending on your merge procedures. Ours are not so clean, and `--first-parent` does NOT give the right answer, by any stretch, because the "correct" path through `master` doesn't always follow the first parent in our repo. Is there any advantage to use `--first-parent` other than efficiency (reduced output)? – MadScientist Apr 15 '14 at 17:13
  • 4
    @JakubNarębski: Do you have a way for `git merge-base --fork-point ...` to give commit B (commit `6aafd7f`) for this tree? I was busy with other stuff when you posted that, and it sounded good, but I finally just tried it, and I'm not getting it to work... I either get something more recent, or just a silent failure (no error message, but exit status 1), trying arguments like `git co master; git merge-base --fork-point topic`, `git co topic; git merge-base --fork-point master`, `git merge-base --fork-point topic master` (for either checkout), etc. Is there something I'm doing wrong or missing? – lindes Nov 29 '14 at 22:31
  • 2
  • 26
    @JakubNarębski @lindes `--fork-point` is based on the reflog, so it will only work if you made the changes locally. Even then, the reflog entries could have expired. It's useful but not reliable at all. – Daniel Lubarov Jun 19 '15 at 23:49
  • 3
    `diff ... | sed ... | head -1` and also `diff ... | tail -1` are 2 too 100 times slower than using `comm` like this: `comm --nocheck-order -1 -2 – Andrei Neculau Sep 07 '15 at 22:00
  • 1
    Any alternatives when zsh is not available? – konyak Sep 30 '15 at 14:28
  • @konyak: bash should work the same, for I think anything in here. If I've missed something, please let me know what fails and how, and I can look into that. – lindes Sep 30 '15 at 21:31
  • 1
    In my case, where there are merges back from master into topic, the oldest-ancestor command only gives the most recent merge from master into topic, not where the branch first started. If there any way around this? – Valyrion May 12 '16 at 11:42
  • 1
    @Andrei-Neculau comm assumes the inputs are lexically sorted. It has optimizations that cause it to sometimes give wrong results when they are not. The hashes are not in lexicographical order, so comm won't work here. I ran into this in practice. – Mitten.O Nov 25 '16 at 15:27
  • @Mitten.O `comm --nocheck-order` assumes the inputs are lexically sorted?! – Andrei Neculau Dec 04 '16 at 15:18
  • To avoid "abigous argument" error, add `--` after the branch names. So in the .gitconfig : `oldest-ancestor = !bash -c 'diff -u – Xorax Mar 14 '17 at 15:28
  • @AndreiNeculau [man comm](https://linux.die.net/man/1/comm): "compare two *sorted* files line by line". Nocheck-order does precisely what it says, it makes comm not check the order. It still assumes they are ordered to function. At least the version I used ( GNU coreutils 8.25, I think) did. – Mitten.O Mar 21 '17 at 08:38
  • 1
    Thanks, this is very educating, but... every time I try to get a simple answer for a simple git question, I'm drowned in smart options - none of which work for me. I have a huge, old, complicated repo. Tried your suggestions, I get commits much later than my branch -and don't make any sense. I do not understand --- doesn't git have any notion of the "branching event" ? WHEN was a branch created? Knowing the time it was created, I could look at "master" and see "B" for myself. With over 1000 branches - no graphic description can help me. – Motti Shneor Apr 04 '17 at 13:20
  • @MottiShneor: No, it doesn't have any notion of that. Commits merely have parents in the "DAG" (see e.g. https://stackoverflow.com/questions/26395521/dag-vs-tree-using-git )... For example, try `git cat-file -p HEAD` just after a merge. You should see two "parent" commits listed... that (and similar sorts of data) is really all git knows, after the fact... the branch names are just labels pointing to commits, that get moved along when you commit on them. For any (not necessarily you) who are confused by all of this, I recommend reading: https://www.sbf5.com/~cduan/technical/git/ – lindes Jul 13 '19 at 00:49
  • And haven't anyone been smart enough to suggest adding a lightweight "original commit" addition to those branch "Labels"? why wasn't this done on the first design? As a user of many source-control systems over the last 35 years, I know how people think of branches, and vast majority of them think of them as "events" where something splits into different ways. What is the "design intelligence" in NOT knowing when and how this happened? – Motti Shneor Jul 20 '19 at 06:05
  • @MottiShneor: I think for the question of _why_ things were done this way in the first place, you're going to need to ask Linus Torvalds (original author of git; or perhaps he's already written about it somewhere). That said, a "branch" in `git` very definitely does _not_ correspond to an "event", and with things like rebasing, often what it refers to can change in a way where doing what you're talking about may well be hard to keep track of and/or lead to misleading or confusing results. – lindes Aug 02 '19 at 18:43
  • @PhilipRego: branch names are... somewhat ephemeral in git, at least as far as history is concerned. I'm fine with a downvote, given how many upvotes this answer has, but I think your frustration is rather with an aspect of git itself, and how it behaves, than with my answer. And some folks do keep track of commit numbers for some purposes. Definitely not what most folks do most of the time, but... there's no other name for commit 6aafd7f... that's just what it's called in git. (Or rather, that's short for a longer name/SHA - see https://git-scm.com/book/en/v2/Git-Internals-Git-Objects .) – lindes Oct 06 '19 at 19:29
  • @PhilipRego: seems like this maybe is running into the realm of specific tech support. (Want to hire me?) And sometimes there aren't easy answers, though if you think you have a distinct question to ask from the original question here, you could to ask it, and maybe get answers that way. I didn't go into the shell magic because why that works is off-topic, but basically: ` – lindes Nov 01 '19 at 01:22
  • 1
    Awesome! You can simplify a little by replacing `--old-line-format='' --new-line-format=''` with `--changed-group-format=''` – sourcedelica Jan 07 '21 at 21:17
146

You may be looking for git merge-base:

git merge-base finds best common ancestor(s) between two commits to use in a three-way merge. One common ancestor is better than another common ancestor if the latter is an ancestor of the former. A common ancestor that does not have any better common ancestor is a best common ancestor, i.e. a merge base. Note that there can be more than one merge base for a pair of commits.

CharlesB
  • 75,315
  • 26
  • 174
  • 199
Greg Hewgill
  • 828,234
  • 170
  • 1,097
  • 1,237
  • 10
    Note also the `--all` option to "`git merge-base`" – Jakub Narębski Oct 06 '09 at 21:49
  • 12
    This doesn't answer the original question, but most people asking the much simpler question for which this is the answer :) – FelipeC Apr 23 '12 at 23:19
  • 7
    he said he didn't wan't the result of git merge-base – Tom Tanner Mar 20 '13 at 17:31
  • 11
    @TomTanner: I just looked at the question history and the original question was edited to include that note about `git merge-base` five hours after my answer was posted (probably in response to my answer). Nevertheless, I will leave this answer as is because it may still be useful for somebody else who finds this question via search. – Greg Hewgill Mar 20 '13 at 18:24
  • I believe you could use the results from merge-base --all and then sift through the list of return commits using merge-base --is-ancestor to find the oldest common ancestor from the list, but this isn't the simplest way. – Matt_G Feb 09 '18 at 16:40
  • 1
    @sourcedelica - you posted your useful suggestion in the wrong answer. You want [this](https://stackoverflow.com/a/4991675/9201239). thank you! – stason Jan 07 '21 at 04:22
  • 1
    Thanks @stason! Moved. – sourcedelica Jan 07 '21 at 21:18
44

I've used git rev-list for this sort of thing. For example, (note the 3 dots)

$ git rev-list --boundary branch-a...master | grep "^-" | cut -c2-

will spit out the branch point. Now, it's not perfect; since you've merged master into branch A a couple of times, that'll split out a couple possible branch points (basically, the original branch point and then each point at which you merged master into branch A). However, it should at least narrow down the possibilities.

I've added that command to my aliases in ~/.gitconfig as:

[alias]
    diverges = !sh -c 'git rev-list --boundary $1...$2 | grep "^-" | cut -c2-'

so I can call it as:

$ git diverges branch-a master
kristopolous
  • 1,558
  • 2
  • 13
  • 23
mipadi
  • 359,228
  • 81
  • 502
  • 469
  • Note: this seems to give the first commit on the branch, rather than the common ancestor. (i.e. it gives `G` instead of `A`, per the graph in the original question.) I have an answer that gets `A`, that I'll be posting presently. – lindes Feb 14 '11 at 10:04
  • @lindes: It gives the common ancestor in every case I've tried it. Do you have an example where it doesn't? – mipadi Feb 14 '11 at 15:11
  • Yes. In [my answer](http://stackoverflow.com/questions/1527234/finding-a-branch-point-with-git/4991675#4991675) (which has a link to a repo you can clone; `git checkout topic` and then run this with `topic` in place of `branch-a`), it lists `648ca357b946939654da12aaf2dc072763f3caee` and `37ad15952db5c065679d7fc31838369712f0b338` -- both `37ad159` and `648ca35` are in the ancestory of the current branches (the latter being the current HEAD of `topic`), but neither is the point before branching happened. Do you get something different? – lindes Feb 15 '11 at 02:51
  • @lindes: I was unable to clone your repo (possibly a permissions issue?). – mipadi Feb 15 '11 at 16:31
  • Oops, sorry! Thank you for letting me know. I forgot to run git update-server-info. It should be good to go now. :) – lindes Feb 16 '11 at 07:28
  • It doesn't work. I tried with the [test repository](http://stackoverflow.com/a/9979346/10474), and --boundary branch_A...master doesn't even show the right commit, so no amount of grepping is going to solve that =/ – FelipeC Apr 23 '12 at 23:25
  • @mipadi I got two commits, one is the ancestor of both branches, and another one is the first commit on my branch-a. It would be ok, since I could always do head/tail, but seems that order of that commits is not determined. Is there any option for rev-list to force specific order, like "master branch commit first". – Strider Jan 15 '17 at 22:22
  • I could not get it working in a CI. I get "ambiguous argument `ci-dev...master': unknown revision or path not in the working tree`. any ideas? – Radagast the Brown Sep 14 '20 at 08:08
  • Oh. I found it. need to `origin/ci-dev...origin/master`. – Radagast the Brown Sep 14 '20 at 08:35
33

If you like terse commands,

git rev-list $(git rev-list --first-parent ^branch_name master | tail -n1)^^! 

Here's an explanation.

The following command gives you the list of all commits in master that occurred after branch_name was created

git rev-list --first-parent ^branch_name master 

Since you only care about the earliest of those commits you want the last line of the output:

git rev-list ^branch_name --first-parent master | tail -n1

The parent of the earliest commit that's not an ancestor of "branch_name" is, by definition, in "branch_name," and is in "master" since it's an ancestor of something in "master." So you've got the earliest commit that's in both branches.

The command

git rev-list commit^^!

is just a way to show the parent commit reference. You could use

git log -1 commit^

or whatever.

PS: I disagree with the argument that ancestor order is irrelevant. It depends on what you want. For example, in this case

_C1___C2_______ master
  \    \_XXXXX_ branch A (the Xs denote arbitrary cross-overs between master and A)
   \_____/ branch B

it makes perfect sense to output C2 as the "branching" commit. This is when the developer branched out from "master." When he branched, branch "B" wasn't even merged in his branch! This is what the solution in this post gives.

If what you want is the last commit C such that all paths from origin to the last commit on branch "A" go through C, then you want to ignore ancestry order. That's purely topological and gives you an idea of since when you have two versions of the code going at the same time. That's when you'd go with merge-base based approaches, and it will return C1 in my example.

seh
  • 14,430
  • 2
  • 48
  • 57
Lionel
  • 27,268
  • 1
  • 13
  • 5
  • 4
    This is by far the cleanest answer, let's get this voted to the top. A suggested edit: `git rev-list commit^^!` can be simplified as `git rev-parse commit^` – Russell Davis Apr 07 '13 at 18:58
  • This should be the answer! – trinth Jun 12 '14 at 17:24
  • 2
    This answer is nice, I just replaced `git rev-list --first-parent ^branch_name master` with `git rev-list --first-parent branch_name ^master` because if the master branch is 0 commits ahead of the other branch (fast-forwardable to it), no output would be created. With my solution, no output is created if master is strictly ahead (i.e. the branch has been fully merged), which is what I want. – Michael Schmeißer Dec 09 '14 at 16:39
  • 3
    This won't work unless I'm totally missing something. There are merges in both directions in the example branches. It sounds like you tried to take that into account, but I believe this will cause your answer to fail. `git rev-list --first-parent ^topic master` will only take you back to the first commit after the last merge from `master` into `topic` (if that even exists). – jerry May 22 '15 at 19:37
  • 1
    @jerry You are correct, this answer is garbage; for instance, in the case that a backmerge has just taken place (merged master into topic), and master has no new commits after that, the first git rev-list --first-parent command outputs nothing at all. – TamaMcGlinn Apr 17 '19 at 11:12
21

Given that so many of the answers in this thread do not give the answer the question was asking for, here is a summary of the results of each solution, along with the script I used to replicate the repository given in the question.

The log

Creating a repository with the structure given, we get the git log of:

$ git --no-pager log --graph --oneline --all --decorate
* b80b645 (HEAD, branch_A) J - Work in branch_A branch
| *   3bd4054 (master) F - Merge branch_A into branch master
| |\  
| |/  
|/|   
* |   a06711b I - Merge master into branch_A
|\ \  
* | | bcad6a3 H - Work in branch_A
| | * b46632a D - Work in branch master
| |/  
| *   413851d C - Merge branch_A into branch master
| |\  
| |/  
|/|   
* | 6e343aa G - Work in branch_A
| * 89655bb B - Work in branch master
|/  
* 74c6405 (tag: branch_A_tag) A - Work in branch master
* 7a1c939 X - Work in branch master

My only addition, is the tag which makes it explicit about the point at which we created the branch and thus the commit we wish to find.

The solution which works

The only solution which works is the one provided by lindes correctly returns A:

$ diff -u <(git rev-list --first-parent branch_A) \
          <(git rev-list --first-parent master) | \
      sed -ne 's/^ //p' | head -1
74c6405d17e319bd0c07c690ed876d65d89618d5

As Charles Bailey points out though, this solution is very brittle.

If you branch_A into master and then merge master into branch_A without intervening commits then lindes' solution only gives you the most recent first divergance.

That means that for my workflow, I think I'm going to have to stick with tagging the branch point of long running branches, since I can't guarantee that they can be reliably be found later.

This really all boils down to gits lack of what hg calls named branches. The blogger jhw calls these lineages vs. families in his article Why I Like Mercurial More Than Git and his follow-up article More On Mercurial vs. Git (with Graphs!). I would recommend people read them to see why some mercurial converts miss not having named branches in git.

The solutions which don't work

The solution provided by mipadi returns two answers, I and C:

$ git rev-list --boundary branch_A...master | grep ^- | cut -c2-
a06711b55cf7275e8c3c843748daaa0aa75aef54
413851dfecab2718a3692a4bba13b50b81e36afc

The solution provided by Greg Hewgill return I

$ git merge-base master branch_A
a06711b55cf7275e8c3c843748daaa0aa75aef54
$ git merge-base --all master branch_A
a06711b55cf7275e8c3c843748daaa0aa75aef54

The solution provided by Karl returns X:

$ diff -u <(git log --pretty=oneline branch_A) \
          <(git log --pretty=oneline master) | \
       tail -1 | cut -c 2-42
7a1c939ec325515acfccb79040b2e4e1c3e7bbe5

The script

mkdir $1
cd $1
git init
git commit --allow-empty -m "X - Work in branch master"
git commit --allow-empty -m "A - Work in branch master"
git branch branch_A
git tag branch_A_tag     -m "Tag branch point of branch_A"
git commit --allow-empty -m "B - Work in branch master"
git checkout branch_A
git commit --allow-empty -m "G - Work in branch_A"
git checkout master
git merge branch_A       -m "C - Merge branch_A into branch master"
git checkout branch_A
git commit --allow-empty -m "H - Work in branch_A"
git merge master         -m "I - Merge master into branch_A"
git checkout master
git commit --allow-empty -m "D - Work in branch master"
git merge branch_A       -m "F - Merge branch_A into branch master"
git checkout branch_A
git commit --allow-empty -m "J - Work in branch_A branch"

I doubt the git version makes much difference to this, but:

$ git --version
git version 1.7.1

Thanks to Charles Bailey for showing me a more compact way to script the example repository.

Community
  • 1
  • 1
Mark Booth
  • 6,794
  • 2
  • 60
  • 88
  • 2
    The solution by Karl is easy to fix: `diff -u – FelipeC Apr 23 '12 at 23:32
  • I think you mean "The cleaned up variation of the solution provided by Karl returns X". The original worked fine it was just ugly :-) – Karl Apr 24 '12 at 05:17
  • Nope, your original does not work fine. Granted, the variation works even worst. But adding the option --topo-order makes your version work :) – FelipeC May 27 '12 at 10:32
  • @felipec - See my final comment on the answer by [Charles Bailey](http://stackoverflow.com/a/1527308/42473). Alas our [chat](http://chat.stackoverflow.com/rooms/9641/discussion-between-mark-booth-and-charles-bailey) (and thus all of the old comments) have now been deleted. I will try to update my answer when I get the time. – Mark Booth May 29 '12 at 09:21
  • Interesting. I'd sort of assumed topological was the default. Silly me :-) – Karl Jun 08 '12 at 03:16
  • Your replication script does not follow chronological order for the letters. X A B G C H I D F J... – eckes Sep 26 '12 at 18:01
11

In general, this is not possible. In a branch history a branch-and-merge before a named branch was branched off and an intermediate branch of two named branches look the same.

In git, branches are just the current names of the tips of sections of history. They don't really have a strong identity.

This isn't usually a big issue as the merge-base (see Greg Hewgill's answer) of two commits is usually much more useful, giving the most recent commit which the two branches shared.

A solution relying on the order of parents of a commit obviously won't work in situations where a branch has been fully integrated at some point in the branch's history.

git commit --allow-empty -m root # actual branch commit
git checkout -b branch_A
git commit --allow-empty -m  "branch_A commit"
git checkout master
git commit --allow-empty -m "More work on master"
git merge -m "Merge branch_A into master" branch_A # identified as branch point
git checkout branch_A
git merge --ff-only master
git commit --allow-empty -m "More work on branch_A"
git checkout master
git commit --allow-empty -m "More work on master"

This technique also falls down if an integration merge has been made with the parents reversed (e.g. a temporary branch was used to perform a test merge into master and then fast-forwarded into the feature branch to build on further).

git commit --allow-empty -m root # actual branch point
git checkout -b branch_A
git commit --allow-empty -m  "branch_A commit"
git checkout master
git commit --allow-empty -m "More work on master"
git merge -m "Merge branch_A into master" branch_A # identified as branch point
git checkout branch_A
git commit --allow-empty -m "More work on branch_A"

git checkout -b tmp-branch master
git merge -m "Merge branch_A into tmp-branch (master copy)" branch_A
git checkout branch_A
git merge --ff-only tmp-branch
git branch -d tmp-branch

git checkout master
git commit --allow-empty -m "More work on master"
CB Bailey
  • 648,528
  • 94
  • 608
  • 638
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/9641/discussion-between-mark-booth-and-charles-bailey) – Mark Booth Apr 03 '12 at 12:28
  • 3
    Thanks Charles, you've convinced me, if I want to know the point at which the branch *originally diverged*, I'm going to have to tag it. I really wish that `git` had an equivalent to `hg`'s named branches, it would make managing long lived maintenance branches *so* much easier. – Mark Booth Apr 04 '12 at 13:11
  • 1
    "In git, branches are just the current names of the tips of sections of history. They don't really have a strong identity" That's a scary thing to say and has convinced me that I need to understand Git branches better - thanks (+1) – dumbledad Oct 13 '15 at 11:47
  • _In a branch history a branch-and-merge before a named branch was branched off and an intermediate branch of two named branches look the same._ Yep. +1. – user1071847 May 14 '18 at 17:36
6

How about something like

git log --pretty=oneline master > 1
git log --pretty=oneline branch_A > 2

git rev-parse `diff 1 2 | tail -1 | cut -c 3-42`^
Karl
  • 668
  • 4
  • 4
  • This works. It's really cumbersome but it's the only thing I've found that actually seems to do the job. – GaryO Sep 08 '10 at 17:40
  • 1
    Git alias equivalent: `diverges = !bash -c 'git rev-parse $(diff – conny Sep 15 '10 at 14:56
  • @conny: Oh, wow -- I'd never seen the – lindes Feb 14 '11 at 09:04
  • this seems to give me the first commit on the branch, rather than the common ancestor. (i.e. it gives `G` instead of `A`, per the graph in the original question.) I think I've found an answer, though, which I'll post presently. – lindes Feb 14 '11 at 10:02
  • Instead of 'git log --pretty=oneline' you can just do 'git rev-list', then you can skip the cut as well, moreover, this gives the parent commit of the point of divergence, so just tail -2 | head 1. So: `diff -u – FelipeC Apr 23 '12 at 23:14
  • Er, looks like both Karl and my cleaned up version need --topo-order to work: `diff -u – FelipeC May 27 '12 at 10:33
  • AFAICS, the `tail -2` relies on a unified diff output of 2 context lines, but for me it is 3 lines, so I am getting the wrong commit (`X` in the OP example) so I need to do `tail -3`. Unless this approach is broken in my case with lots of commits and feature branches left and right. One can set it using `diff -U 2` to make it less fragile, also remove the leading space using cut: `diff -U 2 – Alexander Klimetschek May 21 '14 at 23:50
5

surely I'm missing something, but IMO, all the problems above are caused because we are always trying to find the branch point going back in the history, and that causes all sort of problems because of the merging combinations available.

Instead, I've followed a different approach, based in the fact that both branches share a lot of history, exactly all the history before branching is 100% the same, so instead of going back, my proposal is about going forward (from 1st commit), looking for the 1st difference in both branches. The branch point will be, simply, the parent of the first difference found.

In practice:

#!/bin/bash
diff <( git rev-list "${1:-master}" --reverse --topo-order ) \
     <( git rev-list "${2:-HEAD}" --reverse --topo-order) \
--unified=1 | sed -ne 's/^ //p' | head -1

And it's solving all my usual cases. Sure there are border ones not covered but... ciao :-)

stronk7
  • 51
  • 1
  • 1
4

I recently needed to solve this problem as well and ended up writing a Ruby script for this: https://github.com/vaneyckt/git-find-branching-point

Reck
  • 5,896
  • 2
  • 17
  • 24
4

After a lot of research and discussions, it's clear there's no magic bullet that would work in all situations, at least not in the current version of Git.

That's why I wrote a couple of patches that add the concept of a tail branch. Each time a branch is created, a pointer to the original point is created too, the tail ref. This ref gets updated every time the branch is rebased.

To find out the branch point of the devel branch, all you have to do is use devel@{tail}, that's it.

https://github.com/felipec/git/commits/fc/tail

FelipeC
  • 7,483
  • 4
  • 37
  • 33
  • 1
    Might be the only stable solution. Did you see if this could get into git? I didn't see a pull request. – Alexander Klimetschek May 21 '14 at 23:40
  • @AlexanderKlimetschek I didn't send the patches, and I don't think those would be accepted. However, I tried a different method: an "update-branch" hook which does something very similar. This way by default Git wouldn't do anything, but you could enable the hook to update the tail branch. You wouldn't have devel@{tail} though, but wouldn't be so bad to use tails/devel instead. – FelipeC May 22 '14 at 09:22
4

The following command will reveal the SHA1 of Commit A

git merge-base --fork-point A

Gayan Pathirage
  • 1,695
  • 1
  • 18
  • 20
3

Here's an improved version of my previous answer previous answer. It relies on the commit messages from merges to find where the branch was first created.

It works on all the repositories mentioned here, and I've even addressed some tricky ones that spawned on the mailing list. I also wrote tests for this.

find_merge ()
{
    local selection extra
    test "$2" && extra=" into $2"
    git rev-list --min-parents=2 --grep="Merge branch '$1'$extra" --topo-order ${3:---all} | tail -1
}

branch_point ()
{
    local first_merge second_merge merge
    first_merge=$(find_merge $1 "" "$1 $2")
    second_merge=$(find_merge $2 $1 $first_merge)
    merge=${second_merge:-$first_merge}

    if [ "$merge" ]; then
        git merge-base $merge^1 $merge^2
    else
        git merge-base $1 $2
    fi
}
Community
  • 1
  • 1
FelipeC
  • 7,483
  • 4
  • 37
  • 33
3

Sometimes it is effectively impossible (with some exceptions of where you might be lucky to have additional data) and the solutions here wont work.

Git doesn't preserve ref history (which includes branches). It only stores the current position for each branch (the head). This means you can lose some branch history in git over time. Whenever you branch for example, it's immediately lost which branch was the original one. All a branch does is:

git checkout branch1    # refs/branch1 -> commit1
git checkout -b branch2 # branch2 -> commit1

You might assume that the first commited to is the branch. This tends to be the case but it's not always so. There's nothing stopping you from commiting to either branch first after the above operation. Additionally, git timestamps aren't guaranteed to be reliable. It's not until you commit to both that they truly become branches structurally.

While in diagrams we tend to number commits conceptually, git has no real stable concept of sequence when the commit tree branches. In this case you can assume the numbers (indicating order) are determined by timestamp (it might be fun to see how a git UI handles things when you set all the timestamps to the same).

This is what a human expect conceptually:

After branch:
       C1 (B1)
      /
    -
      \
       C1 (B2)
After first commit:
       C1 (B1)
      /
    - 
      \
       C1 - C2 (B2)

This is what you actually get:

After branch:
    - C1 (B1) (B2)
After first commit (human):
    - C1 (B1)
        \
         C2 (B2)
After first commit (real):
    - C1 (B1) - C2 (B2)

You would assume B1 to be the original branch but it could infact simply be a dead branch (someone did checkout -b but never committed to it). It's not until you commit to both that you get a legitimate branch structure within git:

Either:
      / - C2 (B1)
    -- C1
      \ - C3 (B2)
Or:
      / - C3 (B1)
    -- C1
      \ - C2 (B2)

You always know that C1 came before C2 and C3 but you never reliably know if C2 came before C3 or C3 came before C2 (because you can set the time on your workstation to anything for example). B1 and B2 is also misleading as you can't know which branch came first. You can make a very good and usually accurate guess at it in many cases. It is a bit like a race track. All things generally being equal with the cars then you can assume that a car that comes in a lap behind started a lap behind. We also have conventions that are very reliable, for example master will nearly always represent the longest lived branches although sadly I have seen cases where even this is not the case.

The example given here is a history preserving example:

Human:
    - X - A - B - C - D - F (B1)
           \     / \     /
            G - H ----- I - J (B2)
Real:
            B ----- C - D - F (B1)
           /       / \     /
    - X - A       /   \   /
           \     /     \ /
            G - H ----- I - J (B2)

Real here is also misleading because we as humans read it left to right, root to leaf (ref). Git does not do that. Where we do (A->B) in our heads git does (A<-B or B->A). It reads it from ref to root. Refs can be anywhere but tend to be leafs, at least for active branches. A ref points to a commit and commits only contain a like to their parent/s, not to their children. When a commit is a merge commit it will have more than one parent. The first parent is always the original commit that was merged into. The other parents are always commits that were merged into the original commit.

Paths:
    F->(D->(C->(B->(A->X)),(H->(G->(A->X))))),(I->(H->(G->(A->X))),(C->(B->(A->X)),(H->(G->(A->X)))))
    J->(I->(H->(G->(A->X))),(C->(B->(A->X)),(H->(G->(A->X)))))

This is not a very efficient representation, rather an expression of all the paths git can take from each ref (B1 and B2).

Git's internal storage looks more like this (not that A as a parent appears twice):

    F->D,I | D->C | C->B,H | B->A | A->X | J->I | I->H,C | H->G | G->A

If you dump a raw git commit you'll see zero or more parent fields. If there are zero, it means no parent and the commit is a root (you can actually have multiple roots). If there's one, it means there was no merge and it's not a root commit. If there is more than one it means that the commit is the result of a merge and all of the parents after the first are merge commits.

Paths simplified:
    F->(D->C),I | J->I | I->H,C | C->(B->A),H | H->(G->A) | A->X
Paths first parents only:
    F->(D->(C->(B->(A->X)))) | F->D->C->B->A->X
    J->(I->(H->(G->(A->X))) | J->I->H->G->A->X
Or:
    F->D->C | J->I | I->H | C->B->A | H->G->A | A->X
Paths first parents only simplified:
    F->D->C->B->A | J->I->->G->A | A->X
Topological:
    - X - A - B - C - D - F (B1)
           \
            G - H - I - J (B2)

When both hit A their chain will be the same, before that their chain will be entirely different. The first commit another two commits have in common is the common ancestor and from whence they diverged. there might be some confusion here between the terms commit, branch and ref. You can in fact merge a commit. This is what merge really does. A ref simply points to a commit and a branch is nothing more than a ref in the folder .git/refs/heads, the folder location is what determines that a ref is a branch rather than something else such as a tag.

Where you lose history is that merge will do one of two things depending on circumstances.

Consider:

      / - B (B1)
    - A
      \ - C (B2)

In this case a merge in either direction will create a new commit with the first parent as the commit pointed to by the current checked out branch and the second parent as the commit at the tip of the branch you merged into your current branch. It has to create a new commit as both branches have changes since their common ancestor that must be combined.

      / - B - D (B1)
    - A      /
      \ --- C (B2)

At this point D (B1) now has both sets of changes from both branches (itself and B2). However the second branch doesn't have the changes from B1. If you merge the changes from B1 into B2 so that they are syncronised then you might expect something that looks like this (you can force git merge to do it like this however with --no-ff):

Expected:
      / - B - D (B1)
    - A      / \
      \ --- C - E (B2)
Reality:
      / - B - D (B1) (B2)
    - A      /
      \ --- C

You will get that even if B1 has additional commits. As long as there aren't changes in B2 that B1 doesn't have, the two branches will be merged. It does a fast forward which is like a rebase (rebases also eat or linearise history), except unlike a rebase as only one branch has a change set it doesn't have to apply a changeset from one branch on top of that from another.

From:
      / - B - D - E (B1)
    - A      /
      \ --- C (B2)
To:
      / - B - D - E (B1) (B2)
    - A      /
      \ --- C

If you cease work on B1 then things are largely fine for preserving history in the long run. Only B1 (which might be master) will advance typically so the location of B2 in B2's history successfully represents the point that it was merged into B1. This is what git expects you to do, to branch B from A, then you can merge A into B as much as you like as changes accumulate, however when merging B back into A, it's not expected that you will work on B and further. If you carry on working on your branch after fast forward merging it back into the branch you were working on then your erasing B's previous history each time. You're really creating a new branch each time after fast forward commit to source then commit to branch. You end up with when you fast forward commit is lots of branches/merges that you can see in the history and structure but without the ability to determine what the name of that branch was or if what looks like two separate branches is really the same branch.

         0   1   2   3   4 (B1)
        /-\ /-\ /-\ /-\ /
    ----   -   -   -   -
        \-/ \-/ \-/ \-/ \
         5   6   7   8   9 (B2)

1 to 3 and 5 to 8 are structural branches that show up if you follow the history for either 4 or 9. There's no way in git to know which of this unnamed and unreferenced structural branches belong to with of the named and references branches as the end of the structure. You might assume from this drawing that 0 to 4 belongs to B1 and 4 to 9 belongs to B2 but apart from 4 and 9 was can't know which branch belongs to which branch, I've simply drawn it in a way that gives the illusion of that. 0 might belong to B2 and 5 might belong to B1. There are 16 different possibilies in this case of which named branch each of the structural branches could belong to. This is assuming that none of these structural branches came from a deleted branch or as a result of merging a branch into itself when pulling from master (the same branch name on two repos is infact two branches, a separate repository is like branching all branches).

There are a number of git strategies that work around this. You can force git merge to never fast forward and always create a merge branch. A horrible way to preserve branch history is with tags and/or branches (tags are really recommended) according to some convention of your choosing. I realy wouldn't recommend a dummy empty commit in the branch you're merging into. A very common convention is to not merge into an integration branch until you want to genuinely close your branch. This is a practice that people should attempt to adhere to as otherwise you're working around the point of having branches. However in the real world the ideal is not always practical meaning doing the right thing is not viable for every situation. If what you're doing on a branch is isolated that can work but otherwise you might be in a situation where when multiple developers are working one something they need to share their changes quickly (ideally you might really want to be working on one branch but not all situations suit that either and generally two people working on a branch is something you want to avoid).

jgmjgm
  • 3,028
  • 1
  • 20
  • 18
  • "Git doesn't preserve ref history" It does, but not by default and not for extensive time. See `man git-reflog` and the part about dates: "master@{one.week.ago} means "where master used to point to one week ago in this local repository"". Or the discussion on `@{}` in `man gitrevisions`. And `core.reflogExpire` in `man git-config`. – Patrick Mevzek Mar 12 '19 at 22:38
3

A simple way to just make it easier to see the branching point in git log --graph is to use the option --first-parent.

For example, take the repo from the accepted answer:

$ git log --all --oneline --decorate --graph

*   a9546a2 (HEAD -> master, origin/master, origin/HEAD) merge from topic back to master
|\  
| *   648ca35 (origin/topic) merging master onto topic
| |\  
| * | 132ee2a first commit on topic branch
* | | e7c863d commit on master after master was merged to topic
| |/  
|/|   
* | 37ad159 post-branch commit on master
|/  
* 6aafd7f second commit on master before branching
* 4112403 initial commit on master

Now add --first-parent:

$ git log --all --oneline --decorate --graph --first-parent

* a9546a2 (HEAD -> master, origin/master, origin/HEAD) merge from topic back to master
| * 648ca35 (origin/topic) merging master onto topic
| * 132ee2a first commit on topic branch
* | e7c863d commit on master after master was merged to topic
* | 37ad159 post-branch commit on master
|/  
* 6aafd7f second commit on master before branching
* 4112403 initial commit on master

That makes it easier!

Note if the repo has lots of branches you're going to want to specify the 2 branches you're comparing instead of using --all:

$ git log --decorate --oneline --graph --first-parent master origin/topic
binaryfunt
  • 4,962
  • 3
  • 26
  • 49
2

I seem to be getting some joy with

git rev-list branch...master

The last line you get is the first commit on the branch, so then it's a matter of getting the parent of that. So

git rev-list -1 `git rev-list branch...master | tail -1`^

Seems to work for me and doesn't need diffs and so on (which is helpful as we don't have that version of diff)

Correction: This doesn't work if you are on the master branch, but I'm doing this in a script so that's less of an issue

Tom Tanner
  • 8,967
  • 2
  • 28
  • 58
2

To find commits from the branching point, you could use this.

git log --ancestry-path master..topicbranch
Andor
  • 4,269
  • 4
  • 22
  • 21
2

Not quite a solution to the question but I thought it was worth noting the the approach I use when I have a long-living branch:

At the same time I create the branch, I also create a tag with the same name but with an -init suffix, for example feature-branch and feature-branch-init.

(It is kind of bizarre that this is such a hard question to answer!)

Stephen Darlington
  • 49,617
  • 11
  • 101
  • 147
  • 1
    Considering the sheer mind-boggling stupidity of designing a concept of "branch" without any notion of When and where it was created... plus the immense complications of other suggested solutions - by people trying to out-smart this way-too-smart thing, I think I'd prefer your solution. Only it burdens one with the need to REMEMBER to do this every time you create a branch - a thing git users are making very often. In addition - I read somewhere that 'tag's have a penalty of being 'heavy'. Still, I think that's what I think I'll do. – Motti Shneor Jul 20 '19 at 06:35
0

The problem appears to be to find the most recent, single-commit cut between both branches on one side, and the earliest common ancestor on the other (probably the initial commit of the repo). This matches my intuition of what the "branching off" point is.

That in mind, this is not at all easy to compute with normal git shell commands, since git rev-list -- our most powerful tool -- doesn't let us restrict the path by which a commit is reached. The closest we have is git rev-list --boundary, which can give us a set of all the commits that "blocked our way". (Note: git rev-list --ancestry-path is interesting but I don't how to make it useful here.)

Here is the script: https://gist.github.com/abortz/d464c88923c520b79e3d. It's relatively simple, but due to a loop it's complicated enough to warrant a gist.

Note that most other solutions proposed here can't possibly work in all situations for a simple reason: git rev-list --first-parent isn't reliable in linearizing history because there can be merges with either ordering.

git rev-list --topo-order, on the other hand, is very useful -- for walking commits in topographic order -- but doing diffs is brittle: there are multiple possible topographic orderings for a given graph, so you are depending on a certain stability of the orderings. That said, strongk7's solution probably works damn well most of the time. However it's slower that mine as a result of having to walk the entire history of the repo... twice. :-)

Community
  • 1
  • 1
0

The following implements git equivalent of svn log --stop-on-copy and can also be used to find branch origin.

Approach

  1. Get head for all branches
  2. collect mergeBase for target branch each other branch
  3. git.log and iterate
  4. Stop at first commit that appears in the mergeBase list

Like all rivers run to the sea, all branches run to master and therefore we find merge-base between seemingly unrelated branches. As we walk back from branch head through ancestors, we can stop at the first potential merge base since in theory it should be origin point of this branch.

Notes

  • I haven't tried this approach where sibling and cousin branches merged between each other.
  • I know there must be a better solution.

details: https://stackoverflow.com/a/35353202/9950

Community
  • 1
  • 1
Peter Kahn
  • 10,918
  • 14
  • 64
  • 108
-1

You could use the following command to return the oldest commit in branch_a, which is not reachable from master:

git rev-list branch_a ^master | tail -1

Perhaps with an additional sanity check that the parent of that commit is actually reachable from master...

  • 1
    This doesn't work. If branch_a gets merged into master once, and then continues, the commits on that merge would be considered part of master, so they wouldn't show up in ^master. – FelipeC Apr 23 '12 at 23:08
-2

I believe I've found a way that deals with all the corner-cases mentioned here:

branch=branch_A
merge=$(git rev-list --min-parents=2 --grep="Merge.*$branch" --all | tail -1)
git merge-base $merge^1 $merge^2

Charles Bailey is quite right that solutions based on the order of ancestors have only limited value; at the end of the day you need some sort of record of "this commit came from branch X", but such record already exists; by default 'git merge' would use a commit message such as "Merge branch 'branch_A' into master", this tells you that all the commits from the second parent (commit^2) came from 'branch_A' and was merged to the first parent (commit^1), which is 'master'.

Armed with this information you can find the first merge of 'branch_A' (which is when 'branch_A' really came into existence), and find the merge-base, which would be the branch point :)

I've tried with the repositories of Mark Booth and Charles Bailey and the solution works; how couldn't it? The only way this wouldn't work is if you have manually changed the default commit message for merges so that the branch information is truly lost.

For usefulness:

[alias]
    branch-point = !sh -c 'merge=$(git rev-list --min-parents=2 --grep="Merge.*$1" --all | tail -1) && git merge-base $merge^1 $merge^2'

Then you can do 'git branch-point branch_A'.

Enjoy ;)

FelipeC
  • 7,483
  • 4
  • 37
  • 33
  • 5
    Relying on the merge messages is *more* fragile than hypothesising about parent order. It's not just a hypothetical situation either; I frequently use `git merge -m` to say _what_ I've merged in rather than the name of a potentionally ephemeral branch (e.g. "merge mainline changes into feature x y z refactor"). Suppose I'd been less helpful with my `-m` in my example? The problem is simply not soluble in its full generality because I can make the same history with one or two temporary branches and there is no way to tell the difference. – CB Bailey May 27 '12 at 21:14
  • 1
    @CharlesBailey That is *your* problem then. You shouldn't remove those lines from the commit message, you should add the rest of the message *below* the original one. Nowadays 'git merge' automatically opens an editor for you to add whatever you want, and for old versions of git you can do 'git merge --edit'. Either way, you can use a commit hook to add a "Commited on branch 'foo'" to each and every commit if that's what you really want. However, this solution works **for most people**. – FelipeC May 30 '12 at 17:42
  • 2
    Did not work for me. The branch_A was forked out of master which already had lot of merges. This logic did not give me the exact commit hash where branch_A was created. – Venkat Kotra Sep 16 '15 at 06:49
-2

You can examine the reflog of branch A to find from which commit it was created, as well as the full history of which commits that branch pointed to. Reflogs are in .git/logs.

Paggas
  • 1,611
  • 3
  • 13
  • 15
  • 2
    I don't think this works in general because the reflog can be pruned. And I don't think (?) reflogs get pushed either, so this would only work in a single-repo situation. – GaryO Sep 08 '10 at 17:38