#!/bin/sh
# 
# mkpatch.sh - prepare patches from two source trees
################################################################
# Copyright (C) 2001, 2002 Tom Lord
# 
# See the file "COPYING" for further information about
# the copyright and warranty status of this work.
# 

set -e 

command_line="$*"

################################################################
# special options
# 
# Some options are special:
# 
#	--version | -V
#	--help | -h
# 
if test $# -ne 0 ; then

  for opt in "$@" ; do
    case $opt in

      --version|-V) exec larch --version
                    ;;


      --help|-h)
	        printf "compare two source trees and create a patch tree\\n"
		printf "usage: mkpatch [options] original modified destination\\n"
		printf "\\n"
		printf " -V --version                  print version info\\n"
		printf " -h --help                     display help\\n"
		printf "\\n"
		printf " --silent                      no output (except odd errors)\\n"
		printf " --quiet                       brief output\\n"
		printf " --report                      default output\\n"
		printf " --verbose                     maximal output\\n"
		printf " --debug                       debugging output\\n"
		printf "\\n"
		printf " --explicit                    force the explicit tagging method\\n"
		printf " --implicit                    force the implicit tagging method\\n"
		printf " --names                       force the name-based tagging method\\n"
		printf "\\n"
		printf "Create the output directory DESTINATION (it must not already\\n"
		printf "exist).\\n"
		printf "\\n"
		printf "Compare source trees ORIGINAL and MODIFIED.  Create a patch\\n"
		printf "tree in DESTINATION\\n"
		printf "\\n"
		printf "See also \"dopatch --help\".\\n"
		printf "\\n"
		exit 0
      		;;

      *)
		;;
    esac
  done
fi

################################################################
# Ordinary Options
# 

quiet=--quiet
report=--report
verbose=


method=

while test "$#" -gt 0 ; do

  case "$1" in

    --explicit)		shift
  			method=--explicit
			;;

    --implicit)		shift
			method=--implicit
			;;

    --names)		shift	
	    		method=--names
			;;

    --silent)	shift
    		quiet=
		report=
		verbose=
		;;

    --quiet)	shift
    		quiet=--quiet
		report=
		verbose=
		;;

    --report)	shift
    		quiet=--quiet
		report=--report
		verbose=
		;;

    --verbose)	shift
    		quiet=--quiet
		report=--report
		verbose=--verbose
		;;

    --debug)	shift
    		larch heading "mkpatch: debugging output enabled\\n"
    		set -x
		;;

    --)		shift
    		break
		;;

    -*)		printf "mkpatch: unrecognized option (%s)\\n" "$1" 1>&2
    		printf "\\n" 1>&2
		printf "Try \"larch mkpatch --help\"\\n" 1>&2
		exit 1
		;;

    *)		break
		;;

  esac


done


################################################################
# Ordinary Arguments
# 

if test $# != 3 ; then
  printf "usage: mkpatch [options] original modified destination\\n" 1>&2
  printf "try --help\\n"  
  exit 1
fi

original="$1"
modified="$2"
destination="$3"

here="`pwd`"

cd "$original"
original="`pwd`"

cd "$here"
cd "$modified"
modified="`pwd`"

################################################################
# Sanity Check and Process Defaults
# 

destination_parent="`dirname \"$destination\"`"
destination_base="`basename \"$destination\"`"
cd "$here"
cd "$destination_parent"
destination_parent="`pwd`"
destination="`pwd`/$destination_base"

################################################################
# Greetings
# 

if test ! -z "$quiet" ; then 
  larch heading "mkpatch\\n"
  printf "arguments: %s\\n" "$command_line" | fold -w 60 | larch body-indent
  larch heading --sub "mkpatch start time: %s\\n" "`date`"
  if test ! -z "$report" ; then 
    larch heading --sub "original tree: %s\\n" "$original"
    larch heading --sub "modified tree: %s\\n" "$modified"
  fi
fi


################################################################
# Create the output directory.  It must not already exist.
# 

bail()
{
  cd "$destination_parent"
  rm -rf "$destination"
  exit 1
}

