[bugzilla-gnome-org-extensions] Initial import



commit eb01742aa11a1760a3ec226ac2ea44d9864d19f0
Author: Owen W. Taylor <otaylor fishsoup net>
Date:   Mon Sep 7 20:32:12 2009 -0400

    Initial import

 Makefile                                     |   38 ++
 docs/REVIEW_FORMAT.txt                       |   31 ++
 flattener.py                                 |   96 ++++++
 js/bug.js                                    |  209 ++++++++++++
 js/patch.js                                  |  291 ++++++++++++++++
 js/review.js                                 |  274 +++++++++++++++
 js/splinter.js                               |  388 +++++++++++++++++++++
 js/testutils.js                              |   65 ++++
 js/utils.js                                  |   38 ++
 jstest.c                                     |  473 ++++++++++++++++++++++++++
 testbugs/561745/attachments/123143           |  110 ++++++
 testbugs/561745/bug.xml                      |  120 +++++++
 testpatches/bzr-multi-file.patch             |  235 +++++++++++++
 testpatches/bzr-single-file-no-newline.patch |   34 ++
 testpatches/cvs-multi-file.patch             |  333 ++++++++++++++++++
 testpatches/git-multi-file.patch             |  143 ++++++++
 testpatches/git-one-file.patch               |  110 ++++++
 testpatches/git-plain-diff.patch             |   84 +++++
 testpatches/hg-multi-file.patch              |   50 +++
 testpatches/svn-multi-file.patch             |  117 +++++++
 tests/bug.jst                                |   45 +++
 tests/patch.jst                              |  144 ++++++++
 tests/review.jst                             |  130 +++++++
 tests/testutils.jst                          |   20 ++
 tests/utils.jst                              |   42 +++
 web/config.js.example                        |    6 +
 web/index.html                               |   46 +++
 web/jquery.min.js                            |   19 +
 web/splinter.css                             |  125 +++++++
 29 files changed, 3816 insertions(+), 0 deletions(-)
