#!/bin/sh

#
# This script parses the output of "cvs diff" and uses an external
# graphical difference tool like mgdiff or xxdiff to show you the
# differences one file at a time.
#
#                                             -- Paul Serice
#

CVSMGDIFF_VERSION=2.0.11

: ${MGDIFF:=/usr/bin/mgdiff}
: ${TMP:=/tmp}

# MSYS hijacks "/tmp", "$TMP", and "$TEMP" redirecting all to
# "%LOCALAPPDATA%\Temp" which breaks native tools that don't do the
# same redirection (which is almost all of them).  A further
# complication is that emac's (ediff) command has trouble parsing file
# names under Windows that start with the drive (like "C:/...").  So,
# to work around all of this, just use the current working directory
# if MSYS is detected.
if [ x"$MSYSTEM" != x ] ; then
    TMP="."
fi

# Because this script has to clean up temporary files, an EXIT trap
# called clean_up() is established.  After cleaning up the files,
# clean_up() calls "exit $exit_rv".  So do not exit the program via
# "exit n"; instead, do "exit_rv=n; exit".  This will trigger the EXIT
# trap and pass n back to the shell.
exit_rv=0

##
# Print usage message.  If the output file is specified as "1", the
# error message is printed to stdout (the usual file associated with
# the "1" file descriptor); otherwise, the error message is printed to
# stderr.
#
# @param[in] fd pseudo file descriptor
usage() {
    u_fd="$1"

    msg="\

Usage: $progname [options] [-r <rev1>] [-r <rev2>] [<files>|<dirs>]

    Options:

            -d : Print debugging information including the command
                 used to generate the diff.

      -g <gui> : Graphical difference program to use.

            -h : Print help message.

            -n : No gui is used.  Only the log is printed to stdout.

      -r <rev> : Specify one or two revisions.  To specify two
                 revisions, use the -r flag twice or use one of \"-r
                 <rev1>:<rev2>\" or \"-r <rev1>..<rev2>\".

            -v : Print version.

    Purpose:

        The purpose of this program is to help automate the repetitive
        task of running recursive differences over a cvs source code
        repository.

    Examples:

        # Recursively diff the working tree with the checked out revision.
        cvsmgdiff

        # Diff against a specific tag.
        cvsmgdiff -r foo
"
    if [ "$u_fd" -eq 1 ] ; then
        echo "$msg"
    else
        echo "$msg" 1>&2
    fi
}

##
# Verify that a program is in the user's path and is executable.
#
# @param[in] pname program name
verify_exec() {
    type "$1" 1>/dev/null 2>&1
    if [ $? -ne 0 ] ; then
        echo "*** $progname: Error: Unable to find executable for \"$1\"." 1>&2
        exit_rv=1
        exit
    fi
}

##
# Trap handler for cleaning up temporary files.
clean_up() {
    rm -f "$tmp_file_1"
    rm -f "$tmp_file_2"
    exit $exit_rv
}

##
# Set "tmp_file_1" and "tmp_file_2 to be the name of two unique
# temporary files.
getunique() {
    old_umask=`umask`
    umask 077

    tmp_file_1=`mktemp -q "$TMP/${tmp_base}XXXXXX"`
    tmp_file_1=`realpath "$tmp_file_1"`
    if [ $? -ne 0 ] ; then
        echo "*** $progname:" \
             "Error: Unable to allocate a necessary temporary file." 1>&2
        exit_rv=1
        exit
    fi

    tmp_file_2=`mktemp -q "$TMP/${tmp_base}XXXXXX"`
    tmp_file_2=`realpath "$tmp_file_2"`
    if [ $? -ne 0 ] ; then
        echo "*** $progname:" \
             "Error: Unable to allocate a necessary temporary file." 1>&2
        exit_rv=1
        exit
    fi
    umask $old_umask
}

##
# This function returns true if a particular file revision exits in
# the repository.  To test if the file exists in the checked out
# revision, just pass in an empty string for the revision.
#
# @param[in] rev revision
# @param[in] fname file name
# @return whehter a particular file revision exits
file_revision_exists() {
    fre_rev="$1"
    fre_fname="$2"

    # Remember that 0 is true for bourne shells.
    fre_rv=1

    # Try to get a listing for the file for this revision.
    if [ x"$fre_rev" = x ] ; then
        fre_listing=`cvs ls -- "$fre_fname" 2>/dev/null`
    else
        fre_listing=`cvs ls -r "$fre_rev" -- "$fre_fname" 2>/dev/null`
    fi

    # If a listing was found, the file exists for this revision.
    if [ x"$fre_listing" != x ] ; then
        # Success.
        fre_rv=0
    fi

    return $fre_rv
}