trap "printf \"mkpatch: interrupted -- cleaning up\\n\" 1>&2 ; bail" INT
mkdir "$destination"

################################################################
# Here is the List of Files Generated During mkpatch 
# 
# Files whose names begin with ,, are later deleted.
# 

orig_full_index="$destination/,,orig-full-index"
mod_full_index="$destination/,,mod-full-index"

orig_dirs_full_index="$destination/,,orig-dirs-full-index"
mod_dirs_full_index="$destination/,,mod-dirs-full-index"
orig_files_full_index="$destination/,,orig-files-full-index"
mod_files_full_index="$destination/,,mod-files-full-index"
orig_dirs_full_index_by_name="$destination/,,orig-dirs-full-index-by-name"
mod_dirs_full_index_by_name="$destination/,,mod-dirs-full-index-by-name"
orig_files_full_index_by_name="$destination/,,orig-files-full-index-by-name"
mod_files_full_index_by_name="$destination/,,mod-files-full-index-by-name"

orig_dirs_index="$destination/orig-dirs-index"
mod_dirs_index="$destination/mod-dirs-index"
orig_files_index="$destination/orig-files-index"
mod_files_index="$destination/mod-files-index"

orig_dir_ids="$destination/,,orig-dir-ids"
mod_dir_ids="$destination/,,orig-file-ids"

original_only_dir_ids="$destination/,,original-only-dir-ids"
modified_only_dir_ids="$destination/,,modified-only-dir-ids"
dir_ids_in_both="$destination/,,dir-ids-in-both"

original_only_dirs="$destination/,,original-only-dirs"
modified_only_dirs="$destination/,,modified-only-dirs"

orig_file_ids="$destination/,,orig-file-ids"
mod_file_ids="$destination/,,mod-file-ids"

original_only_file_ids="$destination/,,original-only-file-ids"
modified_only_file_ids="$destination/,,modified-only-file-ids"
file_ids_in_both="$destination/,,file-ids-in-both"

original_only_files="$destination/,,original-only-files"
modified_only_files="$destination/,,modified-only-files"

files_in_both="$destination/,,files-in-both"
dirs_in_both="$destination/,,dirs-in-both"

removed_files_archive="$destination/removed-files-archive"
new_files_archive="$destination/new-files-archive"
patches_dir="$destination/patches"

changed_files="$destination/,,changed-files"
changed_dirs="$destination/,,changed-dirs"

renamed_files="$destination/,,renamed-files"
renamed_dirs="$destination/,,renamed-dirs"



if test ! -z "$verbose" ; then
  larch heading --sub "taking inventory of original tree directories\\n"
  larch heading --sub --sub "start time: %s\\n" "`date`"
fi


################################################################
# Make complete file and directory lists
# 

cd "$original" 
larch inventory $method --source --all --both --kind --tags | sort -k 3 > "$orig_full_index"

cd "$modified"
larch inventory $method --source --all --both --kind --tags | sort -k 3 > "$mod_full_index"


################################################################
# Make lists of directories
# 

cd "$original" 

grep -e "^d" "$orig_full_index" | awk '{ print $2 "\t" $3 }' | tee "$orig_dirs_full_index" | sort -k 1 > "$orig_dirs_full_index_by_name"

if test ! -z "$verbose" ; then
  larch heading --sub --sub "finish time: %s\\n" "`date`"
  larch heading --sub "taking inventory of modified tree directories\\n"
  larch heading --sub --sub "start time: %s\\n" "`date`"
fi

cd "$modified" 

grep -e "^d" "$mod_full_index" | awk '{ print $2 "\t" $3 }' | tee "$mod_dirs_full_index" | sort -k 1 > "$mod_dirs_full_index_by_name"



if test ! -z "$verbose" ; then
  larch heading --sub --sub "finish time: %s\\n" "`date`"
fi


cd "$destination"

cut -s -f 2 "$orig_dirs_full_index" > "$orig_dir_ids"
cut -s -f 2 "$mod_dirs_full_index" > "$mod_dir_ids"

