#!/usr/bin/env perl

# gitudiff
#
#   This script extracts a unified-diff between two git SHAs
#   See
#      $ gitdiff --help
#

use Getopt::Long;
use strict;
use Cwd qw(realpath);

my $verbose = 0;

sub include_me
{
    my ($path, $includes, $excludes) = @_;

    foreach my $pat (@$excludes) {
        if ($path =~ /$pat/) {
            if ($verbose) {
                print(STDERR "exclude '$path', pattern '$pat'\n");
            }
            return 0;
        }
    }
    return 1
        if 0 == scalar(@$includes);

    foreach my $pat (@$includes) {
        if ($path =~ /$pat/) {
            if ($verbose) {
                print(STDERR "include '$path', pattern '$pat'\n");
            }
            return 1;
        }
    }
    if ($verbose > 1) {
        print(STDERR "exclude '$path': no match\n");
    }
    return 0;
}

my @exclude_patterns;
my @include_patterns;
my $suppress_unchanged;
my $prefix = '';
my $ignore_whitespace;
my $repo = '.';    # cwd
my $help;

if (!GetOptions("exclude=s"    => \@exclude_patterns,
                "include=s"    => \@include_patterns,
                'no-unchanged' => \$suppress_unchanged,
                'prefix=s'     => \$prefix,
                'b|blank'      => \$ignore_whitespace,
                'repo=s'       => \$repo,
                'verbose+'     => \$verbose,
                'help'         => \$help) ||
    $help ||
    (2 != scalar(@ARGV) &&
        3 != scalar(@ARGV))
) {
    print(STDERR<<EOF
Usage:
  $0 \\
      [(--exclude|include) regexp[,regexp]] [--repo repo_directory] \\
      [-b] [dir] base_SHA current_SHA

 --repo repo_directory:
          your git repo directory.  Default is '.'.
 --prefix:
          leading path to strip from file pathnames in the diff report
 --no-unchanged:
          In order to eliminate potential ambiguities caused by multiple
          source files with the same basename, by default, we include
          unchanged file references in the diff report.
          This option removes those entries.
   -b|--blank:
          ignore whitespace changes.
 --include regexp:
          regexp compared to the Perforce path (i.e., starting in
          //p4_repo/...).  If 'include' regexps are specified, then only
          Matching references are included in the report; if no
          'include' regexps are specified, then entries are included unless
          they have been excluded.
 --exclude regexp:
          regexp compared to the Perforce path (i.e., starting in
          //p4_repo/...).  Matching entries are removed from the report.
         'exclude' wins if both exclude and include would match.
EOF
    );
    exit(1);
}

@exclude_patterns  = split(',', join(',', @exclude_patterns));
@include_patterns  = split(',', join(',', @include_patterns));
$ignore_whitespace = '-b' if $ignore_whitespace;

if ($verbose) {
    print(STDERR join(' ', @ARGV));
    if (scalar(@exclude_patterns)) {
        print(STDERR " --exclude " . join(" --exclude ", @exclude_patterns));
    }
    if (scalar(@include_patterns)) {
        print(STDERR " --include " . join(" --include ", @include_patterns));
    }
    print(STDERR "\n");
}

$prefix .= '/' if $prefix;
if (3 == scalar(@ARGV)) {
    my $dir = shift @ARGV;
    push(@include_patterns, "$dir");
}
my $base_sha    = shift @ARGV;
my $current_sha = shift @ARGV;

$repo = Cwd::realpath($repo);

my $cmd = "cd $repo ; git diff $ignore_whitespace $base_sha $current_sha";

open(HANDLE, "-|", $cmd) or
    die("failed to exec git diff: $!");

my $includeCurrentFile;
my %allFiles;

while (<HANDLE>) {
    chomp($_);
    s/\r//g;
    my $line = $_;

    if ($line =~ /(^diff|\+\+\+|---) /) {
        # remove the a/b leader from the
        $line =~ s# [ab]/# $prefix#g;
    }

    if ($line =~ /^diff --git (\S+) (\S+)/) {
        # git diff header
        my $fileA = $1;
        my $fileB = $2;
        $includeCurrentFile =
            (include_me($fileA, \@include_patterns, \@exclude_patterns) ||
             include_me($fileB, \@include_patterns, \@exclude_patterns));
        if ($includeCurrentFile) {
            $allFiles{$fileB} = $fileA;
            $line =~ s/($fileA|$fileB)/$1/g;
        }
    }

    printf("%s\n", $line)
        if $includeCurrentFile;
}
close(HANDLE) or die("failed to close git diff pipe: $!");
if (0 != $?) {
    $? & 0x7F && die("git diff died from signal ", ($? & 0x7F), "\n");
    die("git diff exited with error ", ($? >> 8), "\n");
}

exit 0 if defined($suppress_unchanged);

# now find the list of files in the current SHA - we can process that
#  to find the list of all current files which were unchanged since the
#  baseline SHA
die("failed to exec git ls-tree")
    unless (
      open(HANDLE, "-|", "cd $repo ; git ls-tree -r --name-only $current_sha"));

while (<HANDLE>) {
    chomp($_);
    s/\r//g;
    my $filename = $repo . '/' . $_;
    my $path     = $prefix . $filename;
    if (!exists($allFiles{$_}) &&
        include_me($path, \@include_patterns, \@exclude_patterns)) {
        printf("diff --git $prefix$filename $prefix$filename\n");
        printf("=== $path\n");
    }
}
close(HANDLE) or die("failed to close git ls-tree pipe: $!");
if (0 != $?) {
    $? & 0x7F && die("git ls-tree died from signal ", ($? & 0x7F), "\n");
    die("git ls-tree exited with error ", ($? >> 8), "\n");
}
exit 0;
