# mkpatch-files.sh - prepare patches for selected files
# 
################################################################
# 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 select files in two source trees and create a patch tree\\n"
                printf "usage: mkpatch-files [options] original modified destination file...\\n"
                printf "usage: mkpatch-files -f file-list [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 " -f --files FILE-LIST          read the list files from FILE-LIST\\n"
                printf "\\n"
                printf " --file-root DIR               tree root for the file list\\n"
                printf "\\n"
                printf "Create the output directory DESTINATION (it must not already\\n"
                printf "exist).\\n"
                printf "\\n"
                printf "Compare the indicated files in source trees ORIGINAL and MODIFIED.\\n"
                printf "Create a patch tree in DESTINATION\\n"
                printf "\\n"
                printf "Only the indicated files are considered for the patch set.\\n"
                printf "\\n"
                printf "With -f FILE-LIST or --files FILE-LIST, read the list of files from the\\n"
                printf "file FILE-LIST instead of the command line.  If FILE-LIST is a dash (-)\\n"
                printf "the list is read from standard input.\\n"
                printf "\\n"
                printf "By default, relative path names in the file list are interpreted from the\\n"
                printf "current directory.  With --file-root DIR, interpret them relative to DIR.\\n"
                printf "\\n"
                printf "Patches generated by this command may include file data and meta-data changes,\\n"
                printf "but they will not include renames, adds, or deletes.\\n"
                printf "\\n"
                printf "See also \"dopatch --help\".\\n"
                printf "\\n"
                exit 0
                ;;

      *)
                ;;
    esac
  done
fi

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

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

method=
original_locations=
file_list=
file_root=

while test "$#" -gt 0 ; do

  case "$1" in

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

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

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


    --original-locations)	shift
                                original_locations=--original-locations
                                ;;

    -f|--file-list)	shift
                        if test $# -eq 0 ; then
                          printf "mkpatch-files: -f and --file-list require an argument\\n" 1>&2
                          printf "try --help\\n" 1>&2
                        fi
                        file_list="$1"
                        shift
                        ;;

    --file-root)	shift
                        if test $# -eq 0 ; then
                          printf "mkpatch-files: --file-root requires an argument\\n" 1>&2
                          printf "try --help\\n" 1>&2
                        fi
                        file_root="$1"
                        shift
                        ;;

    --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-files --help\"\\n" 1>&2
                exit 1
                ;;

    *)          break
                ;;

  esac


done


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

if test '(' -z "$file_list" -a '(' $# -lt 4 ')' ')' -o '(' ! -z "$file_list" -a '(' $# -ne 3 ')' ')'; then
  printf "usage: mkpatch-files [options] original modified destination file...\\n" 1>&2
  printf "try --help\\n"  
  exit 1
fi

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

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"

if test -z "$file_root" ; then
  file_root="$here"
else
  cd "$here"
  cd "$file_root"
  file_root="`pwd`"
fi


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

if test ! -z "$quiet" ; then 
  larch heading "mkpatch-files\\n"
  printf "arguments: %s\\n" "$command_line" | fold -w 60 | larch body-indent
  larch heading --sub "mkpatch-files 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
# 
# Files whose names begin with ,, are later deleted.
# 

raw_file_list="$destination/,,raw-file-list"
cooked_file_list="$destination/,,cooked-file-list"
cooked_file_tags="$destination/,,cooked-file-tags"

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_files_full_index="$destination/,,orig-files-full-index"
mod_files_full_index="$destination/,,mod-files-full-index"

changed_files="$destination/,,changed-files"
files_in_both="$destination/,,files-in-both"


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"

patches_dir="$destination/patches"


################################################################
# Capture the Raw List of Files to Consider in the Patch
# 

rm -f "$raw_file_list"
touch "$raw_file_list"

cd "$here"

if test -z "$file_list" ; then

  while test $# -ne 0 ; do
    printf "%s\\n" "$1" >> "$raw_file_list"
    shift
  done

else 

  cat "$file_list" >> "$raw_file_list"
  
fi



################################################################
# Convert the File List to Paths Relative to the Modified Tree Root
# 

tmp="$destination/,,tmp"
rm -f "$tmp"

cat "$raw_file_list" \
| (
    while read raw_file ; do

      cd "$file_root"
      raw_dir="`dirname \"$raw_file\"`"
      raw_basename="`basename \"$raw_file\"`"

      cd "$file_root"
      if ! cd "$raw_dir" ; then
        printf "mkpatch-files: directory of file in file list does not exist\\n" 1>&2
        printf "  file: %s\\n" "$raw_file" 1>&2
        exit 1
      fi

      cooked_abs_dir="`pwd`"
      printf "%s/%s\\n" "$cooked_abs_dir" "$raw_basename"
    done
   ) \
