#!/afs/athena/contrib/perl/perl
# makepatch.pl -- generate batch of patches.
# SCCS Status     : @(#)@ makepatch.pl	1.4
# Author          : Johan Vromans
# Created On      : Tue Jul  7 20:39:39 1992
# Last Modified By: Johan Vromans
# Last Modified On: Sat Dec 19 14:27:57 1992
# Update Count    : 82
# Status          : Released to USEnet.

# Generate a patch from two files or directories.
#
# Resembles "diff -c -r -N", but:
#
#   - always recursive
#   - handles 'patchlevel.h' first
#   - supplies 'Index:' and 'Prereq:' lines
#   - can use manifest file
#   - generates shell commands to remove files

$opt_diff = "diff -c";
&options if $ARGV[0] =~ /^-/;
&usage unless @ARGV == 2;

$tmpfile = $ENV{"TMPDIR"} || "/usr/tmp";
$thepatch = "$tmpfile/mp$$.p";
$tmpfile .= "/mp$$.t";
open (PATCH, ">$thepatch") || die ("$thepatch: $!\n");

&doit (@ARGV);
&wrapup;
exit (0);

################ Subroutines ################

sub doit {
    local ($old, $new) = @_;

    if ( -f $old && -f $new ) {
	# Two files.
	&dodiff ("", $old, "", $new);
    }
    elsif ( -f $old && -d $new ) {
	# File and dir -> File and dir/File.
	$new = ( $new =~ m|^\./?$| ) ? "" : "$new/";
	&dodiff ("", $old, $new, $old);
    }
    elsif ( -f $new && -d $old ) {
	$old = ( $old =~ m|^\./?$| ) ? "" : "$old/";
	&dodiff ($old, $new, "", $new);
    }
    else {
	# Should be two directories.
	local (@old, @new);
	@old = &make_filelist ($old);
	if ( defined $opt_manifest ) {
	    local ($/);
	    open (MAN, $opt_manifest) || die ("$opt_manifest: $!\n");
	    @new = sort split (/\n/, <MAN>);
	    close (MAN);
	}
	else {
	    @new = &make_filelist ($new);
	}

	$new = ( $new =~ m|^\./?$| ) ? "" : "$new/";
	$old = ( $old =~ m|^\./?$| ) ? "" : "$old/";

	if ( $opt_debug ) {
	    print STDERR ("Old: @old\nNew: @new\n");
	}

	# Handle patchlevel file first.
	$opt_patchlevel = (grep (/patchlevel\.h/, @new))[0]
	    unless defined $opt_patchlevel;

	if ( defined $opt_patchlevel && $opt_patchlevel ne "" ) {
	    if ( ! -f "$new$opt_patchlevel" ) {
		die ("$new$opt_patchlevel: $!\n");
	    }
	    if ( -f "$old$opt_patchlevel" ) {
		&dodiff ($old, $opt_patchlevel, $new, $opt_patchlevel);
	    }
	    else {
		&dodiff ("", "/dev/null", $new, $opt_patchlevel);
	    }
	}
	else {
	    undef $opt_patchlevel;
	}

	# Process the filelists.
	while ( @old + @new ) {

	    $o = shift (@old) unless defined $o;
	    $n = shift (@new) unless defined $n;
	    
	    if ( !defined $o || $o gt $n ) {
		# New file.
		if ( defined $opt_patchlevel && $n eq $opt_patchlevel ) {
		    undef $opt_patchlevel;
		}
		else {
		    &dodiff ("", "/dev/null", $new, $n);
		}
		undef $n;
	    }
	    elsif ( !defined $n || $o lt $n ) {
		# Obsolete (removed) file.
		push (@goners, $o);
		undef $o;
	    }
	    elsif ( $o eq $n ) {
		# Same file.
		if ( defined $opt_patchlevel && $n eq $opt_patchlevel ) {
		    undef $opt_patchlevel;
		}
		else {
		    &dodiff ($old, $o, $new, $n);
		}
		undef $n;
		undef $o;
	    }
	}
    }
}

sub make_filelist {
    local ($dir, $disp) = @_;

    # Return a list of files, sorted, for this directory.
    # Recurses.

    local (*DIR);
    local (@tmp, @ret);
    local ($fname);

    $disp = "" unless defined $disp;

    print STDERR ("+ recurse $dir\n") if $opt_trace;
    opendir (DIR, $dir) || die ("$dir: $!\n");
    @tmp = sort (readdir (DIR));
    closedir (DIR);
    print STDERR ("Dir $dir: ", 0+@tmp, " entries\n") if $opt_debug;

    @ret = ();
    foreach $file ( @tmp ) {

	# Skip unwanted files.
	next if $file =~ /^\.\.?$/; # dot and dotdot
	next if $file =~ /~$/;	# editor backup files

	# Push on the list.
	$fname = "$dir/$file";
	if ( -d $fname ) {
	    # Recurse.
	    push (@ret, &make_filelist ($fname, "$disp$file/"));
	}
	elsif ( -f _ ) {
	    push (@ret, $disp . $file);
	}
	else {
	    print STDERR ("Ignored $fname: not a file\n");
	}
    }
    @ret;
}

sub dodiff {
    local ($olddir, $old, $newdir, $new) = @_;

    # Produce a patch hunk.

    local ($cmd) = "$opt_diff $olddir$old $newdir$new";
    print STDERR ("+ ", $cmd, "\n") if $opt_trace;
    $result = system ("$cmd > $tmpfile");
    printf STDERR ("+> result = 0x%x\n", $result) 
	if $result && $opt_debug;

    return unless $result == 0x100;	# no diffs

    # print PATCH ($cmd, "\n");
    print PATCH ("Index: ", $new, "\n");

    # Try to find a prereq.
    open (OLD, $olddir . $old);
    while ( <OLD> ) {
	next unless /\@\(\#\)/;		     # SCCS header
	next unless $' =~ /\b\d+(\.\d+)*\b/; # e.g. 5.4
	print PATCH ("Prereq: $&\n");
	last;
    }
    close (OLD);

    # Copy patch.
    open (TMP, $tmpfile);
    print PATCH <TMP>;
    close (TMP);
}

sub wrapup {
    if ( @goners ) {
	print STDOUT 
	    ("# Please remove the following file",
	     @goners == 1 ? "" : "s", " before applying this patch.\n",
	     "# (You can feed this patch to 'sh' to do so.)\n",
	     "\n");
	foreach ( @goners ) {
	    print STDOUT ("rm -f ", $_, "\n");
	}
	print STDOUT ("exit\n\n");
    }

    # Copy patch.
    open (PATCH, $thepatch);
    print while <PATCH>;
    close (PATCH);

    # Cleanup.
    unlink ($tmpfile, $thepatch);
}

sub options {
    require "newgetopt.pl";
    if ( ! &NGetOpt ("patchlevel=s", "diff=s", "manifest=s",
		     "help", "debug", "trace")
	|| defined $opt_help ) {
	&usage;
    }
    $opt_trace = 1 if defined $opt_debug;
}

sub usage {
    print STDERR <<EoU;
This is makepatch.pl version 1.4

Usage: $0 [options] old new

Options:

   -diff cmd		diff command to use, default \"$opt_diff\"
   -patchlevel file	file to use as patchlevel.h
   -manifest file	list of files for new dir
   -help		this message

EoU
    exit (1);
}
