#!/usr/bin/perl
# makepatch.pl -- generate batch of patches.
our $RCS_Id = '$Id: makepatch,v 1.12 2012/07/08 00:48:47 tom Exp $ ';
# Author          : Johan Vromans
# Created On      : Tue Jul  7 20:39:39 1992
# Last Modified By: Johan Vromans
# Last Modified On: Sun Oct 29 12:13:52 1995
# Update Count    : 136
# 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
#   - manipulates manifest files
#
################################################################
#
# Usage:
#
#   makepatch <old-dir> <new-dir
#
#     This will compare all files in <new-dir> against the files in
#     <old-dir>, and generate a bunch of patches to transform every
#     file in <old-dir> into the corresponding one in <new-dir>.
#     Files that appear in <new-dir> but not in <old-dir> are created.
#     For files that appear in <old-dir> but not in <new-dir>
#     'rm'-commands are generated at the beginning of the patch.
#
# Using MANIFEST files:
#
#   makepatch -oldmanifest <oldmanifest> -newmanifest <newmanifest> \
#           <new-dir> <old-dir>
#
#     <oldmanifest> and <newmanifest> list the files in <old-dir>
#     and <new-dir> that are to be examined.
#     Only the files that are named will be examined.
#     <oldmanifest> should contain the names of the files relative to
#     <old-dir> and <newmanifest> should contain the names of the files
#     relative to <new-dir>.
#
#   makepatch -manifest <manifest> <new-dir> <old-dir>
#
#     This is a simplified form of the above example.
#     <manifest> applies to both <old-dir> and <new-dir>.
#
#   makepatch -filelist [ -prefix xxx ] manifest
#
#     The filenames are extracted from the manifest file,
#     optionally prefixed, sorted and written to standard output.
#
# Examples:
#
#   % makepatch -verbose emacs-18.58 emacs-18.59 > emacs-18.58-18.59.diff
#
#   % (cd emacs-18.58; find . -type f -print > MANIFEST)
#   % (cd emacs-18.59; find . -type f -print > MANIFEST)
#   % makepatch -verbose \
#         -oldmanifest emacs-18.58/MANIFEST \
#         -newmanifest emacs-18.59/MANIFEST \
#         emacs-18.58 emacs-18.59 > emacs-18.58-18.59.diff
#
#   % makepatch -filelist -prefix emacs-18.59/ emacs-18.59/MANIFEST |
#	gtar -zcvf emacs-18.59.tar.Z -T -
#
################################################################

################ Common stuff ################

use strict;
use Getopt::Long;

# $LIBDIR = $ENV{'LIBDIR'} || '/usr/local/lib/sample';
# unshift (@INC, $LIBDIR);
# require 'common.pl';
our $my_package = 'Sciurix';
our ($my_name, $my_version) = $RCS_Id =~ /: (.+),v ([\d.]+)/;
$my_version .= '*' if length('$Locker:  $ ') > 12;

our $opt_diff;
our $opt_debug;
our $opt_filelist;
our $opt_follow;
our $opt_help;
our $opt_manifest;
our $opt_newmanifest;
our $opt_nosort;
our $opt_oldmanifest;
our $opt_patchlevel;
our $opt_prefix;
our $opt_quiet;
our $opt_trace;
our $opt_verbose;

our @goners;

################ Program parameters ################

&stdio;
&options;
our ($old, $new) = @ARGV;

print STDERR ("This is $my_name version $my_version\n") if $opt_verbose;

if ( defined $opt_filelist ) {
    my @new = &domanifest (shift (@ARGV));
    foreach ( @new ) {
	print STDOUT ($opt_prefix, $_, "\n");
    }
    exit (0);
}

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

&doit ($old, $new);
&wrapup;
exit (0);

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