comm -2 -3 "$orig_dir_ids" "$mod_dir_ids" >> "$original_only_dir_ids"
comm -1 -3 "$orig_dir_ids" "$mod_dir_ids" >> "$modified_only_dir_ids"
comm -1 -2 "$orig_dir_ids" "$mod_dir_ids" >> "$dir_ids_in_both"

join -o 2.1 -2 2 "$original_only_dir_ids" "$orig_dirs_full_index" | sort > "$original_only_dirs"
join -o 2.1 -2 2 "$modified_only_dir_ids" "$mod_dirs_full_index" | sort > "$modified_only_dirs"

awk -f "$ARCH_SUBCMD_ROOT/patch-sets/compute-renamed.awk" \
       "$orig_dirs_full_index" "$mod_dirs_full_index" \
       "$orig_dirs_full_index" "$mod_dirs_full_index" \
> "$renamed_dirs"

if test ! -z "$report" -a ! -z "`cat \"$original_only_dirs\" | head -1`" ; then
  larch heading --sub "removed directories\\n"
  cat "$original_only_dirs" | larch body-indent --sub
fi

if test ! -z "$report" -a ! -z "`cat \"$modified_only_dirs\" | head -1`" ; then
  larch heading --sub "added directories\\n"
  cat "$modified_only_dirs" | larch body-indent --sub
fi

if test ! -z "$report" -a ! -z "`cat \"$renamed_dirs\" | head -1`" ; then
  larch heading --sub "renamed directories\\n"
  cat "$renamed_dirs" \
  | awk '{
	   print $1
	   print "=> " $2
	   print ""
	 }' \
  | larch body-indent --sub
fi


################################################################
# Make lists of files
# 
if test ! -z "$verbose" ; then
  larch heading --sub "taking inventory of original tree files\\n"
  larch heading --sub --sub "start time: %s\\n" "`date`"
fi

cd "$original"
grep -e "^[^d]" "$orig_full_index" | awk '{ print $2 "\t" $3 }' | tee "$orig_files_full_index" | sort -k 1 >  "$orig_files_full_index_by_name"

if test ! -z "$verbose" ; then
  larch heading --sub --sub "finish time: %s\\n" "`date`"
  larch heading --sub "taking inventory of modified tree files\\n"
  larch heading --sub --sub "start time: %s\\n" "`date`"
fi

cd "$modified"
grep -e "^[^d]" "$mod_full_index" | awk '{ print $2 "\t" $3 }' | tee "$mod_files_full_index" | sort -k 1 >  "$mod_files_full_index_by_name"

if test ! -z "$verbose" ; then
  larch heading --sub --sub "finish time: %s\\n" "`date`"
fi


cd "$destination"

cut -s -f 2 "$orig_files_full_index" > "$orig_file_ids"
cut -s -f 2 "$mod_files_full_index" > "$mod_file_ids"

comm -2 -3 "$orig_file_ids" "$mod_file_ids" >> "$original_only_file_ids"
comm -1 -3 "$orig_file_ids" "$mod_file_ids" >> "$modified_only_file_ids"
comm -1 -2 "$orig_file_ids" "$mod_file_ids" >> "$file_ids_in_both"

join -o 2.1 -2 2 "$original_only_file_ids" "$orig_files_full_index" | sort > "$original_only_files"
join -o 2.1 -2 2 "$modified_only_file_ids" "$mod_files_full_index" | sort > "$modified_only_files"

awk -f "$ARCH_SUBCMD_ROOT/patch-sets/compute-renamed.awk" \
       "$orig_dirs_full_index" "$mod_dirs_full_index" \
       "$orig_files_full_index" "$mod_files_full_index" \
> "$renamed_files"

if test ! -z "$report" -a ! -z "`cat \"$original_only_files\" | head -1`" ; then
  larch heading --sub "removed files\\n"
  cat "$original_only_files" | larch body-indent --sub
fi

if test ! -z "$report" -a ! -z "`cat \"$modified_only_files\" | head -1`" ; then
  larch heading --sub "added files\\n"
  cat "$modified_only_files" | larch body-indent --sub
fi

if test ! -z "$report" -a ! -z "`cat \"$renamed_files\" | head -1`" ; then
  larch heading --sub "renamed files\\n"
  cat "$renamed_files" \
  | awk '{
	   print $1
	   print "=> " $2
	   print ""
	 }' \
  | larch body-indent --sub
