Search code examples
gitgit-log

How to invert `git log --grep=<pattern>` or How to show git logs that don't match a pattern


I want to use git log to show all commits that do not match a given pattern. I know I can use the following to show all commits that do match a pattern:

git log --grep=<pattern>

How do I invert the sense of matching?

I am trying to ignore commits that have "bumped to version ..." in the message.

EDIT: I want my final output to be pretty verbose. e.g. git log --pretty --stat. So output from git log --format=oneline won't work for me.


Solution

  • Generate a list of all commits, subtract those whose log messages contain the offending pattern, and feed the result to git log with your desired options. In the final stage, a couple of options to git log are handy:

    --stdin
    In addition to the commit listed on the command line, read them from the standard input.

    --no-walk
    Only show the given revs, but do not traverse their ancestors.

    You can do it with a single pipeline and process substitution.

    #! /bin/bash
    
    if (( $# < 1 )); then
      echo >&2 "Usage: $0 pattern [<since>..<until>]"
      exit 1
    fi
    
    pattern=$1
    shift
    
    git log --format=%H $@ |
      grep -v -f <(git log --format=%H "--grep=$pattern" $@) |
      git log --pretty --stat --stdin --no-walk
    

    If you don't want to use bash, you could do it with Perl.

    #! /usr/bin/env perl
    
    use strict;
    use warnings;
    no warnings "exec";
    
    sub usage { "Usage: $0 pattern\n" }
    
    sub commits_to_omit {
      my($pattern) = @_;
    
      open my $fh, "-|", "git", "log", "--grep=$pattern", "--format=%H", @ARGV
        or die "$0: exec: $!";
      my %omit = map +($_ => 1), <$fh>;
      %omit;
    }
    
    die usage unless @ARGV >= 1;
    my $pattern = shift;
    
    my %omit = commits_to_omit $pattern;
    
    open my $all, "-|", "git", "log", "--format=%H", @ARGV
      or die "$0: exec: $!";
    
    open my $out, "|-", "git", "log", "--pretty", "--stat", "--stdin", "--no-walk"
      or die "$0: exec: $!";
    
    while (<$all>) {
      print $out $_ unless $omit{$_};
    }
    

    Assuming one of the above is in your PATH as git-log-vgrep and with a history of the form

    $ git lola
    * b0f2a28 (tmp, feature1) D
    * 68f87b0 C
    * d311c65 B
    * a092126 A
    | * 83052e6 (HEAD, origin/master, master) Z
    | * 90c3d28 Y
    | * 4165a42 X
    | * 37844cb W
    |/  
    * f8ba9ea V

    we could say

    $ git log-vgrep X

    to get Z, Y, W, and V.

    You can also log other branches, so

    $ git log-vgrep A tmp

    gives D, C, B, and V; and

    $ git log-vgrep C tmp~2..tmp

    yields just D.

    One limitation of the above implementations is if you use a pattern that matches all commits, e.g., . or ^, then you'll get HEAD. This is how git log works:

    $ git log --stdin --no-walk --pretty=oneline </dev/null
    83052e62f0dc1c6ddfc1aff3463504a4bf23e3c4 Z