Find merge commit which includes a specific commit
Asked Answered
B

14

252

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") ?

Betwixt answered 12/12, 2011 at 14:0 Comment(0)
L
186

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.

Luxe answered 13/12, 2011 at 16:36 Comment(9)
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.Whiff
I just wrote a script git-when-merged that implements this suggestion (with quite a few other features). See github.com/mhagger/git-when-mergedWhiff
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?Danner
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.)Disherison
This is awesome. Is there a scenario where this would not work? or is this 100% reliable?Metts
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 https://mcmap.net/q/22420/-find-merge-commit-which-includes-a-specific-commit 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).Beecham
I'm usually only interested in the first merge. If I do: git log <SHA-1_for_c>..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?Encephalitis
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.Chancey
@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).Luxe
G
231

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

Gish answered 23/6, 2015 at 8:48 Comment(18)
Beware that sort -k2 | uniq -f1 -d | sort -n | tail -1 | cut -f2 is not finding correctly the last row in common. Here's an example where it fails.Tinishatinker
@RosenMatev It finds 16db9fef5c581ab0c56137d04ef08ef1bf82b0b7 here when I run it on your paste, is that not expected? What OS are you on?Gish
@robinst, yours is correct. I've got "(GNU coreutils) 8.4" and for me it finds 29c40c3a3b33196d4e79793bd8e503a03753bad1Tinishatinker
@RosenMatev Can you try replacing sort -n with sort -k 1,1 -n?Gish
@robinst, the result is still the same.Tinishatinker
@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.Gish
@Gish 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?Friend
@Friend Wow, thanks! I'll update my answer. I think -s is more appropriate in this case, as otherwise we're mixing two different orderings.Gish
This works for me, while Gauthier's solution didn't. ThxOsteogenesis
After running find-merge to get the merge commit, what does git show-merge give you that git show merge [themergecommit] won't?Fordham
@powlo: Nothing, it's just one command instead of two for convenience.Gish
@Gish 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?Halitosis
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?Opah
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 commitSakai
@Sakai That would just be git log merge-commit-hash^-Gish
This does NOT work if the commit you're looking for is itself a merge commit. git-get-merge does that.Luzern
Isn't evilstreak's answer a much simpler and equivalent implementation?Dun
Would be cool if this gave a non-zero exit code if there is no mergeCrescentia
L
186

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.

Luxe answered 13/12, 2011 at 16:36 Comment(9)
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.Whiff
I just wrote a script git-when-merged that implements this suggestion (with quite a few other features). See github.com/mhagger/git-when-mergedWhiff
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?Danner
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.)Disherison
This is awesome. Is there a scenario where this would not work? or is this 100% reliable?Metts
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 https://mcmap.net/q/22420/-find-merge-commit-which-includes-a-specific-commit 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).Beecham
I'm usually only interested in the first merge. If I do: git log <SHA-1_for_c>..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?Encephalitis
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.Chancey
@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).Luxe
P
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.

Paquito answered 3/3, 2013 at 0:12 Comment(2)
Didn't work for me, seems to expect a "master" branch where we don't have oneAlvinaalvine
@Alvinaalvine Branch can be passed as the 2nd argument: git get-merge <SHA-1> origin/masterWilkens
I
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.

Inamorata answered 9/4, 2012 at 20:35 Comment(3)
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.Markman
@TheodoreR.Smith unfortunately, the syntax <() is not POSIX compatible. You need to use bash, zsh or a shell supporting process substitution. I edited my answer accordingly.Inamorata
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'".Elviaelvie
A
19

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.

Aurist answered 25/4, 2014 at 11:55 Comment(1)
I actually came to this question from his page.Kimkimball
V
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
}
Voluntary answered 16/12, 2013 at 16:24 Comment(1)
This worked for me. comm did not work when the inputs were not sorted.Beecham
D
8
git log --topo-order

Then look for the first merge before the commit.

Dobbin answered 15/8, 2019 at 11:33 Comment(1)
Thanks. This is a lot simpler than any of the other (much older) answers!Alarice
W
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
Whirlpool answered 1/7, 2015 at 21:36 Comment(0)
B
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.

Beecham answered 1/5, 2017 at 7:57 Comment(0)
C
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
Culottes answered 1/5, 2016 at 16:6 Comment(0)
G
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"
Githens answered 27/8, 2021 at 12:31 Comment(1)
Or simply find-merge = !"f() { git rev-list $1..master --ancestry-path --merges | tail -1; }; f".Humility
A
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
Ailey answered 12/12, 2011 at 14:13 Comment(1)
This is same as: git rev-list <SHA-1_for_c>..master --ancestry-path --mergesIncondensable
F
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.

Featherston answered 17/3, 2014 at 18:39 Comment(0)
S
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.

Shrine answered 6/12, 2022 at 15:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.