#!/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 ] [-r ] [|] Options: -d : Print debugging information including the command used to generate the diff. -g : Graphical difference program to use. -h : Print help message. -n : No gui is used. Only the log is printed to stdout. -r : Specify one or two revisions. To specify two revisions, use the -r flag twice or use one of \"-r :\" or \"-r ..\". -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 .." or "-r :". 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 "$@"