fi

# files-in-both is <old-name> <new-name>
# 
join -o 1.1,2.1 -2 2 "$file_ids_in_both" "$mod_files_full_index" \
| join -o 2.1,1.2 -1 1 -2 2 - "$orig_files_full_index" \
| sort -k 1 \
> "$files_in_both"

join -o 1.1,2.1 -2 2 "$dir_ids_in_both" "$mod_dirs_full_index" \
| join -o 2.1,1.2 -1 1 -2 2 - "$orig_dirs_full_index" \
| sort -k 1 \
> "$dirs_in_both"

################################################################
# Check for Duplicate Inventory Tags
# 
# 
if ! ( cat "$orig_dirs_full_index" "$orig_files_full_index" \
       | awk '{ 
		if (seen[$2])
		  {
		    exit 1
		  }
                seen[$2] = 1;
	      }' \
      && cat "$mod_dirs_full_index" "$mod_files_full_index" \
       | awk '{ 
		if (seen[$2])
		  {
		    exit 1
		  }
                seen[$2] = 1;
	      }' ) \
; then

  printf "mkpatch: duplicate inventory tags found\\n" 1>&2
  printf "  try \"larch tree-lint --help\"\\n" 1>&2
  printf "\\n" 1>&2
  exit 1

fi

################################################################
# Capture the Permissions and Times of Removed and New Directories
# 

if test ! -z "$verbose" ; then
  larch heading --sub "recording metadata of removed directories\\n"
  larch heading --sub --sub "start time: %s\\n" "`date`"
fi

for dir in `cat "$original_only_dirs"` ; do
  printf "%s\\t%s\\n" "`file-metadata --permissions \"$original/$dir\"`" "$dir" > original-only-dir-metadata
done

if test ! -z "$verbose" ; then
  larch heading --sub --sub "finish time: %s\\n" "`date`"
  larch heading --sub "recording metadata of added directories\\n"
  larch heading --sub --sub "start time: %s\\n" "`date`"
fi

for dir in `cat "$modified_only_dirs"` ; do
  printf "%s\\t%s\\n" "`file-metadata --permissions \"$modified/$dir\"`" "$dir" > modified-only-dir-metadata
done

if test ! -z "$verbose" ; then
  larch heading --sub --sub "finish time: %s\\n" "`date`"
fi

################################################################
# Save copies of the removed files
# 
# This preserves permissions, times, and symlinks.
# 
# It archives files that have been removed or replaced by directories.
# 
# This does *not* archive files replaced by symlinks, even symlinks to
# directories.
# 
# This *does* archive symlinks, even symlinks to directories, if they
# have been removed or replaced by directories.
# 

if test ! -z "$verbose" ; then
  larch heading --sub "copying removed files\\n"
  larch heading --sub --sub "start time: %s\\n" "`date`"
fi

mkdir "$removed_files_archive"

copy-file-list -- "$original_only_files" "$original" "$removed_files_archive"

if test ! -z "$verbose" ; then
  larch heading --sub --sub "finish time: %s\\n" "`date`"
fi

################################################################
# Save copies of the new files
# 
# This preserves permissions, times, and symlinks.
# 
# It archives files that are new or that replace directories.
# 
# This does *not* archive files that replace symlinks, even symlinks to
# directories.
# 

if test ! -z "$verbose" ; then
  larch heading --sub "copying new files\\n"
  larch heading --sub --sub "start time: %s\\n" "`date`"
fi


mkdir "$new_files_archive"

copy-file-list -- "$modified_only_files" "$modified" "$new_files_archive"

if test ! -z "$verbose" ; then
  larch heading --sub --sub "finish time: %s\\n" "`date`"
fi

################################################################
# Yumm....Spew
# 

if test ! -z "$report" ; then
  larch heading --sub "comparing common files and directories\\n"
  if test ! -z "$verbose" ; then
    larch heading --sub --sub "comparing files start time: %s\\n" "`date`"
  fi
fi

################################################################
# Save patches for all the common files, along with a record
# of any permission, time, or symlink changes.
# 