> "$tmp"

#
# tmp file there so that the subshell's exit status above is 
# significant, as well as the awk script's exit status below.
# tmp holds the list of files with absolute path names, so now
# make sure they are all under the modified root and make a list
# of their names relative to that root:
# 

cat "$tmp" \
| awk -v modified="$modified/" \
    '{
       if (modified == substr($0, 1, length(modified)))
         {
           printf "./%s\n", substr($0, 1 + length(modified));
         }
       else
         {
           printf "mkpatch-files: directory of file not in modified tree\n" | "cat 1>&2";
           printf "  file: %s\n", $0 | "cat 1>&2";
           exit 1;
         }
     }' \
| sort \
> "$cooked_file_list"
  

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


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


cd "$original" 
larch inventory $method --source --files --tags > "$orig_files_full_index_by_name"
sort -k 2 "$orig_files_full_index_by_name" > "$orig_files_full_index"

cd "$modified"
larch inventory $method --source --files --tags > "$mod_files_full_index_by_name"
sort -k 2 "$mod_files_full_index_by_name" > "$mod_files_full_index"


################################################################
# Sanity Check and Plan The Patch Set
# 

# do all of the files exist in the mod tree?
# 

if test ! -z "`join -v 1 -1 1 -2 1 \"$cooked_file_list\" \"$mod_files_full_index_by_name\" | head -c 1`" ; then

  printf "mkpatch-files: some files not found in modified tree\\n" 1>&2
  join -v 1 -1 1 -2 1 -o 1.1 "$cooked_file_list" "$mod_files_full_index_by_name" | sed -e 's/^/    /' 1>&2
  exit 1

fi

# do all of the files exist in the orig tree?
#

join -o 2.2 -1 1 -2 1 "$cooked_file_list" "$mod_files_full_index_by_name" | sort > "$cooked_file_tags"

if test ! -z "`join -v 1 -1 1 -2 2 \"$cooked_file_tags\" \"$orig_files_full_index\" | head -c 1`" ; then

  printf "mkpatch-files: some files not found in original tree\\n" 1>&2
  join -v 1 -1 1 -2 2 -o 1.1 "$cooked_file_tags" "$orig_files_full_index" | sed -e 's/^/    /' 1>&2
  exit 1

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"
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
}

mkdir "$patches_dir"
cd "$patches_dir"


# Create a directory in `patches' for every common file and directory.
# Note that unlike a normal patch, we use the orig paths here.
# 
join -1 1 -2 2 -o 2.1 "$cooked_file_tags" "$orig_files_full_index" \
| sed -e 's,/[^/]*$,,' \
      -e 's/^/mkdir -p /' \
| $ARCH_SHELL -e


join -o 1.1,2.1,2.2 -1 2 -2 2 "$orig_files_full_index" "$mod_files_full_index" \
| join -o 1.1,1.2 -1 3 -2 1 - "$cooked_file_tags" \
> "$files_in_both"


# 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" > "$old_name.link-orig"
      printf "%s\\n" "$mod_link" > "$old_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" > "$old_name.link-orig"
    cp -p "$modified/$new_name" "$old_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" "$old_name.original"
    printf "%s\\n" "$mod_link" > "$old_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" > "$old_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 "$old_name.patch" | grep -q -i -e "^binary files .*" ) ; then
      diffstat=2
    fi

    if test $diffstat -eq 0 ; then

      # The files are the same.
      # 
      rm "$old_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 "$old_name.patch"
      if ! cmp -s "$original/$old_name" "$modified/$new_name" ; then
        cp -p "$original/$old_name" "$old_name.original"
        cp -p "$modified/$new_name" "$old_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" > "$old_name.meta-orig"
    printf "%s\\n" "$mod_meta" > "$old_name.meta-mod"
    interesting_file "$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"

join -o 2.2 -1 1 -2 1 "$changed_files" "$mod_files_full_index_by_name" \
| sort \
| join -o 2.1,2.2 -1 1 -2 2 - "$orig_files_full_index" \
| sort -u -k 2 \
> "$orig_files_index"

cp "$orig_files_index" "$mod_files_index"

rm -f "$mod_dirs_index" "$orig_dirs_index"
touch "$mod_dirs_index" "$orig_dirs_index"


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


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

if test ! -z "$quiet" ; then

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

fi