##
# This function takes a combined revision of the form "rev1:rev2" or
# "rev1..rev2" and writes "rev1" to stdout.
#
# @param[in] crev combined revision
# @return first revision
get_first_revision() {
    gfr_crev="$1"

    "$NAWK" -v crev="$gfr_crev" 'BEGIN {\

        # Split the string both ways.
        colon_count = split(crev, colon_parts, /:/)
        dotdot_count = split(crev, dotdot_parts, /\.\./)

        # The first separator wins so return the shorter first part.
        if ((colon_count >= 2) && (dotdot_count >=2)) {
            if (length(colon_parts[1]) < length(dotdot_parts[1])) {
                print colon_parts[1]
            } else {
                print dotdot_parts[1]
            }
            exit
        }

        # Only the colon mathched.
        if (colon_count >= 2) {
            print colon_parts[1]
            exit
        }

        # Only the dotdot matched.
        if (dotdot_count >= 2) {
            print dotdot_parts[1]
            exit
        }

        # No match.
        print crev
    }'
}

##
# This function takes a combined revision of the form "rev1:rev2" or
# "rev1..rev2" and writes "rev2" to stdout.
#
# @param[in] crev combined revision
# @return second revision
get_second_revision() {
    gsr_crev="$1"

    "$NAWK" -v crev="$gsr_crev" 'BEGIN {\

        # Split the string both ways.
        colon_count = split(crev, colon_parts, /:/)
        dotdot_count = split(crev, dotdot_parts, /\.\./)

        # The first separator wins so return the complement of the
        # shorter first part.
        if ((colon_count >= 2) && (dotdot_count >=2)) {
            if (length(colon_parts[1]) < length(dotdot_parts[1])) {
                print substr(crev, length (colon_parts[1]) + 2)
            } else {
                print substr(crev, length (dotdot_parts[1]) + 3)
            }
            exit
        }

        # Only the colon mathched.
        if (colon_count >= 2) {
            print substr(crev, length (colon_parts[1]) + 2)
            exit
        }

        # Only the dotdot matched.
        if (dotdot_count >= 2) {
            print substr(crev, length (dotdot_parts[1]) + 3)
            exit
        }

        # No match.
        print ""
    }'
}