---
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..3ddb8cb
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,38 @@
+CFLAGS = -g -O2 -Wall
+CPPFLAGS := $(shell pkg-config --cflags glib-2.0 mozilla-js)
+LIBS := $(shell pkg-config --libs glib-2.0 mozilla-js)
+
+all: web/splinter.flat.js
+
+jstest: jstest.o
+       $(CC) -o jstest jstest.o $(LIBS)
+
+JS_FILES =                                     \
+       js/bug.js                               \
+       js/patch.js                             \
+       js/review.js                            \
+       js/splinter.js                          \
+       js/utils.js
+
+TESTS =                                                \
+       tests/bug.jst                           \
+       tests/patch.jst                         \
+       tests/review.jst                        \
+       tests/testutils.jst                     \
+       tests/utils.jst
+
+CLEAN_FILES =                                  \
+       *.o                                     \
+       jstest                                  \
+       web/splinter.flat.js
+
+web/splinter.flat.js: $(JS_FILES) flattener.py
+       python flattener.py js/splinter.js > $@ || rm -f $@
+
+check: jstest
+       ./jstest $(TESTS)
+
+clean:
+       rm -f $(CLEAN_FILES)
+
+.PHONY: check clean
\ No newline at end of file
diff --git a/docs/REVIEW_FORMAT.txt b/docs/REVIEW_FORMAT.txt
new file mode 100644
index 0000000..e566e70
--- /dev/null
+++ b/docs/REVIEW_FORMAT.txt
@@ -0,0 +1,31 @@
+Reviews are comments that start with the text "Review of attachment <attachment_id>:"
+
+The review can start with free form comments that apply to the whole patch.
+A line of the form ::: <filename> introduces comments about a particular file
+
+Each comment is introduced by a small amount of context, which is formatted as
+a unified diff hunk.
+
+Example:
+
+==========
+Review of attachment 123042:
+
+<overall comments>
+
+::: foo/bar/somefile.c
+@@ -264,2 +264,3 @@ gjs_invoke_c_function(JSContext      *context,
++    return_values = NULL; /* Quiet gcc warning about initialization */
+     if (n_return_values > 0) {
+         if (invoke_ok) {
+
+<comment on change >
+
+@@ -318,1 +317,1 @@ import_file(JSContext  *context,
+-    if (!finish_import(context, obj))
++    if (!finish_import(context, name))
+
+<comment on change>
+==========
+
+
diff --git a/flattener.py b/flattener.py
new file mode 100755
index 0000000..eee73b8
--- /dev/null
+++ b/flattener.py
@@ -0,0 +1,96 @@
+#!/usr/bin/python
+
+import os
+import re
+import sys
+
+CONTINUATION = r".*\n(?:^[ \t].*\n|\n)*"
+
+RE = re.compile(
+r"""
+\s*
+(?:^
+include\s*\(\s*\'([^\']+)\'\s*\)\s*; |
+(?:function\s+(\w+)\s*     (\(.*;|%(c)s^\})) |
+(?:(\w+)\.(\w+)\s*=\s*       (.*;|%(c)s^[\]\}];)) |
+(?:(?:const|let|var)\s+(\w+) (.*;|%(c)s^[\]\}];)) |
+/\*(?:[^*]+|\*[^/])*\*/ |
+//.*
+[ \t]*\n)
+""" % { 'c' : CONTINUATION },
+re.VERBOSE | re.MULTILINE)
+
+NONBLANK_RE = re.compile("\S")
+
+NAME_RE = re.compile("(?<![\w\.])\w+(?!\w)")
+
+class Flattener(object):
+    def __init__(self, outf):
+        self.outf = outf
+        self.flattened_modules = set()
+
+    def flatten(self, filename, namespace=None):
+        locals = {}
+        f = open(filename)
+        contents = f.read()
+
+        def error(pos):
+            m = NONBLANK_RE.search(contents, pos)
+            leading = contents[0:m.start()]
+            line = 1 + leading.count("\n")
+            print >>sys.stderr, "%s: %d: Unparseable content\n" % (filename, line)
+            sys.exit(1)
+
+        def add_local(name):
+            locals[name] = namespace + "." + name
+
+        def substitute_name(m):
+            name = m.group(0)
+            if name in locals:
+                return locals[name]
+            else:
+                return name
+
+        def substitute_locals(str):
+            return NAME_RE.sub(substitute_name, str)
+
+        last_end = 0
+        for m in RE.finditer(contents):
+            if m.start() != last_end:
+                error(last_end)
+
+            if m.group(1) is not None:
+                module_name = m.group(1)
+                if not module_name in self.flattened_modules:
+                    self.flattened_modules.add(module_name)
+                    print "var %s = {}" % module_name
+                    self.flatten(os.path.join("js", module_name.lower() + ".js"), module_name)
+            elif m.group(2) is not None:
+                if namespace is None:
+                    print "function %s%s" % (m.group(2), m.group(3))
+                else:
+                    add_local(m.group(2))
+                    print "%s.%s = function%s" % (namespace, m.group(2), substitute_locals(m.group(3)))
+            elif m.group(4) is not None:
+                if namespace is None:
+                    print "%s.%s = %s" % (m.group(4), m.group(5), m.group(6))
+                else:
+                    print "%s.%s.%s = %s" % (namespace, m.group(4), m.group(5), 
substitute_locals(m.group(6)))
+            elif m.group(7) is not None:
+                if namespace is None:
+                    print "var %s%s" % (m.group(7), m.group(8))
+                else:
+                    add_local(m.group(7))
+                    print "%s.%s%s" % (namespace, m.group(7), substitute_locals(m.group(8)))
+
+            last_end = m.end()
+
+        m = NONBLANK_RE.search(contents, last_end)
+        if m:
+            error(last_end)
+
+if __name__ == '__main__':
+    flattener = Flattener(sys.stderr)
+    for filename in sys.argv[1:]:
+        flattener.flatten(
+            filename)
diff --git a/js/bug.js b/js/bug.js
new file mode 100644
index 0000000..67b75f6
--- /dev/null
+++ b/js/bug.js
@@ -0,0 +1,209 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+include('Utils');
+
+// Until 2009-04, Bugzilla would use symbolic abbrevations for timezones in the XML output.
+// Afterwords it was switched to a UTC offset. We handle some of the more likely to be
+// encountered symbolic timezeones. Anything else is just handled as if it was UTC.
+// See: https://bugzilla.mozilla.org/show_bug.cgi?id=487865
+const TIMEZONES = {
+    CEST: 200,
+    CET:  100,
+    BST:  100,
+    GMT:  000,
+    UTC:  000,
+    EDT: -400,
+    EST: -500,
+    CDT: -500,
+    CST: -600,
+    MDT: -600,
+    MST: -700,
+    PDT: -700,
+    PST: -800
+};
+
+function parseDate(d) {
+    var m = /^\s*(\d+)-(\d+)-(\d+)\s+(\d+):(\d+)(?::(\d+))?\s+(?:([A-Z]{3,})|([-+]\d{3,}))\s*$/.exec(d);
+    if (!m)
+        return null;
+
+    var year = parseInt(m[1], 10);
+    var month = parseInt(m[2] - 1, 10);
+    var day = parseInt(m[3], 10);
+    var hour = parseInt(m[4], 10);
+    var minute = parseInt(m[5], 10);
+    var second = m[6] ? parseInt(m[6], 10) : 0;
+
+    var tzoffset = 0;
+    if (m[7]) {
+        if (m[7] in TIMEZONES)
+            tzoffset = TIMEZONES[m[7]];
+    } else {
+        tzoffset = parseInt(m[8], 10);
+    }
+
+    var unadjustedDate = new Date(Date.UTC(m[1], m[2] - 1, m[3], m[4], m[5]));
+
+    // 430 => 4:30. Easier to do this computation for only positive offsets
+    var sign = tzoffset < 0 ? -1 : 1;
+    tzoffset *= sign;
+    var adjustmentHours = Math.floor(tzoffset/100);
+    var adjustmentMinutes = tzoffset - adjustmentHours * 100;
+
+    return new Date(unadjustedDate.getTime() -
+                    sign * adjustmentHours * 3600000 -
+                    sign * adjustmentMinutes * 60000);
+}
+
+function _formatWho(name, email) {
+    if (name && email)
+        return name + " <" + email + ">";
+    else if (name)
+        return name;
+    else
+        return email;
+}
+
+function Attachment(bug, id) {
+    this._init(bug, id);
+}
+
+Attachment.prototype = {
+    _init : function(bug, id) {
+        this.bug = bug;
+        this.id = id;
+    }
+};
+
+function Comment(bug) {
+    this._init(bug);
+}
+
+Comment.prototype = {
+    _init : function(bug) {
+        this.bug = bug;
+    },
+
+    getWho : function() {
+        return _formatWho(this.whoName, this.whoEmail);
+    }
+};
+
+function Bug() {
+    this._init();
+}
+
+Bug.prototype = {
+    _init : function() {
+        this.attachments = [];
+        this.comments = [];
+    },
+
+    getReporter : function() {
+        return _formatWho(this.reporterName, this.reporterEmail);
+    }
+};
+
+// DOM is not available in the non-Browser environment we use for test cases
+// So we use E4X, which is supported by Spidermonkey. It's not supported
+// by most browsers.
+Bug.fromText = function(bugText) {
+    var bug = new Bug();
+
+    // We need to skip the XML and DOCTYPE declarations that E4X doesn't handle
+    var xmlstart = bugtext.indexOf("<bugzilla");
+    var bugzillaNode = new XML(bugtext.substring(xmlstart));
+    var bugNode = bugzillaNode.bug;
+
+    bug.id = parseInt(bugNode.bug_id);
+    bug.token = bugNode.token;
+    bug.shortDesc = Utils.strip(bugNode.short_desc);
+    bug.creationDate = parseDate(bugNode.creation_ts);
+    bug.reporterName = Utils.strip(bugNode.reporter)  name;
+    bug.reporterEmail = Utils.strip(bugNode.reporter);
+    var longDescNodes = bugNode.long_desc;
+    for (var i = 0; i < longDescNodes.length(); i++) {
+        var longDescNode = longDescNodes[i];
+        var comment = new Comment(bug);
+
+        comment.whoName = Utils.strip(longDescNode who  name);
+        comment.whoEmail = Utils.strip(longDescNode.who);
+        comment.date = parseDate(longDescNode.bug_when);
+        comment.text = longDescNode.thetext;
+
+        bug.comments.push(comment);
+    }
+
+    var attachmentNodes = bugNode.attachment;
+    for (var i = 0; i < attachmentNodes.length(); i++) {
+        var attachmentNode = attachmentNodes[i];
+        var attachid = parseInt(attachmentNode.attachid);
+        var attachment = new Attachment(bug, attachid);
+
+        attachment.description = Utils.strip(attachmentNode.desc);
+        attachment.filename = Utils.strip(attachmentNode.filename);
+        attachment.date = parseDate(attachmentNode.date);
+        attachment.status = Utils.strip(attachmentNode.status);
+        if (attachment.status == "")
+            attachment.status = null;
+        attachment.token = Utils.strip(attachmentNode.token);
+        if (attachment.token == "")
+            attachment.token = null;
+        attachment.isPatch = attachmentNode  ispatch == "1";
+        attachment.isObsolete = attachmentNode  isobsolete == "1";
+        attachment.isPrivate = attachmentNode  isprivate == "1";
+
+        bug.attachments.push(attachment);
+    }
+    return bug;
+};
+
+// In the browser environment we use JQuery to parse the DOM tree
+// for the XML document for the bug
+Bug.fromDOM = function(xml) {
+    var bug = new Bug();
+
+    $(xml).children('bugzilla').children('bug').each(function() {
+        bug.id = parseInt($(this).children('bug_id').text());
+        bug.token = $(this).children('token').text();
+        bug.shortDesc = Utils.strip($(this).children('short_desc').text());
+        bug.creationDate = parseDate($(this).children('creation_ts').text());
+
+        $(this).children('reporter').each(function() {
+            bug.reporterEmail = Utils.strip($(this).text());
+            bug.reporterName = Utils.strip($(this).attr('name'));
+        });
+        $(this).children('long_desc').each(function() {
+            var comment = new Comment(bug);
+
+            $(this).children('who').each(function() {
+                comment.whoEmail = Utils.strip($(this).text());
+                comment.whoName = Utils.strip($(this).attr('name'));
+            });
+            comment.date = parseDate($(this).children('bug_when').text());
+            comment.text = $(this).children('thetext').text();
+
+            bug.comments.push(comment);
+        });
+        $(this).children('attachment').each(function() {
+            var attachid = parseInt($(this).children('attachid').text());
+            var attachment = new Attachment(bug, attachid);
+
+            attachment.description = Utils.strip($(this).children('desc').text());
+            attachment.filename = Utils.strip($(this).children('filename').text());
+            attachment.date = parseDate($(this).children('date').text());
+            attachment.status = Utils.strip($(this).children('status').text());
+            if (attachment.status == "")
+                attachment.status = null;
+            attachment.token = Utils.strip($(this).children('token').text());
+            if (attachment.token == "")
+                attachment.token = null;
+            attachment.isPatch = $(this).attr('ispatch') == "1";
+            attachment.isObsolete = $(this).attr('isobsolete') == "1";
+            attachment.isPrivate = $(this).attr('isprivate') == "1";
+
+            bug.attachments.push(attachment);
+        });
+    });
+
+    return bug;
+};
diff --git a/js/patch.js b/js/patch.js
new file mode 100644
index 0000000..4614747
--- /dev/null
+++ b/js/patch.js
@@ -0,0 +1,291 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+include('Utils');
+
+// A patch is stored as:
+// Patch ::= File *
+// File ::= Hunk *
+// Hunk ::= Line *
+//
+// The lines of a hunk are the lines of the two-column display of the hunk
+// So, e.g., the unified diff hunk:
+//
+// @@ -4,8 +4,7
+//  import time
+// -from gettext import ngettext
+//  from threading import Thread
+// +
+//  import gobject
+//  import gtk
+// -import gc
+// -import sys
+// +from gettext import ngettext
+//
+// Is represented as:
+//
+//    4 import time                      4 import time
+// -  5 from gettext import ngettext
+//    6 from threading import Thread     5 from threading import Thread
+//                                     + 6
+//    7 import gobject                   7 import gobject
+//    8 import gtk                       8 import gtk
+// !  9 import gc                      ! 9 from gettext import ngettext
+// ! 10 import sys                     !
+//   11                                 10
+//
+// Conceptually the hunk is made up of context lines - lines that are unchanged
+// by the patch and "segments" - series of lines that are changed by the patch
+// Each line is stored as an array:
+//
+//  [old_text, new_text, flags]
+//
+// old_text or new_text can be null (but not both). Flags are:
+const ADDED         = 1 << 0; // Part of a pure addition segment
+const REMOVED       = 1 << 1; // Part of a pure removal segment
+const CHANGED       = 1 << 2; // Part of some other segmnet
+const NEW_NONEWLINE = 1 << 3; // Old line doesn't end with \n
+const OLD_NONEWLINE = 1 << 4; // New line doesn't end with \n
+
+// Note that we use this constructor both when parsing a patch and when
+// parsing a review in review.js
+function Hunk(oldStart, oldCount, newStart, newCount, text, parseComment) {
+    this._init(oldStart, oldCount, newStart, newCount, text, parseComment);
+}
+
+Hunk.prototype = {
+    _init : function(oldStart, oldCount, newStart, newCount, text, parseComment) {
+        if (parseComment == null)
+            parseComment = false;
+
+        var rawlines = text.split("\n");
+        if (rawlines.length > 0 && Utils.strip(rawlines[rawlines.length - 1]) == "")
+            rawlines.pop(); // Remove trailing element from final \n
+
+        this.oldStart = oldStart;
+        this.oldCount = oldCount;
+        this.newStart = newStart;
+        this.newCount = newCount;
+        this.comment = null;
+
+        var lines = [];
+        var totalOld = 0;
+        var totalNew = 0;
+
+        var currentStart = -1;
+        var currentOldCount = 0;
+        var currentNewCount = 0;
+
+        // A segment is a series of lines added/removed/changed with no intervening
+        // unchanged lines. We make the classification of ADDED/REMOVED/CHANGED
+        // in the flags for the entire segment
+        function startSegment() {
+            if (currentStart < 0) {
+                currentStart = lines.length;
+            }
+        }
+
+        function endSegment() {
+            if (currentStart >= 0) {
+                if (currentOldCount > 0 && currentNewCount > 0) {
+                    for (var j = currentStart; j < lines.length; j++) {
+                        lines[j][2] &= ~(ADDED | REMOVED);
+                        lines[j][2] |= CHANGED;
+                    }
+                }
+
+                currentStart = -1;
+                currentOldCount = 0;
+                currentNewCount = 0;
+            }
+        }
+
+        for (var i = 0; i < rawlines.length; i++) {
+            var line = rawlines[i];
+            var op = line[0];
+            var strippedLine = line.substring(1);
+            var noNewLine = 0;
+            if (i + 1 < rawlines.length && rawlines[i + 1].substr(0, 1) == '\\') {
+                noNewLine = op == '-' ? OLD_NONEWLINE : NEW_NONEWLINE;
+            }
+
+            if (op == ' ') {
+                endSegment();
+                totalOld++;
+                totalNew++;
+                lines.push([strippedLine, strippedLine, 0]);
+            } else if (op == '-') {
+                totalOld++;
+                startSegment();
+                lines.push([strippedLine, null, REMOVED | noNewLine]);
+                currentOldCount++;
+            } else if (op == '+') {
+                totalNew++;
+                startSegment();
+                if (currentStart + currentNewCount >= lines.length) {
+                    lines.push([null, strippedLine, ADDED | noNewLine]);
+                } else {
+                    lines[currentStart + currentNewCount][1] = strippedLine;
+                    lines[currentStart + currentNewCount][2] |= ADDED | noNewLine;
+                }
+                currentNewCount++;
+            } else if (op == '\\') {
+                // Handled with preceding line
+            } else {
+                Utils.assertNotReached();
+            }
+
+            // When parsing a review, we stop when we've gotten all the lines described
+            // in the chunk header - anything after that is the comment
+            if (parseComment && totalOld >= oldCount && totalNew >= newCount) {
+                this.comment = rawlines.slice(i + 1).join("\n");
+                break;
+            }
+        }
+
+        endSegment();
+
+        // git mail-formatted patches end with --\n<git version> like a signature
+        // This is troublesome since it looks like a subtraction at the end
+        // of last hunk of the last file. Handle this specifically rather than
+        // generically stripping excess lines to be kind to hand-edited patches
+        if (totalOld > oldCount &&
+            ((lines[lines.length - 1][2] & REMOVED) != 0) &&
+            lines[lines.length - 1][0][0] == '-')
+        {
+            lines.pop();
+        }
+
+        this.lines = lines;
+    },
+
+    iterate : function(cb) {
+        var oldLine = this.oldStart - 1;
+        var newLine = this.newStart - 1;
+        for (var i = 0; i < this.lines.length; i++) {
+            var line = this.lines[i];
+            if (line[0] != null)
+                oldLine++;
+            if (line[1] != null)
+                newLine++;
+            cb(this.location + i, oldLine, line[0], newLine, line[1], line[2], line);
+        }
+    }
+};
+
+function File(filename, hunks) {
+    this._init(filename, hunks);
+}
+
+File.prototype = {
+    _init : function(filename, hunks) {
+        this.filename = filename;
+        this.hunks = hunks;
+
+        var l = 0;
+        for (var i = 0; i < this.hunks.length; i++) {
+            var hunk = this.hunks[i];
+            hunk.location = l;
+            l += hunk.lines.length;
+        }
+    },
+
+    // A "location" is just a linear index into the lines of the patch in this file
+    getLocation : function(oldLine, newLine) {
+        for (var i = 0; i < this.hunks.length; i++) {
+            var hunk = this.hunks[i];
+            if (hunk.oldStart > oldLine)
+                continue;
+
+            if (oldLine < hunk.oldStart + hunk.oldCount) {
+                var location = -1;
+                hunk.iterate(function(loc, oldl, oldText, newl, newText, flags) {
+                                 if (oldl == oldLine && newl == newLine)
+                                     location = loc;
+                             });
+
+                if (location != -1)
+                    return location;
+            }
+        }
+
+        throw "Bad oldLine,newLine: " + oldLine + "," + newLine;
+    },
+
+    getHunk : function(location) {
+        for (var i = 0; i < this.hunks.length; i++) {
+            var hunk = this.hunks[i];
+            if (location >= hunk.location && location < hunk.location + hunk.lines.length)
+                return hunk;
+        }
+
+        throw "Bad location: " + location;
+    },
+
+    toString : function() {
+        return "File(" + this.filename + ")";
+    }
+};
+
+// Matches the start unified diffs for a file as produced by different version control tools
+const FILE_START_RE = /^(?:(?:Index|index|===|RCS|diff).*\n)*---[ \t]*(\S+).*\n\+\+\+[ 
\t]*(\S+).*\n(?=@@)/mg;
+
+// Hunk start: @@ -23,12 +30,11 @@
+// Followed by: lines beginning with [ +\-]
+const HUNK_RE = /^@@[ \t]+-(\d+),(\d+)[ \t]+\+(\d+),(\d+)[ \t]+@@.*\n((?:[ +\\-].*\n)*)/mg;
+
+function Patch(text) {
+    this._init(text);
+}
+
+Patch.prototype = {
+    // cf. parsing in Review.Review.parse()
+    _init : function(text) {
+        // Canonicalize newlines to simplify the following
+        if (/\r/.test(text))
+            text = text.replace(/(\r\n|\r|\n)/g, "\n");
+
+        this.files = [];
+
+        var m = FILE_START_RE.exec(text);
+
+        while (m != null) {
+            // git and hg show a diff between a/foo/bar.c and b/foo/bar.c
+            var filename;
+            if (/^a\//.test(m[1]) && /^b\//.test(m[2]))
+                filename = m[1].substring(2);
+            else
+                filename = m[1];
+
+            var hunks = [];
+            var pos = FILE_START_RE.lastIndex;
+            while (true) {
+                HUNK_RE.lastIndex = pos;
+                var m2 = HUNK_RE.exec(text);
+                if (m2 == null || m2.index != pos)
+                    break;
+
+                pos = HUNK_RE.lastIndex;
+                var oldStart = parseInt(m2[1]);
+                var oldCount = parseInt(m2[2]);
+                var newStart = parseInt(m2[3]);
+                var newCount = parseInt(m2[4]);
+
+                var hunk = new Hunk(oldStart, oldCount, newStart, newCount, m2[5]);
+                hunks.push(new Hunk(oldStart, oldCount, newStart, newCount, m2[5]));
+            }
+
+            this.files.push(new File(filename, hunks));
+
+            FILE_START_RE.lastIndex = pos;
+            m = FILE_START_RE.exec(text);
+        }
+    },
+
+    getFile : function(filename) {
+        for (var i = 0; i < this.files.length; i++) {
+            if (this.files[i].filename == filename)
+                return this.files[i];
+        }
+
+        return null;
+    }
+};
diff --git a/js/review.js b/js/review.js
new file mode 100644
index 0000000..80743d6
--- /dev/null
+++ b/js/review.js
@@ -0,0 +1,274 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+include('Patch');
+include('Utils');
+
+function _removeFromArray(a, element) {
+    for (var i = 0; i < a.length; i++) {
+        if (a[i] === element) {
+            a.splice(i, 1);
+            return;
+        }
+    }
+}
+
+function Comment(file, location, comment) {
+    this._init(file, location, comment);
+}
+
+Comment.prototype = {
+    _init : function(file, location, comment) {
+        this.file = file;
+        this.location = location;
+        this.comment = comment;
+    },
+
+    remove : function() {
+        var hunk = this.file.patchFile.getHunk(this.location);
+        var line = hunk.lines[this.location - hunk.location];
+        _removeFromArray(this.file.comments, this);
+        _removeFromArray(line.reviewComments, this);
+    }
+};
+
+function _noNewLine(flags, flag) {
+    return ((flags & flag) != 0) ? "\n\ No newline at end of file" : "";
+}
+
+function _compareSegmentLines(a, b) {
+    var op1 = a.substr(0, 1);
+    var op2 = b.substr(0, 1);
+    if (op1 == op2)
+        return 0;
+    else
+        return op1 == '-' ? -1 : 1;
+}
+
+function File(review, patchFile) {
+    this._init(review, patchFile);
+}
+
+File.prototype = {
+    _init : function(review, patchFile) {
+        this.review = review;
+        this.patchFile = patchFile;
+        this.comments = [];
+    },
+
+    addComment : function(location, comment) {
+        var hunk = this.patchFile.getHunk(location);
+        var line = hunk.lines[location - hunk.location];
+        comment = new Comment(this, location, comment);
+        if (line.reviewComments == null)
+            line.reviewComments = [];
+        line.reviewComments.push(comment);
+        for (var i = 0; i <= this.comments.length; i++) {
+            if (i == this.comments.length || this.comments[i].location > location) {
+                this.comments.splice(i, 0, comment);
+                break;
+            } else if (this.comments[i].location == location) {
+                throw "Two comments at the same location";
+                break;
+            }
+        }
+    },
+
+    getComment : function(location, comment) {
+        for (var i = 0; i < this.comments.length; i++)
+            if (this.comments[i].location == location)
+                return this.comments[i];
+
+        return null;
+    },
+
+    toString : function() {
+        var str = '::: ';
+        str += this.patchFile.filename;
+        str += '\n';
+        var first = true;
+
+        var lastCommentLocation = 0;
+        for (var i = 0; i < this.comments.length; i++) {
+            if (first)
+                first = false;
+            else
+                str += '\n';
+            var comment = this.comments[i];
+            var hunk = this.patchFile.getHunk(comment.location);
+            var context = Math.min(comment.location - lastCommentLocation - 1,
+                                   comment.location - hunk.location,
+                                   2);
+
+            var patchOldStart, patchNewStart;
+            var patchOldLines = 0;
+            var patchNewLines = 0;
+            var patchLines = [];
+
+            hunk.iterate(function(loc, oldLine, oldText, newLine, newText, flags) {
+                             if (loc == comment.location - context) {
+                                 patchOldStart = oldLine;
+                                 patchNewStart = newLine;
+                             }
+
+                             if (loc >= comment.location - context && loc <= comment.location) {
+                                 if (oldText != null)
+                                     patchOldLines++;
+                                 if (newText != null)
+                                     patchNewLines++;
+                                 if ((flags & (Patch.ADDED | Patch.REMOVED | Patch.CHANGED)) != 0) {
+                                     if (oldText != null)
+                                         patchLines.push('-' + oldText +_noNewLine(flags, 
Patch.OLD_NONEWLINE));
+                                     if (newText != null)
+                                         patchLines.push('+' + newText + _noNewLine(flags, 
Patch.NEW_NONEWLINE));
+                                 } else {
+                                     patchLines.push(' ' + oldText + _noNewLine(flags, Patch.OLD_NONEWLINE | 
Patch.NEW_NONEWLINE));
+                                 }
+                             }
+                         });
+
+            var segStart = 0;
+            for (var k = 0; k <= patchLines.length; k++) {
+                if (k == patchLines.length || patchLines[k].substr(0, 1) == ' ') {
+                    if (segStart < k) {
+                        var segmentLines = patchLines.slice(segStart, k);
+                        segmentLines.sort(_compareSegmentLines);
+                        for (var l = 0; l < segmentLines.length; l++)
+                            patchLines[segStart + l] = segmentLines[l];
+                    }
+                    segStart = k + 1;
+                }
+            }
+
+            while (Utils.strip(patchLines[0]) == '') {
+                patchLines.shift();
+                patchOldStart++;
+                patchNewStart++;
+                patchOldLines--;
+                patchNewLines--;
+            }
+
+            str += '@@ -' + patchOldStart + ',' + patchOldLines + ' +' + patchNewStart + ',' + patchNewLines 
+ ' @@\n';
+            str += patchLines.join("\n");
+            str += "\n\n";
+            str += comment.comment;
+            str += "\n";
+
+            lastCommentLocation = comment.location;
+        }
+
+        return str;
+    }
+};
+
+function Review(patch) {
+    this._init(patch);
+}
+
+// Indicates start of review comments about a file
+// ::: foo/bar.c
+const FILE_START_RE = /^:::[ \t]+(\S+)[ \t]*\n/mg;
+
+
+// This is like Patch.HUNK_RE for the starting line, but differs in that it
+// includes trailing lines that are not patch lines up to the next hunk or file
+// (the trailing lines will be split out as the coment.)
+//
+// Hunk start: @@ -23,12 +30,11 @@
+// Followed by: lines that don't start with @@ or :::
+const HUNK_RE = /^@@[ \t]+-(\d+),(\d+)[ \t]+\+(\d+),(\d+)[ \t]+@@.*\n((?:(?!@@|:::).*\n?)*)/mg;
+
+Review.prototype = {
+    _init : function(patch) {
+        this.date = null;
+        this.patch = patch;
+        this.intro = null;
+        this.files = [];
+
+        for (var i = 0; i < patch.files.length; i++) {
+            this.files.push(new File(this, patch.files[i]));
+        }
+    },
+
+    // cf. parsing in Patch.Patch._init()
+    parse : function(text) {
+       FILE_START_RE.lastIndex = 0;
+        var m = FILE_START_RE.exec(text);
+
+        var intro;
+        if (m != null) {
+            this.setIntro(text.substr(0, m.index));
+        } else{
+            this.setIntro(text);
+            return;
+        }
+
+        while (m != null) {
+            var filename = m[1];
+            var file = this.getFile(filename);
+            if (file == null)
+                throw "Review refers to filename '" + filename + "' not in reviewed Patch.";
+
+            var pos = FILE_START_RE.lastIndex;
+
+            while (true) {
+                HUNK_RE.lastIndex = pos;
+                var m2 = HUNK_RE.exec(text);
+                if (m2 == null || m2.index != pos)
+                    break;
+
+                pos = HUNK_RE.lastIndex;
+                var oldStart = parseInt(m2[1]);
+                var oldCount = parseInt(m2[2]);
+                var newStart = parseInt(m2[3]);
+                var newCount = parseInt(m2[4]);
+
+                var hunk = new Patch.Hunk(oldStart, oldCount, newStart, newCount, m2[5], true);
+
+                var location = file.patchFile.getLocation(hunk.oldStart + hunk.oldCount - 1,
+                                                          hunk.newStart + hunk.newCount - 1);
+                file.addComment(location, Utils.strip(hunk.comment));
+            }
+
+            FILE_START_RE.lastIndex = pos;
+            m = FILE_START_RE.exec(text);
+        }
+    },
+
+    setIntro : function(intro) {
+        intro = Utils.strip(intro);
+        this.intro = intro != "" ? intro : null;
+    },
+
+    getFile : function(filename) {
+        for (var i = 0; i < this.files.length; i++) {
+            if (this.files[i].patchFile.filename == filename)
+                return this.files[i];
+        }
+
+        return null;
+    },
+
+    // Making toString() serialize to our seriaization format is maybe a bit sketchy
+    // But the serialization format is designed to be human readable so it works
+    // pretty well.
+    toString : function() {
+        var str = '';
+        if (this.intro != null) {
+            str += Utils.strip(this.intro);
+            str += '\n';
+        }
+
+       var first = this.intro == null;
+        for (var i = 0; i < this.files.length; i++) {
+            var file = this.files[i];
+            if (file.comments.length > 0) {
+               if (first)
+                    first = false;
+               else
+                    str += '\n';
+                str += file.toString();
+            }
+        }
+
+        return str;
+    }
+};
diff --git a/js/splinter.js b/js/splinter.js
new file mode 100644
index 0000000..4de10ca
--- /dev/null
+++ b/js/splinter.js
@@ -0,0 +1,388 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+include('Bug');
+include('Patch');
+include('Review');
+
+var attachmentId;
+var theBug;
+var theAttachment;
+var thePatch;
+var theReview;
+
+const ADD_COMMENT_SUCCESS = /<title>\s*Bug[\S\s]*processed\s*<\/title>/;
+const UPDATE_ATTACHMENT_SUCCESS = /<title>\s*Changes\s+Submitted/;
+
+function updateAttachmentStatus(attachment, newStatus, success, failure) {
+    var data = {
+        action: 'update',
+        id: attachment.id,
+        description: attachment.description,
+        filename: attachment.filename,
+        ispatch: attachment.isPatch ? 1 : 0,
+        isobsolete: attachment.isObsolete ? 1 : 0,
+        isprivate: attachment.isPrivate ? 1 : 0,
+        'attachments.status': newStatus
+    };
+
+    if (attachment.token)
+        data.token = attachment.token;
+
+    $.ajax({
+               data: data,
+               dataType: 'text',
+               error: function(xmlHttpRequest, textStatus, errorThrown) {
+                   failure();
+               },
+               success: function(data, textStatus) {
+                   if (data.search(UPDATE_ATTACHMENT_SUCCESS) != -1) {
+                       success();
+                   } else {
+                       failure();
+                   }
+               },
+               type: 'POST',
+               url: "/attachment.cgi"
+           });
+}
+
+function addComment(bug, comment, success, failure) {
+    var data = {
+        id: bug.id,
+        comment: comment
+    };
+
+    if (bug.token)
+        data.token = bug.token;
+
+    $.ajax({
+               data: data,
+               dataType: 'text',
+               error: function(xmlHttpRequest, textStatus, errorThrown) {
+                   failure();
+               },
+               success: function(data, textStatus) {
+                   if (data.search(ADD_COMMENT_SUCCESS) != -1) {
+                       success();
+                   } else {
+                       failure();
+                   }
+               },
+               type: 'POST',
+               url: "/process_bug.cgi"
+           });
+}
+
+function saveReview() {
+    theReview.setIntro($("#myComment").val());
+
+    var comment = "Review of attachment " + attachmentId + ":\n\n" + theReview;
+
+    var newStatus = null;
+    if (theAttachment.status && $("#attachmentStatus").val() != theAttachment.status) {
+        newStatus = $("#attachmentStatus").val();
+    }
+
+    function success() {
+        alert("Succesfully published the review.");
+    }
+
+    function error(message) {
+        alert(message);
+    }
+
+    addComment(theBug, comment,
+               function(detail) {
+                   if (newStatus)
+                       updateAttachmentStatus(theAttachment, newStatus,
+                                              success,
+                                              function() {
+                                                  error("Published review; patch status could not be 
updated.");
+                                              });
+                   else
+                       success();
+               },
+               function(detail) {
+                   error("Failed to publish review.");
+               });
+}
+
+function getQueryParams() {
+    var query = window.location.search.substring(1);
+    if (query == null || query == "")
+        return {};
+
+    var components = query.split(/&/);
+
+    var params = {};
+    var i;
+    for (i = 0; i < components.length; i++) {
+        var component = components[i];
+        var m = component.match(/([^=]+)=(.*)/);
+        if (m)
+            params[m[1]] = decodeURIComponent(m[2]);
+    }
+
+    return params;
+}
+
+
+function saveComment(row, editorQuery, file, location) {
+    var reviewFile = theReview.getFile(file.filename);
+    var comment = reviewFile.getComment(location);
+
+    var value = Utils.strip(editorQuery.find("textarea").val());
+    if (value != "") {
+        if (comment)
+            comment.comment = value;
+        else
+            reviewFile.addComment(location, value);
+
+        $("<tr class='my-comment'><td colSpan='3'>"
+          + "<div></div>"
+          + "</td></tr>")
+            .find("div").text(value).end()
+            .insertBefore(editorQuery)
+            .dblclick(function() {
+                          insertCommentEditor(row);
+                      });
+    } else {
+        if (comment)
+            comment.remove();
+    }
+
+    editorQuery.remove();
+}
+
+function insertCommentEditor(row) {
+    var file = $(row).data('patchFile');
+    var location = $(row).data('patchLocation');
+
+    var insertAfter = row;
+    while (insertAfter.nextSibling) {
+        if (insertAfter.nextSibling.className == "comment-editor")
+            return;
+        if (insertAfter.nextSibling.className == "my-comment") {
+            $(insertAfter.nextSibling).remove();
+            if (!insertAfter.nextSibling)
+                break;
+        }
+        if (insertAfter.nextSibling.className != "comment")
+            break;
+        insertAfter = insertAfter.nextSibling;
+    }
+
+    var reviewFile = theReview.getFile(file.filename);
+    var comment = reviewFile.getComment(location);
+    var editorRow = $("<tr class='comment-editor'><td colSpan='3'>"
+                      + "<div>"
+                      + "<textarea></textarea>"
+                      + "</div>"
+                      + "</td></tr>");
+    editorRow.insertAfter(insertAfter);
+    editorRow.find('textarea')
+        .text(comment ? comment.comment : "")
+        .blur(function() {
+                  saveComment(row, editorRow, file, location);
+              })
+        .each(function() { this.focus(); });
+}
+
+function EL(element, cls, text) {
+    var e = document.createElement(element);
+    if (text != null)
+        e.appendChild(document.createTextNode(text));
+    if (cls)
+        e.className = cls;
+
+    return e;
+}
+
+function addPatchFile(file) {
+    var fileDiv = $("<div></div>").appendTo("#files");
+
+    $("<div class='file-label'><span></span></div/>")
+        .find("span").text(file.filename).end()
+        .appendTo(fileDiv);
+
+    tbody = $(fileDiv).append("<table class='file-table'>"
+                              + "<col class='old-column'></col>"
+                              + "<col class='middle-column'></col>"
+                              + "<col class='new-column'></col>"
+                              + "<tbody></tbody>"
+                              + "</table>").find("tbody").get(0);
+    for (var i = 0; i  < file.hunks.length; i++) {
+        var hunk = file.hunks[i];
+        var hunkHeader = EL("tr", "hunk-header");
+        tbody.appendChild(hunkHeader);
+        var hunkCell = EL("td", "hunk-cell",
+                          "Lines " + hunk.oldStart + "-" + (hunk.oldStart + hunk.oldCount - 1));
+        hunkCell.colSpan = 3;
+        hunkHeader.appendChild(hunkCell);
+
+        hunk.iterate(function(loc, oldLine, oldText, newLine, newText, flags, line) {
+                         var tr = document.createElement("tr");
+
+                         var oldStyle = "";
+                         var newStyle = "";
+                         if ((flags & Patch.CHANGED) != 0)
+                             oldStyle = newStyle = "changed-line";
+                         else if ((flags & Patch.REMOVED) != 0)
+                         oldStyle = "removed-line";
+                         else if ((flags & Patch.ADDED_LINE) != 0)
+                         newStyle = "added-line";
+
+                         if (oldText != null) {
+                             tr.appendChild(EL("td", "old-line " + oldStyle,
+                                               oldText != "" ? oldText : "\u00a0"));
+                             oldLine++;
+                         } else {
+                             tr.appendChild(EL("td", "old-line"));
+                         }
+
+                         tr.appendChild(EL("td", "line-middle"));
+
+                         if (newText != null) {
+                             tr.appendChild(EL("td", "new-line " + newStyle,
+                                               newText != "" ? newText : "\u00a0"));
+                             newLine++;
+                         } else {
+                             tr.appendChild(EL("td", "new-line"));
+                         }
+
+                         $(tr).data('patchFile', file);
+                         $(tr).data('patchLocation', loc);
+                         $(tr).dblclick(function() {
+                                            insertCommentEditor(this);
+                                        });
+
+                         tbody.appendChild(tr);
+
+                         if (line.reviewComments != null) {
+                             for (var k = 0; k < line.reviewComments.length; k++) {
+                                 var comment = line.reviewComments[k];
+
+                                 $("<tr class='comment'><td colSpan='3'>"
+                                   + "<div></div>"
+                                   + "</td></tr>")
+                                     .find("div").text(comment.comment).end()
+                                     .appendTo(tbody);
+                             }
+                         }
+                     });
+    }
+}
+
+var REVIEW_RE = /^\s*review\s+of\s+attachment\s+(\d+)\s*:\s*/i;
+
+function start(xml) {
+    $("#loading").hide();
+    $("#headers").show();
+    $("#controls").show();
+    $("#files").show();
+
+    var i;
+
+    for (i = 0; i < configAttachmentStatuses.length; i++) {
+        $("<option></option")
+            .text(configAttachmentStatuses[i])
+            .appendTo($("#attachmentStatus"));
+    }
+
+    $("#bugId").text(theBug.id);
+    $("#bugShortDesc").text(theBug.shortDesc);
+    $("#bugReporter").text(theBug.getReporter());
+    $("#bugCreationDate").text(Utils.formatDate(theBug.creationDate));
+
+    for (i = 0; i < theBug.attachments.length; i++) {
+        var attachment = theBug.attachments[i];
+        if (attachment.id == attachmentId) {
+            theAttachment = attachment;
+
+            $("#attachmentId").text(attachment.id);
+            $("#attachmentDesc").text(attachment.description);
+            $("#attachmentDate").text(Utils.formatDate(attachment.date));
+            if (attachment.status != null)
+                $("#attachmentStatus").val(attachment.status);
+            else
+                $("#attachmentStatusSpan").hide();
+
+            break;
+        }
+    }
+
+    for (i = 0; i < theBug.comments.length; i++) {
+        var comment = theBug.comments[i];
+        var m = REVIEW_RE.exec(comment.text);
+
+        if (m && parseInt(m[1]) == attachmentId) {
+            var review = new Review.Review(thePatch);
+            review.parse(comment.text.substr(m[0].length));
+
+            $("<div class='review'>"
+              + "<div class='review-inner'>"
+              + "<div><span class='reviewer'></span> - <span class='review-date'></span></div>"
+              + "<div class='review-intro'></div>"
+              + "</div>"
+              + "</div>")
+                .find(".reviewer").text(comment.getWho()).end()
+                .find(".review-date").text(Utils.formatDate(comment.date)).end()
+                .find(".review-intro").text(review.intro? review.intro : "").end()
+                .appendTo("#oldReviews");
+
+        }
+    }
+
+    for (i = 0; i < thePatch.files.length; i++)
+        addPatchFile(thePatch.files[i]);
+
+    $("#saveButton").click(saveReview);
+}
+
+function gotBug(xml) {
+    theBug = Bug.Bug.fromDOM(xml);
+
+    if (theBug !== undefined && thePatch !== undefined)
+        start();
+}
+
+function gotAttachment(text) {
+    thePatch = new Patch.Patch(text);
+    theReview = new Review.Review(thePatch);
+
+    if (theBug !== undefined && thePatch !== undefined)
+        start();
+}
+
+function init() {
+    var params = getQueryParams();
+    var bug_id;
+
+   if (params.bug) {
+        bug_id = parseInt(params.bug);
+    }
+    if (bug_id === undefined || isNaN(bug_id)) {
+        alert("Must specify a valid bug ID");
+        return;
+    }
+
+   if (params.attachment) {
+        attachmentId = parseInt(params.attachment);
+    }
+    if (attachmentId === undefined || isNaN(attachmentId)) {
+        alert("Must specify a valid attachment ID");
+        return;
+    }
+
+    $.get("/show_bug.cgi",
+          {
+              id: bug_id,
+              ctype: 'xml',
+              excludefield: 'attachmentdata'
+           },
+          gotBug, "xml");
+    $.get("/attachment.cgi",
+          {
+              id: attachmentId
+           },
+          gotAttachment, "text");
+}
diff --git a/js/testutils.js b/js/testutils.js
new file mode 100644
index 0000000..93e6e7c
--- /dev/null
+++ b/js/testutils.js
@@ -0,0 +1,65 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+include('Utils');
+
+function assertEquals(expected, value) {
+    if (expected != value) {
+        throw new Error("Assertion failed: expected '" + expected + "' got '" + value + "'");
+    }
+}
+
+function assertDateEquals(expected, value) {
+    if ((expected != null ? expected.getTime() : null) !=
+        (value != null ? value.getTime() : null)) {
+        throw new Error("Assertion failed: expected '" + expected + "' got '" + value + "'");
+    }
+}
+
+const PAD = '                                                                                ';
+function lalign(str, len) {
+    if (str.length <= len)
+        return str + PAD.substr(0, len - str.length);
+    else
+        return str.substr(0, len);
+}
+
+function ralign(str, len) {
+    if (str.length <= len)
+        return PAD.substr(0, len - str.length) + str;
+    else
+        return str.substr(str.length - len);
+}
+
+function table(template, data) {
+    var i, j, row;
+
+    let ncols = template.length;
+
+    let widths = new Array(ncols);
+    for (j = 0; j < ncols; j++)
+        widths[j] = 0;
+
+    for (i = 0; i < data.length; i++) {
+        row = data[i];
+        for (j = 0; j < ncols; j++) {
+            widths[j] = Math.max(widths[j], ("" + row[j]).length);
+        }
+    }
+
+    var result = '';
+    for (i = 0; i < data.length; i++) {
+        row = data[i];
+        var line = '';
+        for (j = 0; j < ncols; j++) {
+            if (template[j] == 'l') {
+                line += lalign("" + row[j], widths[j]);
+            } else {
+                line += ralign("" + row[j], widths[j]);
+            }
+            if (j < ncols - 1)
+                line += ' ';
+        }
+        result += Utils.rstrip(line) + '\n';
+    }
+
+    return result;
+}
diff --git a/js/utils.js b/js/utils.js
new file mode 100644
index 0000000..2c21b2e
--- /dev/null
+++ b/js/utils.js
@@ -0,0 +1,38 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+function assert(condition) {
+    if (!condition)
+        throw new Error("Assertion failed");
+}
+
+function assertNotReached() {
+    if (expected != value) {
+        throw new Error("Assertion failed: should not be reached");
+    }
+}
+
+function strip(string) {
+    return /^\s*([\s\S]*?)\s*$/.exec(string)[1];
+}
+
+function lstrip(string) {
+    return /^\s*([\s\S]*)$/.exec(string)[1];
+}
+
+function rstrip(string) {
+    return /^([\s\S]*?)\s*$/.exec(string)[1];
+}
+
+function formatDate(date, now) {
+    if (now == null)
+        now = new Date();
+    var daysAgo = (now.getTime() - date.getTime()) / (24 * 60 * 60 * 1000);
+    if (daysAgo < 0 && now.getDate() != date.getDate())
+        return date.toLocaleDateString();
+    else if (daysAgo < 1 && now.getDate() == date.getDate())
+        return date.toLocaleTimeString();
+    else if (daysAgo < 7 && now.getDay() != date.getDay())
+        return ['Sun', 'Mon','Tue','Wed','Thu','Fri','Sat'][date.getDay()] + " " + date.toLocaleTimeString();
+    else
+        return date.toLocaleDateString();
+}
diff --git a/jstest.c b/jstest.c
new file mode 100644
index 0000000..94f9a7a
--- /dev/null
+++ b/jstest.c
@@ -0,0 +1,473 @@
+/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
+#include "jsapi.h"
+#include <glib.h>
+#include <locale.h>
+#include <string.h>
+
+/* The class of the global object. */
+static JSClass global_class = {
+    "global", JSCLASS_GLOBAL_FLAGS,
+    JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub,
+    JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, JS_FinalizeStub,
+    JSCLASS_NO_OPTIONAL_MEMBERS
+};
+
+/* The class of module objects. */
+static JSClass module_class = {
+    "module", JSCLASS_GLOBAL_FLAGS,
+    JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub,
+    JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, JS_FinalizeStub,
+    JSCLASS_NO_OPTIONAL_MEMBERS
+};
+
+/* The error reporter callback. */
+static void
+reportError(JSContext *cx, const char *message, JSErrorReport *report)
+{
+    /* Exceptions will be caught when they get thrown to the toplevel */
+    if (report->flags & JSREPORT_EXCEPTION)
+        return;
+
+    g_warning("%s:%u:%s",
+              report->filename ? report->filename : "<no filename>",
+              (unsigned int) report->lineno,
+              message);
+}
+
+static JSObject *
+get_modules_map(JSContext *cx)
+{
+    jsval value;
+
+    JS_GetProperty(cx, JS_GetGlobalObject(cx), "loaded_modules", &value);
+
+    return JSVAL_TO_OBJECT(value);
+}
+
+static JSBool
+find_module(JSContext *cx, const char *module_name, JSObject **module_out)
+{
+    jsval value;
+
+    if (!JS_GetProperty(cx, get_modules_map(cx), module_name, &value))
+        return JS_FALSE;
+
+    if (value == JSVAL_VOID) {
+        *module_out = NULL;
+        return JS_TRUE;
+    }
+
+    if (!JSVAL_IS_OBJECT(value)) {
+        JS_ReportError(cx, "loaded module '%s' is not an object!", module_name);
+        return JS_FALSE;
+    }
+
+    *module_out = JSVAL_TO_OBJECT(value);
+
+    return JS_TRUE;
+}
+
+static JSBool
+load_module(JSContext *cx, const char *module_name, JSObject **module_out)
+{
+    char *lower_name = NULL;
+    char *file_name = NULL;
+    char *file_path = NULL;
+    char *src = NULL;
+    gsize length;
+    GError *error = NULL;
+    JSObject *module;
+    jsval dummy;
+
+    lower_name = g_ascii_strdown (module_name, -1);
+    file_name = g_strconcat(lower_name, ".js", NULL);
+    file_path = g_build_filename("js", file_name, NULL);
+
+    if (!g_file_get_contents(file_path, &src, &length, &error)) {
+        JS_ReportError(cx, "%s", error->message);
+        g_error_free(error);
+        goto out;
+    }
+
+    /* Create the module object. */
+    module = JS_NewObject(cx, &module_class, JS_GetGlobalObject(cx), NULL);
+    if (module == NULL)
+        goto out;
+
+    /* Define first to allow recursive imports */
+    JS_DefineProperty(cx, get_modules_map(cx), module_name,
+                      OBJECT_TO_JSVAL(module), NULL, NULL,
+                      JSPROP_PERMANENT | JSPROP_READONLY);
+
+    if (!JS_EvaluateScript(cx, module, src, length, file_name, 0, &dummy)) {
+        module = NULL;
+        goto out;
+    }
+
+ out:
+    g_free(src);
+    g_free(lower_name);
+    g_free(file_name);
+    g_free(file_path);
+
+    *module_out = module;
+
+    return module != NULL;
+}
+
+static JSBool
+fn_include(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
+{
+    const char *module_name;
+    JSObject *module = NULL;
+
+    *rval = JSVAL_VOID;
+
+    if (!JS_ConvertArguments(cx, argc, argv, "s", &module_name))
+        goto out;
+
+    if (strchr(module_name, '/') != NULL ||
+        strchr(module_name, '\\') != 0 ||
+        strchr(module_name, '.') != 0)
+    {
+        JS_ReportError(cx,"'%s' is not a valid module name", module_name);
+        goto out;
+    }
+
+    if (!find_module (cx, module_name, &module))
+        goto out;
+
+    if (module != NULL) /* Found */
+        goto out;
+
+    if (!load_module (cx, module_name, &module))
+        goto out;
+
+ out:
+    if (module != NULL)
+        JS_DefineProperty(cx, obj, module_name,
+                          OBJECT_TO_JSVAL(module), NULL, NULL,
+                          JSPROP_PERMANENT | JSPROP_READONLY);
+
+    return module != NULL;
+}
+
+JSBool fn_load(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
+{
+    const char *filename;
+    char *contents = NULL;
+    gsize length;
+    JSBool result = JS_FALSE;
+    GError *error = NULL;
+    JSString *jsstr;
+
+    if (!JS_ConvertArguments(cx, argc, argv, "s", &filename))
+        goto out;
+
+    if (!g_file_get_contents(filename, &contents, &length, &error)) {
+        JS_ReportError(cx, "%s", error->message);
+        g_error_free(error);
+        goto out;
+    }
+
+    if (!g_utf8_validate(contents, length, NULL)) {
+        JS_ReportError(cx, "Contents of '%s' are not valid UTF-8", filename);
+        g_error_free(error);
+        goto out;
+    }
+
+    jsstr = JS_NewStringCopyN(cx, contents, length);
+    if (!rval)
+        goto out;
+
+    *rval = STRING_TO_JSVAL(jsstr);
+
+    result = JS_TRUE;
+
+ out:
+    g_free(contents);
+
+    return result;
+}
+
+JSBool fn_log(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
+{
+    GString *str = g_string_new(NULL);
+    uintN i;
+    JSBool result = JS_FALSE;
+
+    *rval = JSVAL_VOID;
+
+    for (i = 0; i < argc; i++) {
+        JSString *jsstr = JS_ValueToString(cx, argv[i]);
+        if (!jsstr)
+            goto out;
+
+        if (i != 0)
+            g_string_append_c(str, ' ');
+
+        g_string_append(str, JS_GetStringBytes(jsstr));
+    }
+
+    g_printerr("%s\n", str->str);
+    result = JS_TRUE;
+
+ out:
+    g_string_free(str, TRUE);
+
+    return result;
+}
+
+static JSFunctionSpec global_functions[] = {
+    JS_FS("include", fn_include, 1, 0, 0),
+    JS_FS("load", fn_load, 1, 0, 0),
+    JS_FS("log", fn_log, 0, 0, 0),
+    JS_FS_END
+};
+
+static JSBool
+get_string_property(JSContext *cx, JSObject *obj, const char *property, char **out)
+{
+    jsval value;
+    JSString *jsstr;
+
+    if (!JS_GetProperty(cx, obj, property, &value))
+        return JS_FALSE;
+
+    if (JSVAL_IS_VOID(value) || JSVAL_IS_NULL(value))
+        return JS_FALSE;
+
+    jsstr = JS_ValueToString(cx, value);
+    *out = g_strdup(JS_GetStringBytes(jsstr));
+
+    return JS_TRUE;
+}
+
+static gboolean
+process_jst(const char *filename,
+            const char *str,
+            size_t      len,
+            char      **new_str,
+            size_t     *new_len)
+{
+    /* a '.jst" file is a '.js' file with a here document syntax of
+     *   <<<\s*\n[text]>>>
+     * it's very useful for test cases involving long strings.
+     */
+    const char *p = str;
+    const char *end = str + len;
+    gboolean in_string = FALSE;
+    GString *result = g_string_new (NULL);
+    gboolean success = FALSE;
+    int line = 1;
+    int str_start_line = 0;
+    int str_newlines = 0;
+
+    for (p = str; p < end; p++) {
+        if (*p == '\n')
+            line++;
+        if (in_string) {
+            if (p + 3 <= end && p[0] == '<' && p[1] == '<' && p[2] == '<') {
+                /* Better to catch missing closes */
+                g_warning ("%s:%d: nested <<< not allowed", filename, line);
+                goto out;
+            } else if (p + 3 <= end && p[0] == '>' && p[1] == '>' && p[2] == '>') {
+                int i;
+
+                p += 2;
+                in_string = FALSE;
+                g_string_append_c(result, '\'');
+
+                /* Compensate, so that the line numbers end up right */
+                for (i = 0; i < str_newlines; i++)
+                    g_string_append_c(result, '\n');
+            } else {
+                switch (*p) {
+                case '\'':
+                    g_string_append (result, "\\'");
+                    break;
+                case '\n':
+                    g_string_append (result, "\\n");
+                    str_newlines++;
+                    break;
+                case '\\':
+                    g_string_append (result, "\\\\");
+                    break;
+                default:
+                    g_string_append_c (result, *p);
+                    break;
+                }
+            }
+        } else {
+            if (p + 3 <= end && p[0] == '<' && p[1] == '<' && p[2] == '<') {
+                str_start_line = line;
+                p += 3;
+                /* Skip whitespace before up to a newline */
+                while (p < end && *p != '\n') {
+                    if (!g_ascii_isspace(*p)) {
+                        g_warning ("%s:%d: <<< has trailing text on the same line", filename, 
str_start_line);
+                        goto out;
+                    }
+                    p++;
+                }
+
+                if (p == end) {
+                    g_warning ("%s:%d: <<< not closed", filename, str_start_line);
+                    goto out;
+                }
+
+                /* Skipping \n */
+                line++;
+                str_newlines = 1;
+
+                g_string_append_c(result, '\'');
+                in_string = TRUE;
+            } else {
+                g_string_append_c(result, *p);
+            }
+        }
+    }
+
+    if (in_string) {
+        g_warning ("%s:%d: <<< not closed", filename, str_start_line);
+        goto out;
+    }
+
+    success = TRUE;
+
+ out:
+    if (success) {
+        *new_len = result->len;
+        *new_str = g_string_free (result, FALSE);
+    } else {
+        g_string_free (result, TRUE);
+    }
+
+    return success;
+}
+
+int main(int argc, const char *argv[])
+{
+    /* JS variables. */
+    JSRuntime *rt;
+    JSContext *cx;
+    JSObject  *global;
+    JSObject  *loaded_modules;
+    int i;
+
+    setlocale (LC_ALL, "");
+
+    JS_SetCStringsAreUTF8();
+
+    /* Create a JS runtime. */
+    rt = JS_NewRuntime(8L * 1024L * 1024L);
+    if (rt == NULL)
+        return 1;
+
+    for (i = 1; i < argc; i++) {
+        GError *error = NULL;
+        char *src;
+        gsize length;
+        jsval rval;
+
+        /* Create a context. */
+        cx = JS_NewContext(rt, 8192);
+        if (cx == NULL)
+            return 1;
+        JS_SetOptions(cx,
+                      JSOPTION_VAROBJFIX |
+                      JSOPTION_DONT_REPORT_UNCAUGHT |
+                      JSOPTION_STRICT);
+        JS_SetVersion(cx, JSVERSION_LATEST);
+        JS_SetErrorReporter(cx, reportError);
+
+        /* Create the global object. */
+        global = JS_NewObject(cx, &global_class, NULL, NULL);
+        if (global == NULL)
+            return 1;
+
+        /* Populate the global object with the standard globals,
+           like Object and Array. */
+        if (!JS_InitStandardClasses(cx, global))
+            return 1;
+
+        if (!JS_DefineFunctions(cx, global, global_functions))
+            return 1;
+
+        if (!g_file_get_contents(argv[i], &src, &length, &error)) {
+            g_printerr("%s\n", error->message);
+            return 1;
+        }
+
+        if (g_str_has_suffix (argv[i], ".jst")) {
+            char *new_src;
+            gsize new_length;
+            if (!process_jst(argv[i], src, length, &new_src, &new_length)) {
+                g_free (src);
+                continue;
+            }
+            g_free (src);
+            src = new_src;
+            length = new_length;
+        }
+
+        /* Object to hold loaded modules */
+        loaded_modules = JS_NewObject(cx, NULL, NULL, NULL);
+        JS_DefineProperty(cx, global, "loaded_modules",
+                          OBJECT_TO_JSVAL(loaded_modules), NULL, NULL,
+                          JSPROP_PERMANENT | JSPROP_READONLY);
+
+        if (!JS_EvaluateScript(cx, global, src, length, argv[i], 0, &rval)) {
+            if (JS_IsExceptionPending(cx)) {
+                jsval exception_val;
+                JSObject *exception;
+                char *stack, *filename, *lineNumber, *message;
+
+                JS_AddRoot(cx, &exception_val);
+                JS_GetPendingException(cx, &exception_val);
+                JS_ClearPendingException(cx);
+
+                if (JSVAL_IS_OBJECT (exception_val)) {
+                    exception = JSVAL_TO_OBJECT(exception_val);
+
+                    if (!get_string_property(cx, exception, "stack", &stack))
+                        stack = NULL;
+                    if (!get_string_property(cx, exception, "filename", &filename))
+                        filename = NULL;
+                    if (!get_string_property(cx, exception, "lineNumber", &lineNumber))
+                        lineNumber = NULL;
+                    if (!get_string_property(cx, exception, "message", &message))
+                        message = g_strdup("");
+
+                    if (filename)
+                        g_printerr("%s:", filename);
+                    if (lineNumber)
+                        g_printerr("%s:", lineNumber);
+                    g_printerr("%s\n", message);
+
+                    if (stack != NULL)
+                        g_printerr("%s", stack);
+
+                    g_free(stack);
+                    g_free(filename);
+                    g_free(lineNumber);
+                    g_free(message);
+                } else {
+                    JSString *jsstr = JS_ValueToString(cx, exception_val);
+                    g_printerr("Exception: %s\n", JS_GetStringBytes(jsstr));
+
+                }
+
+                JS_RemoveRoot(cx, &exception_val);
+            }
+        }
+        g_free(src);
+
+        /* Cleanup. */
+        JS_DestroyContext(cx);
+    }
+
+    JS_DestroyRuntime(rt);
+    JS_ShutDown();
+    return 0;
+}
diff --git a/testbugs/561745/attachments/123143 b/testbugs/561745/attachments/123143
new file mode 100644
index 0000000..cd30ff2
--- /dev/null
+++ b/testbugs/561745/attachments/123143
@@ -0,0 +1,110 @@
+From 93a8768acedbaab632bee13635509af8fa206051 Mon Sep 17 00:00:00 2001
+From: Owen W. Taylor <otaylor fishsoup net>
+Date: Thu, 20 Nov 2008 19:19:14 -0500
+Subject: [PATCH] Use a Tweener "Frame Ticker" with a ClutterTimeline backend
+
+Call Tweener.setFrameTicker() with a custom object that bridges to
+ClutterTimeline to get new frame notifications. Combined with a
+hack to dynamically adjust the frame ticker's frame rate when
+Clutter drops frames, this means that our animations play in the
+intended time even if rendering is too slow to maintain a full
+60HZ frame rate.
+
+http://bugzilla.gnome.org/show_bug.cgi?id=561745
+---
+ js/ui/main.js |   69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ 1 files changed, 69 insertions(+), 0 deletions(-)
+
+diff --git a/js/ui/main.js b/js/ui/main.js
+index 882e34b..4832d31 100644
+--- a/js/ui/main.js
++++ b/js/ui/main.js
+@@ -1,7 +1,9 @@
+ /* -*- mode: js2; js2-basic-offset: 4; -*- */
+ 
+ const Shell = imports.gi.Shell;
++const Signals = imports.signals;
+ const Clutter = imports.gi.Clutter;
++const Tweener = imports.tweener.tweener;
+ 
+ const Panel = imports.ui.panel;
+ const Overlay = imports.ui.overlay;
+@@ -14,9 +16,76 @@ let panel = null;
+ let overlay = null;
+ let run_dialog = null;
+ 
++// The "FrameTicker" object is an object used to feed new frames to Tweener
++// so it can update values and redraw. The default frame ticker for
++// Tweener just uses a simple timeout at a fixed frame rate and has no idea
++// of "catching up" by dropping frames.
++//
++// We substitute it with custom frame ticker here that connects Tweener to
++// a Clutter.TimeLine. Now, Clutter.Timeline itself isn't a whole lot more
++// sophisticated than a simple timeout at a fixed frame rate, but at least
++// it knows how to drop frames. (See HippoAnimationManager for a more
++// sophisticated view of continous time updates; even better is to pay
++// attention to the vertical vblank and sync to that when possible.)
++//
++function ClutterFrameTicker() {
++    this._init();
++}
++
++ClutterFrameTicker.prototype = {
++    TARGET_FRAME_RATE : 60,
++
++    _init : function() {
++      // We don't have a finite duration; tweener will tell us to stop
++      // when we need to stop, so use 1000 seconds as "infinity"
++      this._timeline = new Clutter.Timeline({ fps: this.TARGET_FRAME_RATE,
++                                              duration: 1000*1000 });
++      this._frame = 0;
++
++      let me = this;
++      this._timeline.connect('new-frame',
++          function(timeline, frame) {
++              me._onNewFrame(frame);
++          });
++    },
++
++    _onNewFrame : function(frame) {
++      // Unfortunately the interface to to send a new frame to tweener
++      // is a simple "next frame" and there is no provision for signaling
++      // that frames have been skipped or just telling it the new time.
++      // But what it actually does internally is just:
++      //
++      //  _currentTime += 1000/_ticker.FRAME_RATE;
++      //
++      // So by dynamically adjusting the value of FRAME_RATE we can trick
++      // it into dealing with dropped frames.
++
++      let delta = frame - this._frame;
++      if (delta == 0)
++          this.FRAME_RATE = this.TARGET_FRAME_RATE;
++      else
++          this.FRAME_RATE = this.TARGET_FRAME_RATE / delta;
++
++      this.emit('prepare-frame');
++    },
++
++    start : function() {
++      this._timeline.start();
++    },
++
++    stop : function() {
++      this._timeline.stop();
++      this._frame = 0;
++    }
++};
++
++Signals.addSignalMethods(ClutterFrameTicker.prototype);
++
+ function start() {
+     let global = Shell.global_get();
+ 
++    Tweener.setFrameTicker(new ClutterFrameTicker());
++
+     // The background color really only matters if there is no desktop
+     // window (say, nautilus) running. We set it mostly so things look good
+     // when we are running inside Xephyr.
+-- 
+1.6.0.3
\ No newline at end of file
diff --git a/testbugs/561745/bug.xml b/testbugs/561745/bug.xml
new file mode 100644
index 0000000..512dc01
--- /dev/null
+++ b/testbugs/561745/bug.xml
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!DOCTYPE bugzilla SYSTEM "http://bugzilla.gnome.org/bugzilla.dtd";>
+<bugzilla version="2.20.5" urlbase="http://bugzilla.gnome.org/"; maintainer="bugmaster gnome org" 
exporter="otaylor redhat com">
+
+    <bug>
+          <bug_id>561745</bug_id>
+          
+          <creation_ts>2008-11-21 00:22 UTC</creation_ts>
+          <short_desc>Use a Tweener "Frame Ticker" with a ClutterTimeline backend</short_desc>
+          <delta_ts>2008-11-23 04:15:58 UTC</delta_ts>
+          <reporter_accessible>1</reporter_accessible>
+          <cclist_accessible>1</cclist_accessible>
+          <classification_id>8</classification_id>
+          <classification>Other</classification>
+          <product>gnome-shell</product>
+          <component>general</component>
+          <version>unspecified</version>
+          <rep_platform>All</rep_platform>
+          <op_sys>All</op_sys>
+          <bug_status>NEW</bug_status>
+          
+          
+          
+          
+          <priority>Normal</priority>
+          <bug_severity>normal</bug_severity>
+          <target_milestone>---</target_milestone>
+          
+          
+          
+          <reporter>otaylor redhat com</reporter>
+          <assigned_to>gnome-shell-maint gnome bugs</assigned_to>
+          <cc>hp pobox com</cc>
+          <gnome_version>Unspecified</gnome_version>
+          <gnome_target>Unspecified</gnome_target>
+          <initialowner_id>250670</initialowner_id>
+          <everconfirmed>1</everconfirmed>
+          <emblems>P</emblems>
+          <qa_contact>gnome-shell-maint gnome bugs</qa_contact>
+
+      
+
+          <long_desc>
+            <who>otaylor redhat com</who>
+            <bug_when>2008-11-21 00:22 UTC</bug_when>
+            <thetext>Call Tweener.setFrameTicker() with a custom object that bridges to
+ClutterTimeline to get new frame notifications. Combined with a
+hack to dynamically adjust the frame ticker's frame rate when
+Clutter drops frames, this means that our animations play in the
+intended time even if rendering is too slow to maintain a full
+60HZ frame rate.</thetext>
+          </long_desc>
+          <long_desc>
+            <who>otaylor redhat com</who>
+            <bug_when>2008-11-21 00:22 UTC</bug_when>
+            <thetext>Created an attachment (id=123143)
+Use a Tweener "Frame Ticker" with a ClutterTimeline backend
+</thetext>
+          </long_desc>
+          <long_desc>
+            <who>otaylor redhat com</who>
+            <bug_when>2008-11-21 00:26 UTC</bug_when>
+            <thetext>Havoc - can you take a look at this and see if it looks reasonable and 
+if the the hacks are really necessary? Is there some easier way to get
+Tweener to deal with dropped frames?
+
+</thetext>
+          </long_desc>
+          <long_desc>
+            <who>hp pobox com</who>
+            <bug_when>2008-11-21 01:53 UTC</bug_when>
+            <thetext>It looks to me like adding an API to drop frames, such as an arg to prepare-frame that 
is the current frame number or delta, might be a clean solution. Then increment the current time inside 
tweener using the delta.
+
+The "frame ticker" API is just something I made up, not in the original tweener. I didn't implement dropping 
frames yet in litl's app, so no thought has been given to that.
+
+The algorithm I am using in litl so far is:
+
+* when ticker is enabled (there are &gt;0 animations), install timeout at interval frame_length
+* inside the timeout,
+** start a GTimer frame_timer
+** emit prepare-frame which should set all tween properties and thus queue draw
+** unqueue draw and force immediate repaint
+** wait for vsync and swap buffers
+** get elapsed time from the GTimer frame_timer
+** reinstall timeout for MAX(0, frame_length - elapsed)
+* when ticker is disabled, remove the timeout
+
+So if this gets behind, it just runs "as fast as possible" (installs the timeout at 0 interval).
+
+According to a comment in the code, it was essential to force the immediate repaint instead of relying on 
repaint idle, because the timeout and X event queue can starve the idle otherwise if the animation gets 
behind.
+
+I have a "FIXME figure out how to drop frames" in the code that has never really been an issue... I think in 
practice for short half-second transition animations, frame dropping is maybe not very important, because if 
things are slow enough to drop many frames you're doomed anyway, and also in practice it can look better to 
just play all the frames too slowly rather than do a fast animation where objects move too much in each frame.
+
+Assuming the animation is keeping up (generally we can draw each frame in less than frame_length), I think 
it was pretty important to implement the timeout reinstallation for frame_length-elapsed, because just a 
plain glib timeout resulted in a visibly uneven frame rate. Checking just now, it looks like 
ClutterTimeoutPool also does this.
+</thetext>
+          </long_desc>
+          <long_desc>
+            <who>otaylor redhat com</who>
+            <bug_when>2008-11-23 04:15 UTC</bug_when>
+            <thetext>I've now committed a version of my patch (with a bug fix to actually
+set this._frame), but I don't think it's really the "last word"; I think
+it would make sense for Tweener to use (new Date()).getTime() to do timing
+in the normal case.
+
+Or does it make sense to have getTime() be a method on the FrameTicker?
+
+I'll leave this bug open for the moment to think about the appropriate
+patch to submit against gjs's Tweener.
+</thetext>
+          </long_desc>
+      
+          <attachment ispatch="1">
+            <attachid>123143</attachid>
+            <date>2008-11-21 00:22 UTC</date>
+            <desc>Use a Tweener "Frame Ticker" with a ClutterTimeline backend</desc>
+            <ctype>text/plain</ctype>
+          </attachment>
+    </bug>
+
+</bugzilla>
\ No newline at end of file
diff --git a/testpatches/bzr-multi-file.patch b/testpatches/bzr-multi-file.patch
new file mode 100644
index 0000000..3763938
--- /dev/null
+++ b/testpatches/bzr-multi-file.patch
@@ -0,0 +1,235 @@
+https://launchpad.net/~gnome-doc-centric-playground
+
+=== modified file 'src/zeitgeist_engine/zeitgeist_base.py'
+--- src/zeitgeist_engine/zeitgeist_base.py     2008-11-19 22:56:01 +0000
++++ src/zeitgeist_engine/zeitgeist_base.py     2008-11-20 21:27:45 +0000
+@@ -1,13 +1,13 @@
+-
+ import datetime
++import gc
+ import string
++import sys
+ import time
+-from gettext import ngettext, gettext as _
+ from threading import Thread
++
+ import gobject
+ import gtk
+-import gc
+-import sys
++from gettext import ngettext, gettext as _
+ 
+ from zeitgeist_util import Thumbnailer,  icon_factory, launcher
+ 
+
+=== modified file 'src/zeitgeist_engine/zeitgeist_datasink.py'
+--- src/zeitgeist_engine/zeitgeist_datasink.py 2008-11-20 02:46:24 +0000
++++ src/zeitgeist_engine/zeitgeist_datasink.py 2008-11-20 21:27:45 +0000
+@@ -1,12 +1,13 @@
+-#!/usr/bin/env python
++import sys
++import time
++import urllib
++
++from gettext import gettext as _
++
+ from zeitgeist_engine.zeitgeist_base import ItemSource
+ from zeitgeist_engine.zeitgeist_firefox import FirefoxSource
+ from zeitgeist_engine.zeitgeist_tomboy import TomboySource
+ from zeitgeist_engine.zeitgeist_recent import *
+-from gettext import gettext as _
+-import urllib
+-import time
+-import sys
+ 
+ class DataSinkSource(ItemSource):
+     def __init__(self, note_path=None):
+
+=== modified file 'src/zeitgeist_engine/zeitgeist_firefox.py'
+--- src/zeitgeist_engine/zeitgeist_firefox.py  2008-11-19 18:43:27 +0000
++++ src/zeitgeist_engine/zeitgeist_firefox.py  2008-11-20 21:27:45 +0000
+@@ -2,17 +2,17 @@
+ import os
+ import re
+ import glob
+-import sqlite3 as db
+-from gettext import gettext as _
+ from xml.dom.minidom import parse
+ from xml.parsers.expat import ExpatError
+ 
++import gnomevfs
+ import gobject
+ import gtk
+-import gnomevfs
++import shutil
++import sqlite3 as db
++import tempfile
+ import W3CDate
+-import tempfile
+-import shutil
++from gettext import gettext as _
+ 
+ from zeitgeist_base import Item, ItemSource
+ from zeitgeist_util import FileMonitor, launcher
+
+=== modified file 'src/zeitgeist_engine/zeitgeist_recent.py'
+--- src/zeitgeist_engine/zeitgeist_recent.py   2008-11-19 18:43:27 +0000
++++ src/zeitgeist_engine/zeitgeist_recent.py   2008-11-20 21:27:45 +0000
+@@ -1,17 +1,19 @@
++import datetime
++import gc
++import os
+ import re
+ import sys
+-import gc
++import time
++import urllib
+ import urlparse
+-import datetime
+-import os
+-import urllib
+-import time
+-from gettext import gettext as _
+-import gobject
+-import gtk
++
+ import gnome.ui
+ import gnomevfs
+ import gnomevfs.async
++import gobject
++import gtk
++from gettext import gettext as _
++
+ from zeitgeist_base import Item, ItemSource
+ 
+ class RecentlyUsedManagerGtk(ItemSource):
+
+=== modified file 'src/zeitgeist_engine/zeitgeist_tomboy.py'
+--- src/zeitgeist_engine/zeitgeist_tomboy.py   2008-11-19 22:37:12 +0000
++++ src/zeitgeist_engine/zeitgeist_tomboy.py   2008-11-20 21:27:45 +0000
+@@ -1,8 +1,6 @@
+-
+ import datetime
+ import os
+ import re
+-from gettext import gettext as _
+ from xml.dom.minidom import parse
+ from xml.parsers.expat import ExpatError
+ 
+@@ -10,6 +8,7 @@
+ import gtk
+ import gnomevfs
+ import W3CDate
++from gettext import gettext as _
+ 
+ from zeitgeist_base import Item, ItemSource
+ from zeitgeist_util import FileMonitor, launcher
+
+=== modified file 'src/zeitgeist_engine/zeitgeist_util.py'
+--- src/zeitgeist_engine/zeitgeist_util.py     2008-11-19 18:43:27 +0000
++++ src/zeitgeist_engine/zeitgeist_util.py     2008-11-20 21:27:45 +0000
+@@ -1,16 +1,17 @@
+ import datetime
++import gc
+ import os
+ import urllib
+-from gettext import gettext as _
+ import sys     # for ImplementMe
+ import inspect # for ImplementMe
++
+ import dbus
+ import gobject
+ import gtk
+-import gc
+ import gnome.ui
+ import gnomevfs
+ import gconf
++from gettext import gettext as _
+ 
+ class FileMonitor(gobject.GObject):
+     '''
+
+=== modified file 'src/zeitgeist_gui/zeitgeist_calendar_gui.py'
+--- src/zeitgeist_gui/zeitgeist_calendar_gui.py        2008-11-19 18:43:27 +0000
++++ src/zeitgeist_gui/zeitgeist_calendar_gui.py        2008-11-20 21:27:45 +0000
+@@ -11,7 +11,7 @@
+ import gobject
+ import gtk
+ import gtk.glade
+-import datetime
++
+ from zeitgeist_engine.zeitgeist_util import icon_factory, icon_theme, launcher
+ from zeitgeist_engine.zeitgeist_datasink import DataSinkSource
+ 
+
+=== modified file 'src/zeitgeist_gui/zeitgeist_gui.py'
+--- src/zeitgeist_gui/zeitgeist_gui.py 2008-11-20 02:46:24 +0000
++++ src/zeitgeist_gui/zeitgeist_gui.py 2008-11-20 21:27:45 +0000
+@@ -1,24 +1,16 @@
+-#!/usr/bin/env python
++import datetime
++import math
++import sys
++import os
++
++import gtk
++import gtk.glade
++import gobject
++import gnomeapplet
++
+ from zeitgeist_panel_widgets import timeline,StarredWidget,FilterAndOptionBox,calendar
+ from zeitgeist_engine.zeitgeist_util import icon_factory, icon_theme, launcher
+ #from zeitgeist_calendar_gui import zeitgeistGUI
+-import sys
+-import os
+-import gnomeapplet
+-
+-import datetime
+-import math
+-try:
+-     import pygtk
+-     pygtk.require("2.0")
+-except:
+-      pass
+-try:
+-    import gtk
+-    import gtk.glade
+-except:
+-    sys.exit(1)
+-import gobject
+ 
+ class zeitgeistGUI:   
+     
+@@ -75,4 +67,4 @@
+         self.topicWindow.add(self.mainbox)
+         self.topicWindow.show_all()
+         
+-    
+\ No newline at end of file
++    
+
+=== modified file 'src/zeitgeist_gui/zeitgeist_panel_widgets.py'
+--- src/zeitgeist_gui/zeitgeist_panel_widgets.py       2008-11-20 21:14:40 +0000
++++ src/zeitgeist_gui/zeitgeist_panel_widgets.py       2008-11-20 21:27:45 +0000
+@@ -1,13 +1,15 @@
+-from zeitgeist_engine.zeitgeist_datasink import datasink
+-from zeitgeist_engine.zeitgeist_util import launcher
+-import pango
++import datetime
+ import gc
++import os
+ import time
++
+ import gtk
+ import gobject
+-import datetime
+-import os
++import pango
+  
++from zeitgeist_engine.zeitgeist_datasink import datasink
++from zeitgeist_engine.zeitgeist_util import launcher
++
+ class TimelineWidget(gtk.HBox):
+     
+     def __init__(self):
+
diff --git a/testpatches/bzr-single-file-no-newline.patch b/testpatches/bzr-single-file-no-newline.patch
new file mode 100644
index 0000000..86e9377
--- /dev/null
+++ b/testpatches/bzr-single-file-no-newline.patch
@@ -0,0 +1,34 @@
+https://launchpad.net/~gnome-doc-centric-playground
+
+=== modified file 'src/zeitgeist_gui/zeitgeist_panel_widgets.py'
+--- src/zeitgeist_gui/zeitgeist_panel_widgets.py       2008-11-20 02:46:24 +0000
++++ src/zeitgeist_gui/zeitgeist_panel_widgets.py       2008-11-20 21:14:40 +0000
+@@ -514,22 +514,20 @@
+         gc.collect()
+         
+     def _set_item(self, item):
+-        
+-        name =item.get_name()
++        name = item.get_name()
+         comment = "<span size='large' color='red'>%s</span>" % item.get_comment() #+ "  <span size='small' 
color='blue'> %s </span>" % str(item.count)
+-        #text = name + "\n"  + comment 
+-        count="<span size='small' color='blue'>%s</span>" %  item.count
++        count = "<span size='small' color='blue'>%s</span>" %  item.count
+         try:
+             icon = item.get_icon(24)
+         except (AssertionError, AttributeError):
+             print("exception")
+             icon = None
+         
+-        self.store.append([comment,icon,name,count,item])
++        self.store.append([None, icon, name, count, item])
+         
+         #del icon,name,comment,text
+         
+ 
+ 
+ calendar = CalendarWidget()
+-timeline = TimelineWidget()
+\ No newline at end of file
++timeline = TimelineWidget()
+
diff --git a/testpatches/cvs-multi-file.patch b/testpatches/cvs-multi-file.patch
new file mode 100644
index 0000000..da16003
--- /dev/null
+++ b/testpatches/cvs-multi-file.patch
@@ -0,0 +1,333 @@
+https://bugzilla.mozilla.org/show_bug.cgi?id=388251
+
+Index: mozilla/webtools/bugzilla/Bugzilla/Attachment.pm
+===================================================================
+RCS file: /cvsroot/mozilla/webtools/bugzilla/Bugzilla/Attachment.pm,v
+--- mozilla/webtools/bugzilla/Bugzilla/Attachment.pm   8 Sep 2008 16:21:33 -0000       1.57
++++ mozilla/webtools/bugzilla/Bugzilla/Attachment.pm   8 Sep 2008 16:41:23 -0000
+@@ -28,23 +28,26 @@ package Bugzilla::Attachment;
+ 
+ =head1 NAME
+ 
+-Bugzilla::Attachment - a file related to a bug that a user has uploaded
+-                       to the Bugzilla server
++Bugzilla::Attachment - Bugzilla attachment class.
+ 
+ =head1 SYNOPSIS
+ 
+   use Bugzilla::Attachment;
+ 
+   # Get the attachment with the given ID.
+-  my $attachment = Bugzilla::Attachment->get($attach_id);
++  my $attachment = new Bugzilla::Attachment($attach_id);
+ 
+   # Get the attachments with the given IDs.
+-  my $attachments = Bugzilla::Attachment->get_list($attach_ids);
++  my $attachments = Bugzilla::Attachment->new_from_list($attach_ids);
+ 
+ =head1 DESCRIPTION
+ 
+-This module defines attachment objects, which represent files related to bugs
+-that users upload to the Bugzilla server.
++Attachment.pm represents an attachment object. It is an implementation
++of L<Bugzilla::Object>, and thus provides all methods that
++L<Bugzilla::Object> provides.
++
++The methods that are specific to C<Bugzilla::Attachment> are listed
++below.
+ 
+ =cut
+ 
+@@ -55,60 +58,37 @@ use Bugzilla::User;
+ use Bugzilla::Util;
+ use Bugzilla::Field;
+ 
+-sub get {
+-    my $invocant = shift;
+-    my $id = shift;
+-
+-    my $attachments = _retrieve([$id]);
+-    my $self = $attachments->[0];
+-    bless($self, ref($invocant) || $invocant) if $self;
++use base qw(Bugzilla::Object);
+ 
+-    return $self;
+-}
++###############################
++####    Initialization     ####
++###############################
+ 
+-sub get_list {
+-    my $invocant = shift;
+-    my $ids = shift;
++use constant DB_TABLE   => 'attachments';
++use constant ID_FIELD   => 'attach_id';
++use constant LIST_ORDER => ID_FIELD;
+ 
+-    my $attachments = _retrieve($ids);
+-    foreach my $attachment (@$attachments) {
+-        bless($attachment, ref($invocant) || $invocant);
+-    }
+-
+-    return $attachments;
+-}
+-
+-sub _retrieve {
+-    my ($ids) = @_;
+-
+-    return [] if scalar(@$ids) == 0;
+-
+-    my @columns = (
+-        'attachments.attach_id AS id',
+-        'attachments.bug_id AS bug_id',
+-        'attachments.description AS description',
+-        'attachments.mimetype AS contenttype',
+-        'attachments.submitter_id AS attacher_id',
+-        Bugzilla->dbh->sql_date_format('attachments.creation_ts',
+-                                       '%Y.%m.%d %H:%i') . " AS attached",
+-        'attachments.modification_time',
+-        'attachments.filename AS filename',
+-        'attachments.ispatch AS ispatch',
+-        'attachments.isurl AS isurl',
+-        'attachments.isobsolete AS isobsolete',
+-        'attachments.isprivate AS isprivate'
+-    );
+-    my $columns = join(", ", @columns);
++sub DB_COLUMNS {
+     my $dbh = Bugzilla->dbh;
+-    my $records = $dbh->selectall_arrayref(
+-                      "SELECT $columns
+-                         FROM attachments
+-                        WHERE " 
+-                       . Bugzilla->dbh->sql_in('attach_id', $ids) 
+-                 . " ORDER BY attach_id",
+-                       { Slice => {} });
+-    return $records;
+-}
++
++    return qw(
++        attach_id
++        bug_id
++        description
++        filename
++        isobsolete
++        ispatch
++        isprivate
++        isurl
++        mimetype
++        modification_time
++        submitter_id),
++        $dbh->sql_date_format('attachments.creation_ts', '%Y.%m.%d %H:%i') . ' AS creation_ts';
++}
++
++###############################
++####      Accessors      ######
++###############################
+ 
+ =pod
+ 
+@@ -116,21 +96,6 @@ sub _retrieve {
+ 
+ =over
+ 
+-=item C<id>
+-
+-the unique identifier for the attachment
+-
+-=back
+-
+-=cut
+-
+-sub id {
+-    my $self = shift;
+-    return $self->{id};
+-}
+-
+-=over
+-
+ =item C<bug_id>
+ 
+ the ID of the bug to which the attachment is attached
+@@ -189,7 +154,7 @@ the attachment's MIME media type
+ 
+ sub contenttype {
+     my $self = shift;
+-    return $self->{contenttype};
++    return $self->{mimetype};
+ }
+ 
+ =over
+@@ -205,7 +170,7 @@ the user who attached the attachment
+ sub attacher {
+     my $self = shift;
+     return $self->{attacher} if exists $self->{attacher};
+-    $self->{attacher} = new Bugzilla::User($self->{attacher_id});
++    $self->{attacher} = new Bugzilla::User($self->{submitter_id});
+     return $self->{attacher};
+ }
+ 
+@@ -221,7 +186,7 @@ the date and time on which the attacher 
+ 
+ sub attached {
+     my $self = shift;
+-    return $self->{attached};
++    return $self->{creation_ts};
+ }
+ 
+ =over
+@@ -367,7 +332,7 @@ sub data {
+                                                       FROM attach_data
+                                                       WHERE id = ?",
+                                                      undef,
+-                                                     $self->{id});
++                                                     $self->id);
+ 
+     # If there's no attachment data in the database, the attachment is stored
+     # in a local file, so retrieve it from there.
+@@ -412,7 +377,7 @@ sub datasize {
+         Bugzilla->dbh->selectrow_array("SELECT LENGTH(thedata)
+                                         FROM attach_data
+                                         WHERE id = ?",
+-                                       undef, $self->{id}) || 0;
++                                       undef, $self->id) || 0;
+ 
+     # If there's no attachment data in the database, either the attachment
+     # is stored in a local file, and so retrieve its size from the file,
+@@ -470,6 +435,10 @@ sub flag_types {
+     return $self->{flag_types};
+ }
+ 
++###############################
++####      Validators     ######
++###############################
++
+ # Instance methods; no POD documentation here yet because the only ones so far
+ # are private.
+ 
+@@ -595,7 +564,8 @@ sub get_attachments_by_bug {
+     my $attach_ids = $dbh->selectcol_arrayref("SELECT attach_id FROM attachments
+                                                WHERE bug_id = ? $and_restriction",
+                                                undef, @values);
+-    my $attachments = Bugzilla::Attachment->get_list($attach_ids);
++
++    my $attachments = Bugzilla::Attachment->new_from_list($attach_ids);
+ 
+     # To avoid $attachment->flags to run SQL queries itself for each
+     # attachment listed here, we collect all the data at once and
+@@ -769,10 +739,9 @@ sub validate_obsolete {
+         detaint_natural($attachid)
+           || ThrowCodeError('invalid_attach_id_to_obsolete', $vars);
+ 
+-        my $attachment = Bugzilla::Attachment->get($attachid);
+-
+         # Make sure the attachment exists in the database.
+-        ThrowUserError('invalid_attach_id', $vars) unless $attachment;
++        my $attachment = new Bugzilla::Attachment($attachid)
++          || ThrowUserError('invalid_attach_id', $vars);
+ 
+         # Check that the user can view and edit this attachment.
+         $attachment->validate_can_edit($bug->product_id);
+@@ -794,10 +763,13 @@ sub validate_obsolete {
+     return @obsolete_attachments;
+ }
+ 
++###############################
++####     Constructors     #####
++###############################
+ 
+ =pod
+ 
+-=item C<insert_attachment_for_bug($throw_error, $bug, $user, $timestamp, $hr_vars)>
++=item C<create($throw_error, $bug, $user, $timestamp, $hr_vars)>
+ 
+ Description: inserts an attachment from CGI input for the given bug.
+ 
+@@ -814,7 +786,8 @@ Returns:    the ID of the new attachment
+ 
+ =cut
+ 
+-sub insert_attachment_for_bug {
++# FIXME: needs to follow the way Object->create() works.
++sub create {
+     my ($class, $throw_error, $bug, $user, $timestamp, $hr_vars) = @_;
+ 
+     my $cgi = Bugzilla->cgi;
+@@ -957,7 +930,7 @@ sub insert_attachment_for_bug {
+                           $timestamp, $fieldid, 0, 1));
+     }
+ 
+-    my $attachment = Bugzilla::Attachment->get($attachid);
++    my $attachment = new Bugzilla::Attachment($attachid);
+ 
+     # 1. Add flags, if any. To avoid dying if something goes wrong
+     # while processing flags, we will eval() flag validation.
+Index: mozilla/webtools/bugzilla/Bugzilla/Flag.pm
+===================================================================
+RCS file: /cvsroot/mozilla/webtools/bugzilla/Bugzilla/Flag.pm,v
+--- mozilla/webtools/bugzilla/Bugzilla/Flag.pm 8 Sep 2008 16:21:33 -0000       1.98
++++ mozilla/webtools/bugzilla/Bugzilla/Flag.pm 8 Sep 2008 16:41:23 -0000
+@@ -180,7 +180,7 @@ sub attachment {
+     return undef unless $self->attach_id;
+ 
+     require Bugzilla::Attachment;
+-    $self->{'attachment'} ||= Bugzilla::Attachment->get($self->attach_id);
++    $self->{'attachment'} ||= new Bugzilla::Attachment($self->attach_id);
+     return $self->{'attachment'};
+ }
+ 
+Index: mozilla/webtools/bugzilla/attachment.cgi
+===================================================================
+RCS file: /cvsroot/mozilla/webtools/bugzilla/attachment.cgi,v
+--- mozilla/webtools/bugzilla/attachment.cgi   8 Sep 2008 16:21:24 -0000       1.147
++++ mozilla/webtools/bugzilla/attachment.cgi   8 Sep 2008 16:41:23 -0000
+@@ -161,7 +161,7 @@ sub validateID {
+      || ThrowUserError("invalid_attach_id", { attach_id => $cgi->param($param) });
+   
+     # Make sure the attachment exists in the database.
+-    my $attachment = Bugzilla::Attachment->get($attach_id)
++    my $attachment = new Bugzilla::Attachment($attach_id)
+       || ThrowUserError("invalid_attach_id", { attach_id => $attach_id });
+ 
+     # Make sure the user is authorized to access this attachment's bug.
+@@ -320,7 +320,7 @@ sub enter {
+ 
+   # Define the variables and functions that will be passed to the UI template.
+   $vars->{'bug'} = $bug;
+-  $vars->{'attachments'} = Bugzilla::Attachment->get_list($attach_ids);
++  $vars->{'attachments'} = Bugzilla::Attachment->new_from_list($attach_ids);
+ 
+   my $flag_types = Bugzilla::FlagType::match({'target_type'  => 'attachment',
+                                               'product_id'   => $bug->product_id,
+@@ -374,8 +374,7 @@ sub insert {
+     }
+ 
+     my $attachment =
+-        Bugzilla::Attachment->insert_attachment_for_bug(THROW_ERROR, $bug, $user,
+-                                                        $timestamp, $vars);
++        Bugzilla::Attachment->create(THROW_ERROR, $bug, $user, $timestamp, $vars);
+ 
+     # Insert a comment about the new attachment into the database.
+     my $comment = "Created an attachment (id=" . $attachment->id . ")\n" .
+@@ -558,7 +557,7 @@ sub update {
+             $cgi->param('ispatch'), $cgi->param('isobsolete'), 
+             $cgi->param('isprivate'), $timestamp, $attachment->id));
+ 
+-  my $updated_attachment = Bugzilla::Attachment->get($attachment->id);
++  my $updated_attachment = new Bugzilla::Attachment($attachment->id);
+   # Record changes in the activity table.
+   my $sth = $dbh->prepare('INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
+                                                       fieldid, removed, added)
+Index: mozilla/webtools/bugzilla/post_bug.cgi
+===================================================================
+RCS file: /cvsroot/mozilla/webtools/bugzilla/post_bug.cgi,v
+--- mozilla/webtools/bugzilla/post_bug.cgi     25 Feb 2008 16:06:24 -0000      1.196
++++ mozilla/webtools/bugzilla/post_bug.cgi     8 Sep 2008 16:41:24 -0000
+@@ -194,7 +194,7 @@ if (defined $cgi->param('version')) {
+ # Add an attachment if requested.
+ if (defined($cgi->upload('data')) || $cgi->param('attachurl')) {
+     $cgi->param('isprivate', $cgi->param('commentprivacy'));
+-    my $attachment = Bugzilla::Attachment->insert_attachment_for_bug(!THROW_ERROR,
++    my $attachment = Bugzilla::Attachment->create(!THROW_ERROR,
+                                                   $bug, $user, $timestamp, $vars);
+ 
+     if ($attachment) {
diff --git a/testpatches/git-multi-file.patch b/testpatches/git-multi-file.patch
new file mode 100644
index 0000000..cba8f02
--- /dev/null
+++ b/testpatches/git-multi-file.patch
@@ -0,0 +1,143 @@
+From f7111674a7f28067b5e295fe0068c95aa8551c4d Mon Sep 17 00:00:00 2001
+From: Owen W. Taylor <otaylor fishsoup net>
+Date: Thu, 13 Nov 2008 12:45:37 -0500
+Subject: [PATCH] =?utf-8?q?Bug=20560670=20=E2=80=93=20Turn=20on=20compilation=20warnings?=
+MIME-Version: 1.0
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: 8bit
+
+configure.ac: Add -Wall and selected other warnings
+
+gjs/importer.c: Pass the right value to finish_import()
+gjs/jsapi-util.c: Reorder includes so that __GJS_UTIL_LOG_H__
+ gets defined before jsapi-util.h is included.
+
+gi/function.c: Initialize a variable to quiet GCC
+gi/arg.c: Remove unused variables, fix missing case labels and
+ return value in gjs_g_arg_release_in_arg()
+---
+ configure.ac     |   24 ++++++++++++++++++++++++
+ gi/arg.c         |   10 +++++-----
+ gi/function.c    |    1 +
+ gjs/importer.c   |    2 +-
+ gjs/jsapi-util.c |    6 +++---
+ 5 files changed, 34 insertions(+), 9 deletions(-)
+
+diff --git a/configure.ac b/configure.ac
+index 9e31ec4..1ad219b 100644
+--- a/configure.ac
++++ b/configure.ac
+@@ -24,6 +24,30 @@ AM_DISABLE_STATIC
+ AC_PROG_LIBTOOL
+ dnl DOLT
+ 
++# Add extra warning flags
++changequote(,)dnl
++ensureflag() {
++  flag="$1"; shift
++  result="$@"
++
++  case " ${result} " in
++  *[\ \       ]${flag}[\ \    ]*) ;;
++  *) result="${flag} ${result}" ;;
++  esac
++
++  echo ${result}
++}
++changequote([,])dnl
++
++if test "$GCC" = "yes"; then
++    for flag in -Wall -Wchar-subscripts -Wmissing-declarations \
++        -Wmissing-prototypes -Wnested-externs -Wpointer-arith -Wcast-align \
++        -Wsign-compare -fno-strict-aliasing;
++    do
++        CFLAGS="`ensureflag $flag $CFLAGS`"
++    done
++fi
++
+ # coverage
+ AC_ARG_ENABLE([coverage],
+               [AS_HELP_STRING([--enable-coverage],
+diff --git a/gi/arg.c b/gi/arg.c
+index b37e1a7..51da8f7 100644
+--- a/gi/arg.c
++++ b/gi/arg.c
+@@ -212,8 +212,6 @@ gjs_array_to_array(JSContext   *context,
+                    GITypeInfo  *param_info,
+                    void       **arr_p)
+ {
+-    guint32 i;
+-    jsval elem;
+     GITypeTag element_type;
+ 
+     element_type = g_type_info_get_tag(param_info);
+@@ -1126,7 +1124,7 @@ gjs_g_arg_release_in_arg(JSContext  *context,
+ 
+     /* we don't own the argument anymore */
+     if (transfer == GI_TRANSFER_EVERYTHING)
+-        return;
++        return JS_TRUE;
+ 
+     type_tag = g_type_info_get_tag( (GITypeInfo*) type_info);
+ 
+@@ -1143,8 +1141,10 @@ gjs_g_arg_release_in_arg(JSContext  *context,
+     case GI_TYPE_TAG_ARRAY:
+         return gjs_g_arg_release_internal(context, GI_TRANSFER_EVERYTHING,
+                                           type_info, type_tag, arg);
++    default:
++        return JS_TRUE;
+     }
+-
+-    return JS_TRUE;
+ }
+ 
++
++
+diff --git a/gi/function.c b/gi/function.c
+index 2ef8642..b8aae11 100644
+--- a/gi/function.c
++++ b/gi/function.c
+@@ -261,6 +261,7 @@ gjs_invoke_c_function(JSContext      *context,
+     if (return_tag != GI_TYPE_TAG_VOID)
+         n_return_values += 1;
+ 
++    return_values = NULL; /* Quiet gcc warning about initialization */
+     if (n_return_values > 0) {
+         if (invoke_ok) {
+             return_values = g_newa(jsval, n_return_values);
+diff --git a/gjs/importer.c b/gjs/importer.c
+index 5cb8bd8..bcd6d33 100644
+--- a/gjs/importer.c
++++ b/gjs/importer.c
+@@ -315,7 +315,7 @@ import_file(JSContext  *context,
+ 
+     g_free(script);
+ 
+-    if (!finish_import(context, obj))
++    if (!finish_import(context, name))
+         goto out;
+ 
+     retval = JS_TRUE;
+diff --git a/gjs/jsapi-util.c b/gjs/jsapi-util.c
+index db2186e..fe51441 100644
+--- a/gjs/jsapi-util.c
++++ b/gjs/jsapi-util.c
+@@ -23,12 +23,12 @@
+ 
+ #include <config.h>
+ 
+-#include "jsapi-util.h"
+-#include "context-jsapi.h"
+-
+ #include <util/log.h>
+ #include <util/glib.h>
+ 
++#include "jsapi-util.h"
++#include "context-jsapi.h"
++
+ #include <string.h>
+ 
+ typedef struct {
+-- 
+1.6.0.3
+
diff --git a/testpatches/git-one-file.patch b/testpatches/git-one-file.patch
new file mode 100644
index 0000000..cd30ff2
--- /dev/null
+++ b/testpatches/git-one-file.patch
@@ -0,0 +1,110 @@
+From 93a8768acedbaab632bee13635509af8fa206051 Mon Sep 17 00:00:00 2001
+From: Owen W. Taylor <otaylor fishsoup net>
+Date: Thu, 20 Nov 2008 19:19:14 -0500
+Subject: [PATCH] Use a Tweener "Frame Ticker" with a ClutterTimeline backend
+
+Call Tweener.setFrameTicker() with a custom object that bridges to
+ClutterTimeline to get new frame notifications. Combined with a
+hack to dynamically adjust the frame ticker's frame rate when
+Clutter drops frames, this means that our animations play in the
+intended time even if rendering is too slow to maintain a full
+60HZ frame rate.
+
+http://bugzilla.gnome.org/show_bug.cgi?id=561745
+---
+ js/ui/main.js |   69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ 1 files changed, 69 insertions(+), 0 deletions(-)
+
+diff --git a/js/ui/main.js b/js/ui/main.js
+index 882e34b..4832d31 100644
+--- a/js/ui/main.js
++++ b/js/ui/main.js
+@@ -1,7 +1,9 @@
+ /* -*- mode: js2; js2-basic-offset: 4; -*- */
+ 
+ const Shell = imports.gi.Shell;
++const Signals = imports.signals;
+ const Clutter = imports.gi.Clutter;
++const Tweener = imports.tweener.tweener;
+ 
+ const Panel = imports.ui.panel;
+ const Overlay = imports.ui.overlay;
+@@ -14,9 +16,76 @@ let panel = null;
+ let overlay = null;
+ let run_dialog = null;
+ 
++// The "FrameTicker" object is an object used to feed new frames to Tweener
++// so it can update values and redraw. The default frame ticker for
++// Tweener just uses a simple timeout at a fixed frame rate and has no idea
++// of "catching up" by dropping frames.
++//
++// We substitute it with custom frame ticker here that connects Tweener to
++// a Clutter.TimeLine. Now, Clutter.Timeline itself isn't a whole lot more
++// sophisticated than a simple timeout at a fixed frame rate, but at least
++// it knows how to drop frames. (See HippoAnimationManager for a more
++// sophisticated view of continous time updates; even better is to pay
++// attention to the vertical vblank and sync to that when possible.)
++//
++function ClutterFrameTicker() {
++    this._init();
++}
++
++ClutterFrameTicker.prototype = {
++    TARGET_FRAME_RATE : 60,
++
++    _init : function() {
++      // We don't have a finite duration; tweener will tell us to stop
++      // when we need to stop, so use 1000 seconds as "infinity"
++      this._timeline = new Clutter.Timeline({ fps: this.TARGET_FRAME_RATE,
++                                              duration: 1000*1000 });
++      this._frame = 0;
++
++      let me = this;
++      this._timeline.connect('new-frame',
++          function(timeline, frame) {
++              me._onNewFrame(frame);
++          });
++    },
++
++    _onNewFrame : function(frame) {
++      // Unfortunately the interface to to send a new frame to tweener
++      // is a simple "next frame" and there is no provision for signaling
++      // that frames have been skipped or just telling it the new time.
++      // But what it actually does internally is just:
++      //
++      //  _currentTime += 1000/_ticker.FRAME_RATE;
++      //
++      // So by dynamically adjusting the value of FRAME_RATE we can trick
++      // it into dealing with dropped frames.
++
++      let delta = frame - this._frame;
++      if (delta == 0)
++          this.FRAME_RATE = this.TARGET_FRAME_RATE;
++      else
++          this.FRAME_RATE = this.TARGET_FRAME_RATE / delta;
++
++      this.emit('prepare-frame');
++    },
++
++    start : function() {
++      this._timeline.start();
++    },
++
++    stop : function() {
++      this._timeline.stop();
++      this._frame = 0;
++    }
++};
++
++Signals.addSignalMethods(ClutterFrameTicker.prototype);
++
+ function start() {
+     let global = Shell.global_get();
+ 
++    Tweener.setFrameTicker(new ClutterFrameTicker());
++
+     // The background color really only matters if there is no desktop
+     // window (say, nautilus) running. We set it mostly so things look good
+     // when we are running inside Xephyr.
+-- 
+1.6.0.3
\ No newline at end of file
diff --git a/testpatches/git-plain-diff.patch b/testpatches/git-plain-diff.patch
new file mode 100644
index 0000000..774ab13
--- /dev/null
+++ b/testpatches/git-plain-diff.patch
@@ -0,0 +1,84 @@
+diff --git a/clutter/cogl/gl/cogl.c b/clutter/cogl/gl/cogl.c
+index 2cc67b3..412b0ba 100644
+--- a/clutter/cogl/gl/cogl.c
++++ b/clutter/cogl/gl/cogl.c
+@@ -860,39 +860,57 @@ cogl_setup_viewport (guint        width,
+                    ClutterFixed z_far)
+ {
+   GLfloat z_camera;
++  GLfloat projection_matrix[16];
+ 
+   GE( glViewport (0, 0, width, height) );
+ 
+   cogl_perspective (fovy, aspect, z_near, z_far);
+ 
+-  GE( glLoadIdentity () );
+-
+   /*
+-   * camera distance from screen, 0.5 * tan (FOV)
++   * In theory, we can compute the camera distance from screen as:
++   *
++   *   0.5 * tan (FOV)
++   *
++   * However, due to limited accuracy in clutter_sinx/cosx, and thus
++   * cogl_perspective, we'll end up with a value that's off by about
++   * 0.5%. It's better to compute the z_camera from our projection
++   * matrix so that we get a 1:1 mapping at the screen distance. Consider
++   * the upper-left corner of the screen. It has object coordinates
++   * (0,0,0), so by the transform below, ends up with eye coordinate
++   *
++   *   x_eye = x_object / width - 0.5 = - 0.5
++   *   y_eye = (height - y_object) / width - 0.5 = 0.5
++   *   z_eye = z_object / width - z_camera = - z_camera
++   *
++   * From cogl_perspective(), we know that the projection matrix has
++   * the form:
++   *
++   *  (x, 0,  0, 0)
++   *  (0, y,  0, 0)
++   *  (0, 0,  c, d)
++   *  (0, 0, -1, 0)
+    *
+-   * We have been having some problems with this; the theoretically correct
+-   * value of 0.866025404f for the default 60 deg fovy angle happens to be
+-   * touch to small in reality, which on full-screen stage with an actor of
+-   * the same size results in about 1px on the left and top edges of the
+-   * actor being offscreen. Perhaps more significantly, it also causes
+-   * hinting artifacts when rendering text.
++   * Applied to the above, we get clip coordinates of
+    *
+-   * So for the default 60 deg angle we worked out that the value of 0.869
+-   * is giving correct stretch and no noticeable artifacts on text. Seems
+-   * good on all drivers too.
++   *  x_clip = x * (- 0.5)
++   *  y_clip = y * 0.5
++   *  w_clip = - 1 * (- z_camera) = z_camera
++   *
++   * Dividing through by w to get normalized device coordinates, we
++   * have, x_nd = x * 0.5 / z_camera, y_nd = - y * 0.5 / z_camera.
++   * The upper left corner of the screen has normalized device coordinates,
++   * (-1, 1), so to have the correct 1:1 mapping, we have to have:
++   *
++   *   z_camera = 0.5 * x = 0.5 * y
++   *
++   * If x != y, then we have a non-uniform aspect ration, and a 1:1 mapping
++   * doesn't make sense.
+    */
+-#define DEFAULT_Z_CAMERA 0.869f
+-  z_camera = DEFAULT_Z_CAMERA;
+ 
++  GE( glGetFloatv (GL_PROJECTION_MATRIX, projection_matrix) );
++  z_camera = 0.5 * projection_matrix[0];
+ 
+-  if (fovy != CFX_60)
+-  {
+-    ClutterFixed fovy_rad = CFX_MUL (fovy, CFX_PI) / 180;
+-
+-    z_camera =
+-      CLUTTER_FIXED_TO_FLOAT (CFX_DIV (clutter_sinx (fovy_rad),
+-                                     clutter_cosx (fovy_rad)) >> 1);
+-  }
++  GE( glLoadIdentity () );
+ 
+   GE( glTranslatef (-0.5f, -0.5f, -z_camera) );
+   GE( glScalef ( 1.0f / width,
diff --git a/testpatches/hg-multi-file.patch b/testpatches/hg-multi-file.patch
new file mode 100644
index 0000000..ea5e68b
--- /dev/null
+++ b/testpatches/hg-multi-file.patch
@@ -0,0 +1,50 @@
+# HG changeset patch
+# User Benoit Boissinot <benoit boissinot ens-lyon org>
+# Date 1228243003 -3600
+# Node ID 3342e6ada4b9abe8115941f8078f0f2604a9210a
+# Parent  3fb5c142a9f073b27c5ea07a9ac4fb540640b3ed
+push: use the fast changegroup() path on push
+
+The race doesn't happen on push (because the discovery is done
+in the same hg process), so use the fast path instead.
+
+diff -r 3fb5c142a9f0 -r 3342e6ada4b9 mercurial/localrepo.py
+--- a/mercurial/localrepo.py   Mon Dec 01 10:45:22 2008 -0500
++++ b/mercurial/localrepo.py   Tue Dec 02 19:36:43 2008 +0100
+@@ -1496,11 +1496,11 @@
+         return self.push_addchangegroup(remote, force, revs)
+ 
+     def prepush(self, remote, force, revs):
+-        base = {}
++        common = {}
+         remote_heads = remote.heads()
+-        inc = self.findincoming(remote, base, remote_heads, force=force)
++        inc = self.findincoming(remote, common, remote_heads, force=force)
+ 
+-        update, updated_heads = self.findoutgoing(remote, base, remote_heads)
++        update, updated_heads = self.findoutgoing(remote, common, remote_heads)
+         if revs is not None:
+             msng_cl, bases, heads = self.changelog.nodesbetween(update, revs)
+         else:
+@@ -1546,7 +1546,8 @@
+ 
+ 
+         if revs is None:
+-            cg = self.changegroup(update, 'push')
++            # use the fast path, no race possible on push
++            cg = self._changegroup(common.keys(), 'push')
+         else:
+             cg = self.changegroupsubset(update, revs, 'push')
+         return cg, remote_heads
+diff -r 3fb5c142a9f0 -r 3342e6ada4b9 tests/test-push-warn.out
+--- a/tests/test-push-warn.out Mon Dec 01 10:45:22 2008 -0500
++++ b/tests/test-push-warn.out Tue Dec 02 19:36:43 2008 +0100
+@@ -22,7 +22,7 @@
+ adding changesets
+ adding manifests
+ adding file changes
+-added 2 changesets with 1 changes to 2 files
++added 2 changesets with 1 changes to 1 files
+ adding foo
+ updating working directory
+ 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
diff --git a/testpatches/svn-multi-file.patch b/testpatches/svn-multi-file.patch
new file mode 100644
index 0000000..04ff80d
--- /dev/null
+++ b/testpatches/svn-multi-file.patch
@@ -0,0 +1,117 @@
+Index: test/js/testEverythingBasic.js
+===================================================================
+--- test/js/testEverythingBasic.js     (revision 75)
++++ test/js/testEverythingBasic.js     (working copy)
+@@ -1,5 +1,8 @@
+ const Everything = imports.gi.Everything;
+ 
++// We use Gio to have some objects that we know exist
++const Gio = imports.gi.Gio;
++
+ const INT8_MIN = (-128);
+ const INT16_MIN = (-32767-1);
+ const INT32_MIN = (-2147483647-1);
+@@ -68,4 +71,12 @@
+     assertRaises(function() { return Everything.test_size(-42); });
+ }
+ 
++function testBadConstructor() {
++    try {
++      Gio.AppLaunchContext();
++    } catch (e) {
++      assert(e.message.indexOf("Constructor called as normal method") >= 0);
++    }
++}
++
+ gjstestRun();
+Index: gjs/jsapi-util.c
+===================================================================
+--- gjs/jsapi-util.c   (revision 75)
++++ gjs/jsapi-util.c   (working copy)
+@@ -384,6 +384,18 @@
+     return prototype;
+ }
+ 
++gboolean
++gjs_check_constructing (JSContext *context)
++{
++    if (!JS_IsConstructing(context)) {
++        gjs_throw(context,
++                  "Constructor called as normal method. Use 'new SomeObject()' not 'SomeObject()'");
++        return FALSE;
++    }
++
++    return TRUE;
++}
++
+ void*
+ gjs_get_instance_private_dynamic(JSContext      *context,
+                                  JSObject       *obj,
+Index: gjs/jsapi-util.h
+===================================================================
+--- gjs/jsapi-util.h   (revision 75)
++++ gjs/jsapi-util.h   (working copy)
+@@ -125,6 +125,7 @@
+                                               JSFunctionSpec  *fs,
+                                               JSPropertySpec  *static_ps,
+                                               JSFunctionSpec  *static_fs);
++gboolean    gjs_check_constructing           (JSContext       *context);
+ void*       gjs_get_instance_private_dynamic (JSContext       *context,
+                                               JSObject        *obj,
+                                               JSClass         *static_clasp,
+Index: gi/param.c
+===================================================================
+--- gi/param.c (revision 75)
++++ gi/param.c (working copy)
+@@ -155,6 +155,9 @@
+     JSObject *proto;
+     gboolean is_proto;
+ 
++    if (!gjs_check_constructing(context))
++        return JS_FALSE;
++
+     priv = g_slice_new0(Param);
+ 
+     GJS_INC_COUNTER(param);
+Index: gi/boxed.c
+===================================================================
+--- gi/boxed.c (revision 75)
++++ gi/boxed.c (working copy)
+@@ -211,6 +211,9 @@
+     JSObject *proto;
+     gboolean is_proto;
+ 
++    if (!gjs_check_constructing(context))
++        return JS_FALSE;
++
+     priv = g_slice_new0(Boxed);
+ 
+     GJS_INC_COUNTER(boxed);
+Index: gi/object.c
+===================================================================
+--- gi/object.c        (revision 75)
++++ gi/object.c        (working copy)
+@@ -617,6 +617,9 @@
+     JSClass *obj_class;
+     JSClass *proto_class;
+ 
++    if (!gjs_check_constructing(context))
++        return JS_FALSE;
++
+     priv = g_slice_new0(ObjectInstance);
+ 
+     GJS_INC_COUNTER(object);
+Index: gi/union.c
+===================================================================
+--- gi/union.c (revision 75)
++++ gi/union.c (working copy)
+@@ -211,6 +211,9 @@
+     JSObject *proto;
+     gboolean is_proto;
+ 
++    if (!gjs_check_constructing(context))
++        return JS_FALSE;
++
+     priv = g_slice_new0(Union);
+ 
+     GJS_INC_COUNTER(boxed);
diff --git a/tests/bug.jst b/tests/bug.jst
new file mode 100644
index 0000000..7d27497
--- /dev/null
+++ b/tests/bug.jst
@@ -0,0 +1,45 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+include('Bug');
+include('TestUtils');
+include('Utils');
+
+let assert = Utils.assert;
+let assertEquals = TestUtils.assertEquals;
+let assertDateEquals = TestUtils.assertDateEquals;
+
+let referenceDate = new Date('Wed Nov 21, 2008 00:22 UTC');
+
+assertDateEquals(referenceDate, Bug.parseDate('2008-11-21 00:22  UTC'));
+assertDateEquals(referenceDate, Bug.parseDate('2008-11-21 00:22 +0000'));
+assertDateEquals(referenceDate, Bug.parseDate('2008-11-21 01:22 +0100'));
+assertDateEquals(referenceDate, Bug.parseDate('2008-11-21 01:52 +0130'));
+assertDateEquals(referenceDate, Bug.parseDate('2008-11-20 20:22 -0400'));
+assertDateEquals(referenceDate, Bug.parseDate('2008-11-20 20:22  EDT'));
+assertDateEquals(referenceDate, Bug.parseDate('2008-11-20 19:52 -0430'));
+assertDateEquals(new Date('Sat Aug 29, 2009 14:03:00 UTC'),
+                 Bug.parseDate('2009-08-29 10:03:00 -0400'));
+
+let bugtext = load('testbugs/561745/bug.xml');
+let bug_561745 = Bug.Bug.fromText(bugtext);
+
+// This test is mostly testing the E4X parser which is different from the DOM/JQuery parser
+// we use on the web page.
+
+assertEquals('Use a Tweener "Frame Ticker" with a ClutterTimeline backend', bug_561745.shortDesc);
+assertEquals("otaylor redhat com", bug_561745.getReporter());
+assertDateEquals(new Date('Wed Nov 21, 2008 00:22 UTC'), bug_561745.creationDate);
+assertEquals(1, bug_561745.attachments.length);
+
+let attachment = bug_561745.attachments[0];
+assertEquals(123143, attachment.id);
+assert(attachment.isPatch);
+assert(!attachment.isObsolete);
+assertDateEquals(new Date('Wed Nov 21, 2008 00:22 UTC'), attachment.date);
+assertEquals('Use a Tweener "Frame Ticker" with a ClutterTimeline backend', attachment.description);
+
+assertEquals(5, bug_561745.comments.length);
+
+let comment = bug_561745.comments[0];
+assertEquals("otaylor redhat com", comment.getWho());
+assertEquals("Call Tweener.setFrameTicker()", comment.text.substring(0, 29));
+assertDateEquals(new Date('Wed Nov 21, 2008 00:22 UTC'), comment.date);
diff --git a/tests/patch.jst b/tests/patch.jst
new file mode 100644
index 0000000..d2e93ca
--- /dev/null
+++ b/tests/patch.jst
@@ -0,0 +1,144 @@
+include('Patch');
+include('TestUtils');
+
+let assertEquals = TestUtils.assertEquals;
+
+let patch;
+let file;
+let hunk;
+
+function hunkToString(hunk) {
+    var data = [];
+    hunk.iterate(function(location, oldLine, oldText, newLine, newText, flags) {
+                     var oldOp = '';
+                     var newOp = '';
+
+                    if ((flags & Patch.CHANGED) != 0)
+                        oldOp = newOp = '!';
+                    else if ((flags & Patch.REMOVED) != 0)
+                        oldOp = '-';
+                    else if ((flags & Patch.ADDED) != 0)
+                        newOp = '+';
+                    if ((flags & Patch.OLD_NONEWLINE) != 0)
+                        oldOp += '*';
+                    if ((flags & Patch.NEW_NONEWLINE) != 0)
+                        newOp += '*';
+                     data.push([oldText != null ? oldOp   : '',
+                                oldText != null ? oldLine : '',
+                                oldText != null ? oldText : '',
+                                newText != null ? newOp   : '',
+                                newText != null ? newLine : '',
+                                newText != null ? newText : '']);
+
+                 });
+
+    return ('@@ -' + hunk.oldStart + ',' + hunk.oldCount + ' +' + hunk.newStart + ',' + hunk.newCount + '\n' 
+
+            TestUtils.table('lrllrl', data));
+}
+
+function fileToString(file) {
+    return ('::: ' + file.filename + '\n' +
+            [hunkToString(hunk) for each (hunk in file.hunks)].join(""));
+}
+
+function patchToString(patch) {
+    return [fileToString(file) for each (file in patch.files)].join("\n");
+}
+
+patch = new Patch.Patch(<<<
+Git output looks like
+
+diff --git a/js/ui/main.js b/js/ui/main.js
+index 882e34b..4832d31 100644
+--- a/js/ui/main.js
++++ b/js/ui/main.js
+@@ -1,6 +1,6 @@
+ const Shell = imports.gi.Shell;
++const Signals = imports.signals;
+ const Clutter = imports.gi.Clutter;
+-const Tweener = imports.tweener.tweener;
++const Animation = imports.gi.Animation
+ const Panel = imports.ui.panel;
+-const Overview = imports.ui.overview;
+ const Utils. = imports.ui.utils;
+-- 
+1.6.0.3
+>>>)
+
+assertEquals(<<<
+::: js/ui/main.js
+@@ -1,6 +1,6
+  1 const Shell = imports.gi.Shell;            1 const Shell = imports.gi.Shell;
+                                             + 2 const Signals = imports.signals;
+  2 const Clutter = imports.gi.Clutter;        3 const Clutter = imports.gi.Clutter;
+! 3 const Tweener = imports.tweener.tweener; ! 4 const Animation = imports.gi.Animation
+  4 const Panel = imports.ui.panel;            5 const Panel = imports.ui.panel;
+- 5 const Overview = imports.ui.overview;
+  6 const Utils. = imports.ui.utils;           6 const Utils. = imports.ui.utils;
+>>>, patchToString(patch))
+
+patch = new Patch.Patch(<<<
+https://launchpad.net/~gnome-doc-centric-playground
+
+=== modified file 'src/zeitgeist_gui/zeitgeist_panel_widgets.py'
+--- src/zeitgeist_gui/zeitgeist_panel_widgets.py        2008-11-20 02:46:24 +0000
++++ src/zeitgeist_gui/zeitgeist_panel_widgets.py        2008-11-20 21:14:40 +0000
+@@ -1,5 +1,9 @@
+-import zeitgeist_engine.zeitgeist_datasink
+-import zeitgeist_engine.zeitgeist_util
++import datetime
+ import gc
++import os
+ import time
++
++import zeitgeist_engine.zeitgeist_datasink
++import zeitgeist_engine.zeitgeist_util
++
+ class TimelineWidget(gtk.HBox):
+@@ -514,2 +514,2 @@
+ calendar = CalendarWidget()
+-timeline = TimelineWidget()
+\ No newline at end of file
++timeline = TimelineWidget()
+
+=== modified file 'src/zeitgeist_gui/zeitgeist_calendar_gui.py'
+--- src/zeitgeist_gui/zeitgeist_calendar_gui.py        2008-11-19 18:43:27 +0000
++++ src/zeitgeist_gui/zeitgeist_calendar_gui.py        2008-11-20 21:27:45 +0000
+@@ -11,5 +11,4 @@
+ import gtk
+ import gtk.glade
+-import datetime
+ import zeitgeist_engine.zeitgeist_util
+ 
+
+>>>);
+
+assertEquals(<<<
+::: src/zeitgeist_gui/zeitgeist_panel_widgets.py
+@@ -1,5 +1,9
+! 1 import zeitgeist_engine.zeitgeist_datasink ! 1 import datetime
+! 2 import zeitgeist_engine.zeitgeist_util
+  3 import gc                                    2 import gc
+                                               + 3 import os
+  4 import time                                  4 import time
+                                               + 5
+                                               + 6 import zeitgeist_engine.zeitgeist_datasink
+                                               + 7 import zeitgeist_engine.zeitgeist_util
+                                               + 8
+  5 class TimelineWidget(gtk.HBox):              9 class TimelineWidget(gtk.HBox):
+@@ -514,2 +514,2
+   514 calendar = CalendarWidget()   514 calendar = CalendarWidget()
+!* 515 timeline = TimelineWidget() ! 515 timeline = TimelineWidget()
+
+::: src/zeitgeist_gui/zeitgeist_calendar_gui.py
+@@ -11,5 +11,4
+  11 import gtk                              11 import gtk
+  12 import gtk.glade                        12 import gtk.glade
+- 13 import datetime
+  14 import zeitgeist_engine.zeitgeist_util  13 import zeitgeist_engine.zeitgeist_util
+  15                                         14
+>>>, patchToString(patch));
+
+file = patch.getFile('src/zeitgeist_gui/zeitgeist_panel_widgets.py');
+assertEquals(file.getLocation(515, 515), 11);
+
diff --git a/tests/review.jst b/tests/review.jst
new file mode 100644
index 0000000..b662688
--- /dev/null
+++ b/tests/review.jst
@@ -0,0 +1,130 @@
+include('Patch');
+include('Review');
+include('TestUtils');
+
+let assertEquals = TestUtils.assertEquals;
+
+let patch_text = <<<
+diff --git a/gi/arg.c b/gi/arg.c
+index b37e1a7..51da8f7 100644
+--- a/gi/arg.c
++++ b/gi/arg.c
+@@ -212,8 +212,6 @@ gjs_array_to_array(JSContext   *context,
+                    GITypeInfo  *param_info,
+                    void       **arr_p)
+ {
+-    guint32 i;
+-    jsval elem;
+     GITypeTag element_type;
+ 
+     element_type = g_type_info_get_tag(param_info);
+@@ -1126,8 +1124,8 @@ gjs_g_arg_release_in_arg(JSContext  *context,
+ 
+     /* we don't own the argument anymore */
+     if (transfer == GI_TRANSFER_EVERYTHING)
+-        /* We're done */
+-        return;
++        /* Success! */
++        return JS_TRUE;
+ 
+     type_tag = g_type_info_get_tag( (GITypeInfo*) type_info);
+ ' +
+diff --git a/gi/function.c b/gi/function.c
+index 2ef8642..b8aae11 100644
+--- a/gi/function.c
++++ b/gi/function.c
+@@ -261,6 +261,7 @@ gjs_invoke_c_function(JSContext      *context,
+     if (return_tag != GI_TYPE_TAG_VOID)
+         n_return_values += 1;
+ 
++    return_values = NULL; /* Quiet gcc warning about initialization */
+     if (n_return_values > 0) {
+         if (invoke_ok) {
+             return_values = g_newa(jsval, n_return_values);
+>>>;
+
+let p = new Patch.Patch(patch_text);
+let r = new Review.Review(p);
+
+r.setIntro('I like this patch');
+assertEquals(<<<
+I like this patch
+>>>, r.toString());
+
+let argC = r.getFile('gi/arg.c');
+let loc = argC.patchFile.getLocation(216,214);
+r.getFile('gi/arg.c').addComment(loc, 'Should you have removed elem?');
+assertEquals(<<<
+I like this patch
+
+::: gi/arg.c
+@@ -214,3 +214,1 @@
+ {
+-    guint32 i;
+-    jsval elem;
+
+Should you have removed elem?
+>>>, r.toString());
+
+let loc = argC.patchFile.getLocation(1130,1128);
+r.getFile('gi/arg.c').addComment(loc, 'This comment seems unnecessary');
+assertEquals(<<<
+I like this patch
+
+::: gi/arg.c
+@@ -214,3 +214,1 @@
+ {
+-    guint32 i;
+-    jsval elem;
+
+Should you have removed elem?
+
+@@ -1128,3 +1126,3 @@
+     if (transfer == GI_TRANSFER_EVERYTHING)
+-        /* We're done */
+-        return;
++        /* Success! */
++        return JS_TRUE;
+
+This comment seems unnecessary
+>>>, r.toString());
+
+loc = argC.patchFile.getLocation(1128,1126);
+argC.addComment(loc, "Why this transfer?");
+assertEquals(<<<
+I like this patch
+
+::: gi/arg.c
+@@ -214,3 +214,1 @@
+ {
+-    guint32 i;
+-    jsval elem;
+
+Should you have removed elem?
+
+@@ -1127,2 +1125,2 @@
+     /* we don't own the argument anymore */
+     if (transfer == GI_TRANSFER_EVERYTHING)
+
+Why this transfer?
+
+@@ -1129,2 +1127,2 @@
+-        /* We're done */
+-        return;
++        /* Success! */
++        return JS_TRUE;
+
+This comment seems unnecessary
+>>>, r.toString());
+
+
+let r2 = new Review.Review(p);
+r2.parse(r.toString());
+assertEquals(r.toString(), r2.toString());
+
+loc = argC.patchFile.getLocation(216,214);
+let comment = argC.getComment(loc);
+assertEquals(loc, comment.location);
+comment.remove();
+
+assertEquals(null, argC.getComment(loc));
diff --git a/tests/testutils.jst b/tests/testutils.jst
new file mode 100644
index 0000000..4cbaf61
--- /dev/null
+++ b/tests/testutils.jst
@@ -0,0 +1,20 @@
+include('TestUtils')
+
+let assertEquals = TestUtils.assertEquals;
+
+let lalign = TestUtils.lalign;
+let ralign = TestUtils.ralign;
+
+assertEquals('ab   ', lalign('ab', 5));
+assertEquals('   ab', ralign('ab', 5));
+assertEquals('abcde', lalign('abcdefg', 5));
+assertEquals('cdefg', ralign('abcdefg', 5));
+
+assertEquals(<<<
+1  222 3
+42  15 15
+1    6 3213
+>>>, TestUtils.table('lrl',
+                     [[1,  222, 3],
+                     [42, 15, 15],
+                     [1,  6,  3213]]));
diff --git a/tests/utils.jst b/tests/utils.jst
new file mode 100644
index 0000000..403a704
--- /dev/null
+++ b/tests/utils.jst
@@ -0,0 +1,42 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+include('TestUtils');
+include('Utils');
+
+let assertEquals = TestUtils.assertEquals;
+
+assertEquals('a ', Utils.lstrip(' a '));
+assertEquals(' a', Utils.rstrip(' a '));
+assertEquals('a',  Utils.strip('a'));
+
+// ========================================
+
+let now = new Date();
+// A Sunday
+now.setFullYear(2009);
+now.setMonth(8);
+now.setDate(6);
+now.setHours(10);
+now.setMinutes(0);
+now.setSeconds(0);
+
+let then = new Date(now.getTime());
+
+// Short time in the past
+then.setHours(9);
+assertEquals(then.toLocaleTimeString(), Utils.formatDate(then, now));
+
+// Short time in the future
+then.setHours(11);
+assertEquals(then.toLocaleTimeString(), Utils.formatDate(then, now));
+
+// In the future and not today
+then.setDate(7);
+assertEquals(then.toLocaleDateString(), Utils.formatDate(then, now));
+
+// Less than 24 hours in the past, but not today
+then.setDate(5);
+assertEquals("Sat " + then.toLocaleTimeString(), Utils.formatDate(then, now));
+
+// Further in the past
+then.setMonth(7);
+assertEquals(then.toLocaleDateString(), Utils.formatDate(then, now));
diff --git a/web/config.js.example b/web/config.js.example
new file mode 100644
index 0000000..b676ad5
--- /dev/null
+++ b/web/config.js.example
@@ -0,0 +1,6 @@
+configAttachmentStatuses = [
+    'none',
+    'reviewed',
+    'rejected',
+    'committed'
+];
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..4d15957
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,46 @@
+<html>
+  <head>
+    <title>Patch Review</title>
+    <link rel="stylesheet" href="splinter.css" type="text/css"></link>
+    <script src="jquery.min.js" type="text/javascript"></script>
+    <script src="config.js" type="text/javascript"></script>
+    <script src="splinter.flat.js" type="text/javascript"></script>
+    <script type="text/javascript">
+      $(function() { init(); });
+    </script>
+  </head>
+  <body>
+    <div id="loading">Loading....</div>
+    <div id="headers" style="display: none;">
+      <div id="bugInfo">
+       Bug <span id="bugId"></span> -
+       <span id="bugShortDesc"></span> -
+       <span id="bugReporter"></span> -
+       <span id="bugCreationDate"></span>
+      </div>
+      <div id="attachmentInfo">
+       Attachment <span id="attachmentId"></span> -
+       <span id="attachmentDesc"></span> -
+       <span id="attachmentDate"></span>
+      </div>
+    </div>
+    <div id="controls" style="display: none;">
+      <div id="oldReviews">
+      </div>
+      <div>
+       <div>Overall Comment:</div>
+       <textarea id="myComment"></textarea>
+       <div>
+       </div>
+       <div id="buttonBox">
+         <span id="attachmentStatusSpan">Patch Status:
+           <select id="attachmentStatus"> </select>
+         </span>
+         <input id="saveButton" type="button" value="Save"></input>
+         <input id="cancelButton" type="button" value="Cancel"></input>
+       </div>
+       <div id="buttonSeparator"></div>
+      </div>
+    <div id="files" style="display: none;"></div>
+  </body>
+</html>
diff --git a/web/jquery.min.js b/web/jquery.min.js
new file mode 100644
index 0000000..c327fae
--- /dev/null
+++ b/web/jquery.min.js
@@ -0,0 +1,19 @@
+/*
+ * jQuery JavaScript Library v1.3.1
+ * http://jquery.com/
+ *
+ * Copyright (c) 2009 John Resig
+ * Dual licensed under the MIT and GPL licenses.
+ * http://docs.jquery.com/License
+ *
+ * Date: 2009-01-21 20:42:16 -0500 (Wed, 21 Jan 2009)
+ * Revision: 6158
+ */
+(function(){var l=this,g,y=l.jQuery,p=l.$,o=l.jQuery=l.$=function(E,F){return new 
o.fn.init(E,F)},D=/^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,f=/^.[^:#\[\.,]*$/;o.fn=o.prototype={init:function(E,H){E=E||document;if(E.nodeType){this[0]=E;this.length=1;this.context=E;return
 this}if(typeof E==="string"){var G=D.exec(E);if(G&&(G[1]||!H)){if(G[1]){E=o.clean([G[1]],H)}else{var 
I=document.getElementById(G[3]);if(I&&I.id!=G[3]){return o().find(E)}var 
F=o(I||[]);F.context=document;F.selector=E;return F}}else{return 
o(H).find(E)}}else{if(o.isFunction(E)){return 
o(document).ready(E)}}if(E.selector&&E.context){this.selector=E.selector;this.context=E.context}return 
this.setArray(o.makeArray(E))},selector:"",jquery:"1.3.1",size:function(){return 
this.length},get:function(E){return E===g?o.makeArray(this):this[E]},pushStack:function(F,H,E){var 
G=o(F);G.prevObject=this;G.context=this.context;if(H==="find"){G.selector=this.selector+(this.selector?" 
":"")+E}else{if(H){G.selector=this.selector+"."
 +H+"("+E+")"}}return G},setArray:function(E){this.length=0;Array.prototype.push.apply(this,E);return 
this},each:function(F,E){return o.each(this,F,E)},index:function(E){return 
o.inArray(E&&E.jquery?E[0]:E,this)},attr:function(F,H,G){var E=F;if(typeof F==="string"){if(H===g){return 
this[0]&&o[G||"attr"](this[0],F)}else{E={};E[F]=H}}return this.each(function(I){for(F in 
E){o.attr(G?this.style:this,F,o.prop(this,E[F],G,I,F))}})},css:function(E,F){if((E=="width"||E=="height")&&parseFloat(F)<0){F=g}return
 this.attr(E,F,"curCSS")},text:function(F){if(typeof F!=="object"&&F!=null){return 
this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(F))}var 
E="";o.each(F||this,function(){o.each(this.childNodes,function(){if(this.nodeType!=8){E+=this.nodeType!=1?this.nodeValue:o.fn.text([this])}})});return
 E},wrapAll:function(E){if(this[0]){var 
F=o(E,this[0].ownerDocument).clone();if(this[0].parentNode){F.insertBefore(this[0])}F.map(function(){var 
G=this;while(G.first
 Child){G=G.firstChild}return G}).append(this)}return this},wrapInner:function(E){return 
this.each(function(){o(this).contents().wrapAll(E)})},wrap:function(E){return 
this.each(function(){o(this).wrapAll(E)})},append:function(){return 
this.domManip(arguments,true,function(E){if(this.nodeType==1){this.appendChild(E)}})},prepend:function(){return
 
this.domManip(arguments,true,function(E){if(this.nodeType==1){this.insertBefore(E,this.firstChild)}})},before:function(){return
 this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this)})},after:function(){return 
this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this.nextSibling)})},end:function(){return
 this.prevObject||o([])},push:[].push,find:function(E){if(this.length===1&&!/,/.test(E)){var 
G=this.pushStack([],"find",E);G.length=0;o.find(E,this[0],G);return G}else{var 
F=o.map(this,function(H){return o.find(E,H)});return this.pushStack(/[^+>] 
[^+>]/.test(E)?o.unique(F):F,"find",E)}},clone:
 function(F){var E=this.map(function(){if(!o.support.noCloneEvent&&!o.isXMLDoc(this)){var 
I=this.cloneNode(true),H=document.createElement("div");H.appendChild(I);return 
o.clean([H.innerHTML])[0]}else{return this.cloneNode(true)}});var 
G=E.find("*").andSelf().each(function(){if(this[h]!==g){this[h]=null}});if(F===true){this.find("*").andSelf().each(function(I){if(this.nodeType==3){return}var
 H=o.data(this,"events");for(var K in H){for(var J in 
H[K]){o.event.add(G[I],K,H[K][J],H[K][J].data)}}})}return E},filter:function(E){return 
this.pushStack(o.isFunction(E)&&o.grep(this,function(G,F){return 
E.call(G,F)})||o.multiFilter(E,o.grep(this,function(F){return 
F.nodeType===1})),"filter",E)},closest:function(E){var F=o.expr.match.POS.test(E)?o(E):null;return 
this.map(function(){var G=this;while(G&&G.ownerDocument){if(F?F.index(G)>-1:o(G).is(E)){return 
G}G=G.parentNode}})},not:function(E){if(typeof E==="string"){if(f.test(E)){return 
this.pushStack(o.multiFilter(E,this,true),"not",E)}el
 se{E=o.multiFilter(E,this)}}var F=E.length&&E[E.length-1]!==g&&!E.nodeType;return 
this.filter(function(){return F?o.inArray(this,E)<0:this!=E})},add:function(E){return 
this.pushStack(o.unique(o.merge(this.get(),typeof E==="string"?o(E):o.makeArray(E))))},is:function(E){return 
!!E&&o.multiFilter(E,this).length>0},hasClass:function(E){return 
!!E&&this.is("."+E)},val:function(K){if(K===g){var 
E=this[0];if(E){if(o.nodeName(E,"option")){return(E.attributes.value||{}).specified?E.value:E.text}if(o.nodeName(E,"select")){var
 I=E.selectedIndex,L=[],M=E.options,H=E.type=="select-one";if(I<0){return null}for(var 
F=H?I:0,J=H?I+1:M.length;F<J;F++){var G=M[F];if(G.selected){K=o(G).val();if(H){return K}L.push(K)}}return 
L}return(E.value||"").replace(/\r/g,"")}return g}if(typeof K==="number"){K+=""}return 
this.each(function(){if(this.nodeType!=1){return}if(o.isArray(K)&&/radio|checkbox/.test(this.type)){this.checked=(o.inArray(this.value,K)>=0||o.inArray(this.name,K)>=0)}else{if(o.nodeName(
 this,"select")){var 
N=o.makeArray(K);o("option",this).each(function(){this.selected=(o.inArray(this.value,N)>=0||o.inArray(this.text,N)>=0)});if(!N.length){this.selectedIndex=-1}}else{this.value=K}}})},html:function(E){return
 E===g?(this[0]?this[0].innerHTML:null):this.empty().append(E)},replaceWith:function(E){return 
this.after(E).remove()},eq:function(E){return this.slice(E,+E+1)},slice:function(){return 
this.pushStack(Array.prototype.slice.apply(this,arguments),"slice",Array.prototype.slice.call(arguments).join(","))},map:function(E){return
 this.pushStack(o.map(this,function(G,F){return E.call(G,F,G)}))},andSelf:function(){return 
this.add(this.prevObject)},domManip:function(K,N,M){if(this[0]){var 
J=(this[0].ownerDocument||this[0]).createDocumentFragment(),G=o.clean(K,(this[0].ownerDocument||this[0]),J),I=J.firstChild,E=this.length>1?J.cloneNode(true):J;if(I){for(var
 H=0,F=this.length;H<F;H++){M.call(L(this[H],I),H>0?E.cloneNode(true):J)}}if(G){o.each(G,z)}}return this;fun
 ction L(O,P){return 
N&&o.nodeName(O,"table")&&o.nodeName(P,"tr")?(O.getElementsByTagName("tbody")[0]||O.appendChild(O.ownerDocument.createElement("tbody"))):O}}};o.fn.init.prototype=o.fn;function
 
z(E,F){if(F.src){o.ajax({url:F.src,async:false,dataType:"script"})}else{o.globalEval(F.text||F.textContent||F.innerHTML||"")}if(F.parentNode){F.parentNode.removeChild(F)}}function
 e(){return +new Date}o.extend=o.fn.extend=function(){var 
J=arguments[0]||{},H=1,I=arguments.length,E=false,G;if(typeof 
J==="boolean"){E=J;J=arguments[1]||{};H=2}if(typeof 
J!=="object"&&!o.isFunction(J)){J={}}if(I==H){J=this;--H}for(;H<I;H++){if((G=arguments[H])!=null){for(var F 
in G){var K=J[F],L=G[F];if(J===L){continue}if(E&&L&&typeof 
L==="object"&&!L.nodeType){J[F]=o.extend(E,K||(L.length!=null?[]:{}),L)}else{if(L!==g){J[F]=L}}}}}return 
J};var 
b=/z-?index|font-?weight|opacity|zoom|line-?height/i,q=document.defaultView||{},s=Object.prototype.toString;o.extend({noConflict:function(E){l.$=p;if(E){l.jQuery=y
 }return o},isFunction:function(E){return s.call(E)==="[object Function]"},isArray:function(E){return 
s.call(E)==="[object Array]"},isXMLDoc:function(E){return 
E.nodeType===9&&E.documentElement.nodeName!=="HTML"||!!E.ownerDocument&&o.isXMLDoc(E.ownerDocument)},globalEval:function(G){G=o.trim(G);if(G){var
 
F=document.getElementsByTagName("head")[0]||document.documentElement,E=document.createElement("script");E.type="text/javascript";if(o.support.scriptEval){E.appendChild(document.createTextNode(G))}else{E.text=G}F.insertBefore(E,F.firstChild);F.removeChild(E)}},nodeName:function(F,E){return
 F.nodeName&&F.nodeName.toUpperCase()==E.toUpperCase()},each:function(G,K,F){var 
E,H=0,I=G.length;if(F){if(I===g){for(E in 
G){if(K.apply(G[E],F)===false){break}}}else{for(;H<I;){if(K.apply(G[H++],F)===false){break}}}}else{if(I===g){for(E
 in G){if(K.call(G[E],E,G[E])===false){break}}}else{for(var 
J=G[0];H<I&&K.call(J,H,J)!==false;J=G[++H]){}}}return G},prop:function(H,I,G,F,E){if(o.isFunction(
 I)){I=I.call(H,F)}return typeof 
I==="number"&&G=="curCSS"&&!b.test(E)?I+"px":I},className:{add:function(E,F){o.each((F||"").split(/\s+/),function(G,H){if(E.nodeType==1&&!o.className.has(E.className,H)){E.className+=(E.className?"
 
":"")+H}})},remove:function(E,F){if(E.nodeType==1){E.className=F!==g?o.grep(E.className.split(/\s+/),function(G){return
 !o.className.has(F,G)}).join(" "):""}},has:function(F,E){return 
F&&o.inArray(E,(F.className||F).toString().split(/\s+/))>-1}},swap:function(H,G,I){var E={};for(var F in 
G){E[F]=H.style[F];H.style[F]=G[F]}I.call(H);for(var F in 
G){H.style[F]=E[F]}},css:function(G,E,I){if(E=="width"||E=="height"){var 
K,F={position:"absolute",visibility:"hidden",display:"block"},J=E=="width"?["Left","Right"]:["Top","Bottom"];function
 H(){K=E=="width"?G.offsetWidth:G.offsetHeight;var 
M=0,L=0;o.each(J,function(){M+=parseFloat(o.curCSS(G,"padding"+this,true))||0;L+=parseFloat(o.curCSS(G,"border"+this+"Width",true))||0});K-=Math.round(M+L)}if(o(G).is(":vi
 sible")){H()}else{o.swap(G,F,H)}return Math.max(0,K)}return o.curCSS(G,E,I)},curCSS:function(I,F,G){var 
L,E=I.style;if(F=="opacity"&&!o.support.opacity){L=o.attr(E,"opacity");return 
L==""?"1":L}if(F.match(/float/i)){F=w}if(!G&&E&&E[F]){L=E[F]}else{if(q.getComputedStyle){if(F.match(/float/i)){F="float"}F=F.replace(/([A-Z])/g,"-$1").toLowerCase();var
 
M=q.getComputedStyle(I,null);if(M){L=M.getPropertyValue(F)}if(F=="opacity"&&L==""){L="1"}}else{if(I.currentStyle){var
 J=F.replace(/\-(\w)/g,function(N,O){return 
O.toUpperCase()});L=I.currentStyle[F]||I.currentStyle[J];if(!/^\d+(px)?$/i.test(L)&&/^\d/.test(L)){var 
H=E.left,K=I.runtimeStyle.left;I.runtimeStyle.left=I.currentStyle.left;E.left=L||0;L=E.pixelLeft+"px";E.left=H;I.runtimeStyle.left=K}}}}return
 L},clean:function(F,K,I){K=K||document;if(typeof 
K.createElement==="undefined"){K=K.ownerDocument||K[0]&&K[0].ownerDocument||document}if(!I&&F.length===1&&typeof
 F[0]==="string"){var H=/^<(\w+)\s*\/?>$/.exec(F[0]);if(H){return[K.cr
 eateElement(H[1])]}}var G=[],E=[],L=K.createElement("div");o.each(F,function(P,R){if(typeof 
R==="number"){R+=""}if(!R){return}if(typeof 
R==="string"){R=R.replace(/(<(\w+)[^>]*?)\/>/g,function(T,U,S){return 
S.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?T:U+"></"+S+">"});var 
O=o.trim(R).toLowerCase();var Q=!O.indexOf("<opt")&&[1,"<select 
multiple='multiple'>","</select>"]||!O.indexOf("<leg")&&[1,"<fieldset>","</fieldset>"]||O.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"<table>","</table>"]||!O.indexOf("<tr")&&[2,"<table><tbody>","</tbody></table>"]||(!O.indexOf("<td")||!O.indexOf("<th"))&&[3,"<table><tbody><tr>","</tr></tbody></table>"]||!O.indexOf("<col")&&[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"]||!o.support.htmlSerialize&&[1,"div<div>","</div>"]||[0,"",""];L.innerHTML=Q[1]+R+Q[2];while(Q[0]--){L=L.lastChild}if(!o.support.tbody){var
 N=!O.indexOf("<table")&&O.indexOf("<tbody")<0?L.firstChild&&L.firstChild.childNodes:Q[1]=="<table>"&&O
 .indexOf("<tbody")<0?L.childNodes:[];for(var 
M=N.length-1;M>=0;--M){if(o.nodeName(N[M],"tbody")&&!N[M].childNodes.length){N[M].parentNode.removeChild(N[M])}}}if(!o.support.leadingWhitespace&&/^\s/.test(R)){L.insertBefore(K.createTextNode(R.match(/^\s*/)[0]),L.firstChild)}R=o.makeArray(L.childNodes)}if(R.nodeType){G.push(R)}else{G=o.merge(G,R)}});if(I){for(var
 
J=0;G[J];J++){if(o.nodeName(G[J],"script")&&(!G[J].type||G[J].type.toLowerCase()==="text/javascript")){E.push(G[J].parentNode?G[J].parentNode.removeChild(G[J]):G[J])}else{if(G[J].nodeType===1){G.splice.apply(G,[J+1,0].concat(o.makeArray(G[J].getElementsByTagName("script"))))}I.appendChild(G[J])}}return
 E}return G},attr:function(J,G,K){if(!J||J.nodeType==3||J.nodeType==8){return g}var 
H=!o.isXMLDoc(J),L=K!==g;G=H&&o.props[G]||G;if(J.tagName){var 
F=/href|src|style/.test(G);if(G=="selected"&&J.parentNode){J.parentNode.selectedIndex}if(G in 
J&&H&&!F){if(L){if(G=="type"&&o.nodeName(J,"input")&&J.parentNode){throw"type proper
 ty can't be changed"}J[G]=K}if(o.nodeName(J,"form")&&J.getAttributeNode(G)){return 
J.getAttributeNode(G).nodeValue}if(G=="tabIndex"){var I=J.getAttributeNode("tabIndex");return 
I&&I.specified?I.value:J.nodeName.match(/(button|input|object|select|textarea)/i)?0:J.nodeName.match(/^(a|area)$/i)&&J.href?0:g}return
 J[G]}if(!o.support.style&&H&&G=="style"){return o.attr(J.style,"cssText",K)}if(L){J.setAttribute(G,""+K)}var 
E=!o.support.hrefNormalized&&H&&F?J.getAttribute(G,2):J.getAttribute(G);return 
E===null?g:E}if(!o.support.opacity&&G=="opacity"){if(L){J.zoom=1;J.filter=(J.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(K)+""=="NaN"?"":"alpha(opacity="+K*100+")")}return
 
J.filter&&J.filter.indexOf("opacity=")>=0?(parseFloat(J.filter.match(/opacity=([^)]*)/)[1])/100)+"":""}G=G.replace(/-([a-z])/ig,function(M,N){return
 N.toUpperCase()});if(L){J[G]=K}return 
J[G]},trim:function(E){return(E||"").replace(/^\s+|\s+$/g,"")},makeArray:function(G){var E=[];if(G!=null){var 
F=G.length;if
 (F==null||typeof G==="string"||o.isFunction(G)||G.setInterval){E[0]=G}else{while(F){E[--F]=G[F]}}}return 
E},inArray:function(G,H){for(var E=0,F=H.length;E<F;E++){if(H[E]===G){return E}}return 
-1},merge:function(H,E){var 
F=0,G,I=H.length;if(!o.support.getAll){while((G=E[F++])!=null){if(G.nodeType!=8){H[I++]=G}}}else{while((G=E[F++])!=null){H[I++]=G}}return
 H},unique:function(K){var F=[],E={};try{for(var G=0,H=K.length;G<H;G++){var 
J=o.data(K[G]);if(!E[J]){E[J]=true;F.push(K[G])}}}catch(I){F=K}return F},grep:function(F,J,E){var 
G=[];for(var H=0,I=F.length;H<I;H++){if(!E!=!J(F[H],H)){G.push(F[H])}}return G},map:function(E,J){var 
F=[];for(var G=0,H=E.length;G<H;G++){var I=J(E[G],G);if(I!=null){F[F.length]=I}}return 
F.concat.apply([],F)}});var 
C=navigator.userAgent.toLowerCase();o.browser={version:(C.match(/.+(?:rv|it|ra|ie)[\/: 
]([\d.]+)/)||[0,"0"])[1],safari:/webkit/.test(C),opera:/opera/.test(C),msie:/msie/.test(C)&&!/opera/.test(C),mozilla:/mozilla/.test(C)&&!/(compatible|web
 kit)/.test(C)};o.each({parent:function(E){return E.parentNode},parents:function(E){return 
o.dir(E,"parentNode")},next:function(E){return o.nth(E,2,"nextSibling")},prev:function(E){return 
o.nth(E,2,"previousSibling")},nextAll:function(E){return o.dir(E,"nextSibling")},prevAll:function(E){return 
o.dir(E,"previousSibling")},siblings:function(E){return 
o.sibling(E.parentNode.firstChild,E)},children:function(E){return 
o.sibling(E.firstChild)},contents:function(E){return 
o.nodeName(E,"iframe")?E.contentDocument||E.contentWindow.document:o.makeArray(E.childNodes)}},function(E,F){o.fn[E]=function(G){var
 H=o.map(this,F);if(G&&typeof G=="string"){H=o.multiFilter(G,H)}return 
this.pushStack(o.unique(H),E,G)}});o.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(E,F){o.fn[E]=function(){var
 G=arguments;return this.each(function(){for(var 
H=0,I=G.length;H<I;H++){o(G[H])[F](this)}})}});o.each({removeAttr:function(E){o.at
 
tr(this,E,"");if(this.nodeType==1){this.removeAttribute(E)}},addClass:function(E){o.className.add(this,E)},removeClass:function(E){o.className.remove(this,E)},toggleClass:function(F,E){if(typeof
 
E!=="boolean"){E=!o.className.has(this,F)}o.className[E?"add":"remove"](this,F)},remove:function(E){if(!E||o.filter(E,[this]).length){o("*",this).add([this]).each(function(){o.event.remove(this);o.removeData(this)});if(this.parentNode){this.parentNode.removeChild(this)}}},empty:function(){o(">*",this).remove();while(this.firstChild){this.removeChild(this.firstChild)}}},function(E,F){o.fn[E]=function(){return
 this.each(F,arguments)}});function j(E,F){return E[0]&&parseInt(o.curCSS(E[0],F,true),10)||0}var 
h="jQuery"+e(),v=0,A={};o.extend({cache:{},data:function(F,E,G){F=F==l?A:F;var 
H=F[h];if(!H){H=F[h]=++v}if(E&&!o.cache[H]){o.cache[H]={}}if(G!==g){o.cache[H][E]=G}return 
E?o.cache[H][E]:H},removeData:function(F,E){F=F==l?A:F;var H=F[h];if(E){if(o.cache[H]){delete 
o.cache[H][E];E="";fo
 r(E in o.cache[H]){break}if(!E){o.removeData(F)}}}else{try{delete 
F[h]}catch(G){if(F.removeAttribute){F.removeAttribute(h)}}delete 
o.cache[H]}},queue:function(F,E,H){if(F){E=(E||"fx")+"queue";var 
G=o.data(F,E);if(!G||o.isArray(H)){G=o.data(F,E,o.makeArray(H))}else{if(H){G.push(H)}}}return 
G},dequeue:function(H,G){var 
E=o.queue(H,G),F=E.shift();if(!G||G==="fx"){F=E[0]}if(F!==g){F.call(H)}}});o.fn.extend({data:function(E,G){var
 H=E.split(".");H[1]=H[1]?"."+H[1]:"";if(G===g){var 
F=this.triggerHandler("getData"+H[1]+"!",[H[0]]);if(F===g&&this.length){F=o.data(this[0],E)}return 
F===g&&H[1]?this.data(H[0]):F}else{return 
this.trigger("setData"+H[1]+"!",[H[0],G]).each(function(){o.data(this,E,G)})}},removeData:function(E){return 
this.each(function(){o.removeData(this,E)})},queue:function(E,F){if(typeof 
E!=="string"){F=E;E="fx"}if(F===g){return o.queue(this[0],E)}return this.each(function(){var 
G=o.queue(this,E,F);if(E=="fx"&&G.length==1){G[0].call(this)}})},dequeue:function(E){retur
 n this.each(function(){o.dequeue(this,E)})}});
+/*
+ * Sizzle CSS Selector Engine - v0.9.3
+ *  Copyright 2009, The Dojo Foundation
+ *  Released under the MIT, BSD, and GPL Licenses.
+ *  More information: http://sizzlejs.com/
+ */
+(function(){var Q=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]+['"]|[^[\]'"]+)+\]|\\.|[^ 
+~,(\[]+)+|[>+~])(\s*,\s*)?/g,K=0,G=Object.prototype.toString;var 
F=function(X,T,aa,ab){aa=aa||[];T=T||document;if(T.nodeType!==1&&T.nodeType!==9){return[]}if(!X||typeof 
X!=="string"){return aa}var 
Y=[],V,ae,ah,S,ac,U,W=true;Q.lastIndex=0;while((V=Q.exec(X))!==null){Y.push(V[1]);if(V[2]){U=RegExp.rightContext;break}}if(Y.length>1&&L.exec(X)){if(Y.length===2&&H.relative[Y[0]]){ae=I(Y[0]+Y[1],T)}else{ae=H.relative[Y[0]]?[T]:F(Y.shift(),T);while(Y.length){X=Y.shift();if(H.relative[X]){X+=Y.shift()}ae=I(X,ae)}}}else{var
 
ad=ab?{expr:Y.pop(),set:E(ab)}:F.find(Y.pop(),Y.length===1&&T.parentNode?T.parentNode:T,P(T));ae=F.filter(ad.expr,ad.set);if(Y.length>0){ah=E(ae)}else{W=false}while(Y.length){var
 
ag=Y.pop(),af=ag;if(!H.relative[ag]){ag=""}else{af=Y.pop()}if(af==null){af=T}H.relative[ag](ah,af,P(T))}}if(!ah){ah=ae}if(!ah){throw"Syntax
 error, unrecognized expression: "+(ag||X)}if
 (G.call(ah)==="[object Array]"){if(!W){aa.push.apply(aa,ah)}else{if(T.nodeType===1){for(var 
Z=0;ah[Z]!=null;Z++){if(ah[Z]&&(ah[Z]===true||ah[Z].nodeType===1&&J(T,ah[Z]))){aa.push(ae[Z])}}}else{for(var 
Z=0;ah[Z]!=null;Z++){if(ah[Z]&&ah[Z].nodeType===1){aa.push(ae[Z])}}}}}else{E(ah,aa)}if(U){F(U,T,aa,ab)}return 
aa};F.matches=function(S,T){return F(S,null,null,T)};F.find=function(Z,S,aa){var Y,W;if(!Z){return[]}for(var 
V=0,U=H.order.length;V<U;V++){var X=H.order[V],W;if((W=H.match[X].exec(Z))){var 
T=RegExp.leftContext;if(T.substr(T.length-1)!=="\\"){W[1]=(W[1]||"").replace(/\\/g,"");Y=H.find[X](W,S,aa);if(Y!=null){Z=Z.replace(H.match[X],"");break}}}}if(!Y){Y=S.getElementsByTagName("*")}return{set:Y,expr:Z}};F.filter=function(ab,aa,ae,V){var
 U=ab,ag=[],Y=aa,X,S;while(ab&&aa.length){for(var Z in H.filter){if((X=H.match[Z].exec(ab))!=null){var 
T=H.filter[Z],af,ad;S=false;if(Y==ag){ag=[]}if(H.preFilter[Z]){X=H.preFilter[Z](X,Y,ae,ag,V);if(!X){S=af=true}else{if(X===true){continue}}}
 if(X){for(var W=0;(ad=Y[W])!=null;W++){if(ad){af=T(ad,X,W,Y);var 
ac=V^!!af;if(ae&&af!=null){if(ac){S=true}else{Y[W]=false}}else{if(ac){ag.push(ad);S=true}}}}}if(af!==g){if(!ae){Y=ag}ab=ab.replace(H.match[Z],"");if(!S){return[]}break}}}ab=ab.replace(/\s*,\s*/,"");if(ab==U){if(S==null){throw"Syntax
 error, unrecognized expression: "+ab}else{break}}U=ab}return Y};var 
H=F.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF_-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF_-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*_-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF_-]|\\.)+)(?:\((['"]*)((?:\([^\)]+\)|[^\2\(\)]*)+)\2\))?/},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(S){return
 S.getAttribute("href
 ")}},relative:{"+":function(W,T){for(var U=0,S=W.length;U<S;U++){var V=W[U];if(V){var 
X=V.previousSibling;while(X&&X.nodeType!==1){X=X.previousSibling}W[U]=typeof 
T==="string"?X||false:X===T}}if(typeof T==="string"){F.filter(T,W,true)}},">":function(X,T,Y){if(typeof 
T==="string"&&!/\W/.test(T)){T=Y?T:T.toUpperCase();for(var U=0,S=X.length;U<S;U++){var W=X[U];if(W){var 
V=W.parentNode;X[U]=V.nodeName===T?V:false}}}else{for(var U=0,S=X.length;U<S;U++){var 
W=X[U];if(W){X[U]=typeof T==="string"?W.parentNode:W.parentNode===T}}if(typeof 
T==="string"){F.filter(T,X,true)}}},"":function(V,T,X){var U="done"+(K++),S=R;if(!T.match(/\W/)){var 
W=T=X?T:T.toUpperCase();S=O}S("parentNode",T,U,V,W,X)},"~":function(V,T,X){var U="done"+(K++),S=R;if(typeof 
T==="string"&&!T.match(/\W/)){var 
W=T=X?T:T.toUpperCase();S=O}S("previousSibling",T,U,V,W,X)}},find:{ID:function(T,U,V){if(typeof 
U.getElementById!=="undefined"&&!V){var S=U.getElementById(T[1]);return 
S?[S]:[]}},NAME:function(S,T,U){if(typeof 
 T.getElementsByName!=="undefined"&&!U){return T.getElementsByName(S[1])}},TAG:function(S,T){return 
T.getElementsByTagName(S[1])}},preFilter:{CLASS:function(V,T,U,S,Y){V=" "+V[1].replace(/\\/g,"")+" ";var 
X;for(var W=0;(X=T[W])!=null;W++){if(X){if(Y^(" "+X.className+" 
").indexOf(V)>=0){if(!U){S.push(X)}}else{if(U){T[W]=false}}}}return false},ID:function(S){return 
S[1].replace(/\\/g,"")},TAG:function(T,S){for(var U=0;S[U]===false;U++){}return 
S[U]&&P(S[U])?T[1]:T[1].toUpperCase()},CHILD:function(S){if(S[1]=="nth"){var 
T=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(S[2]=="even"&&"2n"||S[2]=="odd"&&"2n+1"||!/\D/.test(S[2])&&"0n+"+S[2]||S[2]);S[2]=(T[1]+(T[2]||1))-0;S[3]=T[3]-0}S[0]="done"+(K++);return
 S},ATTR:function(T){var S=T[1].replace(/\\/g,"");if(H.attrMap[S]){T[1]=H.attrMap[S]}if(T[2]==="~="){T[4]=" 
"+T[4]+" "}return 
T},PSEUDO:function(W,T,U,S,X){if(W[1]==="not"){if(W[3].match(Q).length>1){W[3]=F(W[3],null,null,T)}else{var 
V=F.filter(W[3],T,U,true^X);if(!U){S.push.apply(S,V)}return fa
 lse}}else{if(H.match.POS.test(W[0])){return true}}return W},POS:function(S){S.unshift(true);return 
S}},filters:{enabled:function(S){return S.disabled===false&&S.type!=="hidden"},disabled:function(S){return 
S.disabled===true},checked:function(S){return 
S.checked===true},selected:function(S){S.parentNode.selectedIndex;return 
S.selected===true},parent:function(S){return !!S.firstChild},empty:function(S){return 
!S.firstChild},has:function(U,T,S){return 
!!F(S[3],U).length},header:function(S){return/h\d/i.test(S.nodeName)},text:function(S){return"text"===S.type},radio:function(S){return"radio"===S.type},checkbox:function(S){return"checkbox"===S.type},file:function(S){return"file"===S.type},password:function(S){return"password"===S.type},submit:function(S){return"submit"===S.type},image:function(S){return"image"===S.type},reset:function(S){return"reset"===S.type},button:function(S){return"button"===S.type||S.nodeName.toUpperCase()==="BUTTON"},input:function(S){return/input|select|t
 extarea|button/i.test(S.nodeName)}},setFilters:{first:function(T,S){return 
S===0},last:function(U,T,S,V){return T===V.length-1},even:function(T,S){return 
S%2===0},odd:function(T,S){return S%2===1},lt:function(U,T,S){return T<S[3]-0},gt:function(U,T,S){return 
T>S[3]-0},nth:function(U,T,S){return S[3]-0==T},eq:function(U,T,S){return 
S[3]-0==T}},filter:{CHILD:function(S,V){var Y=V[1],Z=S.parentNode;var X=V[0];if(Z&&(!Z[X]||!S.nodeIndex)){var 
W=1;for(var 
T=Z.firstChild;T;T=T.nextSibling){if(T.nodeType==1){T.nodeIndex=W++}}Z[X]=W-1}if(Y=="first"){return 
S.nodeIndex==1}else{if(Y=="last"){return S.nodeIndex==Z[X]}else{if(Y=="only"){return 
Z[X]==1}else{if(Y=="nth"){var ab=false,U=V[2],aa=V[3];if(U==1&&aa==0){return 
true}if(U==0){if(S.nodeIndex==aa){ab=true}}else{if((S.nodeIndex-aa)%U==0&&(S.nodeIndex-aa)/U>=0){ab=true}}return
 ab}}}}},PSEUDO:function(Y,U,V,Z){var T=U[1],W=H.filters[T];if(W){return 
W(Y,V,U,Z)}else{if(T==="contains"){return(Y.textContent||Y.innerText||"").indexOf(U[3])
=0}else{if(T==="not"){var X=U[3];for(var V=0,S=X.length;V<S;V++){if(X[V]===Y){return false}}return 
true}}}},ID:function(T,S){return 
T.nodeType===1&&T.getAttribute("id")===S},TAG:function(T,S){return(S==="*"&&T.nodeType===1)||T.nodeName===S},CLASS:function(T,S){return
S.test(T.className)},ATTR:function(W,U){var 
S=H.attrHandle[U[1]]?H.attrHandle[U[1]](W):W[U[1]]||W.getAttribute(U[1]),X=S+"",V=U[2],T=U[4];return 
S==null?V==="!=":V==="="?X===T:V==="*="?X.indexOf(T)>=0:V==="~="?(" "+X+" 
").indexOf(T)>=0:!U[4]?S:V==="!="?X!=T:V==="^="?X.indexOf(T)===0:V==="$="?X.substr(X.length-T.length)===T:V==="|="?X===T||X.substr(0,T.length+1)===T+"-":false},POS:function(W,T,U,X){var
S=T[2],V=H.setFilters[S];if(V){return V(W,U,T,X)}}}};var L=H.match.POS;for(var N in 
H.match){H.match[N]=RegExp(H.match[N].source+/(?![^\[]*\])(?![^\(]*\))/.source)}var 
E=function(T,S){T=Array.prototype.slice.call(T);if(S){S.push.apply(S,T);return S}return 
T};try{Array.prototype.slice.call(document.documentElement.
 childNodes)}catch(M){E=function(W,V){var T=V||[];if(G.call(W)==="[object 
Array]"){Array.prototype.push.apply(T,W)}else{if(typeof W.length==="number"){for(var 
U=0,S=W.length;U<S;U++){T.push(W[U])}}else{for(var U=0;W[U];U++){T.push(W[U])}}}return T}}(function(){var 
T=document.createElement("form"),U="script"+(new Date).getTime();T.innerHTML="<input name='"+U+"'/>";var 
S=document.documentElement;S.insertBefore(T,S.firstChild);if(!!document.getElementById(U)){H.find.ID=function(W,X,Y){if(typeof
 X.getElementById!=="undefined"&&!Y){var V=X.getElementById(W[1]);return V?V.id===W[1]||typeof 
V.getAttributeNode!=="undefined"&&V.getAttributeNode("id").nodeValue===W[1]?[V]:g:[]}};H.filter.ID=function(X,V){var
 W=typeof X.getAttributeNode!=="undefined"&&X.getAttributeNode("id");return 
X.nodeType===1&&W&&W.nodeValue===V}}S.removeChild(T)})();(function(){var 
S=document.createElement("div");S.appendChild(document.createComment(""));if(S.getElementsByTagName("*").length>0){H.find.TAG=function
 (T,X){var W=X.getElementsByTagName(T[1]);if(T[1]==="*"){var V=[];for(var 
U=0;W[U];U++){if(W[U].nodeType===1){V.push(W[U])}}W=V}return W}}S.innerHTML="<a 
href='#'></a>";if(S.firstChild&&S.firstChild.getAttribute("href")!=="#"){H.attrHandle.href=function(T){return 
T.getAttribute("href",2)}}})();if(document.querySelectorAll){(function(){var 
S=F,T=document.createElement("div");T.innerHTML="<p 
class='TEST'></p>";if(T.querySelectorAll&&T.querySelectorAll(".TEST").length===0){return}F=function(X,W,U,V){W=W||document;if(!V&&W.nodeType===9&&!P(W)){try{return
 E(W.querySelectorAll(X),U)}catch(Y){}}return 
S(X,W,U,V)};F.find=S.find;F.filter=S.filter;F.selectors=S.selectors;F.matches=S.matches})()}if(document.getElementsByClassName&&document.documentElement.getElementsByClassName){H.order.splice(1,0,"CLASS");H.find.CLASS=function(S,T){return
 T.getElementsByClassName(S[1])}}function O(T,Z,Y,ac,aa,ab){for(var W=0,U=ac.length;W<U;W++){var 
S=ac[W];if(S){S=S[T];var X=false;while(S&&S.nodeType)
 {var 
V=S[Y];if(V){X=ac[V];break}if(S.nodeType===1&&!ab){S[Y]=W}if(S.nodeName===Z){X=S;break}S=S[T]}ac[W]=X}}}function
 R(T,Y,X,ab,Z,aa){for(var V=0,U=ab.length;V<U;V++){var S=ab[V];if(S){S=S[T];var 
W=false;while(S&&S.nodeType){if(S[X]){W=ab[S[X]];break}if(S.nodeType===1){if(!aa){S[X]=V}if(typeof 
Y!=="string"){if(S===Y){W=true;break}}else{if(F.filter(Y,[S]).length>0){W=S;break}}}S=S[T]}ab[V]=W}}}var 
J=document.compareDocumentPosition?function(T,S){return T.compareDocumentPosition(S)&16}:function(T,S){return 
T!==S&&(T.contains?T.contains(S):true)};var P=function(S){return 
S.nodeType===9&&S.documentElement.nodeName!=="HTML"||!!S.ownerDocument&&P(S.ownerDocument)};var 
I=function(S,Z){var 
V=[],W="",X,U=Z.nodeType?[Z]:Z;while((X=H.match.PSEUDO.exec(S))){W+=X[0];S=S.replace(H.match.PSEUDO,"")}S=H.relative[S]?S+"*":S;for(var
 Y=0,T=U.length;Y<T;Y++){F(S,U[Y],V)}return 
F.filter(W,V)};o.find=F;o.filter=F.filter;o.expr=F.selectors;o.expr[":"]=o.expr.filters;F.selectors.filters.hidden=fun
 
ction(S){return"hidden"===S.type||o.css(S,"display")==="none"||o.css(S,"visibility")==="hidden"};F.selectors.filters.visible=function(S){return"hidden"!==S.type&&o.css(S,"display")!=="none"&&o.css(S,"visibility")!=="hidden"};F.selectors.filters.animated=function(S){return
 o.grep(o.timers,function(T){return 
S===T.elem}).length};o.multiFilter=function(U,S,T){if(T){U=":not("+U+")"}return 
F.matches(U,S)};o.dir=function(U,T){var 
S=[],V=U[T];while(V&&V!=document){if(V.nodeType==1){S.push(V)}V=V[T]}return 
S};o.nth=function(W,S,U,V){S=S||1;var T=0;for(;W;W=W[U]){if(W.nodeType==1&&++T==S){break}}return 
W};o.sibling=function(U,T){var S=[];for(;U;U=U.nextSibling){if(U.nodeType==1&&U!=T){S.push(U)}}return 
S};return;l.Sizzle=F})();o.event={add:function(I,F,H,K){if(I.nodeType==3||I.nodeType==8){return}if(I.setInterval&&I!=l){I=l}if(!H.guid){H.guid=this.guid++}if(K!==g){var
 G=H;H=this.proxy(G);H.data=K}var 
E=o.data(I,"events")||o.data(I,"events",{}),J=o.data(I,"handle")||o.data(I,"handle",
 function(){return typeof 
o!=="undefined"&&!o.event.triggered?o.event.handle.apply(arguments.callee.elem,arguments):g});J.elem=I;o.each(F.split(/\s+/),function(M,N){var
 O=N.split(".");N=O.shift();H.type=O.slice().sort().join(".");var 
L=E[N];if(o.event.specialAll[N]){o.event.specialAll[N].setup.call(I,K,O)}if(!L){L=E[N]={};if(!o.event.special[N]||o.event.special[N].setup.call(I,K,O)===false){if(I.addEventListener){I.addEventListener(N,J,false)}else{if(I.attachEvent){I.attachEvent("on"+N,J)}}}}L[H.guid]=H;o.event.global[N]=true});I=null},guid:1,global:{},remove:function(K,H,J){if(K.nodeType==3||K.nodeType==8){return}var
 G=o.data(K,"events"),F,E;if(G){if(H===g||(typeof H==="string"&&H.charAt(0)==".")){for(var I in 
G){this.remove(K,I+(H||""))}}else{if(H.type){J=H.handler;H=H.type}o.each(H.split(/\s+/),function(M,O){var 
Q=O.split(".");O=Q.shift();var 
N=RegExp("(^|\\.)"+Q.slice().sort().join(".*\\.")+"(\\.|$)");if(G[O]){if(J){delete G[O][J.guid]}else{for(var 
P in G[O]){if(N.test(G[
 O][P].type)){delete G[O][P]}}}if(o.event.specialAll[O]){o.event.specialAll[O].teardown.call(K,Q)}for(F in 
G[O]){break}if(!F){if(!o.event.special[O]||o.event.special[O].teardown.call(K,Q)===false){if(K.removeEventListener){K.removeEventListener(O,o.data(K,"handle"),false)}else{if(K.detachEvent){K.detachEvent("on"+O,o.data(K,"handle"))}}}F=null;delete
 G[O]}}})}for(F in G){break}if(!F){var 
L=o.data(K,"handle");if(L){L.elem=null}o.removeData(K,"events");o.removeData(K,"handle")}}},trigger:function(I,K,H,E){var
 G=I.type||I;if(!E){I=typeof 
I==="object"?I[h]?I:o.extend(o.Event(G),I):o.Event(G);if(G.indexOf("!")>=0){I.type=G=G.slice(0,-1);I.exclusive=true}if(!H){I.stopPropagation();if(this.global[G]){o.each(o.cache,function(){if(this.events&&this.events[G]){o.event.trigger(I,K,this.handle.elem)}})}}if(!H||H.nodeType==3||H.nodeType==8){return
 g}I.result=g;I.target=H;K=o.makeArray(K);K.unshift(I)}I.currentTarget=H;var 
J=o.data(H,"handle");if(J){J.apply(H,K)}if((!H[G]||(o.nodeName(H,"a
 
")&&G=="click"))&&H["on"+G]&&H["on"+G].apply(H,K)===false){I.result=false}if(!E&&H[G]&&!I.isDefaultPrevented()&&!(o.nodeName(H,"a")&&G=="click")){this.triggered=true;try{H[G]()}catch(L){}}this.triggered=false;if(!I.isPropagationStopped()){var
 F=H.parentNode||H.ownerDocument;if(F){o.event.trigger(I,K,F,true)}}},handle:function(K){var 
J,E;K=arguments[0]=o.event.fix(K||l.event);var 
L=K.type.split(".");K.type=L.shift();J=!L.length&&!K.exclusive;var 
I=RegExp("(^|\\.)"+L.slice().sort().join(".*\\.")+"(\\.|$)");E=(o.data(this,"events")||{})[K.type];for(var G 
in E){var H=E[G];if(J||I.test(H.type)){K.handler=H;K.data=H.data;var 
F=H.apply(this,arguments);if(F!==g){K.result=F;if(F===false){K.preventDefault();K.stopPropagation()}}if(K.isImmediatePropagationStopped()){break}}}},props:"altKey
 attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail 
eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue r
 elatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" 
"),fix:function(H){if(H[h]){return H}var F=H;H=o.Event(F);for(var 
G=this.props.length,J;G;){J=this.props[--G];H[J]=F[J]}if(!H.target){H.target=H.srcElement||document}if(H.target.nodeType==3){H.target=H.target.parentNode}if(!H.relatedTarget&&H.fromElement){H.relatedTarget=H.fromElement==H.target?H.toElement:H.fromElement}if(H.pageX==null&&H.clientX!=null){var
 
I=document.documentElement,E=document.body;H.pageX=H.clientX+(I&&I.scrollLeft||E&&E.scrollLeft||0)-(I.clientLeft||0);H.pageY=H.clientY+(I&&I.scrollTop||E&&E.scrollTop||0)-(I.clientTop||0)}if(!H.which&&((H.charCode||H.charCode===0)?H.charCode:H.keyCode)){H.which=H.charCode||H.keyCode}if(!H.metaKey&&H.ctrlKey){H.metaKey=H.ctrlKey}if(!H.which&&H.button){H.which=(H.button&1?1:(H.button&2?3:(H.button&4?2:0)))}return
 H},proxy:function(F,E){E=E||function(){return F.apply(this,arguments)};E.guid=F.guid=F.guid||E.guid||thi
 s.guid++;return 
E},special:{ready:{setup:B,teardown:function(){}}},specialAll:{live:{setup:function(E,F){o.event.add(this,F[0],c)},teardown:function(G){if(G.length){var
 
E=0,F=RegExp("(^|\\.)"+G[0]+"(\\.|$)");o.each((o.data(this,"events").live||{}),function(){if(F.test(this.type)){E++}});if(E<1){o.event.remove(this,G[0],c)}}}}}};o.Event=function(E){if(!this.preventDefault){return
 new 
o.Event(E)}if(E&&E.type){this.originalEvent=E;this.type=E.type}else{this.type=E}this.timeStamp=e();this[h]=true};function
 k(){return false}function u(){return 
true}o.Event.prototype={preventDefault:function(){this.isDefaultPrevented=u;var 
E=this.originalEvent;if(!E){return}if(E.preventDefault){E.preventDefault()}E.returnValue=false},stopPropagation:function(){this.isPropagationStopped=u;var
 
E=this.originalEvent;if(!E){return}if(E.stopPropagation){E.stopPropagation()}E.cancelBubble=true},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=u;this.stopPropagation()},isDefaultPreve
 nted:k,isPropagationStopped:k,isImmediatePropagationStopped:k};var a=function(F){var 
E=F.relatedTarget;while(E&&E!=this){try{E=E.parentNode}catch(G){E=this}}if(E!=this){F.type=F.data;o.event.handle.apply(this,arguments)}};o.each({mouseover:"mouseenter",mouseout:"mouseleave"},function(F,E){o.event.special[E]={setup:function(){o.event.add(this,F,a,E)},teardown:function(){o.event.remove(this,F,a)}}});o.fn.extend({bind:function(F,G,E){return
 F=="unload"?this.one(F,G,E):this.each(function(){o.event.add(this,F,E||G,E&&G)})},one:function(G,H,F){var 
E=o.event.proxy(F||H,function(I){o(this).unbind(I,E);return(F||H).apply(this,arguments)});return 
this.each(function(){o.event.add(this,G,E,F&&H)})},unbind:function(F,E){return 
this.each(function(){o.event.remove(this,F,E)})},trigger:function(E,F){return 
this.each(function(){o.event.trigger(E,F,this)})},triggerHandler:function(E,G){if(this[0]){var 
F=o.Event(E);F.preventDefault();F.stopPropagation();o.event.trigger(F,G,this[0]);return F.re
 sult}},toggle:function(G){var E=arguments,F=1;while(F<E.length){o.event.proxy(G,E[F++])}return 
this.click(o.event.proxy(G,function(H){this.lastToggle=(this.lastToggle||0)%F;H.preventDefault();return 
E[this.lastToggle++].apply(this,arguments)||false}))},hover:function(E,F){return 
this.mouseenter(E).mouseleave(F)},ready:function(E){B();if(o.isReady){E.call(document,o)}else{o.readyList.push(E)}return
 this},live:function(G,F){var 
E=o.event.proxy(F);E.guid+=this.selector+G;o(document).bind(i(G,this.selector),this.selector,E);return 
this},die:function(F,E){o(document).unbind(i(F,this.selector),E?{guid:E.guid+this.selector+F}:null);return 
this}});function c(H){var 
E=RegExp("(^|\\.)"+H.type+"(\\.|$)"),G=true,F=[];o.each(o.data(this,"events").live||[],function(I,J){if(E.test(J.type)){var
 
K=o(H.target).closest(J.data)[0];if(K){F.push({elem:K,fn:J})}}});o.each(F,function(){if(this.fn.call(this.elem,H,this.fn.data)===false){G=false}});return
 G}function i(F,E){return["live",F,E.replace(/
 \./g,"`").replace(/ 
/g,"|")].join(".")}o.extend({isReady:false,readyList:[],ready:function(){if(!o.isReady){o.isReady=true;if(o.readyList){o.each(o.readyList,function(){this.call(document,o)});o.readyList=null}o(document).triggerHandler("ready")}}});var
 x=false;function 
B(){if(x){return}x=true;if(document.addEventListener){document.addEventListener("DOMContentLoaded",function(){document.removeEventListener("DOMContentLoaded",arguments.callee,false);o.ready()},false)}else{if(document.attachEvent){document.attachEvent("onreadystatechange",function(){if(document.readyState==="complete"){document.detachEvent("onreadystatechange",arguments.callee);o.ready()}});if(document.documentElement.doScroll&&typeof
 
l.frameElement==="undefined"){(function(){if(o.isReady){return}try{document.documentElement.doScroll("left")}catch(E){setTimeout(arguments.callee,0);return}o.ready()})()}}}o.event.add(l,"load",o.ready)}o.each(("blur,focus,load,resize,scroll,unload,click,dblclick,mousedown,mouseup
 
,mousemove,mouseover,mouseout,mouseenter,mouseleave,change,select,submit,keydown,keypress,keyup,error").split(","),function(F,E){o.fn[E]=function(G){return
 G?this.bind(E,G):this.trigger(E)}});o(l).bind("unload",function(){for(var E in 
o.cache){if(E!=1&&o.cache[E].handle){o.event.remove(o.cache[E].handle.elem)}}});(function(){o.support={};var 
F=document.documentElement,G=document.createElement("script"),K=document.createElement("div"),J="script"+(new 
Date).getTime();K.style.display="none";K.innerHTML='   <link/><table></table><a href="/a" 
style="color:red;float:left;opacity:.5;">a</a><select><option>text</option></select><object><param/></object>';var
 
H=K.getElementsByTagName("*"),E=K.getElementsByTagName("a")[0];if(!H||!H.length||!E){return}o.support={leadingWhitespace:K.firstChild.nodeType==3,tbody:!K.getElementsByTagName("tbody").length,objectAll:!!K.getElementsByTagName("object")[0].getElementsByTagName("*").length,htmlSerialize:!!K.getElementsByTagName("link").length,sty
 
le:/red/.test(E.getAttribute("style")),hrefNormalized:E.getAttribute("href")==="/a",opacity:E.style.opacity==="0.5",cssFloat:!!E.style.cssFloat,scriptEval:false,noCloneEvent:true,boxModel:null};G.type="text/javascript";try{G.appendChild(document.createTextNode("window."+J+"=1;"))}catch(I){}F.insertBefore(G,F.firstChild);if(l[J]){o.support.scriptEval=true;delete
 
l[J]}F.removeChild(G);if(K.attachEvent&&K.fireEvent){K.attachEvent("onclick",function(){o.support.noCloneEvent=false;K.detachEvent("onclick",arguments.callee)});K.cloneNode(true).fireEvent("onclick")}o(function(){var
 
L=document.createElement("div");L.style.width="1px";L.style.paddingLeft="1px";document.body.appendChild(L);o.boxModel=o.support.boxModel=L.offsetWidth===2;document.body.removeChild(L)})})();var
 
w=o.support.cssFloat?"cssFloat":"styleFloat";o.props={"for":"htmlFor","class":"className","float":w,cssFloat:w,styleFloat:w,readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",tabi
 ndex:"tabIndex"};o.fn.extend({_load:o.fn.load,load:function(G,J,K){if(typeof G!=="string"){return 
this._load(G)}var I=G.indexOf(" ");if(I>=0){var E=G.slice(I,G.length);G=G.slice(0,I)}var 
H="GET";if(J){if(o.isFunction(J)){K=J;J=null}else{if(typeof J==="object"){J=o.param(J);H="POST"}}}var 
F=this;o.ajax({url:G,type:H,dataType:"html",data:J,complete:function(M,L){if(L=="success"||L=="notmodified"){F.html(E?o("<div/>").append(M.responseText.replace(/<script(.|\s)*?\/script>/g,"")).find(E):M.responseText)}if(K){F.each(K,[M.responseText,L,M])}}});return
 this},serialize:function(){return o.param(this.serializeArray())},serializeArray:function(){return 
this.map(function(){return this.elements?o.makeArray(this.elements):this}).filter(function(){return 
this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password/i.test(this.type))}).map(function(E,F){var
 G=o(this).val();return G==null?null:o.isArray(G)?o.map(G,function(I,H){return{name:F.name
 
,value:I}}):{name:F.name,value:G}}).get()}});o.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(E,F){o.fn[F]=function(G){return
 this.bind(F,G)}});var r=e();o.extend({get:function(E,G,H,F){if(o.isFunction(G)){H=G;G=null}return 
o.ajax({type:"GET",url:E,data:G,success:H,dataType:F})},getScript:function(E,F){return 
o.get(E,null,F,"script")},getJSON:function(E,F,G){return 
o.get(E,F,G,"json")},post:function(E,G,H,F){if(o.isFunction(G)){H=G;G={}}return 
o.ajax({type:"POST",url:E,data:G,success:H,dataType:F})},ajaxSetup:function(E){o.extend(o.ajaxSettings,E)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return
 l.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest()},accepts:{xml:"application/xml, 
text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, 
text/javascript",text:"text/pl
 
ain",_default:"*/*"}},lastModified:{},ajax:function(M){M=o.extend(true,M,o.extend(true,{},o.ajaxSettings,M));var
 W,F=/=\?(&|$)/g,R,V,G=M.type.toUpperCase();if(M.data&&M.processData&&typeof 
M.data!=="string"){M.data=o.param(M.data)}if(M.dataType=="jsonp"){if(G=="GET"){if(!M.url.match(F)){M.url+=(M.url.match(/\?/)?"&":"?")+(M.jsonp||"callback")+"=?"}}else{if(!M.data||!M.data.match(F)){M.data=(M.data?M.data+"&":"")+(M.jsonp||"callback")+"=?"}}M.dataType="json"}if(M.dataType=="json"&&(M.data&&M.data.match(F)||M.url.match(F))){W="jsonp"+r++;if(M.data){M.data=(M.data+"").replace(F,"="+W+"$1")}M.url=M.url.replace(F,"="+W+"$1");M.dataType="script";l[W]=function(X){V=X;I();L();l[W]=g;try{delete
 
l[W]}catch(Y){}if(H){H.removeChild(T)}}}if(M.dataType=="script"&&M.cache==null){M.cache=false}if(M.cache===false&&G=="GET"){var
 E=e();var 
U=M.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+E+"$2");M.url=U+((U==M.url)?(M.url.match(/\?/)?"&":"?")+"_="+E:"")}if(M.data&&G=="GET"){M.url+=(M.url.match(/\?/)?
 "&":"?")+M.data;M.data=null}if(M.global&&!o.active++){o.event.trigger("ajaxStart")}var 
Q=/^(\w+:)?\/\/([^\/?#]+)/.exec(M.url);if(M.dataType=="script"&&G=="GET"&&Q&&(Q[1]&&Q[1]!=location.protocol||Q[2]!=location.host)){var
 H=document.getElementsByTagName("head")[0];var 
T=document.createElement("script");T.src=M.url;if(M.scriptCharset){T.charset=M.scriptCharset}if(!W){var 
O=false;T.onload=T.onreadystatechange=function(){if(!O&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){O=true;I();L();H.removeChild(T)}}}H.appendChild(T);return
 g}var K=false;var 
J=M.xhr();if(M.username){J.open(G,M.url,M.async,M.username,M.password)}else{J.open(G,M.url,M.async)}try{if(M.data){J.setRequestHeader("Content-Type",M.contentType)}if(M.ifModified){J.setRequestHeader("If-Modified-Since",o.lastModified[M.url]||"Thu,
 01 Jan 1970 00:00:00 
GMT")}J.setRequestHeader("X-Requested-With","XMLHttpRequest");J.setRequestHeader("Accept",M.dataType&&M.accepts[M.dataType]?M.accepts[M.da
 taType]+", 
*/*":M.accepts._default)}catch(S){}if(M.beforeSend&&M.beforeSend(J,M)===false){if(M.global&&!--o.active){o.event.trigger("ajaxStop")}J.abort();return
 false}if(M.global){o.event.trigger("ajaxSend",[J,M])}var 
N=function(X){if(J.readyState==0){if(P){clearInterval(P);P=null;if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}}else{if(!K&&J&&(J.readyState==4||X=="timeout")){K=true;if(P){clearInterval(P);P=null}R=X=="timeout"?"timeout":!o.httpSuccess(J)?"error":M.ifModified&&o.httpNotModified(J,M.url)?"notmodified":"success";if(R=="success"){try{V=o.httpData(J,M.dataType,M)}catch(Z){R="parsererror"}}if(R=="success"){var
 
Y;try{Y=J.getResponseHeader("Last-Modified")}catch(Z){}if(M.ifModified&&Y){o.lastModified[M.url]=Y}if(!W){I()}}else{o.handleError(M,J,R)}L();if(X){J.abort()}if(M.async){J=null}}}};if(M.async){var
 
P=setInterval(N,13);if(M.timeout>0){setTimeout(function(){if(J&&!K){N("timeout")}},M.timeout)}}try{J.send(M.data)}catch(S){o.handleError(M,J,null,S)}if(!M.as
 ync){N()}function 
I(){if(M.success){M.success(V,R)}if(M.global){o.event.trigger("ajaxSuccess",[J,M])}}function 
L(){if(M.complete){M.complete(J,R)}if(M.global){o.event.trigger("ajaxComplete",[J,M])}if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}return
 
J},handleError:function(F,H,E,G){if(F.error){F.error(H,E,G)}if(F.global){o.event.trigger("ajaxError",[H,F,G])}},active:0,httpSuccess:function(F){try{return
 
!F.status&&location.protocol=="file:"||(F.status>=200&&F.status<300)||F.status==304||F.status==1223}catch(E){}return
 false},httpNotModified:function(G,E){try{var H=G.getResponseHeader("Last-Modified");return 
G.status==304||H==o.lastModified[E]}catch(F){}return false},httpData:function(J,H,G){var 
F=J.getResponseHeader("content-type"),E=H=="xml"||!H&&F&&F.indexOf("xml")>=0,I=E?J.responseXML:J.responseText;if(E&&I.documentElement.tagName=="parsererror"){throw"parsererror"}if(G&&G.dataFilter){I=G.dataFilter(I,H)}if(typeof
 I==="string"){if(H=="script"){o.globalEval(I)}if(H
 =="json"){I=l["eval"]("("+I+")")}}return I},param:function(E){var G=[];function 
H(I,J){G[G.length]=encodeURIComponent(I)+"="+encodeURIComponent(J)}if(o.isArray(E)||E.jquery){o.each(E,function(){H(this.name,this.value)})}else{for(var
 F in 
E){if(o.isArray(E[F])){o.each(E[F],function(){H(F,this)})}else{H(F,o.isFunction(E[F])?E[F]():E[F])}}}return 
G.join("&").replace(/%20/g,"+")}});var 
m={},n,d=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];function
 t(F,E){var G={};o.each(d.concat.apply([],d.slice(0,E)),function(){G[this]=F});return 
G}o.fn.extend({show:function(J,L){if(J){return this.animate(t("show",3),J,L)}else{for(var 
H=0,F=this.length;H<F;H++){var 
E=o.data(this[H],"olddisplay");this[H].style.display=E||"";if(o.css(this[H],"display")==="none"){var 
G=this[H].tagName,K;if(m[G]){K=m[G]}else{var I=o("<"+G+" 
/>").appendTo("body");K=I.css("display");if(K==="none"){K="block"}I.remove(
 );m[G]=K}this[H].style.display=o.data(this[H],"olddisplay",K)}}return this}},hide:function(H,I){if(H){return 
this.animate(t("hide",3),H,I)}else{for(var G=0,F=this.length;G<F;G++){var 
E=o.data(this[G],"olddisplay");if(!E&&E!=="none"){o.data(this[G],"olddisplay",o.css(this[G],"display"))}this[G].style.display="none"}return
 this}},_toggle:o.fn.toggle,toggle:function(G,F){var E=typeof G==="boolean";return 
o.isFunction(G)&&o.isFunction(F)?this._toggle.apply(this,arguments):G==null||E?this.each(function(){var 
H=E?G:o(this).is(":hidden");o(this)[H?"show":"hide"]()}):this.animate(t("toggle",3),G,F)},fadeTo:function(E,G,F){return
 this.animate({opacity:G},E,F)},animate:function(I,F,H,G){var E=o.speed(F,H,G);return 
this[E.queue===false?"each":"queue"](function(){var 
K=o.extend({},E),M,L=this.nodeType==1&&o(this).is(":hidden"),J=this;for(M in 
I){if(I[M]=="hide"&&L||I[M]=="show"&&!L){return 
K.complete.call(this)}if((M=="height"||M=="width")&&this.style){K.display=o.css(this,"display");K.
 
overflow=this.style.overflow}}if(K.overflow!=null){this.style.overflow="hidden"}K.curAnim=o.extend({},I);o.each(I,function(O,S){var
 R=new o.fx(J,K,O);if(/toggle|show|hide/.test(S)){R[S=="toggle"?L?"show":"hide":S](I)}else{var 
Q=S.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),T=R.cur(true)||0;if(Q){var 
N=parseFloat(Q[2]),P=Q[3]||"px";if(P!="px"){J.style[O]=(N||1)+P;T=((N||1)/R.cur(true))*T;J.style[O]=T+P}if(Q[1]){N=((Q[1]=="-="?-1:1)*N)+T}R.custom(T,N,P)}else{R.custom(T,S,"")}}});return
 true})},stop:function(F,E){var G=o.timers;if(F){this.queue([])}this.each(function(){for(var 
H=G.length-1;H>=0;H--){if(G[H].elem==this){if(E){G[H](true)}G.splice(H,1)}}});if(!E){this.dequeue()}return 
this}});o.each({slideDown:t("show",1),slideUp:t("hide",1),slideToggle:t("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(E,F){o.fn[E]=function(G,H){return
 this.animate(F,G,H)}});o.extend({speed:function(G,H,F){var E=typeof 
G==="object"?G:{complete:F||!F&&H||o.isFunction(G)&&G,d
 uration:G,easing:F&&H||H&&!o.isFunction(H)&&H};E.duration=o.fx.off?0:typeof 
E.duration==="number"?E.duration:o.fx.speeds[E.duration]||o.fx.speeds._default;E.old=E.complete;E.complete=function(){if(E.queue!==false){o(this).dequeue()}if(o.isFunction(E.old)){E.old.call(this)}};return
 E},easing:{linear:function(G,H,E,F){return 
E+F*G},swing:function(G,H,E,F){return((-Math.cos(G*Math.PI)/2)+0.5)*F+E}},timers:[],fx:function(F,E,G){this.options=E;this.elem=F;this.prop=G;if(!E.orig){E.orig={}}}});o.fx.prototype={update:function(){if(this.options.step){this.options.step.call(this.elem,this.now,this)}(o.fx.step[this.prop]||o.fx.step._default)(this);if((this.prop=="height"||this.prop=="width")&&this.elem.style){this.elem.style.display="block"}},cur:function(F){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null)){return
 this.elem[this.prop]}var E=parseFloat(o.css(this.elem,this.prop,F));return 
E&&E>-10000?E:parseFloat(o.curCSS(this.elem,this.prop))||0},cus
 
tom:function(I,H,G){this.startTime=e();this.start=I;this.end=H;this.unit=G||this.unit||"px";this.now=this.start;this.pos=this.state=0;var
 E=this;function F(J){return 
E.step(J)}F.elem=this.elem;if(F()&&o.timers.push(F)==1){n=setInterval(function(){var K=o.timers;for(var 
J=0;J<K.length;J++){if(!K[J]()){K.splice(J--,1)}}if(!K.length){clearInterval(n)}},13)}},show:function(){this.options.orig[this.prop]=o.attr(this.elem.style,this.prop);this.options.show=true;this.custom(this.prop=="width"||this.prop=="height"?1:0,this.cur());o(this.elem).show()},hide:function(){this.options.orig[this.prop]=o.attr(this.elem.style,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(H){var
 
G=e();if(H||G>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var
 E=true;for(var F in 
this.options.curAnim){if(this.options.curAnim[F]!==true){E=false}}if(E){if(this.options.display!=null){this.elem.style.ove
 
rflow=this.options.overflow;this.elem.style.display=this.options.display;if(o.css(this.elem,"display")=="none"){this.elem.style.display="block"}}if(this.options.hide){o(this.elem).hide()}if(this.options.hide||this.options.show){for(var
 I in 
this.options.curAnim){o.attr(this.elem.style,I,this.options.orig[I])}}this.options.complete.call(this.elem)}return
 false}else{var 
J=G-this.startTime;this.state=J/this.options.duration;this.pos=o.easing[this.options.easing||(o.easing.swing?"swing":"linear")](this.state,J,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update()}return
 
true}};o.extend(o.fx,{speeds:{slow:600,fast:200,_default:400},step:{opacity:function(E){o.attr(E.elem.style,"opacity",E.now)},_default:function(E){if(E.elem.style&&E.elem.style[E.prop]!=null){E.elem.style[E.prop]=E.now+E.unit}else{E.elem[E.prop]=E.now}}}});if(document.documentElement.getBoundingClientRect){o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]=
 ==this[0].ownerDocument.body){return o.offset.bodyOffset(this[0])}var 
G=this[0].getBoundingClientRect(),J=this[0].ownerDocument,F=J.body,E=J.documentElement,L=E.clientTop||F.clientTop||0,K=E.clientLeft||F.clientLeft||0,I=G.top+(self.pageYOffset||o.boxModel&&E.scrollTop||F.scrollTop)-L,H=G.left+(self.pageXOffset||o.boxModel&&E.scrollLeft||F.scrollLeft)-K;return{top:I,left:H}}}else{o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return
 o.offset.bodyOffset(this[0])}o.offset.initialized||o.offset.initialize();var 
J=this[0],G=J.offsetParent,F=J,O=J.ownerDocument,M,H=O.documentElement,K=O.body,L=O.defaultView,E=L.getComputedStyle(J,null),N=J.offsetTop,I=J.offsetLeft;while((J=J.parentNode)&&J!==K&&J!==H){M=L.getComputedStyle(J,null);N-=J.scrollTop,I-=J.scrollLeft;if(J===G){N+=J.offsetTop,I+=J.offsetLeft;if(o.offset.doesNotAddBorder&&!(o.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(J.tagName))){N+=parseInt(M.borderTopW
 
idth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}F=G,G=J.offsetParent}if(o.offset.subtractsBorderForOverflowNotVisible&&M.overflow!=="visible"){N+=parseInt(M.borderTopWidth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}E=M}if(E.position==="relative"||E.position==="static"){N+=K.offsetTop,I+=K.offsetLeft}if(E.position==="fixed"){N+=Math.max(H.scrollTop,K.scrollTop),I+=Math.max(H.scrollLeft,K.scrollLeft)}return{top:N,left:I}}}o.offset={initialize:function(){if(this.initialized){return}var
 L=document.body,F=document.createElement("div"),H,G,N,I,M,E,J=L.style.marginTop,K='<div 
style="position:absolute;top:0;left:0;margin:0;border:5px solid 
#000;padding:0;width:1px;height:1px;"><div></div></div><table 
style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;" 
cellpadding="0" 
cellspacing="0"><tr><td></td></tr></table>';M={position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"};for(E
 in M){F.style[E]=
 
M[E]}F.innerHTML=K;L.insertBefore(F,L.firstChild);H=F.firstChild,G=H.firstChild,I=H.nextSibling.firstChild.firstChild;this.doesNotAddBorder=(G.offsetTop!==5);this.doesAddBorderForTableAndCells=(I.offsetTop===5);H.style.overflow="hidden",H.style.position="relative";this.subtractsBorderForOverflowNotVisible=(G.offsetTop===-5);L.style.marginTop="1px";this.doesNotIncludeMarginInBodyOffset=(L.offsetTop===0);L.style.marginTop=J;L.removeChild(F);this.initialized=true},bodyOffset:function(E){o.offset.initialized||o.offset.initialize();var
 
G=E.offsetTop,F=E.offsetLeft;if(o.offset.doesNotIncludeMarginInBodyOffset){G+=parseInt(o.curCSS(E,"marginTop",true),10)||0,F+=parseInt(o.curCSS(E,"marginLeft",true),10)||0}return{top:G,left:F}}};o.fn.extend({position:function(){var
 I=0,H=0,F;if(this[0]){var 
G=this.offsetParent(),J=this.offset(),E=/^body|html$/i.test(G[0].tagName)?{top:0,left:0}:G.offset();J.top-=j(this,"marginTop");J.left-=j(this,"marginLeft");E.top+=j(G,"borderTopWidth");E.left+=j
 (G,"borderLeftWidth");F={top:J.top-E.top,left:J.left-E.left}}return F},offsetParent:function(){var 
E=this[0].offsetParent||document.body;while(E&&(!/^body|html$/i.test(E.tagName)&&o.css(E,"position")=="static")){E=E.offsetParent}return
 o(E)}});o.each(["Left","Top"],function(F,E){var G="scroll"+E;o.fn[G]=function(H){if(!this[0]){return 
null}return 
H!==g?this.each(function(){this==l||this==document?l.scrollTo(!F?H:o(l).scrollLeft(),F?H:o(l).scrollTop()):this[G]=H}):this[0]==l||this[0]==document?self[F?"pageYOffset":"pageXOffset"]||o.boxModel&&document.documentElement[G]||document.body[G]:this[0][G]}});o.each(["Height","Width"],function(H,F){var
 E=H?"Left":"Top",G=H?"Right":"Bottom";o.fn["inner"+F]=function(){return 
this[F.toLowerCase()]()+j(this,"padding"+E)+j(this,"padding"+G)};o.fn["outer"+F]=function(J){return 
this["inner"+F]()+j(this,"border"+E+"Width")+j(this,"border"+G+"Width")+(J?j(this,"margin"+E)+j(this,"margin"+G):0)};var
 I=F.toLowerCase();o.fn[I]=function(J){return 
 
this[0]==l?document.compatMode=="CSS1Compat"&&document.documentElement["client"+F]||document.body["client"+F]:this[0]==document?Math.max(document.documentElement["client"+F],document.body["scroll"+F],document.documentElement["scroll"+F],document.body["offset"+F],document.documentElement["offset"+F]):J===g?(this.length?o.css(this[0],I):null):this.css(I,typeof
 J==="string"?J:J+"px")}})})();
\ No newline at end of file
diff --git a/web/splinter.css b/web/splinter.css
new file mode 100644
index 0000000..3f4502a
--- /dev/null
+++ b/web/splinter.css
@@ -0,0 +1,125 @@
+body {
+    margin: 0px;
+}
+
+#loading {
+    padding: 0.5em;
+}
+
+#headers {
+    background: black;
+    color: white;
+    font-size: 80%;
+    padding: 0.5em;
+}
+
+#controls {
+    padding: 0.5em;
+}
+
+.review {
+    border: 1px solid black;
+    font-size: 90%;
+    margin-bottom: 1em;
+}
+
+.review-inner {
+    border-left: 10px solid blue;
+    padding: 0.5em;
+}
+
+#myComment {
+    width: 100%;
+    height: 10em;
+}
+
+#buttonBox {
+    float: right;
+}
+
+#buttonSeparator {
+    clear: both;
+}
+
+#files {
+   position: relative;
+   padding: 0.5em;
+}
+
+.file-label {
+    margin-top: 1em;
+    padding: 0.5em;
+}
+
+.file-label span {
+    border: 1px solid black;
+    padding: 0.5em;
+    background: #ccffff;
+}
+
+.hunk-header {
+    color: red;
+    border: 1px solid black;
+    background: #dddddd;
+}
+
+.old-line, .new-line {
+    white-space: pre;
+    font-family: "DejaVu Sans Mono", monospace;
+    font-size: 80%;
+    overflow: hidden;
+}
+
+.removed-line {
+    background: #ffccaa;;
+}
+
+.added-line {
+    background: #aaffcc;
+}
+
+.changed-line {
+    background: #aaccff;
+}
+
+.file-table {
+    width: 100%;
+    border-collapse: collapse;
+    table-layout: fixed;
+}
+
+.middle-column {
+    width: 3px;
+}
+
+.my-comment {
+    border: 1px solid black;
+}
+
+.my-comment td {
+    padding: 0px;
+}
+
+.my-comment div {
+    padding: 0.5em;
+    border-left: 10px solid green;
+    white-space: pre-wrap;
+}
+
+.comment {
+    border: 1px solid black;
+}
+
+.comment td {
+    padding: 0px;
+}
+
+.comment div {
+    padding: 0.5em;
+    border-left: 10px solid blue;
+    white-space: pre-wrap;
+}
+
+.comment-editor textarea {
+    width: 100%;
+}
\ No newline at end of file


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]