#!/bin/sh # # This script parses the output of "bzr diff" and uses an external # graphical difference tool like mgdiff or xxdiff to show you the # differences one file at a time. # # -- Paul Serice # BZRMGDIFF_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 : Pass a revision directly to \"bzr diff\". This script does not attempt to map tags or branches to the correct revision syntax. Use \"-r branch:foo\" to specify the \"foo\" branch and \"-r tag:foo\" to specify the \"foo\" tag. If the branch or tag is local, consider passing in the path to the branch or tag directory. See the output of the following command for more: \"bzr help revisionspec\" -v : Print version. -w : Walk the by diffing each version with its predecessor. Purpose: The purpose of this program is to help automate the repetitive task of running recursive differences over a Bazaar (bzr) source code repository. Examples: # Recursively diff the working tree with the checked out revision. bzrmgdiff # To diff two specific revisions of the working tree use the # following. Note that you cannot use a \":\" character to # separate revisions because bzr uses \":\" to separate the # tag type from the tag name (e.g. -r tag:foo means the tag # \"foo\"): bzrmgdiff -r 67..68 # Recursively diff the working tree with the latest version of # the \"foo\" tag. bzrmgdiff -r tag:foo # Recursively diff the working tree with the latest version of # the \"foo\" branch. bzrmgdiff -r branch:$url/branches/foo # To diff a branch against the revision that is checked out # (assuming it is the trunk that is checked out) do the # following (note that this is not the same as diffing against # the working tree): bzrmgdiff -r branch:$url/branches/foo -r branch:$url/trunk Warning: The -r flag for bzrmgdiff does not work like it does in the other mgdiff scripts. The problem is that branches are not constrained and are not visible like they are in the other version control systems making it hard for bzrmgdiff to do the right thing if just given a branch name. To compensate for this, bazaar's -r flag is very complicated and requires careful parsing and careful consideration about what to do with what you parse. In fact, in writing this script, I triggered two internal errors in bzr-2.1.0 on relatively trivial things. (One of the errors was fixed upstream after reporting the error, but the other is still pending and prevents the -w option from working properly). So, rather than go down the road of trying to use heuristics to figure these complicated things out, this script requires the user to specify the correct, bazaar-specific -r syntax. See the following output for more: bzr help revisionspec " 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() { ve_pname="$1" type "$ve_pname" 1>/dev/null 2>&1 if [ $? -ne 0 ] ; then echo "$progname: Error: Unable to find executable" \ "for \"$ve_pname\"." 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 will try to cat the correct revison of a file to stdout. # # @param[in] rev revision of file # @param[in] fname file name # @return revision of file printed to stdout bzrcat() { bc_rev="$1" bc_fname="$2" if [ x"$bc_rev" = x ] ; then bzr cat "$bc_fname" 2>/dev/null else bzr cat -r "$bc_rev" "$bc_fname" 2>/dev/null fi } ## # Walk all the commits associated with a particular file by # recursively calling this script in order to compare the file before # and after the commit. # # @param[in] fname file name walk_single_file() { wsf_fname="$1" # Make sure the user only passed in a single file. if [ $# -ne 1 ] ; then echo "***" 1>&2 echo "*** $progname: Error: Exactly one file must be specified: $@" 1>&2 echo "***" 1>&2 exit_rv=1 exit fi bzr log -r . "$wsf_fname" \ | "$NAWK" '/^revno: / {print $2}' \ | { while read prev ; do # If $curr is set, show the log file entry for that commit. if [ x"$curr" != x ] ; then # If $curr is set, show the log file entry for that commit. if [ x"$curr" != x ] ; then set -x bzr log -l 1 -r "$curr" fi # Diff the single file using the previous and # current revision. if [ x"$usegui" = x"true" ] ; then bzrmgdiff -r "$prev" ${curr:+-r "$curr"} "$wsf_fname" \ 1>/dev/null 2>&1 fi fi curr="$prev" done # Print the separator. if [ x"$curr" != x ] ; then printf "%s%s\n" \ "------------------------------------" \ "------------------------------------" fi } } ## # This function is the main part of the script. It runs "bzr 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() { bzr_diff_cmd="bzr diff \ ${rev0+-r \"$rev0\"} \ ${rev1+-r \"$rev1\"} \ ${1+\"\$@\"}" if [ x"$debug" != x ] ; then echo "$bzr_diff_cmd ${1+ [where \$@ = $@]}" fi # Parse the output of "bzr 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 "$bzr_diff_cmd" \ | "$NAWK" 'BEGIN { print_ranges = 0 } /^=== added / { print "NEW" print substr($0, 17, length($0) - 17) } /^=== removed / { print "OLD" print substr($0, 19, length($0) - 19) } /^=== modified / { print "DIF" print substr($0, 20, length($0) - 20) }' \ | while read fchange; \ read fname; do # Print log message. echo "$fchange $fname" # Nothing to do if the change is simplye a new or old file. if [ \( x"$fchange" = x"NEW" \) -o \( x"$fchange" = x"OLD" \) ] ; then continue fi # 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 bzrcat "$rev0" "$fname" > "$tmp_file_1" fi if [ x"$rev1" != x ] ; then bzrcat "$rev1" "$fname" > "$tmp_file_2" 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=`bzr version-info "$fname" \ | "$NAWK" '/^revno:/ {print $2;}'` 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 bzrcat "$rev0" "$fname" > "$tmp_file_1" else bzrcat "" "$fname" > "$tmp_file_1" 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="bzrmgdiff.sh" verify_exec "basename" progname=`basename "$0"` # You have to generate the temporary file names before registering the # trap handler. tmp_base="bzrmgdiff-$$." 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 "echo" verify_exec "expr" verify_exec "$MGDIFF" verify_exec "mktemp" verify_exec "$NAWK" verify_exec "realpath" verify_exec "rm" verify_exec "bzr" mgdiff_basename=`basename "$MGDIFF"` # Get bzr revision(s) to use. usegui="true" while getopts "dg:hnr:u: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) if [ x"$OPTARG" != x ] ; then if [ x"$rev0" = x ] ; then rev0="$OPTARG" elif [ x"$rev1" = x ] ; then rev1="$OPTARG" else echo "*** $progname: Error: too many revisions" \ "\"$rev0\", \"$rev1\", \"$OPTARG\"" 1>&2 exit_rv=1 exit fi fi ;; v) echo "$progname: ${BZRMGDIFF_VERSION}" exit ;; # If the user specifies the -w flag and a single file, this # script will walk the commit tree and pop up the gui to show # how the single file changed after each commit. w) walk="true" ;; \?) 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 if [ x"$walk" != x ] ; then walk_single_file "$@" else diff_revisions "$@" fi