##
# This function is the main part of the script.  It runs "cvs diff"
# and parses the output.  It writes a simple log to stdout of the
# files that are NEW, OLD, or DIF (i.e. different).  For the DIF
# files, it invokes the graphical diff program.
diff_revisions() {

    # Determine the "cvs diff" command to use.
    cvs_diff_cmd="cvs diff -u -N \
${rev0:+-r \"$rev0\"} \
${rev1:+-r \"$rev1\"} \
${1+\"\$@\"}"

    if [ x"$debug" != x ] ; then
        echo "$cvs_diff_cmd ${1+  [where \$@ = $@]}"
    fi

    # Parse the output of "cvs diff" and pass the relevant parts of
    # the "Index:" and "@@ R1 R2 @@" lines to the subshell.  The
    # "print_ranges" boolean is needed because we only want to print
    # the first set of ranges for any file.
    eval "$cvs_diff_cmd 2>/dev/null" \
        | "$NAWK" 'BEGIN {
                   # We need this boolean to make sure the while loop
                   # below is always fed the expected number of
                   # arguments.
                   print_ranges = 0
               }
               /^Index: / {
                   # For files that are empty or not present in one
                   # version, "cvs diff" does not print the rest of
                   # the information that it does for other new files.
                   # If this is the case, print the remaining
                   # arguments for the while() loop before starting to
                   # process the next file.
                   if (print_ranges) {
                       print ""
                       print ""
                       print_ranges = 0
                   }

                   # Print the file name.
                   print substr($0, 8)
                   print_ranges = 1
               }
               /^@@ / {
                   if (print_ranges) {
                       print $2
                       print $3
                       print_ranges = 0
                   }
               }
               END {
                   # If the last file is empty or not present in one
                   # version, make sure we print the remaining values
                   # so the while() loop will run one last time.
                   if (print_ranges) {
                       print ""
                       print ""
                   }
               }' \
        | while read fname;
                read range_old; \
                read range_new; do

        # Empty ranges or ranges of "-0,0" or "+0,0" mean the file is
        # either empty or missing.  If the file is missing, it means
        # it is either old or new.
        if [    \( x"$range_old" = x"" \) \
             -o \( x"$range_new" = x"" \) \
             -o \( x"$range_old" = x"-0,0" \) \
             -o \( x"$range_new" = x"+0,0" \) ]
        then
            # If the second revision was not specified, the diff is
            # against the working tree.  If the file doesn't exist on
            # the file system, it is considered to be old because it
            # has been removed from the project.
            if [ \( x"$rev1" = x"" \) -a \( ! -e "$fname" \) ] ; then
                echo "OLD $fname"
                continue
            fi

            # If the second revision was specified, the diff is
            # against the repository.  If the second revision of the
            # file doesn't exist in the repository, it is considered
            # to be old because it has been removed from the project.
            if [ x"$rev1" != x"" ] ; then
                file_revision_exists "$rev1" "$fname"
                if [ $? -ne 0 ] ; then
                    echo "OLD $fname"
                    continue
                fi
            fi

            # If the first revision was specified and doesn't exist in
            # the repository, it is considered to be new because the
            # second revision is known to be either on the file system
            # or in the repository.
            if [ x"$rev0" != x"" ] ; then
                file_revision_exists "$rev0" "$fname"
                if [ $? -ne 0 ] ; then
                    echo "NEW $fname"
                    continue
                fi
            fi

            # If the neither revision was specified and the file was
            # not part of the original check out, it is considered to
            # be new because the file is known to be on the file
            # system.
            if [ x"$rev0" != x"" ] ; then
                file_revision_exists "" "$fname"
                if [ $? -ne 0 ] ; then
                    echo "NEW $fname"
                    continue
                fi
            fi
        fi

        echo "DIF $fname"

        # Handle diffing two revisions.
        if [ \( x"$rev0" != x \) -a \( x"$rev1" != x \) ] ; then

            # Copy rev0 and rev1 to a temporary files.
            if [ x"$rev0" != x ] ; then
                cvs up -p -r "$rev0" "$fname" > "$tmp_file_1" 2>/dev/null
            fi
            if [ x"$rev1" != x ] ; then
                cvs up -p -r "$rev1" "$fname" > "$tmp_file_2" 2>/dev/null
            fi

            # Pop up the graphical diff.
            if [ x"$usegui" = x"true" ] ; then

                # Generate the command.
                dr_cmd="\"$MGDIFF\""
                if [ x"$QUIET_OPT" != x ] ; then
                    dr_cmd="$dr_cmd \"$QUIET_OPT\""
                fi
                if [ x"$TITLE_1_OPT" != x ] ; then
                    dr_cmd="$dr_cmd \"$TITLE_1_OPT\""
                    dr_cmd="$dr_cmd \"$fname (rev $rev0)\""
                fi
                if [ x"$TITLE_2_OPT" != x ] ; then
                    dr_cmd="$dr_cmd \"$TITLE_2_OPT\""
                    dr_cmd="$dr_cmd \"$fname (rev $rev1)\""
                fi
                # Native Windows executables need native Windows paths.
                # The test for native Windows executable is just if it
                # ends with ".exe" or ".EXE".
                if [ \( x"${mgdiff_basename%.exe}" != x"$mgdiff_basename" \) \
                  -o \( x"${mgdiff_basename%.EXE}" != x"$mgdiff_basename" \) ]
                then
                    tmp_file_1=$(cygpath -w "$tmp_file_1")
                    tmp_file_2=$(cygpath -w "$tmp_file_2")
                fi
                dr_cmd="$dr_cmd \"$tmp_file_1\" \"$tmp_file_2\""

                # Evaluate command.
                eval "$dr_cmd"
            fi

        else

            #
            # Here, we force all the GUIs to use a temporary file even
            # if they claim to be able to handle input on stdin.  The
            # reason for this is that most of the GUIs leave temporary
            # files behind in /tmp when the exit unexpectedly.  By
            # using the temporary files created by this script, they
            # are virtually guaranteed to be cleaned up in all cases.
            #

            if [ x"$rev0" != x ] ; then
                title_rev="$rev0"
            else
                title_rev=`cvs status "$fname" \
                             | "$NAWK" '/Working revision:/{print $3;}'`
            fi

            # The convention that "diff" uses is that the old file is
            # on the left and the new file is on the right.  We use
            # this to display the files.
            file_first="$tmp_file_1"
            file_second="$fname"
            if [ ! -r "$file_second" ] ; then
                file_second="/dev/null"
            fi
            file_second=`realpath "$file_second"`

            # Copy rev0 to a temporary file.
            if [ x"$rev0" != x ] ; then
                cvs up -p -r "$rev0" "$fname" > "$tmp_file_1" 2>/dev/null
            else
                cvs up -p "$fname" > "$tmp_file_1" 2>/dev/null
            fi
            
            # Pop up the graphical diff.
            if [ x"$usegui" = x"true" ] ; then

                # Generate the command.
                dr_cmd="\"$MGDIFF\""
                if [ x"$QUIET_OPT" != x ] ; then
                    dr_cmd="$dr_cmd \"$QUIET_OPT\""
                fi
                if [ x"$TITLE_1_OPT" != x ] ; then
                    dr_cmd="$dr_cmd \"$TITLE_1_OPT\""
                    dr_cmd="$dr_cmd \"$fname (rev $title_rev)\""
                fi
                if [ x"$TITLE_2_OPT" != x ] ; then
                    dr_cmd="$dr_cmd \"$TITLE_2_OPT\" \"$fname\""
                fi
                # Native Windows executables need native Windows paths.
                # The test for native Windows executable is just if it
                # ends with ".exe" or ".EXE".
                if [ \( x"${mgdiff_basename%.exe}" != x"$mgdiff_basename" \) \
                  -o \( x"${mgdiff_basename%.EXE}" != x"$mgdiff_basename" \) ]
                then
                    file_first=$(cygpath -w "$file_first")
                    file_second=$(cygpath -w "$file_second")
                fi
                dr_cmd="$dr_cmd \"$file_first\" \"$file_second\""

                # Evaluate the command.
                eval "$dr_cmd"

            fi
        fi

    done
}

