Saturday, January 16, 2010

A visual diff for Mercurial

I have already mentioned in the past how much I like Mercurial as a distributed VCS, and how strongly I encourage anyone who is doing development (in team with others, or on your own) to seriously consider adopting it.

However, one of the features I've been greatly missing is a quick way to check which files have been changed between two revisions in a way as simple as the hg status does for the current working directory v. the last committed change: perhaps there is a way (if so, please do let me know) but it's certainly well hidden.

After a bit of tinkering with regular expressions and grep, one can easily come up with the following:
hg diff -g --nodates ${REV1} ${REV2}|grep -E '^\-{3}|^\+{3}'|grep -v '/dev/null'
and get a reasonable (if not terribly readable) list of files changed: there are obviously "duplicates," (they are actually the majority of the list's entries) but one cannot limit the RegEx to just the '- - -' or '+++', as one would miss either the new files or the deleted ones.

I then came across this nice little trick by James Falkner that, in one shot, taught me about Zenity and gave me an idea as to how achieve the optional bonus of being also able to view the changes in tkdiff.

Before we get into the actual script, a very warm recommendation about installing Zenity (why that doesn't come as standard with every Ubuntu install is now beyond me: it is so totally easy to use and mind-blowingly useful that I'm kicking myself for not having discovered it earlier).

In a standard Ubuntu desktop install, you are likely to miss a few packages: gtk+ 2.0, libglade 2.0 and libgnomecanvas 2.0, and it's not obvious which packages contain them: they are, respectively, libgtk2.0-dev, libglade2-dev and libgnomecanvas2-dev.
sudo apt-get install libgtk2.0-dev libglade2-dev libgnomecanvas2-dev
I also recommend installing GTK's documentation, the build-essential package and devhelp (the latter can be found in Applications > Programming and is a simple tool to visualize a wealth of docs and information about various libraries, including GTK+ 2.0 and Libglade):
sudo apt-get install libgtk2.0-doc devhelp build-essential
To install Zenity, the usual drill: unpack the tar file into a directory of your own chosing, then run:
$ ./configure
$ make
$ sudo make install
(note the last command to be run as root, or you won't be able to install the binaries and other files in directories owned by root).

Help about Zenity and examples as how to use it can be found in the standard Gnome Help Centre (there is typically a shortcut in the menu bar at the far right top end, and in the standard Ubuntu theme looks like a lifesaver).

I now have all the bits in place, and the script looks thus:
#!/bin/bash
# Lists the files changed from two revisions
# Usage: hgfiles 50 55
#
# Clones the current repository at -r 50 and -r 55, into two temporary directories, and, optionally, saves
# the SECOND clone in a directory of the user's choosing, possibly after the user has edited the files.
# Uses tkdiff to show the diffs and Zenity to show the dialogs
# See http://codetrips.blogspot.com/ for further information

TMPDIR=/tmp
FILES=$TMPDIR/hg_files.tmp

if [ -n "$1" ]; then
REV1="-r $1"
TMPDIR1=$TMPDIR/clone_$1
else
echo "At least one revision must be specified"
exit -1
fi

if [ -n "$2" ]; then
REV2="-r $2"
TMPDIR2=$TMPDIR/clone_$2
else
zenity --question --width=400 --text="Using current revision for diff: was this expected?"
echo "Zenity says: $?"
if [ $? == 1 ]; then
zenity --error --text="Diff canceled"
exit 0
fi
TMPDIR2=$TMPDIR/clonelatest
fi

hg diff -g --nodates ${REV1} ${REV2}|grep -E '^\-{3}|^\+{3}'|grep -v '/dev/null' > $FILES

FILELIST=`cat $FILES |java -cp /home/marco/bin com.alertavert.simple.HgDiff`

hg clone $REV1 ./ $TMPDIR1
hg clone $REV2 ./ $TMPDIR2

while [ 1 ] ; do
FILE=`zenity --list --width=700 --column="File to show diffs for" $FILELIST`
if [ -z "$FILE" ] ; then
FOLDERCOPY=`zenity --file-selection --directory --save \
--title="Select a folder to save changes"`
case $? in
0)
echo "Folder selected: $FOLDERCOPY" && cp -r --target-directory="$FOLDERCOPY" "$TMPDIR2"/*;;
1)
echo "No folder selected";;
-1)
echo "No folder selected";;
esac
rm -rf $TMPDIR1 $TMPDIR2
exit 0
fi
tkdiff $TMPDIR1/$FILE $TMPDIR2/$FILE
done
Again, not pretty, but it does the job.

The only bit of "mystery" may be that command piped just after the grep:
java -cp /home/marco/bin com.alertavert.simple.HgDiff
but that is a really trivial class to remove the "+++ a/" (or "--- b/" from each of the lines grepped from the output of hg diff:
package com.alertavert.simple;

import java.io.*;

public class HgDiff {
public static void main(String args[]) {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String line;
try {
while ((line = reader.readLine()) != null) {
if (line.startsWith("+++") || line.startsWith("+++")) {
String trimmedLine = line.substring(6);
System.out.println(trimmedLine);
}
}
} catch(IOException ex) {
// ignore
System.exit(-1);
}
}
}
It's that trivial: there's a reason why it wound up in the 'simple' package...

I could have probably achieved the same effect with some twisting of sed/awk, but honestly, it took me less time to knock this together (it was a strange feeling going back to using gedit and javac, as I really felt that really, just booting up Eclipse to create it would have taken longer!) than even look'em up on Google.

Just remember to change the -cp /home/marco/bin to reflect the folder under which you will install the .class file.
Blogged with the Flock Browser

4 comments:

  1. There is a really easy way to make a list of what files changes, that way is differed. So, to get a list of files that have changed between change sets A and B do:

    hg diff -r A -r B | differed -l

    Then feed that list.

    ReplyDelete
  2. Not differed but diffstat, i..e:

    hg diff -r A -r B | diffstat -l

    Bloody autocorrect can give interesting results :)

    ReplyDelete
  3. Thanks, Kevin - that works very nicely indeed!

    ReplyDelete
  4. Well, turns out that doesn't work so well for 'added' files.
    Diffstat adds a a/ to the filename:

    $ hg diff -r 75 -r 79|diffstat -l
    .hgtags
    AndroidManifest.xml
    a/common/META-INF/persistence.xml
    a/common/com/alertavert/receiptscan/ReceiptscanModel.gwt.xml
    a/common/com/alertavert/receiptscan/model/PictureStorage.java
    a/common/receiptscan_model.jardesc
    common/com/alertavert/receiptscan/model/Receipt.java
    default.properties
    res/values/strings.xml
    src/com/alertavert/android/applications/receiptscan/ControllerActivity.java
    src/com/alertavert/android/applications/receiptscan/ReceiptsGalleryActivity.java
    src/com/alertavert/android/applications/receiptscan/connectivity/MailSender.java
    src/com/alertavert/android/applications/receiptscan/settings/SettingsDialog.java
    src/com/alertavert/android/applications/receiptscan/storage/FileUtils.java
    src/com/alertavert/android/applications/receiptscan/ui/ReceiptsImagesAdapter.java

    and I don't seem to find an option to turn that off.

    ReplyDelete