diff --git a/data/ui/patchview-ui.xml b/data/ui/patchview-ui.xml
new file mode 100644
index 0000000..5fa107f
--- /dev/null
+++ b/data/ui/patchview-ui.xml
@@ -0,0 +1,33 @@
+<ui>
+  <menubar name="Menubar">
+    <menu action="FileMenu">
+      <placeholder name="FileActionsPlaceholder">
+        <menuitem action="ApplyPatch"/>
+      </placeholder>
+    </menu>
+    <menu action="ViewMenu">
+      <placeholder name="ViewPlaceholder">
+        <menuitem action="Flatten" />
+        <menuitem action="ShowMatched"/>
+      </placeholder>
+    </menu>
+    <menu action="ChangesMenu">
+      <placeholder name="ChangesActions">
+        <menuitem action="UseFuzzFactor"/>
+      </placeholder>
+	</menu>
+  </menubar>
+
+  <toolbar name="Toolbar">
+    <placeholder name="SpecialActions">
+      <toolitem action="ApplyPatch"/>
+    </placeholder>
+    <placeholder name="PatchViewActions">
+      <toolitem action="Flatten"/>
+      <toolitem action="ShowMatched"/>
+      <toolitem action="UseFuzzFactor"/>
+    </placeholder>
+  </toolbar>
+
+</ui>
+
diff --git a/data/ui/patchview.ui b/data/ui/patchview.ui
new file mode 100644
index 0000000..0f470c4
--- /dev/null
+++ b/data/ui/patchview.ui
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+	<!-- interface-requires gtk+ 2.12 -->
+	<!-- interface-naming-policy toplevel-contextual -->
+	<object class="GtkWindow" id="container">
+		<property name="visible">True</property>
+		<property name="title" translatable="yes">window1</property>
+		<child>
+			<object class="GtkVBox" id="patchview">
+				<property name="visible">True</property>
+				<property name="homogeneous">False</property>
+				<property name="spacing">0</property>
+				<child>
+					<object class="MsgAreaController" id="msgarea">
+						<property name="visible">True</property>
+						<property name="can_focus">False</property>
+						<child>
+							<placeholder />
+						</child>
+					</object>
+					<packing>
+						<property name="expand">False</property>
+						<property name="fill">True</property>
+						<property name="position">0</property>
+					</packing>
+				</child>
+				<child>
+					<object class="GtkHBox" id="commit_message_hbox">
+						<property name="height_request">70</property>
+						<property name="visible">True</property>
+						<property name="homogeneous">False</property>
+						<property name="spacing">0</property>
+						<child>
+							<object class="GtkEventBox" id="commit_message_hide_box">
+								<property name="visible">True</property>
+								<property name="visible_window">True</property>
+								<property name="above_child">False</property>
+								<signal handler="on_commit_message_view_toggle"
+									last_modification_time="Sat, 27 Sep 2003 16:10:20 GMT" name="button_press_event" />
+								<child>
+									<object class="GtkArrow" id="commit_message_arrow_down">
+										<property name="width_request">14</property>
+										<property name="height_request">14</property>
+										<property name="visible">True</property>
+										<property name="arrow_type">GTK_ARROW_DOWN</property>
+										<property name="shadow_type">GTK_SHADOW_OUT</property>
+										<property name="xalign">0.5</property>
+										<property name="yalign">0</property>
+										<property name="xpad">0</property>
+										<property name="ypad">0</property>
+									</object>
+								</child>
+							</object>
+							<packing>
+								<property name="padding">0</property>
+								<property name="expand">False</property>
+								<property name="fill">False</property>
+							</packing>
+						</child>
+						<child>
+							<object class="GtkScrolledWindow" id="commit_message_scrolledwindow">
+								<property name="visible">True</property>
+								<property name="can_focus">True</property>
+								<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+								<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+								<property name="shadow_type">GTK_SHADOW_IN</property>
+								<property name="window_placement">GTK_CORNER_TOP_LEFT</property>
+								<child>
+									<object class="GtkTextView" id="commit_message">
+										<property name="visible">True</property>
+										<property name="can_focus">True</property>
+										<property name="editable">False</property>
+										<property name="overwrite">False</property>
+										<property name="accepts_tab">True</property>
+										<property name="justification">GTK_JUSTIFY_LEFT</property>
+										<property name="wrap_mode">GTK_WRAP_NONE</property>
+										<property name="cursor_visible">False</property>
+										<property name="pixels_above_lines">0</property>
+										<property name="pixels_below_lines">0</property>
+										<property name="pixels_inside_wrap">0</property>
+										<property name="left_margin">0</property>
+										<property name="right_margin">0</property>
+										<property name="indent">0</property>
+									</object>
+								</child>
+							</object>
+							<packing>
+								<property name="padding">0</property>
+								<property name="expand">True</property>
+								<property name="fill">True</property>
+							</packing>
+						</child>
+					</object>
+					<packing>
+						<property name="padding">0</property>
+						<property name="expand">False</property>
+						<property name="fill">True</property>
+						<property name="position">1</property>
+					</packing>
+				</child>
+				<child>
+					<object class="GtkEventBox" id="commit_message_show_box">
+						<property name="visible_window">True</property>
+						<property name="above_child">False</property>
+						<signal handler="on_commit_message_view_toggle"
+							last_modification_time="Sat, 27 Sep 2003 16:10:28 GMT" name="button_press_event" />
+						<child>
+							<object class="GtkHBox" id="hbox3">
+								<property name="visible">True</property>
+								<property name="homogeneous">False</property>
+								<property name="spacing">0</property>
+								<child>
+									<object class="GtkArrow" id="commit_message_arrow_right">
+										<property name="width_request">14</property>
+										<property name="height_request">14</property>
+										<property name="visible">True</property>
+										<property name="arrow_type">GTK_ARROW_RIGHT</property>
+										<property name="shadow_type">GTK_SHADOW_OUT</property>
+										<property name="xalign">0.5</property>
+										<property name="yalign">0.5</property>
+										<property name="xpad">0</property>
+										<property name="ypad">0</property>
+									</object>
+									<packing>
+										<property name="padding">0</property>
+										<property name="expand">False</property>
+										<property name="fill">True</property>
+									</packing>
+								</child>
+								<child>
+									<object class="GtkHSeparator" id="hseparator1">
+										<property name="visible">True</property>
+									</object>
+									<packing>
+										<property name="padding">0</property>
+										<property name="expand">True</property>
+										<property name="fill">True</property>
+									</packing>
+								</child>
+							</object>
+						</child>
+					</object>
+					<packing>
+						<property name="padding">0</property>
+						<property name="expand">False</property>
+						<property name="fill">False</property>
+						<property name="position">1</property>
+					</packing>
+				</child>
+				<child>
+					<object class="GtkVPaned" id="vpaned">
+						<property name="visible">True</property>
+						<property name="can_focus">True</property>
+						<property name="position">250</property>
+						<child>
+							<object class="GtkScrolledWindow" id="scrolledwindow">
+								<property name="visible">True</property>
+								<property name="can_focus">True</property>
+								<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+								<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+								<property name="shadow_type">GTK_SHADOW_IN</property>
+								<property name="window_placement">GTK_CORNER_TOP_LEFT</property>
+								<child>
+									<object class="GtkTreeView" id="treeview">
+										<property name="visible">True</property>
+										<property name="can_focus">True</property>
+										<signal name="row_activated" handler="on_row_activated" />
+									</object>
+								</child>
+							</object>
+							<packing>
+								<property name="resize">True</property>
+								<property name="shrink">False</property>
+							</packing>
+						</child>
+					</object>
+					<packing>
+						<property name="padding">0</property>
+						<property name="expand">True</property>
+						<property name="fill">True</property>
+						<property name="position">2</property>
+						<property name="pack_type">GTK_PACK_END</property>
+					</packing>
+				</child>
+			</object>
+		</child>
+	</object>
+</interface>
diff --git a/meld/meldapp.py b/meld/meldapp.py
index e8085ce..d15f422 100644
--- a/meld/meldapp.py
+++ b/meld/meldapp.py
@@ -152,6 +152,27 @@ class MeldApp(gobject.GObject):
                 _("wrong number of arguments supplied to --diff"))
         parser.values.diff.append(diff_files_args)
 