sub doit {
    my ($old, $new) = @_;
    my $o;
    my $n;

    if ( -f $old && -f $new ) {
	# Two files.
	if ( $opt_verbose ) {
	    print STDERR ("Old file = $old.\n",
			  "New file = $new.\n");
	}
	&dodiff ("", $old, "", $new);
    }
    elsif ( -f $old && -d $new ) {
	# File and dir -> File and dir/File.
	$new = ( $new =~ m|^\./?$| ) ? "" : "$new/";
	if ( $opt_verbose ) {
	    print STDERR ("Old file = $old.\n",
			  "New file = $new$old.\n");
	}
	&dodiff ("", $old, $new, $old);
    }
    elsif ( -f $new && -d $old ) {
	$old = ( $old =~ m|^\./?$| ) ? "" : "$old/";
	if ( $opt_verbose ) {
	    print STDERR ("Old file = $old$new.\n",
			  "New file = $new.\n");
	}
	&dodiff ($old, $new, "", $new);
    }
    else {
	# Should be two directories.
	my (@old, @new);
	if ( defined $opt_oldmanifest ) {
	    @old = &domanifest ($opt_oldmanifest);
	}
	else {
	    @old = &make_filelist ($old);
	}
	if ( defined $opt_newmanifest ) {
	    @new = &domanifest ($opt_newmanifest);
	}
	else {
	    @new = &make_filelist ($new);
	}

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

	@new = sort @new;
	@old = sort @old;

	if ( $opt_verbose ) {
	    my ($old) = $old; chop ($old);
	    my ($new) = $new; chop ($new);
	    print STDERR ("Old dir = $old, file list = ",
			  defined $opt_oldmanifest ? $opt_oldmanifest : "<*>",
			  ", ", 0+@old, " files.\n");
	    print STDERR ("New dir = $new, file list = ",
			  defined $opt_newmanifest ? $opt_newmanifest : "<*>",
			  ", ", 0+@new, " files.\n");
	}
	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 {
		$created++;
		&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 $n && (!defined $o || $o gt $n) ) {
		# New file.
		if ( defined $opt_patchlevel && $n eq $opt_patchlevel ) {
		    undef $opt_patchlevel;
		}
		else {
		    $created++;
		    &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 {
    my ($dir, $disp) = @_;

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

    my (@ret);
    my (@tmp);
    my ($fname);
    my ($file);

    $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 && ( $opt_follow || ! -l $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 domanifest {
    my ($man) = @_;
    my (@ret) = ();

    open (MAN, $man) || die ("$man: $!\n");
    while ( <MAN> ) {
	if ( $. == 2 && /^[-=_\s]*$/ ) {
	    @ret = ();
	    next;
	}
	next if /^#/;
	next unless /\S/;
	$_ = $` if /\s/;
	push (@ret, $_);
    }
    close (MAN);
    @ret = sort @ret unless defined $opt_nosort;
    @ret;
}

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

    # Produce a patch hunk.

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

    if ( $result && $result < 128 ) {
	&wrapup (($result == 2 || $result == 3)
		 ? "User request" : "System error");
	exit (1);
    }
    return unless $result == 0x100;	# no diffs
    $patched++;

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

    # Try to find a prereq.
    # The RCS code is based on a suggestion by jima@netcom.com, who also
    # pointed out that patch requires blanks around the prereq string.
    open (OLD, $olddir . $old);
    while ( <OLD> ) {
	next unless (/\@\(\#\)/		# SCCS header
		     || /\$Header:/ 	# RCS Header
		     || /\$Id:/); 	# RCS Header
	next unless $' =~ /\s\d+(\.\d+)*\s/; # e.g. 5.4
	print PATCH ("Prereq: $&\n");
	last;
    }
    close (OLD);

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

sub wrapup {
    my ($reason) = @_;

    if ( defined $reason ) {
	print STDERR ("*** Aborted: $reason ***\n");
    }
    if ( $opt_verbose ) {
	my ($goners) = scalar (@goners);
	print STDERR ("Collecting: $patched patch",
		      $patched == 1 ? "" : "es");
	print STDERR (" ($created new file",
		      $created == 1 ? "" : "s", ")") if $created;
	print STDERR (", $goners goner",
		      $goners == 1 ? "" : "s") if $goners;
	print STDERR (".\n");
    }
    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 stdio {
    # Since output to STDERR seems to be character based (and slow),
    # we connect STDERR to STDOUT if they both point to the terminal.
    if ( -t STDOUT && -t STDERR ) {
	close (STDERR);
	open (STDERR, '>&STDOUT');
	select (STDERR); $| = 1;
	select (STDOUT);
    }
}

sub options {
    # Defaults...
    $opt_diff = "diff -c";
    $opt_verbose = 1;
    $opt_follow = 0;

    # Process options, if any...
    if ( $ARGV[0] =~ /^-/ ) {

	# Aliases.
	*opt_man = *opt_manifest;
	*opt_oldman = *opt_oldmanifest;
	*opt_newman = *opt_newmanifest;
	*opt_v = *opt_verbose;
	*opt_list = *opt_filelist;

	if ( ! &GetOptions ("patchlevel=s", "diff=s",
			 "manifest=s", "newmanifest=s", "oldmanifest=s",
			 "man=s", "newman=s", "oldman=s", "follow",
			 "list", "filelist", "prefix=s", "nosort",
			 "quiet", "verbose", "v", "help", "debug", "trace")
	    || defined $opt_help ) {
	    &usage;
	}
	$opt_trace = 1 if defined $opt_debug;
	$opt_verbose = 0 if defined $opt_quiet;
	if ( defined $opt_prefix ) {
	    die ("$0: option \"-prefix\" requires \"-filelist\"\n")
		unless defined $opt_filelist;
	}
	if ( defined $opt_nosort ) {
	    die ("$0: option \"-nosort\" requires \"-filelist\"\n")
		unless defined $opt_filelist;
	}
	if ( defined $opt_filelist ) {
	    die ("$0: option \"-filelist\" only uses \"-manifest\"\n")
		if defined $opt_oldmanifest || defined $opt_newmanifest;
	}
	if ( defined $opt_manifest ) {
	    die ("$0: do not use \"-manifest\" with \"-oldmanifest\"".
		 " or \"-newmanifest\"\n")
		if defined $opt_newmanifest || defined $opt_oldmanifest;
	    $opt_newmanifest = $opt_oldmanifest = $opt_manifest;
	}
    }

    # Argument check.
    if ( defined $opt_filelist ) {
	if ( defined $opt_manifest ) {
	    &usage if @ARGV;
	    @ARGV = ( $opt_manifest );
	}
	else {
	    &usage unless @ARGV == 1;
	}
    }
    else {
	&usage unless @ARGV == 2;
    }
}

sub usage {
    print STDERR <<EoU;
This is $my_name version $my_version

Usage: $0 [options] old new
Usage: $0 -filelist [ -prefix XXX ] [ -nosort ] [ -manifest ] file

Makepatch options:
   -diff cmd		diff command to use, default \"$opt_diff\"
   -patchlevel file	file to use as patchlevel.h
   -man[ifest] file	list of files for old and new dir
   -newman[ifest] file	list of files for new dir
   -oldman[ifest] file	list of files for old dir
   -follow		follow symbolic links
Filelist options:
   -[file]list		extract filenames from manifest file
   -prefix XXX		add a prefix to these filenames
   -nosort		do not sort manifest entries
General options:
   -verbose		verbose output (default)
   -quiet		no verbose output
   -help		this message
EoU
    exit (1);
}
