#!/usr/bin/perl
#
# TarDiff - Compare two tarballs and report differences
# Homepage: http://tardiff.coolprojects.org/
# Copyright (C) 2005 Josef Spillner <josef@coolprojects.org>
# Published under GNU GPL conditions

use strict;
use Text::Diff;
use File::Temp qw(tempdir);

my $VERSION = '0.1';

my ($tarball1, $tarball2);
my ($opt_list, $opt_modified, $opt_autoskip, $opt_stats);
my $tempdir = tempdir( CLEANUP => 1 );

$SIG{'__DIE__'} = 'cleanup';
$SIG{'TERM'} = 'cleanup';
$SIG{'INT'} = 'cleanup';

sub arguments{
	for my $i(0..$#ARGV){
		my $arg = $ARGV[$i];
		if(($arg eq "--help") or ($arg eq "-h")){
			print "TarDiff - Compare two tarballs and report differences\n";
			print "Call: tardiff [options] file1.tar file2.tar[.gz/.bz2]\n";
			print "\n";
			print "Options:\n";
			print "[-m / --modified] Report on all changed files, including those present in both tarballs\n";
			print "[-l / --list    ] List all files, even those not changed at all\n";
			print "[-a / --autoskip] Skip files which belong to the GNU autotools (for --modified)\n";
			print "[-s / --stats   ] Run statistics (diffstat) on all modified files (for --modified)\n";
			print "\n";
			print "[-v / --version ] Display tardiff version\n";
			print "[-h / --help    ] Display this help screen\n";
			print "\n";
			exit;
		}elsif(($arg eq "--version")or ($arg eq "-v")){
			print $VERSION, "\n";
			exit;
		}elsif(($arg eq "--modified")or ($arg eq "-m")){
			$opt_modified = 1;
		}elsif(($arg eq "--list") or ($arg eq "-l")){
			$opt_list = 1;
		}elsif(($arg eq "--autoskip") or ($arg eq "-a")){
			$opt_autoskip = 1;
		}elsif(($arg eq "--stats") or ($arg eq "-s")){
			$opt_stats = 1;
		}else{
			if(!$tarball1){
				$tarball1 = $arg;
			}elsif(!$tarball2){
				$tarball2 = $arg;
			}else{
				print "Too many arguments: $arg\n";
				exit 1;
			}
		}
	}

	if(not($tarball1 and $tarball2)){
		print "Missing arguments; see --help\n";
		exit 1;
	}
}

sub untar{
	my $tarball = shift(@_);

	my $flag = "";
	if($tarball =~ /\.t?gz$/){
		$flag = "-z";
	}elsif($tarball =~ /\.t?bz2$/){
		$flag = "-j";
	}

	open(TARLIST, '-|', qw(gnutar -C), $tempdir, $flag, qw(-xvf), $tarball)
	    or die "Can't call tar as expected: $!";
	local $/ = undef; # slurp mode
	my $list = <TARLIST> or die "Couldn't read from tar";
	close(TARLIST) or warn "tar exited with non-zero exit code";

	return $list;
}

sub analyzetar{
	my $filelist = shift(@_);
	my $filehash = shift(@_);
	my $tarball = shift(@_);

	my %files = %{$filehash};

	my ($uniquebase, $base, $remainder);
	my @remainders;

	foreach my $file(split(/\n/, $filelist)){
		($base, @remainders) = split(/\//, $file);
		$remainder = join("/", @remainders);
		if(!$uniquebase){
			$uniquebase = $base;
		}else{
			($base eq $uniquebase) or die "$tarball contains different base dirs: $base and $uniquebase";
		}
		if($files{$remainder}){
			$files{$remainder} = "__both";
		}else{
			$files{$remainder} = "$tarball";
		}
	}

	return ($uniquebase, %files);
}

sub comparefile{
	my $base1 = shift(@_);
	my $base2 = shift(@_);
	my $file = shift(@_);

	my $file1 = "$tempdir/$base1/$file";
	my $file2 = "$tempdir/$base2/$file";

	if(-d $file1 and -d $file2){
		return 0;
	}elsif(-f $file1 and -f $file2){
		my $diff = diff $file1, $file2, { STYLE => "OldStyle" };
		if($diff){
			if($opt_stats){
				my $plus = 0;
				my $minus = 0;
				foreach my $line(split(/\n/, $diff)){
					if($line =~ /^>/){
						$plus++;
					}elsif($line =~ /^</){
						$minus++;
					}
				}
				#return "$plus +/$minus -";
				my $len = 50 - length($file);
				return sprintf("%${len}s%9s%9s", "(", "$plus + /", "$minus -)");
			}else{
				return 1;
			}
		}
	}else{
		return 1;
	}

	return 0;
}

sub autofile{
	my $file = shift(@_);

	my @parts = split(/\//, $file);
	@parts = reverse(@parts);
	my $filename = @parts[0];

	if($file eq "missing"){return 1};
	if($file eq "aclocal.m4"){return 1};
	if($file eq "config.guess"){return 1};
	if($file eq "config.h.in"){return 1};
	if($file eq "config.sub"){return 1};
	if($file eq "configure"){return 1};
	if($file eq "depcomp"){return 1};
	if($file eq "install-sh"){return 1};
	if($file eq "ltmain.sh"){return 1};

	if($filename eq "Makefile.in"){return 1};

	return 0;
}

sub tardiff{
	my $error = 0;

	my $filelist1 = untar($tarball1) or die "Error: Could not unpack $tarball1.";
	my $filelist2 = untar($tarball2) or die "Error: Could not unpack $tarball2.";

	my %files;

	my ($base1, %files) = analyzetar($filelist1, \%files, $tarball1);
	my ($base2, %files) = analyzetar($filelist2, \%files, $tarball2);

	foreach my $file(sort(keys(%files))){
		next if $file eq "";
		my $base = $files{$file};
		if($base eq "__both"){
			next if $opt_autoskip and autofile($file);
			my $modified = 0;
			if($opt_modified){
				$modified = comparefile($base1, $base2, $file);
				if($modified){
					if($opt_stats){
						print "/ $file $modified\n";
					}else{
						print "/ $file\n";
					}
				}
			}
			if($opt_list and not $modified){
				print "  $file\n";
			}
		}elsif($base eq $tarball1){
			print "- $file\n";
		}elsif($base eq $tarball2){
			print "+ $file\n";
		}else{
			print "? $file\n";
		}
	}
}

sub cleanup{
	my $handler = shift(@_);

	if($handler eq "INT" or $handler eq "TERM"){
		exit 1;
	}
}

arguments();
tardiff();
cleanup();

