Ben
Ben

Reputation: 6907

Git log: excluding commits from a specific merge

I'm trying to work out how to print out a git log, between two tags, excluding all commits which were merged in on one specific merge commit.

For example. If my commit log looked something like this:

   TAGA                                        TAGB
    |     C---F---H---K           Q---T---V     |
    v    /             \         /         \    v
    A---B---D---G---J---L---M---O---R---U---W---X
             \     /         \         /
              E---I           N---P---S

And I wanted a to exclude all commits which were introduced to the trunk on merge-commit L. (So in this example, I want to exclude C,F,H,K).

Currently my git line looks like this:

git log --oneline --no-merges TAGB ^TAGA

which will list all commits between the two tags (note, I'm excluding merges-commits themselves, which is why J,L,U & W are not present.):

A,B,C,D,E,F,G,H,I,K,M,N,O,P,Q,R,S,T,V,X

Does anyone know a way of excluding C,F,H & K ? i.e:

A,B,D,E,G,I,M,N,O,P,Q,R,S,T,V,X

Any help would be appreciated.

Upvotes: 3

Views: 1855

Answers (2)

torek
torek

Reputation: 490078

There is no cheap, all-in-one-Git-expression way to do this. The graph walking code either follows --first-parent only (as in Vimsha's answer) or follows all parents of a merge. Hence, the exclusionary ^K (or ^L^2 or some other way to name commit K) has the side effect of pruning all commits reachable from K, rather than just the C--F--H--K sequence.

We can still get the list we want, it just takes more work. First, generate the complete list of interesting hashes:

tempfile1=$(mktemp)
git rev-list --no-merges TAGB ^TAGA > $tempfile1

(note that git log and git rev-list are almost the same command, and are built from the same source; the primary difference between them is that rev-list just prints the full hash IDs). Next, generate the complete list of hashes to exclude:

tempfile2=$(mktemp)
git rev-list --no-merges K ^B > $tempfile2

(How do you find B? Well, I wonder, how did you find K in the first place?—but once you have found K, and know that it is, say, L^2, B is the merge base between L^ and L^2, so you can find its ID with git merge-base and those two names.) We don't really need --no-merges in this example but in more complex examples we might want it and/or --first-parent as well. (Edit to add note: of course we never need --no-merges: this is an exclusion list and merges are already excluded.)

Now we want "all hashes found in tempfile 1, excluding any hashes that are in tempfile2" and the Unix toolset command that does this is comm. (The Linux variant requires that the inputs be sorted and actually checks for this, so that you need --nocheck-order to defeat that. The underlying algorithm that comm uses merely needs the files' lines to be in the same order, which will be the case. Alternatively, you could sort both files: sort -o $tempfile1 $tempfile1 for instance. But that is expensive and unnecessary.)

The comm utility reads file1 and file2, which should be sorted lexically, and produces three text columns as output: lines only in file1; lines only in file2; and lines in both files.

We need "lines only in file1", i.e., discard lines that appear in file2. That would be "text column 1", which would seem to need yet more post-processing, but fortunately:

-1      Suppress printing of column 1.

-2      Suppress printing of column 2.

-3      Suppress printing of column 3.

So we suppress columns 2 and 3:

comm -23 $tempfile1 $tempfile2

and this produces, on standard output, the complete list of SHA-1s we would like git log to show.

The final step is to hook this output to git log --oneline, which is easy enough:

git log --oneline --stdin --no-walk

The --no-walk prevents git log (or git rev-list) from doing a commit graph walk at all, which is what we want since we are feeding it a complete list of every commit to visit. The --stdin feeds in the revisions we got from comm.

All we need now is a little shell packaging to clean up the temporary files:

#! /bin/sh -e

# note the -e option -- this avoids the need for a lot of || exit
# clauses (to handle any early failures)

tempfile1=$(mktemp)
trap "rm -f $tempfile1" 0 1 2 3 15
tempfile2=$(mktemp)
trap "rm -f $tempfile1 $tempfile2" 0 1 2 3 15

git rev-list --no-merges TAGB ^TAGA > $tempfile1
git rev-list --no-merges K ^B > $tempfile2
comm -23 $tempfile1 $tempfile2 | git log --oneline --stdin --no-walk

(This is all untested, of course, and it omits the places where you would want to find, or get as arguments, the two tags and the merge commit L that you want one-side-of excluded, along with the side-number, 1 or 2. Fancy this up as needed.)

Upvotes: 3

usha
usha

Reputation: 29369

Try --first-parent

git log --oneline --no-merges --first-parent TAGB ^TAGA

Documentation here

--first-parent Follow only the first parent commit upon seeing a merge commit. This option can give a better overview when viewing the evolution of a particular topic branch, because merges into a topic branch tend to be only about adjusting to updated upstream from time to time, and this option allows you to ignore the individual commits brought in to your history by such a merge. Cannot be combined with --bisect.

Upvotes: 0

Related Questions