248

Imagine the following history:

       c---e---g--- feature
      /         \
-a---b---d---f---h--- master

How can I find when commit "c" has been merged into master (ie, find merge commit "h") ?

jsageryd
  • 4,234
  • 21
  • 34
Guillaume Morin
  • 3,910
  • 6
  • 25
  • 40

14 Answers14

224

Add this to your ~/.gitconfig:

[alias]
    find-merge = "!sh -c 'commit=$0 && branch=${1:-HEAD} && (git rev-list $commit..$branch --ancestry-path | cat -n; git rev-list $commit..$branch --first-parent | cat -n) | sort -k2 -s | uniq -f1 -d | sort -n | tail -1 | cut -f2'"
    show-merge = "!sh -c 'merge=$(git find-merge $0 $1) && [ -n \"$merge\" ] && git show $merge'"

Then you can use the aliases like this:

# current branch
git find-merge <SHA-1>
# specify master
git find-merge <SHA-1> master

To see the merge commit's message and other details, use git show-merge with the same arguments.

(Based on Gauthier's answer. Thanks to Rosen Matev and javabrett for correcting a problem with sort.)

Community
  • 1
  • 1
robinst
  • 30,027
  • 10
  • 102
  • 108
  • 1
    Beware that `sort -k2 | uniq -f1 -d | sort -n | tail -1 | cut -f2` is not finding correctly the last row in common. [Here](http://pastebin.com/4YjjqQzN)'s an example where it fails. – Rosen Matev Jan 09 '17 at 13:28
  • 1
    @RosenMatev It finds `16db9fef5c581ab0c56137d04ef08ef1bf82b0b7` here when I run it on your paste, is that not expected? What OS are you on? – robinst Jan 09 '17 at 23:56
  • 1
    @robinst, yours is correct. I've got "(GNU coreutils) 8.4" and for me it finds `29c40c3a3b33196d4e79793bd8e503a03753bad1` – Rosen Matev Jan 11 '17 at 08:28
  • @RosenMatev Can you try replacing `sort -n` with `sort -k 1,1 -n`? – robinst Jan 12 '17 at 01:50
  • @robinst, the result is still the same. – Rosen Matev Jan 13 '17 at 14:49
  • @RosenMatev I'm out of ideas then, you'll have to help me debug this. Can you try to find a Docker image that has the problem, so I can reproduce it? I tried with `fedora`. – robinst Jan 23 '17 at 06:34
  • 2
    @robinst The problem is caused by the first sort (`-k2`) applying a byte-based tiebreaker, not preserving the input-order when the second field (commit-hash) is equal. This behaviour can be halted in a number of ways: the `-s|--stable` switch does this - if you add it to the sort for @RosenMatev's paste, it will sort as-expected i.e. result is `16db9fef5c581ab0c56137d04ef08ef1bf82b0b7` not `29c40c3a3b33196d4e79793bd8e503a03753bad1`. This result could also be achieved by changing that sort to `sort -k2,2 -k1,1n`. Which is correct - original cat+cat order, or sorted numerically? – javabrett May 11 '17 at 05:55
  • 1
    @javabrett Wow, thanks! I'll update my answer. I think `-s` is more appropriate in this case, as otherwise we're mixing two different orderings. – robinst May 12 '17 at 03:23
  • This works for me, while Gauthier's solution didn't. Thx – Daniel Alder Mar 28 '18 at 08:51
  • 1
    After running find-merge to get the merge commit, what does `git show-merge` give you that `git show merge [themergecommit]` won't? – powlo Apr 12 '18 at 15:33
  • 3
    @powlo: Nothing, it's just one command instead of two for convenience. – robinst Apr 19 '18 at 01:56
  • @robinst I ran the command, and gets commit that seems not related to the result I expect to get, maybe after the merge commit I am looking for there was additional merge from master to dev branch and to master from that dev branch -all this was done by another developer, and actually I merge commit I get is the last one that added the original commit to master. Is it sound reasonable? if that's the case - What can I do to get the *first* merge commit that added the commit to master? – Bazuka Nov 05 '18 at 08:37
  • Does this rely on the feature branch existing or can it work without it existing anymore. And as for specifying the branch, is that the branch I want to find where the merge commit is in? – CMCDragonkai Apr 09 '20 at 05:21
  • Hi, Do you also have a command for the other way around i.e I have a merge commit and I need to find a list of commit IDs that are part of this merge commit – firstpostcommenter Apr 14 '20 at 09:36
  • @firstpostcommenter That would just be `git log merge-commit-hash^-` – robinst Apr 15 '20 at 10:21
  • This does NOT work if the commit you're looking for is itself a merge commit. git-get-merge does that. – Felix Dombek Jul 15 '20 at 09:29
  • Isn't evilstreak's [answer](https://stackoverflow.com/a/20615706/95735) a much simpler and equivalent implementation? – Piotr Dobrogost Nov 27 '20 at 12:10
  • Would be cool if this gave a non-zero exit code if there is no merge – Synthetica Oct 21 '21 at 10:44
184

Your example shows that the branch feature is still available.

In that case h is the last result of:

git log master ^feature --ancestry-path

If the branch feature is not available anymore, you can show the merge commits in the history line between c and master:

git log <SHA-1_for_c>..master --ancestry-path --merges

This will however also show all the merges that happened after h, and between e and g on feature.


Comparing the result of the following commands:

git rev-list <SHA-1_for_c>..master --ancestry-path

git rev-list <SHA-1_for_c>..master --first-parent

will give you the SHA-1 of h as the last row in common.

If you have it available, you can use comm -1 -2 on these results. If you are on msysgit, you can use the following perl code to compare:

perl -ne 'print if ($seen{$_} .= @ARGV) =~ /10$/'  file1 file2

(perl code from http://www.cyberciti.biz/faq/command-to-display-lines-common-in-files/ , which took it from "someone at comp.unix.shell news group").

See process substitution if you want to make it a one-liner.

Gauthier
  • 40,309
  • 11
  • 63
  • 97
  • 5
    Even if you have comm, you can't use it because the output of the "git rev-list" command is not sorted lexicographically. You could of course sort the output of each command before looking for common lines, but then the desired commit would not necessarily be the last one. So I think something like the perl command (though obscure) is necessary. – mhagger Jan 22 '13 at 12:34
  • 17
    I just wrote a script git-when-merged that implements this suggestion (with quite a few other features). See https://github.com/mhagger/git-when-merged – mhagger Feb 03 '13 at 08:07
  • 3
    Suppose at some point `master` was merged into `feature`, then immediately `feature` is merged into `master` as a fast-forward (tip of `feature` replaces `master`). Would that cause `--first-parent` to return the wrong parent? – Kelvin Jul 09 '13 at 17:54
  • 5
    I tried `comm -1 -2` but it didn't work. `comm` only works on sorted lines. (The perl one-liner works, although I couldn't read it.) – Domon Nov 20 '13 at 02:15
  • 1
    This is awesome. Is there a scenario where this would not work? or is this 100% reliable? – jhuynh Apr 10 '15 at 18:18
  • 2
    This is bit late but adding so people can find it useful as git natively does not provide this. There are some corners cases that I think this answer does not handle (see my answer below http://stackoverflow.com/a/43716029/58678 which handles most corner cases). Here are the corner cases: `git find-merge h master` (returns nothing but should return h), `git find-merge d master` (returns f but should return d), `git find-merge c feature` (returns e but should return g). – hIpPy May 02 '17 at 00:27
  • 1
    I'm usually only interested in the first merge. If I do: `git log ..master --ancestry-path --merges --oneline | tail -1` so far, I always get the answer I'm looking for. Under what condition might that not work? – TTT Apr 13 '21 at 19:25
  • `This will however also show all the merges that happened after h, and between e and g on feature.` I don't understand how that can be when according to the graph `h` comes after `e` and `g`. – Michael Welch Jul 14 '22 at 21:40
  • @MichaelWelch Any merge commits would appear in the result. If there are other merge commits between h and master, they'd show up. If there happens to be merge commits between e and g, they'd show up (they are between c and master). – Gauthier Jul 30 '22 at 20:34
29

git-get-merge will locate and show the merge commit you're looking for:

pip install git-get-merge
git get-merge <SHA-1>

The command follows the children of the given commit until a merge into another branch (presumably master) is found.

Jian
  • 10,320
  • 7
  • 38
  • 43
24

That is, to summarize Gauthier's post:

perl -ne 'print if ($seen{$_} .= @ARGV) =~ /10$/' <(git rev-list --ancestry-path <SHA-1_for_c>..master) <(git rev-list --first-parent <SHA-1_for_c>..master) | tail -n 1

EDIT: because this uses process substitution "<()", it is not POSIX compatible, and it may not work with your shell. It works with bash or zsh though.

Totor
  • 1,148
  • 1
  • 10
  • 21
  • 4
    I don't often copy/paste code from the internet, but when I do it works perfectly! Thanks Totor for allowing me not to think. – Ben Jan 11 '13 at 03:22
  • 2
    @TheodoreR.Smith unfortunately, the syntax `<()` is not POSIX compatible. You need to use `bash`, `zsh` or a shell supporting [process substitution](http://en.wikipedia.org/wiki/Process_substitution). I edited my answer accordingly. – Totor Aug 20 '14 at 12:52
  • 1
    Didn't work for me. I think my feature branch first got merged to another branch before it got merged to master. That could explain why your commands results in "Merge branch 'release_branch'" rather than "Merge branch 'feature_branch'". – Tim Kuipers Sep 20 '16 at 13:02
18

I needed to do this, and somehow found git-when-merged (which actually references this SO question, but Michael Haggerty never added a reference to his very nice Python script here). So now I have.

Alex Dupuy
  • 5,984
  • 3
  • 39
  • 35
15

Building on Gauthier's great answer, we don't need to use comm to compare the lists. Since we're looking for the last result in --ancestry-path which is also in --first-parent, we can simply grep for the latter in the output of the former:

git rev-list <SHA>..master --ancestry-path | grep -f <(git rev-list <SHA>..master --first-parent) | tail -1

Or for something snappy and reusable, here's a function to pop into .bashrc:

function git-find-merge() {
  git rev-list $1..master --ancestry-path | grep -f <(git rev-list $1..master --first-parent) | tail -1
}
evilstreak
  • 231
  • 3
  • 4
7
git log --topo-order

Then look for the first merge before the commit.

Roman
  • 71
  • 1
  • 1
6

For the Ruby crowd, there's git-whence. Very easy.

$ gem install git-whence
$ git whence 1234567
234557 Merge pull request #203 from branch/pathway
steel
  • 11,883
  • 7
  • 72
  • 109
6

I use below bash script which I place at path ~/bin/git-find-merge. It's based on Gauthier's answer and evilstreak's answer with few tweaks to handle corner cases. comm throws when the inputs are not sorted. grep -f works perfectly.

Corner cases:

  • If commit is a merge commit on first-parent path of branch, then return commit.
  • If commit is a NON-merge commit on first-parent path of branch, then return branch. It's either a ff merge or commit is only on branch and there is not a good way to figure out the right commit.
  • If commit and branch are same, then return commit.

~/bin/git-find-merge script:

#!/bin/bash

commit=$1
if [ -z $commit ]; then
    echo 1>&2 "fatal: commit is required"
    exit 1
fi
commit=$(git rev-parse $commit)
branch=${2-@}

# if branch points to commit (both are same), then return commit
if [ $commit == $(git rev-parse $branch) ]; then
    git log -1 $commit
    exit
fi

# if commit is a merge commit on first-parent path of branch,
# then return commit
# if commit is a NON-merge commit on first-parent path of branch,
# then return branch as it's either a ff merge or commit is only on branch
# and there is not a good way to figure out the right commit
if [[ $(git log --first-parent --pretty='%P' $commit..$branch | \
    cut -d' ' -f1 | \
    grep $commit | wc -l) -eq 1 ]]; then
    if [ $(git show -s --format="%P" $commit | wc -w) -gt 1 ]; then
        # if commit is a merge commit
        git log -1 $commit
    else
        # if commit is a NON-merge commit
        echo 1>&2 ""
        echo 1>&2 "error: returning the branch commit (ff merge or commit only on branch)"
        echo 1>&2 ""
        git log -1 $branch
    fi
    exit
fi

# 1st common commit from bottom of first-parent and ancestry-path
merge=$(grep -f \
    <(git rev-list --first-parent  $commit..$branch) \
    <(git rev-list --ancestry-path $commit..$branch) \
        | tail -1)
if [ ! -z $merge ]; then
    git log -1 $merge
    exit
fi

# merge commit not found
echo 1>&2 "fatal: no merge commit found"
exit 1

Which lets me do this:

(master)
$ git find-merge <commit>    # to find when commit merged to current branch
$ git find-merge <branch>    # to find when branch merged to current branch
$ git find-merge <commit> pu # to find when commit merged to pu branch

This script is also available on my github.

hIpPy
  • 4,649
  • 6
  • 51
  • 65
1

My ruby version of @robinst's idea, works twice faster (which is important when searching for very old commit).

find-commit.rb

commit = ARGV[0]
master = ARGV[1] || 'origin/master'

unless commit
  puts "Usage: find-commit.rb commit [master-branch]"
  puts "Will show commit that merged <commit> into <master-branch>"
  exit 1
end

parents = `git rev-list #{commit}..#{master} --reverse --first-parent --merges`.split("\n")
ancestry = `git rev-list #{commit}..#{master} --reverse --ancestry-path --merges`.split("\n")
merge = (parents & ancestry)[0]

if merge
  system "git show #{merge}"
else
  puts "#{master} doesn't include #{commit}"
  exit 2
end

You can just use it like this:

ruby find-commit.rb SHA master
Tunaki
  • 132,869
  • 46
  • 340
  • 423
Kaplan Ilya
  • 479
  • 3
  • 12
1

This is the best way to find the commit in master:

[alias]
    find-merge = !"f() { git rev-list $1..master --ancestry-path --merges --reverse | head -1; }; f"
Trublux
  • 31
  • 5
0

I've had to do this several times (thanks to everyone that answered this question!), and ended up writing a script (using Gauthier's method) that I could add to my little collection of git utilities. You can find it here: https://github.com/mwoehlke/git-utils/blob/master/bin/git-merge-point.

Matthew
  • 2,593
  • 22
  • 25
0

A graphical solution is to find it in gitk (using the “SHA1 ID” box) and follow the line of commits up to the merge commit.

user2394284
  • 5,520
  • 4
  • 32
  • 38
0

You can try something like this. The idea is to iterate through all merge commit and see if the commit "c" is reachable from one of them:

$ git log --merges --format='%h' master | while read mergecommit; do
  if git log --format='%h' $mergecommit|grep -q $c; then
    echo $mergecommit;
    break
  fi
done
holygeek
  • 15,653
  • 1
  • 40
  • 50