#
# Script Starts Here !!!
#

progname="cvsmgdiff.sh"
verify_exec "basename"
progname=`basename "$0"`

# You have to generate the temporary file names before registering the
# trap handler.
tmp_base="cvsmgdiff-$$."
getunique

# Signal Trap handler to clean up temporary files in "$TMP".
trap 'clean_up' HUP INT QUIT TERM EXIT
if [ $? -ne 0 ] ; then
    echo "$progname: Unable to register signal handler." >&2
    exit 1
fi

# Use GNU awk if possible.
for NAWK in gawk nawk awk ; do
    type "$NAWK" 1>/dev/null 2>&1
    if [ $? -eq 0 ] ; then
        break
    fi
done

verify_exec "cvs"
verify_exec "echo"
verify_exec "expr"
verify_exec "$MGDIFF"
verify_exec "mktemp"
verify_exec "$NAWK"
verify_exec "printf"
verify_exec "realpath"
verify_exec "rm"

mgdiff_basename=`basename "$MGDIFF"`

# Get cvs revision(s) to use.
usegui="true"
while getopts "dg:hnr:vw" OPT ; do
    case "$OPT" in

        d)  debug="true"
            ;;

        g)  MGDIFF="$OPTARG"
            export MGDIFF
            ;;

        h)  usage 1
            exit
            ;;

        n)  usegui="false"
            ;;

        # Allow the user to pass in two revisions at once with the
        # form "-r <rev0>..<rev1>" or "-r <rev0>:<rev1>".
        r)  tmp=`get_first_revision "$OPTARG"`
            if [ x"$tmp" != x ] ; then
                if [ x"$rev0" = x ] ; then
                    rev0="$tmp"
                elif [ x"$rev1" = x ] ; then
                    rev1="$tmp"
                else
                    echo "*** $progname: Error: too many revisions" \
                         "\"$rev0\", \"$rev1\", \"$OPTARG\"" 1>&2
                    exit_rv=1
                    exit
                fi
            fi

            tmp=`get_second_revision "$OPTARG"`
            if [ x"$tmp" != x ] ; then
                if [ x"$rev0" = x ] ; then
                    rev0="$tmp"
                elif [ x"$rev1" = x ] ; then
                    rev1="$tmp"
                else
                    echo "*** $progname: Error: too many revisions" \
                         "\"$rev0\", \"$rev1\", \"$OPTARG\"" 1>&2
                    exit_rv=1
                    exit
                fi
            fi
            ;;

        v)  echo "$progname: ${CVSMGDIFF_VERSION}"
            exit
            ;;

        \?) usage 2
            exit_rv=1
            exit
            ;;
    esac
done
shift `expr $OPTIND - 1`

#
# Portability issues.
#
#       QUIET_OPT -- How to prevent the gui from starting if there are no diffs.
#     TITLE_1_OPT -- How to override the name of file1.
#     TITLE_2_OPT -- How to override the name of file2.
#
if [ "$mgdiff_basename" = "mgdiff" ] ; then
    QUIET_OPT="-quit"
elif [ "$mgdiff_basename" = "tkdiff" ] ; then
    TITLE_1_OPT="-L"
    TITLE_2_OPT="-L"
elif [ "$mgdiff_basename" = "xdiff" ] ; then
    QUIET_OPT="-D"
elif [ "$mgdiff_basename" = "xxdiff" ] ; then
    QUIET_OPT="-D"
    TITLE_1_OPT="--title1"
    TITLE_2_OPT="--title2"
elif [ "$mgdiff_basename" = "WinMergeU.exe" ] ; then
    QUIET_OPT="/x"
    TITLE_1_OPT="/dl"
    TITLE_2_OPT="/dr"
fi

# Warn if this is not a cvs working directory.
if [ \( ! -d "CVS" \) -a \( ! -d "$1/CVS" \) ] ; then
    echo "*** $progname: Error: not a cvs directory." 1>&2
    exit_rv=1
    exit
fi

diff_revisions "$@"