+    def patch_mode_callback(self, option, opt_str, value, parser):
+        """Gather --patch arguments and append to a list"""
+        assert value is None
+        patch_mode_args = []
+        while parser.rargs:
+            # Stop if we find a short- or long-form arg, or a '--'
+            # Note that this doesn't handle negative numbers.
+            arg = parser.rargs[0]
+            if arg[:2] == "--" or (arg[:1] == "-" and len(arg) > 1):
+                break
+            else:
+                patch_mode_args.append(arg)
+                del parser.rargs[0]
+
+        if len(patch_mode_args) not in (1, 2):
+            raise optparse.OptionValueError(
+                _("wrong number of arguments supplied to --patch"))
+        if not os.path.isfile(patch_mode_args[0]):
+            parser.error(_("no patch file supplied"))
+        parser.values.patch.append(patch_mode_args)
+
     def parse_args(self, rawargs):
         usages = [("", _("Start with an empty window")),
                   ("<%s|%s>" % (_("file"), _("dir")), _("Start a version control comparison")),
@@ -177,12 +198,17 @@ class MeldApp(gobject.GObject):
         parser.add_option("", "--diff", action="callback", callback=self.diff_files_callback,
                           dest="diff", default=[],
                           help=_("Creates a diff tab for up to 3 supplied files or directories."))
+        parser.add_option("", "--patch", action="callback", callback=self.patch_mode_callback,
+                          dest="patch", default=[],
+                          help=_("Opens patch mode."))
         options, args = parser.parse_args(rawargs)
         if len(args) > 4:
             parser.error(_("too many arguments (wanted 0-4, got %d)") % len(args))
         elif len(args) == 4 and any([os.path.isdir(f) for f in args]):
             parser.error(_("can't compare more than three directories"))
 
+        for paths in options.patch:
+            self.window.open_patch(paths)
         for files in options.diff:
             if len(files) == 4 and any([os.path.isdir(f) for f in files]):
                 parser.error(_("can't compare more than three directories"))
diff --git a/meld/meldwindow.py b/meld/meldwindow.py
index 55e3061..237ecbc 100644
--- a/meld/meldwindow.py
+++ b/meld/meldwindow.py
@@ -30,6 +30,7 @@ from ui import gnomeglade
 import misc
 from ui import notebooklabel
 import preferences
+import patchview
 import task
 import vcview
 
@@ -648,6 +649,14 @@ class MeldWindow(gnomeglade.Component):
             doc.on_button_diff_clicked(None)
         return doc
 
+    def append_patchview(self, location, basedir=None):
+        doc = patchview.PatchView(app.prefs)
+        self._append_page(doc, "text-x-generic")
+        if basedir is None:
+            basedir = os.getcwd()
+        doc.init_patcher(location, basedir)
+        return doc
+
     def _single_file_open(self, path):
         doc = vcview.VcView(app.prefs)
         def cleanup():
@@ -659,6 +668,16 @@ class MeldWindow(gnomeglade.Component):
         doc.connect("create-diff", lambda obj,arg: self.append_diff(arg))
         doc.run_diff([path])
 
+    def open_patch(self, paths):
+        if len(paths) == 1:
+            a = paths[0]
+            if os.path.isfile(a):
+                self.append_patchview(a)
+        elif len(paths) == 2:
+            a = paths[0]
+            if os.path.isfile(a):
+                self.append_patchview(a, paths[1])
+
     def open_paths(self, paths, auto_compare=False):
         tab = None
         if len(paths) == 1:
diff --git a/meld/patchdiff.py b/meld/patchdiff.py
new file mode 100644
index 0000000..94a38b7
--- /dev/null
+++ b/meld/patchdiff.py
@@ -0,0 +1,116 @@
+### Copyright (C) 2012 Piotr Piastucki <the_leech@users.berlios.de>
+
+### This program is free software; you can redistribute it and/or modify
+### it under the terms of the GNU General Public License as published by
+### the Free Software Foundation; either version 2 of the License, or
+### (at your option) any later version.
+
+### This program is distributed in the hope that it will be useful,
+### but WITHOUT ANY WARRANTY; without even the implied warranty of
+### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+### GNU General Public License for more details.
+
+### You should have received a copy of the GNU General Public License
+### along with this program; if not, write to the Free Software
+### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import difflib
+import filediff
+import tempfile
+import os
+import diffutil
+from meld.util.patcher import PatcherFileResult
+from meld.util.patchparser import PatchFile
+
+class PatchDiffSequenceMatcher(difflib.SequenceMatcher):
+
+    def __init__(self, patch_file):
+        self.a = patch_file.get_text_before()
+        self.b = patch_file.get_text_after()
+        self.matching_blocks = patch_file.get_matching_blocks()
+        self.opcodes = None
+
+    def get_difference_opcodes(self):
+        return filter(lambda x: x[0] != "equal", self.get_opcodes())
+
+class PatchDiffer(diffutil.Differ):
+
+    _matcher = PatchDiffSequenceMatcher
+
+    def __init__(self, patch_file_or_chunk):
+        diffutil.Differ.__init__(self)
+        self.patch_file_or_chunk = patch_file_or_chunk
+
+    def set_sequences_iter(self, sequences):
+        assert 0 <= len(sequences) <= 3
+        self.diffs = [[], []]
+        self.num_sequences = len(sequences)
+        self.seqlength = [len(s) for s in sequences]
+        for i in range(self.num_sequences - 1):
+            matcher = PatchDiffSequenceMatcher(self.patch_file_or_chunk)
+            self.diffs[i] = matcher.get_difference_opcodes()
+        self._initialised = True
+        self._update_merge_cache(sequences)
+        yield 1
+
+class PatchQuickDiff(filediff.FileDiff):
+
+    def __init__(self, prefs):
+        filediff.FileDiff.__init__(self, prefs, 2)
+        self.tempfile1 = None
+        self.tempfile2 = None
+        for fe in self.fileentry:
+            fe.set_property('history-id', 'dummy');
+            fe.set_visible(False)
+
+    def _cleanup(self):
+        if self.tempfile1 is not None:
+            try:
+                os.remove(self.tempfile1)
+            except OSError:
+                pass
+        if self.tempfile2 is not None:
+            try:
+                os.remove(self.tempfile2)
+            except OSError:
+                pass
+
+    def _save_tmp_file(self, filename, content):
+        fd, filename = tempfile.mkstemp(suffix="-" + os.path.basename(filename))
+        file = open(filename, 'w')
+        if content is not None:
+            file.write(content)
+        file.close()
+        os.close(fd)
+        return filename
+
+    def clear(self):
+        self.set_patch_file("", PatcherFileResult(PatchFile("", ""), ""))
+
+    def set_patch_file(self, filename, patch_result):
+        self._cleanup()
+        if patch_result.is_file():
+            self.linediffer = self.differ()
+            self.tempfile1 = self._save_tmp_file(filename, patch_result.get_unpatched_content())
+            self.tempfile2 = self._save_tmp_file(filename, patch_result.get_patched_content())
+            self.set_files([self.tempfile1, self.tempfile2])
+        else:
+            patch_object = patch_result.get_patch_object()
+            self.linediffer = PatchDiffer(patch_object)
+            self.tempfile1 = self._save_tmp_file(filename, patch_result.get_unpatched_content())
+            self.tempfile2 = self._save_tmp_file(filename, patch_result.get_patched_content())
+            self.set_files([self.tempfile1, self.tempfile2])
+
+    def _connect_buffer_handlers(self):
+        filediff.FileDiff._connect_buffer_handlers(self)
+        self.textview[0].set_editable(0)
+        self.textview[1].set_editable(0)
+
+    def on_delete_event(self, appquit=0):
+        self._cleanup()
+
+    def on_textview_popup_menu(self, textview):
+        return True
+
+    def on_textview_button_press_event(self, textview, event):
+        return False
diff --git a/meld/patchview.py b/meld/patchview.py
new file mode 100644
index 0000000..a3729ab
--- /dev/null
+++ b/meld/patchview.py
@@ -0,0 +1,232 @@
+### Copyright (C) 2012 Piotr Piastucki <the_leech@users.berlios.de>
+
+### This program is free software; you can redistribute it and/or modify
+### it under the terms of the GNU General Public License as published by
+### the Free Software Foundation; either version 2 of the License, or
+### (at your option) any later version.
+
+### This program is distributed in the hope that it will be useful,
+### but WITHOUT ANY WARRANTY; without even the implied warranty of
+### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+### GNU General Public License for more details.
+
+### You should have received a copy of the GNU General Public License
+### along with this program; if not, write to the Free Software
+### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from ui import gnomeglade
+import ui.emblemcellrenderer
+import paths
+import melddoc
+import tree
+import gtk
+import os
+from util.patcher import Patcher
+import patchdiff
+
+COL_LOCATION, COL_END = range(tree.COL_END, tree.COL_END + 2)
+
+class PatchTreeStore(tree.DiffTreeStore):
+    def __init__(self):
+        ntree = 1
+        types = [str] * COL_END * ntree
+        tree.DiffTreeStore.__init__(self, ntree, types)
+
+    def set_state(self, it, pane, state, isdir=0):
+        tree.DiffTreeStore.set_state(self, it, pane, state, isdir)
+
+class PatchView(melddoc.MeldDoc, gnomeglade.Component):
+    def __init__(self, prefs):
+        self.prefs = prefs
+        melddoc.MeldDoc.__init__(self, prefs)
+        gnomeglade.Component.__init__(self, paths.ui_dir("patchview.ui"), "patchview")
+
+        actions = [
+            ("ApplyPatch", gtk.STOCK_EXECUTE, _("Apply patch"), None, _("Apply patch"), self.apply_patch)
+        ]
+
+        toggleactions = (
+            ("Flatten", gtk.STOCK_GOTO_BOTTOM, _("_Flatten"), None, _("Flatten directories"), self.on_button_flatten_toggled, True),
+            ("ShowMatched", gtk.STOCK_YES, _("_Show matched chunks"), None, _("Show matched chunks"), self.on_button_matched_toggled, False),
+            ("UseFuzzFactor", gtk.STOCK_DIALOG_QUESTION, _("_Use fuzz factor"), None, _("Use fuzz factor"), self.on_button_fuzz_factor_toggled, False)
+        )
+
+        vpaned = getattr(self, "vpaned")
+        self.patchdiff = patchdiff.PatchQuickDiff(self.prefs)
+        self.scheduler.add_scheduler(self.patchdiff.scheduler)
+        vpaned.pack2(self.patchdiff.widget, True, True)
+
+        self.ui_file = paths.ui_dir("patchview-ui.xml")
+        self.actiongroup = gtk.ActionGroup("PatchViewActions")
+        self.actiongroup.set_translation_domain("meld")
+        self.actiongroup.add_actions(actions)
+        self.actiongroup.add_toggle_actions(toggleactions)
+        self.actiongroup.get_action("ApplyPatch").props.is_important = True
+        self.actiongroup.get_action("Flatten").props.is_important = True
+        self.actiongroup.get_action("ShowMatched").props.is_important = True
+        self.actiongroup.get_action("UseFuzzFactor").props.is_important = True
+
+        self.model = PatchTreeStore()
+        self.treeview.set_model(self.model)
+        self.treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
+        self.treeview.set_headers_visible(1)
+
+        column = gtk.TreeViewColumn(_("Name"))
+        renicon = ui.emblemcellrenderer.EmblemCellRenderer()
+        rentext = gtk.CellRendererText()
+        column.pack_start(renicon, expand=0)
+        column.pack_start(rentext, expand=1)
+        col_index = self.model.column_index
+        column.set_attributes(renicon,
+                              icon_name=col_index(tree.COL_ICON, 0),
+                              icon_tint=col_index(tree.COL_TINT, 0))
+        column.set_attributes(rentext, markup=col_index(tree.COL_TEXT, 0))
+        self.treeview.append_column(column)
+
+        column = gtk.TreeViewColumn(_("Location"))
+        rentext = gtk.CellRendererText()
+        column.pack_start(rentext, expand=0)
+        column.set_attributes(rentext, markup=self.model.column_index(COL_LOCATION, 0))
+        self.treeview.append_column(column)
+        self.treeview_column_location = column
+        self.nodes = {}
+        self.patch_file = None
+        self.patcher = None
+        self.patchdiff.connect("next-diff-changed", self.on_next_diff_changed)
+
+    def init_patcher(self, patchfile, basedir):
+        self.patcher = Patcher(patchfile, basedir)
+        self.patch_file = patchfile
+        if self.patcher.patch.commit_message is not None:
+            b = self.commit_message.get_buffer()
+            b.delete(*b.get_bounds())
+            b.insert(b.get_end_iter(), self.patcher.patch.commit_message)
+        else:
+            self.hide_commit_message()
+        self.refresh_patch()
+
+    def chunk_to_string(self, cresult):
+        s = _("%s, %s -> %s, %s") % (cresult.chunk.start_before, cresult.chunk.get_lines_number_before(), cresult.chunk.start_after, cresult.chunk.get_lines_number_after())
+        if cresult.is_matched() and cresult.fuzz_factor > 0:
+            s += _(" (fuzz factor used: %s)") % cresult.fuzz_factor
+        return s
+
+    def refresh_patch(self):
+        max_fuzz_factor = 0
+        if self.actiongroup.get_action("UseFuzzFactor").get_active():
+            max_fuzz_factor = 2
+        self.patcher_result = self.patcher.apply_patch(max_fuzz_factor)
+        self.set_location(self.patch_file)
+
+    def set_location(self, location):
+        self.model.clear()
+        self.location = location
+
+        root = self.model.add_entries(None, [location])
+        self.model.set_state(root, 0, tree.STATE_NORMAL, isdir=1)
+        self.nodes = {}
+        flatten = self.actiongroup.get_action("Flatten").get_active()
+        show_matched = self.actiongroup.get_action("ShowMatched").get_active()
+        for fresult in self.patcher_result.get_file_results():
+            it = root
+            subdirs = [x for x in fresult.actual_filename.split("/") if x]
+            for i in range(len(subdirs)):
+                if not flatten or i == len(subdirs) - 1:
+                    path = os.path.sep.join(subdirs[:i + 1])
+                    if path in self.nodes:
+                        it = self.nodes[path][0]
+                    else:
+                        it = self.model.add_entries(it, [path])
+                        self.model.set_value(it, self.model.column_index(COL_LOCATION, 0), os.path.dirname(path))
+                        if i < len(subdirs) - 1:
+                            self.model.set_state(it, 0, tree.STATE_NORMAL, isdir=1)
+                        else:
+                            self.nodes[path] = (it, fresult)
+                            if not fresult.is_matched():
+                                self.model.set_state(it, 0, tree.STATE_CONFLICT, isdir=0)
+                            elif fresult.file.is_added():
+                                self.model.set_state(it, 0, tree.STATE_NEW, isdir=0)
+                            elif fresult.file.is_removed():
+                                self.model.set_state(it, 0, tree.STATE_REMOVED, isdir=0)
+                            else:
+                                self.model.set_state(it, 0, tree.STATE_MODIFIED, isdir=0)
+                            for cresult in fresult.get_chunk_results():
+                                if cresult.is_matched() and not show_matched:
+                                    continue
+                                chunkpath = path + " " + self.chunk_to_string(cresult)
+                                cit = self.model.add_entries(it, [chunkpath])
+                                self.nodes[chunkpath] = (cit, cresult)
+                                self.model.set_value(cit, self.model.column_index(COL_LOCATION, 0), os.path.dirname(path))
+                                chunk_state = tree.STATE_NORMAL
+                                if not cresult.is_matched():
+                                    chunk_state = tree.STATE_CONFLICT
+                                elif cresult.is_fuzzy_matched():
+                                    chunk_state = tree.STATE_MODIFIED
+                                self.model.set_state(cit, 0, chunk_state, isdir=0)
+        self.treeview.expand_all()
+        self.treeview.grab_focus()
+        self.treeview.get_selection().select_iter(root)
+        self.recompute_label()
+        self.scheduler.remove_all_tasks()
+        self.patchdiff.clear()
+
+    def on_button_flatten_toggled(self, button):
+        self.treeview_column_location.set_visible(self.actiongroup.get_action("Flatten").get_active())
+        self.refresh()
+
+    def on_button_matched_toggled(self, button):
+        self.refresh()
+
+    def on_button_fuzz_factor_toggled(self, button):
+        self.refresh_patch()
+
+    def apply_patch(self, widget):
+        self.actiongroup.get_action("ApplyPatch").set_sensitive(False)
+        self.patcher.persist_changes(self.patcher_result)
+        msgarea = self.msgarea.new_from_text_and_icon(gtk.STOCK_INFO, _("Patch has been applied"), None)
+        msgarea.add_stock_button_with_text(_("Hi_de"), gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)
+        msgarea.connect("response", lambda *args: self.msgarea.clear())
+        msgarea.show_all()
+
+    def recompute_label(self):
+        self.label_text = os.path.basename(self.location)
+        # TRANSLATORS: This is the location of the directory the user is diffing
+        self.tooltip_text = _("%s: %s") % (_("Location"), self.location)
+        self.label_changed()
+
+    def refresh(self):
+        self.set_location(self.patch_file)
+
+    def on_row_activated(self, treeview, path, tvc):
+        it = self.model.get_iter(path)
+        filepath = self.model.value_path(it, 0)
+        if filepath in self.nodes:
+            result = self.nodes[filepath][1]
+            self.patchdiff.set_patch_file(filepath, result)
+        elif self.model.iter_has_child(it):
+            if self.treeview.row_expanded(path):
+                self.treeview.collapse_row(path)
+            else:
+                self.treeview.expand_row(path, 0)
+
+    def on_delete_event(self, appquit=0):
+        self.patchdiff.on_delete_event(appquit)
+
+    def hide_commit_message(self):
+        self.commit_message_hbox.hide()
+        self.commit_message_show_box.hide()
+
+    def on_commit_message_view_toggle(self, box, event=None):
+        if box == self.commit_message_hide_box:
+            self.commit_message_hbox.hide()
+            self.commit_message_show_box.show()
+        else:
+            self.commit_message_hbox.show()
+            self.commit_message_show_box.hide()
+
+    def next_diff(self, direction):
+        self.patchdiff.next_diff(direction)
+
+    def on_next_diff_changed(self, doc, have_prev, have_next):
+        self.emit("next-diff-changed", have_prev, have_next)
+
diff --git a/meld/util/patcher.py b/meld/util/patcher.py
new file mode 100644
index 0000000..72c9ccc
--- /dev/null
+++ b/meld/util/patcher.py
@@ -0,0 +1,311 @@
+### Copyright (C) 2012 Piotr Piastucki <the_leech@users.berlios.de>
+
+### This program is free software; you can redistribute it and/or modify
+### it under the terms of the GNU General Public License as published by
+### the Free Software Foundation; either version 2 of the License, or
+### (at your option) any later version.
+
+### This program is distributed in the hope that it will be useful,
+### but WITHOUT ANY WARRANTY; without even the implied warranty of
+### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+### GNU General Public License for more details.
+
+### You should have received a copy of the GNU General Public License
+### along with this program; if not, write to the Free Software
+### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import os
+import meld.matchers
+from patchparser import Patch
+
+STATUS_OK, STATUS_FUZZ_FACTOR, STATUS_FAILED = range(3)
+
+class PatcherChunkResult():
+
+    def __init__(self, chunk, matched, fuzz_factor):
+        self.chunk = chunk
+        self.matched = matched
+        self.fuzz_factor = fuzz_factor
+        if matched and fuzz_factor == 0:
+            self.status = STATUS_OK
+        elif matched:
+            self.status = STATUS_FUZZ_FACTOR
+        else:
+            self.status = STATUS_FAILED
+
+    def is_matched(self):
+        return self.matched
+
+    def is_fuzzy_matched(self):
+        return self.matched and self.fuzz_factor > 0
+
+    def get_patched_content(self):
+        return self.chunk.get_text_after()
+
+    def get_unpatched_content(self):
+        return self.chunk.get_text_before()
+
+    def get_patch_object(self):
+        return self.chunk
+
+    def is_file(self):
+        return False
+
+    def __str__(self):
+        if self.status == STATUS_OK:
+            s = " "
+        elif self.status == STATUS_FUZZ_FACTOR:
+            s = "?"
+        else:
+            s = "!"
+        s += str(self.chunk.start_before) + ", " + str(self.chunk.get_lines_number_before()) + " -> " + str(self.chunk.start_after) + ", " + str(self.chunk.get_lines_number_after())
+        if self.fuzz_factor > 0:
+            s += " (fuzz factor used: " + str(self.fuzz_factor) + ")"
+        return s
+
+class PatcherFileResult():
+
+    def __init__(self, file, actual_filename):
+        self.file = file
+        self.target_exists = False
+        self.matched = False
+        self.actual_filename = actual_filename
+        self.status = STATUS_OK
+        self.fuzz_factor = 0
+        self.chunk_results = []
+        self.unpatched_content = None
+        self.patched_content = None
+
+    def add_chunk_result(self, result):
+        if self.status < result.status:
+            self.status = result.status
+        if result.is_matched():
+            self.matched = True
+        self.chunk_results.append(result)
+
+    def get_chunk_results(self):
+        return self.chunk_results
+
+    def is_matched(self):
+        return self.matched
+
+    def is_file(self):
+        return True
+
+    def get_patch_object(self):
+        return self.file
+
+    def get_patched_content(self):
+        if self.patched_content is not None:
+            return self.patched_content
+        return self.file.get_text_after()
+
+    def get_unpatched_content(self):
+        if self.unpatched_content is not None:
+            return self.unpatched_content
+        return self.file.get_text_before()
+
+class PatcherResult():
+
+    def __init__(self):
+        self.ignored_path_segments = 0
+        self.results = []
+
+    def add_file_result(self, result):
+        self.results.append(result)
+
+    def get_file_results(self):
+        return self.results
+
+    def __str__(self):
+        s = "PatcherResult\n"
+        for f in self.get_file_results():
+            s += f.file.filename + "\n"
+            for chunk in f.chunk_results:
+                s += str(chunk) + "\n"
+        return s
+
+class Patcher():
+
+    def __init__(self, filename, basedir):
+        self.patch = Patch(filename)
+        self.basedir = basedir
+
+    def locate_text(self, filecontent, lines, chunkpos, minchunkpos):
+        if lines is not None:
+            blocklen = len(lines)
+            maxchunkpos = len(filecontent)
+            lineset = frozenset(lines)
+            if filecontent[chunkpos:chunkpos + blocklen] == lines:
+                return (chunkpos, blocklen)
+            else:
+                foundpos = None
+                i = chunkpos - 1
+                j = chunkpos + blocklen + 1
+                while i >= minchunkpos or j <= maxchunkpos:
+                    if i >= minchunkpos:
+                        if filecontent[i:i + blocklen] == lines:
+                            foundpos = i
+                            break
+                        elif filecontent[i] in lineset:
+                            i -= 1
+                        else:
+                            i -= blocklen
+                    if j <= maxchunkpos:
+                        if filecontent[j - blocklen:j] == lines:
+                            foundpos = j - blocklen
+                            break
+                        elif filecontent[j - 1] in lineset:
+                            j += 1
+                        else:
+                            j += blocklen
+                if foundpos is not None:
+                    return (foundpos, blocklen)
+        return None
+
+    def get_files(self):
+        return self.patch.files
+
+    def get_patch_file_name(self):
+        return self.patch.filename
+
+    def _get_file_name(self, f, ignored_path_segments):
+        filename = f.get_filename()
+        if ignored_path_segments > 0:
+            path_segments = filename.split("/")
+            filename = os.path.sep.join(path_segments[min(len(path_segments) - 1, ignored_path_segments):])
+        return os.path.abspath(self.basedir + os.path.sep + filename)
+
+    def _discover_ignored_path_segments(self):
+        ignored_path_segments = 0
+        if self.basedir is not None:
+            for file in self.patch.files:
+                filename = file.get_filename()
+                if file.is_added():
+                    if filename[:2] == "a/" or filename[:2] == "b/":
+                        ignored_path_segments = 1
+                    continue
+                path_segments = file.get_filename().split("/")
+                for i in range(len(path_segments) - 1):
+                    path = self.basedir + os.path.sep + os.path.sep.join(path_segments[i:])
+                    if os.path.isfile(path):
+                        ignored_path_segments = i
+                        break
+        return ignored_path_segments
+
+    def get_patched_file(self, file, ignored_path_segments, max_fuzz_factor=0):
+        filecontent = None
+        if self.basedir is not None:
+            filename = self._get_file_name(file, ignored_path_segments)
+            # try to open a file, if it does not exist all chunks will be handled as unmatched ones
+            try:
+                f = open(filename)
+                filecontent = f.read().split('\n')
+                f.close()
+            except (IOError):
+                pass
+        if filecontent is None and file.is_added():
+            filecontent = []
+        currentline = 0
+        patched_content = []
+        pfr = PatcherFileResult(file, filename)
+        if filecontent is not None:
+            pfr.unpatched_content = "\n".join(filecontent)
+        for chunk in file.chunks:
+            chunk_found = False
+            fuzz_factor = 0
+            # try to apply the chunk as long as the file exists
+            if filecontent is not None:
+                chunk_start = chunk.start_before - 1
+                chunk_text_before = chunk.get_lines_before()
+                chunk_text_after = chunk.get_lines_after()
+                startcontext = chunk.get_start_context_block()
+                endcontext = chunk.get_end_context_block()
+                max_context_len = max(startcontext is not None and len(startcontext.lines) or 1, endcontext is not None and len(endcontext.lines) or 1)
+                start_ctx_location = None
+                end_ctx_location = None
+                for fuzz_factor in range(0, min(max_fuzz_factor, max_context_len - 1) + 1):
+                    start_ctx_location = None
+                    end_ctx_location = None
+                    if startcontext is not None:
+                        # if there is any context before the chunk try to match it
+                        lines = startcontext.lines[min(len(startcontext.lines) - 1, fuzz_factor):]
+                        start_ctx_location = self.locate_text(filecontent, lines, chunk_start, currentline)
+                    else:
+                        start_ctx_location = (0, 0)
+                    if start_ctx_location is not None:
+                        if endcontext is not None:
+                            # if there is any context after the chunk try to match it
+                            start_location_end = start_ctx_location[0] + start_ctx_location[1]
+                            lines = endcontext.lines[0:max(1, len(endcontext.lines) - fuzz_factor)]
+                            end_ctx_location = self.locate_text(filecontent, lines, start_location_end + len(chunk_text_before), start_location_end)
+                        else:
+                            end_ctx_location = (len(filecontent), 0)
+
+                    # measure the match between the chunk and the existing content 
+                    if start_ctx_location is not None and end_ctx_location is not None:
+                        startmatch = start_ctx_location[0] + start_ctx_location[1]
+                        endmatch = end_ctx_location[0]
+                        filecontentpart = filecontent[startmatch:endmatch]
+                        ratio = 0
+                        # compare what should be in the file based on the chunk and what actually is in the file,
+                        # maybe the files has already been patched
+                        if filecontentpart[:len(chunk_text_after)] == chunk_text_after:
+                            ratio = 1
+                        # First, try to use length to decide if the chunks are similar enough or not.
+                        # Assume the chunks are similar if they are very small or one of them is empty 
+                        # and the difference in length is small too. Some examples:
+                        # chunk 1: 0 lines, chunk 2: 1 lines, match: 83%
+                        # chunk 1: 1 lines, chunk 2: 2 lines, match: 66%
+                        # chunk 1: 2 lines, chunk 2: 4 lines, match: 33%
+                        if ratio < 0.5:
+                            len_filecontentpart = len(filecontentpart)
+                            minlen = min(len(chunk_text_before), len_filecontentpart)
+                            diff = abs(len(chunk_text_before) - len_filecontentpart)
+                            ratio = (6 - (minlen + diff)) / 6.0
+                        if ratio < 0.5:
+                            # if the chunks are not similar based on their length, try to find the ratio using a matcher
+                            # to compare what should be in the file before the change based on the chunk and what 
+                            # actually is in the file  
+                            matcher = meld.matchers.MyersSequenceMatcher(None, filecontentpart, chunk_text_before)
+                            ratio = matcher.ratio()
+                        chunk_found = ratio >= 0.5
+                        break
+                if chunk_found:
+                    # Append outstanding lines of original file and patch the content
+                    patched_content.extend(filecontent[currentline:start_ctx_location[0] + start_ctx_location[1]])
+                    patched_content.extend(chunk_text_after)
+                    currentline = end_ctx_location[0]
+            pfr.add_chunk_result(PatcherChunkResult(chunk, chunk_found, fuzz_factor))
+        if filecontent is not None:
+            # Append any outstanding lines
+            patched_content.extend(filecontent[currentline:len(filecontent)])
+        if pfr.is_matched():
+            pfr.patched_content = "\n".join(patched_content)
+        return pfr
+
+    def apply_patch(self, max_fuzz_factor=0):
+        ignored_path_segments = self._discover_ignored_path_segments()
+        result = PatcherResult()
+        # try to locate the files, go at most 2 levels down to find the file, if not found report an error
+        for file in self.patch.files:
+            result.add_file_result(self.get_patched_file(file, ignored_path_segments, max_fuzz_factor))
+        return result
+
+    def persist_changes(self, result, save_rej=True):
+        for file in result.get_file_results():
+            f = open(file.actual_filename, "w")
+            f.write(file.patched_content)
+            f.close()
+        if save_rej:
+            pass
+
+def main():
+    patcher = Patcher("patch_test/0001-Add-post-processing-cleanup-algoritm-to-MyersSequenc.patch", "patch_src/s3")
+    result = patcher.apply_patch(2)
+    print result
+
+if __name__ == "__main__":
+    main()
+
+
diff --git a/meld/util/patchparser.py b/meld/util/patchparser.py
new file mode 100644
index 0000000..ace3411
--- /dev/null
+++ b/meld/util/patchparser.py
@@ -0,0 +1,268 @@
+### Copyright (C) 2012 Piotr Piastucki <the_leech@users.berlios.de>
+
+### This program is free software; you can redistribute it and/or modify
+### it under the terms of the GNU General Public License as published by
+### the Free Software Foundation; either version 2 of the License, or
+### (at your option) any later version.
+
+### This program is distributed in the hope that it will be useful,
+### but WITHOUT ANY WARRANTY; without even the implied warranty of
+### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+### GNU General Public License for more details.
+
+### You should have received a copy of the GNU General Public License
+### along with this program; if not, write to the Free Software
+### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import re
+
+UNIFIED_OLD_FILE_NAME = re.compile("^--- (\S+)[ \t]*")
+UNIFIED_NEW_FILE_NAME = re.compile("^\+\+\+ (\S+)[ \t]*")
+UNIFIED_CHUNK_START = re.compile("^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@")
+UNIFIED_CHUNK_ADDED = re.compile("^\\+(.*)")
+UNIFIED_CHUNK_REMOVED = re.compile("^-(.*)")
+UNIFIED_CHUNK_CONTEXT = re.compile("^ (.*)")
+GIT_FROM = re.compile("^From: (.*)")
+GIT_SUBJECT = re.compile("^Subject: (.*)")
+GIT_CONTENT = re.compile("^ (.+)")
+GIT_HEADER_END = re.compile("^---")
+
+PATCH_CHUNK_BLOCK_REMOVE, PATCH_CHUNK_BLOCK_ADD, PATCH_CHUNK_BLOCK_CONTEXT = range(0, 3)
+SECTION_HEADER, SECTION_COMMIT_MESSAGE, SECTION_PATCH, SECTION_FILENAMES, SECTION_CHUNKS, SECTION_CHUNK_CONTENT = range(0, 6)
+
+class PatchChunkBlock():
+    def __init__(self, blocktype):
+        self.blocktype = blocktype
+        self.lines = []
+
+class PatchChunk():
+    def __init__(self, start_before, start_after):
+        self.file = None
+        self.start_before = start_before
+        self.start_after = start_after
+        self.blocks = []
+
+    def get_start_context_block(self):
+        if len(self.blocks) > 0 and self.blocks[0].blocktype == PATCH_CHUNK_BLOCK_CONTEXT:
+            return self.blocks[0]
+        return None
+
+    def get_end_context_block(self):
+        if len(self.blocks) > 0 and self.blocks[-1].blocktype == PATCH_CHUNK_BLOCK_CONTEXT:
+            return self.blocks[-1]
+        return None
+
+    def get_blocks_text(self, blocktype, start, end):
+        lines = []
+        for i in range(start, end):
+            block = self.blocks[i]
+            if block.blocktype == PATCH_CHUNK_BLOCK_CONTEXT or block.blocktype == blocktype:
+                lines.extend(block.lines)
+        return lines
+
+    def get_blocks(self, include_context=False):
+        start = 0
+        end = len(self.blocks)
+        if not include_context:
+            if self.get_start_context_block() is not None:
+                start += 1
+            if self.get_end_context_block() is not None:
+                end -= 1
+        return (start, end)
+
+    def get_text(self, blocktype):
+        start, end = self.get_blocks(True)
+        return "\n".join(self.get_blocks_text(blocktype, start, end))
+
+    def get_text_before(self):
+        return self.get_text(PATCH_CHUNK_BLOCK_REMOVE)
+
+    def get_text_after(self):
+        return self.get_text(PATCH_CHUNK_BLOCK_ADD)
+
+    def get_lines(self, blocktype, include_context=False):
+        start, end = self.get_blocks(include_context)
+        return self.get_blocks_text(blocktype, start, end)
+
+    def get_lines_before(self, include_context=False):
+        return self.get_lines(PATCH_CHUNK_BLOCK_REMOVE, include_context)
+
+    def get_lines_after(self, include_context=False):
+        return self.get_lines(PATCH_CHUNK_BLOCK_ADD, include_context)
+
+    def get_lines_number(self, blocktype):
+        lines = 0
+        for block in self.blocks:
+            if block.blocktype == PATCH_CHUNK_BLOCK_CONTEXT or block.blocktype == blocktype:
+                lines += len(block.lines)
+        return lines
+
+    def get_lines_number_before(self):
+        lines_before = self.get_lines_number(PATCH_CHUNK_BLOCK_REMOVE)
+        return lines_before
+
+    def get_lines_number_after(self):
+        lines_after = self.get_lines_number(PATCH_CHUNK_BLOCK_ADD)
+        return lines_after
+
+    def is_empty_before(self):
+        return self.get_lines_number_before() == 0
+
+    def is_empty_after(self):
+        return self.get_lines_number_after() == 0
+
+    def add_line(self, blocktype, line):
+        if len(self.blocks) == 0 or self.blocks[-1].blocktype != blocktype:
+            self.blocks.append(PatchChunkBlock(blocktype))
+        self.blocks[-1].lines.append(line)
+
+    def get_matching_blocks(self):
+        line_number_len = 0
+        l1 = 0
+        l2 = 0
+        matching_blocks = []
+        for block in self.blocks:
+            if block.blocktype == PATCH_CHUNK_BLOCK_CONTEXT:
+                l = len(block.lines) + line_number_len
+                matching_blocks.append((l2, l1, l))
+                l1 += l
+                l2 += l
+            elif block.blocktype == PATCH_CHUNK_BLOCK_REMOVE:
+                l1 += len(block.lines)
+            else:
+                l2 += len(block.lines)
+            line_number_len = 0
+        matching_blocks.append((l2, l1, 0))
+        return matching_blocks
+
+class PatchFile():
+    def __init__(self, oldfilename, filename):
+        self.chunks = []
+        self.oldfilename = oldfilename
+        self.filename = filename
+        self.matching_blocks = None
+
+    def add(self, chunk):
+        chunk.file = self
+        self.chunks.append(chunk)
+
+    def get_text_before(self):
+        text = [chunk.get_text_before() for chunk in self.chunks]
+        return "\n".join(text)
+
+    def get_text_after(self):
+        text = [chunk.get_text_after() for chunk in self.chunks]
+        return "\n".join(text)
+
+    def is_added(self):
+        return len(self.chunks) == 1 and self.chunks[0].is_empty_before()
+
+    def is_removed(self):
+        return len(self.chunks) == 0 or (len(self.chunks) == 1 and self.chunks[0].is_empty_after())
+
+    def get_matching_blocks(self):
+        if self.matching_blocks is not None:
+            return self.matching_blocks
+        self.matching_blocks = []
+        len1 = 0
+        len2 = 0
+        for chunk in self.chunks:
+            for block in chunk.get_matching_blocks():
+                if block[2] > 0:
+                    self.matching_blocks.append((len1 + block[0], len2 + block[1], block[2]))
+                else:
+                    len1 += block[0]
+                    len2 += block[1]
+        self.matching_blocks.append((len1, len2, 0))
+        return self.matching_blocks
+
+    def get_filename(self):
+        if self.is_added() or self.oldfilename == "/dev/null":
+            return self.filename
+        return self.oldfilename
+
+class Patch():
+    """Simple unified patch parser
+    """
+    def __init__(self, filename):
+        self.filename = filename
+        self.files = []
+        patch_lines = open(filename).read().split('\n')
+        cur_file = None
+        cur_chunk = None
+        start_before = 0
+        old_len = 0
+        start_after = 0
+        new_len = 0
+        old_filename = None
+        section = SECTION_HEADER
+        self.commit_from = None
+        self.commit_message = None
+        for line in patch_lines:
+            if section == SECTION_HEADER:
+                m = GIT_FROM.match(line)
+                if m is not None:
+                    self.commit_from = m.group(1)
+                    continue
+                m = GIT_SUBJECT.match(line)
+                if m is not None:
+                    self.commit_message = m.group(1)
+                    section = SECTION_COMMIT_MESSAGE
+                    continue
+            elif section == SECTION_COMMIT_MESSAGE:
+                m = GIT_HEADER_END.match(line)
+                if m is not None:
+                    section = SECTION_PATCH
+                    continue
+                else:
+                    self.commit_message += "\n" + line
+                    continue
+            m = UNIFIED_OLD_FILE_NAME.search(line)
+            if m is not None:
+                if old_len != 0 or new_len != 0:
+                    print "Incorrect patch file - unfinished previous file", old_filename
+                cur_chunk = None
+                cur_file = None
+                old_filename = m.group(1)
+                section = SECTION_FILENAMES
+                continue
+            if section == SECTION_FILENAMES:
+                m = UNIFIED_NEW_FILE_NAME.search(line)
+                if m is not None:
+                    cur_file = PatchFile(old_filename, m.group(1))
+                    self.files.append(cur_file)
+                    section = SECTION_CHUNKS
+                    continue
+                else:
+                    print "Incorrect patch file - new filename not provided for", old_filename
+                    continue
+            elif section == SECTION_CHUNKS:
+                m = UNIFIED_CHUNK_START.search(line)
+                if m is not None:
+                    start_before = int(m.group(1))
+                    old_len = int(m.group(2))
+                    start_after = int(m.group(3))
+                    new_len = int(m.group(4))
+                    cur_chunk = PatchChunk(start_before, start_after)
+                    cur_file.add(cur_chunk)
+                    section = SECTION_CHUNK_CONTENT
+            elif section == SECTION_CHUNK_CONTENT:
+                if new_len > 0:
+                    m = UNIFIED_CHUNK_ADDED.match(line)
+                    if m is not None:
+                        cur_chunk.add_line(PATCH_CHUNK_BLOCK_ADD, m.group(1))
+                        new_len -= 1
+                        continue
+                if old_len > 0:
+                    m = UNIFIED_CHUNK_REMOVED.match(line)
+                    if m is not None:
+                        cur_chunk.add_line(PATCH_CHUNK_BLOCK_REMOVE, m.group(1))
+                        old_len -= 1
+                        continue
+                m = UNIFIED_CHUNK_CONTEXT.match(line)
+                if m is not None:
+                    cur_chunk.add_line(PATCH_CHUNK_BLOCK_CONTEXT, m.group(1))
+                    old_len -= 1
+                    new_len -= 1
+                if new_len == 0 and old_len == 0:
+                    section = SECTION_CHUNKS
