Switch to side-by-side view

--- a/Allura/allura/model/repo.py
+++ b/Allura/allura/model/repo.py
@@ -36,6 +36,8 @@
 VIEWABLE_EXTENSIONS = ['.php','.py','.js','.java','.html','.htm','.yaml','.sh',
     '.rb','.phtml','.txt','.bat','.ps1','.xhtml','.css','.cfm','.jsp','.jspx',
     '.pl','.php4','.php3','.rhtml','.svg','.markdown','.json','.ini','.tcl','.vbs','.xsl']
+
+DIFF_SIMILARITY_THRESHOLD = .5  # used for determining file renames
 
 # Basic commit information
 # One of these for each commit in the physical repo on disk. The _id is the
@@ -267,6 +269,28 @@
                 added.append(change.name)
             else:
                 changed.append(change.name)
+        prev_commit = self.log(1, 1)[0]
+        for r_name in removed[:]:
+            r_blob = prev_commit.tree.get_obj_by_path(r_name)
+            r_type = 'blob' if isinstance(r_blob, Blob) else 'tree'
+            best = dict(ratio=0, name='')
+            for a_name in added:
+                a_blob = self.tree.get_obj_by_path(a_name)
+                if r_type == 'tree':
+                    if isinstance(a_blob, Tree):
+                        if r_blob._id == a_blob._id:
+                            best['ratio'] = 100
+                            best['name'] = a_name
+                            break;
+                else:
+                    diff = SequenceMatcher(None, r_blob.text, a_blob.text)
+                    if diff.ratio() > best['ratio']:
+                        best['ratio'] = diff.ratio()
+                        best['name'] = a_name
+            if best['ratio'] > DIFF_SIMILARITY_THRESHOLD:
+                copied.append(dict(old=r_name, new=a_name))
+                removed.remove(r_name)
+                added.remove(best['name'])
         return Object(
             added=added, removed=removed,
             changed=changed, copied=copied)
@@ -311,7 +335,7 @@
         obj.set_context(self, name)
         return obj
 
-    def get_blob_by_path(self, path):
+    def get_obj_by_path(self, path):
         path = path.split('/')
         obj = self
         for p in path:
@@ -319,6 +343,10 @@
                 obj = obj[p]
             except KeyError:
                 return None
+        return obj
+
+    def get_blob_by_path(self, path):
+        obj = self.get_obj_by_path(path)
         return obj if isinstance(obj, Blob) else None
 
     def set_context(self, commit_or_tree, name=None):