touch "$changed_files"
touch "$changed_dirs"

file_list_titled=

interesting_file()
{
  printf "%s\\n" "$1" >> "$changed_files"

  if test ! -z "$report" -a -z "$file_list_titled" ; then
    file_list_titled=yes
    larch heading --sub --sub "file list\\n"
  fi
  if test ! -z "$report" ; then
    printf "%s %s\\n" "$2" "$1" | larch body-indent --sub --sub --nonl
  fi
}

interesting_dir()
{
  printf "%s\\n" "$1" >> "$changed_dirs"

  if test ! -z "$report" -a -z "$file_list_titled" ; then
    file_list_titled=yes
    larch heading --sub --sub "file list\\n"
  fi
  if test ! -z "$report" ; then
    printf "%s %s\\n" "$2" "$1" | larch body-indent --sub --sub --nonl
  fi
}


mkdir "$patches_dir"
cd "$patches_dir"

# create a directory in `patches' for every common file and directory.
# 
(  ( cut -s -d " " -f 2 "$files_in_both" | xargs -n 1 need-args dirname ) \
   && cut -s -d " " -f 2 "$dirs_in_both" ) \
 | sort -u \
 | xargs -n 1 need-args "mkdir -p"

# compare regular files and symlinks:
# 
OLD_IFS="$IFS"
IFS="
"
for name_pair in `cat "$files_in_both"` ; do

  IFS="$OLD_IFS"

  new_name=${name_pair#* }
  old_name=${name_pair% *}

  changed_type=
  original_is_symlink=
  modified_is_symlink=

  if test -h "$original/$old_name" ; then
    original_is_symlink=yes
  fi

  if test -h "$modified/$new_name" ; then
    modified_is_symlink=yes
  fi

  if test ! -z "$original_is_symlink" -a ! -z "$modified_is_symlink" ; then

    # both are symlinks -- are they the same?
    # 

    orig_link="`read-link \"$original/$old_name\"`"
    mod_link="`read-link \"$modified/$new_name\"`"

    if test "$orig_link" != "$mod_link" ; then

      printf "%s\\n" "$orig_link" > "$new_name.link-orig"
      printf "%s\\n" "$mod_link" > "$new_name.link-mod"
      interesting_file "$new_name" "->"

    fi

  elif test ! -z "$original_is_symlink" ; then

    # originally a symlink, now a regular file.
    # 

    printf "%s\\n" "$orig_link" > "$new_name.link-orig"
    cp -p "$modified/$new_name" "$new_name.modified"
    changed_type=yes
    interesting_file "$new_name" ">r"

  elif test ! -z "$modified_is_symlink" ; then

    # originally a regular file, now a symlink
    # 

    cp -p "$original/$old_name" "$new_name.original"
    printf "%s\\n" "$mod_link" > "$new_name.link-mod"
    changed_type=yes
    interesting_file "$new_name" "r>"

  else

    # Two regular files.  Can we diff them?
    # 

    set +e
    diff -u "$original/$old_name" "$modified/$new_name" > "$new_name.patch" 2> /dev/null
    diffstat=$?
    set -e
  
    # if the files differ, but diff produced "--brief" output, 
    # treat that as "trouble" (exit status 2)
    # 
    if test $diffstat -eq 1 && ( head -n 1 "$new_name.patch" | grep -q -i -e "^binary files .*" ) ; then
      diffstat=2
    fi

    if test $diffstat -eq 0 ; then

      # The files are the same.
      # 
      rm "$new_name".patch

    elif test $diffstat -eq 1 ; then

      interesting_file "$new_name" "M "

    elif test $diffstat -ne 1 ; then

      # Can't diff these files.
      # 
      # What to do?  Assume it is binary, and 
      # save both the original and mofified copies.
      # 
      rm "$new_name.patch"
      if ! cmp -s "$original/$old_name" "$modified/$new_name" ; then
        cp -p "$original/$old_name" "$new_name.original"
        cp -p "$modified/$new_name" "$new_name.modified"
        interesting_file "$new_name" "B "
      fi
    fi

  fi

  # did permissions change?
  # 
  # We record them if they changed, or if the file changed type
  # 

  orig_meta="`file-metadata --symlink --permissions \"$original/$old_name\"`"
  mod_meta="`file-metadata --symlink --permissions \"$modified/$new_name\"`"

  if test ! -z "$changed_type" -o "(" "$orig_meta" != "$mod_meta" ")" ; then
    printf "%s\\n" "$orig_meta" > "$new_name.meta-orig"
    printf "%s\\n" "$mod_meta" > "$new_name.meta-mod"
    interesting_file "$new_name" "%%"
  fi

done
IFS="$OLD_IFS"

# compare directories in both
#
OLD_IFS="$IFS"
IFS="
"
for name_pair in `cat "$dirs_in_both"` ; do

  IFS="$OLD_IFS"

  new_name=${name_pair#* }
  old_name=${name_pair% *}

  # only the meta-data is of interest
  # 

  orig_meta="`file-metadata --symlink --permissions \"$original/$old_name\"`"
  mod_meta="`file-metadata --symlink --permissions \"$modified/$new_name\"`"

  if test "$orig_meta" != "$mod_meta" ; then
    printf "%s\\n" "$orig_meta" > "$new_name/=dir-meta-orig"
    printf "%s\\n" "$mod_meta" > "$new_name/=dir-meta-mod"
    interesting_dir "$new_name" "%%"
  fi

done
IFS="$OLD_IFS"


# remove empty directories from `patches'
# 
find . -type d ! -name . ! -name .. -depth -exec rmdir {} ";" 2> /dev/null

if test ! -z "$report" -a ! -z "$file_list_titled" ; then
  printf "\\n"
fi

if test ! -z "$verbose" ; then
  larch heading --sub --sub "comparing files finish time: %s\\n" "`date`"
fi


################################################################
# Create File Indexes
# 
cd "$destination"

rm -f ,,tmp
mv "$changed_files" ,,tmp
cat ,,tmp | sort -u > "$changed_files"

rm -f ,,tmp
mv "$changed_dirs" ,,tmp
cat ,,tmp | sort -u > "$changed_dirs"

( join -o 2.1,2.2 -1 1 -2 2 "$original_only_file_ids" "$orig_files_full_index" ; \
  join -o 1.2 -1 1 -2 1 "$mod_files_full_index_by_name" "$changed_files" \
  | sort \
  | join -o 2.1,2.2 -1 1 -2 2 - "$orig_files_full_index" ; \
  cut -s -d ' ' -f 1,3 "$renamed_files" ) \
| sort -u -k 2 \
> "$orig_files_index"

( join -o 2.1,2.2 -1 1 -2 2 "$original_only_dir_ids" "$orig_dirs_full_index" ; \
  join -o 1.2 -1 1 -2 1 "$mod_dirs_full_index_by_name" "$changed_dirs" \
  | sort \
  | join -o 2.1,2.2 -1 1 -2 2 - "$orig_dirs_full_index" ; \
  cut -s -d ' ' -f 1,3 "$renamed_dirs" ) \
| sort -u -k 2 \
> "$orig_dirs_index"

( join -o 2.1,2.2 -1 1 -2 2 "$modified_only_file_ids" "$mod_files_full_index" ; \
  join -o 1.1,1.2 -1 1 -2 1 "$mod_files_full_index_by_name" "$changed_files" ; \
  cut -s -d ' ' -f 2,3 "$renamed_files" ) \
| sort -u -k 2 \
> "$mod_files_index"

( join -o 2.1,2.2 -1 1 -2 2 "$modified_only_dir_ids" "$mod_dirs_full_index" ; \
  join -o 1.1,1.2 -1 1 -2 1 "$mod_dirs_full_index_by_name" "$changed_dirs" ; \
  cut -s -d ' ' -f 2,3 "$renamed_dirs" ) \
| sort -u -k 2 \
> "$mod_dirs_index"


################################################################
# remove temporary files
# 
cd "$destination"
# rm -f ,,*


################################################################
# Spewn
# 

if test ! -z "$quiet" ; then

  larch heading --sub "mkpatch finish time: %s\\n" "`date`"

fi
