From 1f49c90478419dba2150617c24014b37f77a209d Mon Sep 17 00:00:00 2001
From: Cristian Dinu<goc9000@gmail.com>
Date: Fri, 28 Dec 2012 04:25:51 +0100
Subject: [PATCH] Add directory diff options for shallow file comparison mode

---
 data/ui/preferences.ui |  281 +++++++++++++++++++++++++++++++++++++++++++++++-
 meld/dirdiff.py        |   45 ++++++--
 meld/meldapp.py        |    5 +-
 meld/preferences.py    |   47 +++++++-
 4 files changed, 367 insertions(+), 11 deletions(-)

diff --git a/data/ui/preferences.ui b/data/ui/preferences.ui
index a686f6e..35580d6 100644
--- a/data/ui/preferences.ui
+++ b/data/ui/preferences.ui
@@ -9,6 +9,12 @@
     <property name="step_increment">1</property>
     <property name="page_increment">10</property>
   </object>
+  <object class="GtkAdjustment" id="adjustment2">
+    <property name="upper">3600</property>
+    <property name="value">1</property>
+    <property name="step_increment">1</property>
+    <property name="page_increment">1</property>
+  </object>
   <object class="GtkDialog" id="preferencesdialog">
     <property name="can_focus">False</property>
     <property name="border_width">5</property>
@@ -271,7 +277,7 @@
                                     <property name="can_focus">True</property>
                                     <property name="adjustment">adjustment1</property>
                                     <property name="climb_rate">1</property>
-                                    <signal name="value_changed" handler="on_spinbutton_tabsize_changed" swapped="no"/>
+                                    <signal name="value-changed" handler="on_spinbutton_tabsize_changed" swapped="no"/>
                                   </object>
                                   <packing>
                                     <property name="expand">False</property>
@@ -813,6 +819,279 @@
                 <property name="tab_fill">False</property>
               </packing>
             </child>
+            <child>
+              <object class="GtkVBox" id="dir_diff_tab">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="border_width">12</property>
+                <property name="spacing">12</property>
+                <child>
+                  <object class="GtkVBox" id="vbox120">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="spacing">6</property>
+                    <child>
+                      <object class="GtkLabel" id="label132">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="xalign">0</property>
+                        <property name="label" translatable="yes">Quick comparison</property>
+                        <property name="use_markup">True</property>
+                        <attributes>
+                          <attribute name="weight" value="bold"/>
+                        </attributes>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">False</property>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkHBox" id="hbox115">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <child>
+                          <object class="GtkLabel" id="label133">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">False</property>
+                            <property name="padding">12</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkVBox" id="vbox110">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="spacing">6</property>
+                            <child>
+                              <object class="GtkCheckButton" id="checkbutton_superficial_compare">
+                                <property name="label" translatable="yes">_Trust size and modification date</property>
+                                <property name="visible">True</property>
+                                <property name="can_focus">True</property>
+                                <property name="receives_default">False</property>
+                                <property name="use_action_appearance">False</property>
+                                <property name="use_underline">True</property>
+                                <property name="draw_indicator">True</property>
+                                <signal name="toggled" handler="on_checkbutton_superficial_compare_toggled" swapped="no"/>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">True</property>
+                                <property name="position">0</property>
+                              </packing>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="expand">True</property>
+                            <property name="fill">True</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">True</property>
+                        <property name="fill">True</property>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkVBox" id="vbox1">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="spacing">6</property>
+                    <child>
+                      <object class="GtkLabel" id="label203">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="xalign">0</property>
+                        <property name="label" translatable="yes">Compare across filesystems</property>
+                        <property name="use_markup">True</property>
+                        <attributes>
+                          <attribute name="weight" value="bold"/>
+                        </attributes>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">False</property>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkHBox" id="hbox1">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <child>
+                          <object class="GtkLabel" id="label4">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="xpad">12</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">False</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkVBox" id="vbox2">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="spacing">6</property>
+                            <child>
+                              <object class="GtkCheckButton" id="checkbutton_ignore_modes">
+                                <property name="label" translatable="yes">Ignore differences in file _modes</property>
+                                <property name="visible">True</property>
+                                <property name="can_focus">True</property>
+                                <property name="receives_default">False</property>
+                                <property name="use_action_appearance">False</property>
+                                <property name="use_underline">True</property>
+                                <property name="draw_indicator">True</property>
+                                <signal name="toggled" handler="on_checkbutton_ignore_modes_toggled" swapped="no"/>
+                              </object>
+                              <packing>
+                                <property name="expand">True</property>
+                                <property name="fill">True</property>
+                                <property name="position">0</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkCheckButton" id="checkbutton_limit_time_resolution">
+                                <property name="label" translatable="yes">_Limit timestamp resolution</property>
+                                <property name="visible">True</property>
+                                <property name="can_focus">True</property>
+                                <property name="receives_default">False</property>
+                                <property name="use_action_appearance">False</property>
+                                <property name="use_underline">True</property>
+                                <property name="draw_indicator">True</property>
+                                <signal name="toggled" handler="on_checkbutton_limit_time_resolution_toggled" swapped="no"/>
+                              </object>
+                              <packing>
+                                <property name="expand">True</property>
+                                <property name="fill">True</property>
+                                <property name="position">1</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkHBox" id="hbox2">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="spacing">6</property>
+                                <child>
+                                  <object class="GtkLabel" id="label106">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                    <property name="xpad">12</property>
+                                  </object>
+                                  <packing>
+                                    <property name="expand">False</property>
+                                    <property name="fill">False</property>
+                                    <property name="position">0</property>
+                                  </packing>
+                                </child>
+                                <child>
+                                  <object class="GtkLabel" id="label_resolution">
+                                    <property name="visible">True</property>
+                                    <property name="sensitive">False</property>
+                                    <property name="can_focus">False</property>
+                                    <property name="xalign">0</property>
+                                    <property name="label" translatable="yes">_Resolution:</property>
+                                    <property name="use_underline">True</property>
+                                    <property name="mnemonic_widget">custom_edit_command_entry</property>
+                                  </object>
+                                  <packing>
+                                    <property name="expand">False</property>
+                                    <property name="fill">True</property>
+                                    <property name="position">1</property>
+                                  </packing>
+                                </child>
+                                <child>
+                                  <object class="GtkSpinButton" id="spin_time_resolution">
+                                    <property name="visible">True</property>
+                                    <property name="sensitive">False</property>
+                                    <property name="can_focus">True</property>
+                                    <property name="width_chars">6</property>
+                                    <property name="adjustment">adjustment2</property>
+                                    <property name="digits">3</property>
+                                    <property name="numeric">True</property>
+                                    <property name="update_policy">if-valid</property>
+                                    <signal name="value-changed" handler="on_spin_time_resolution_value_changed" swapped="no"/>
+                                  </object>
+                                  <packing>
+                                    <property name="expand">False</property>
+                                    <property name="fill">True</property>
+                                    <property name="position">2</property>
+                                  </packing>
+                                </child>
+                                <child>
+                                  <object class="GtkLabel" id="label_seconds">
+                                    <property name="visible">True</property>
+                                    <property name="sensitive">False</property>
+                                    <property name="can_focus">False</property>
+                                    <property name="xalign">0</property>
+                                    <property name="label" translatable="yes">sec</property>
+                                  </object>
+                                  <packing>
+                                    <property name="expand">True</property>
+                                    <property name="fill">True</property>
+                                    <property name="position">3</property>
+                                  </packing>
+                                </child>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">False</property>
+                                <property name="position">2</property>
+                              </packing>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="expand">True</property>
+                            <property name="fill">True</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">True</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="position">4</property>
+              </packing>
+            </child>
+            <child type="tab">
+              <object class="GtkLabel" id="dir_diff_label">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="label" translatable="yes">Directory Comparison</property>
+              </object>
+              <packing>
+                <property name="position">5</property>
+                <property name="tab_fill">False</property>
+              </packing>
+            </child>
           </object>
           <packing>
             <property name="expand">True</property>
diff --git a/meld/dirdiff.py b/meld/dirdiff.py
index 3db55a5..eef2edf 100644
--- a/meld/dirdiff.py
+++ b/meld/dirdiff.py
@@ -49,6 +49,9 @@ gdk = gtk.gdk
 #
 ################################################################################
 
+def normalize_file_time(ftime, resolution):
+    return ftime if resolution <= 0 else (ftime // resolution) * resolution
+
 class StatItem(namedtuple('StatItem', 'mode size time')):
     __slots__ = ()
 
@@ -57,6 +60,22 @@ class StatItem(namedtuple('StatItem', 'mode size time')):
         return StatItem(stat.S_IFMT(stat_result.st_mode),
                         stat_result.st_size, stat_result.st_mtime)
 
+    def equal(self, other, options=None):
+        if options is None: options = dict()
+        
+        if self.size != other.size:
+            return False
+        if self.mode != other.mode and not options.get('ignore_mode_differences'):
+            return False
+        
+        mtime1 = self.time
+        mtime2 = other.time
+        if options.get('limit_time_resolution'):
+            mtime1 = normalize_file_time(mtime1, options.get('time_resolution'))
+            mtime2 = normalize_file_time(mtime2, options.get('time_resolution'))
+        
+        return mtime1 == mtime2
+
 
 CacheResult = namedtuple('CacheResult', 'stats result')
 
@@ -71,8 +90,7 @@ CHUNK_SIZE = 4096
 def all_same(lst):
     return not lst or lst.count(lst[0]) == len(lst)
 
-
-def _files_same(files, regexes):
+def _files_same(files, regexes, options=None):
     """Determine whether a list of files are the same.
 
     Possible results are:
@@ -82,7 +100,6 @@ def _files_same(files, regexes):
       DodgyDifferent: The files are superficially different
       FileError: There was a problem reading one or more of the files
     """
-
     # One file is the same as itself
     if len(files) < 2:
         return Same
@@ -103,6 +120,11 @@ def _files_same(files, regexes):
     if not regexes and not all_same([s.size for s in stats]):
         return Different
 
+    # Compare files superficially if the options tells us to
+    if options.get('superficial_compare'):
+        if all(s.equal(stats[0], options) for s in stats):
+            return DodgySame
+
     # Check the cache before doing the expensive comparison
     cache = _cache.get((files, regexes))
     if cache and cache.stats == stats:
@@ -254,10 +276,13 @@ class DirDiff(melddoc.MeldDoc, gnomeglade.Component):
         self.text_filters = []
         self.create_name_filters()
         self.create_text_filters()
+        self.options = app.prefs.get_dir_diff_options()
         self.app_handlers = [app.connect("file-filters-changed",
                                          self.on_file_filters_changed),
                              app.connect("text-filters-changed",
-                                         self.on_text_filters_changed)]
+                                         self.on_text_filters_changed),
+                             app.connect("dir-diff-options-changed",
+                                         self.on_dir_diff_options_changed)]
 
         for button in ("DirCompare", "DirCopyLeft", "DirCopyRight",
                        "DirDelete", "ShowSame",
@@ -421,6 +446,12 @@ class DirDiff(melddoc.MeldDoc, gnomeglade.Component):
         relevant_change = self.create_text_filters()
         if relevant_change:
             self.refresh()
+    
+    def on_dir_diff_options_changed(self, app):
+        new_options = app.prefs.get_dir_diff_options()
+        if self.options != new_options:
+            self.options = new_options
+            self.refresh()
 
     def create_text_filters(self):
         # In contrast to file filters, ordering of text filters can matter
@@ -1021,7 +1052,7 @@ class DirDiff(melddoc.MeldDoc, gnomeglade.Component):
             is_present = [ os.path.exists( f ) for f in curfiles ]
             all_present = 0 not in is_present
             if all_present:
-                if _files_same(curfiles, regexes) in (Same, SameFiltered):
+                if _files_same(curfiles, regexes, self.options) in (Same, SameFiltered):
                     state = tree.STATE_NORMAL
                 else:
                     state = tree.STATE_MODIFIED
@@ -1049,7 +1080,7 @@ class DirDiff(melddoc.MeldDoc, gnomeglade.Component):
             newest_index = -1 # all same
         all_present = 0 not in mod_times
         if all_present:
-            all_same = _files_same(files, regexes)
+            all_same = _files_same(files, regexes, self.options)
             all_present_same = all_same
         else:
             lof = []
@@ -1057,7 +1088,7 @@ class DirDiff(melddoc.MeldDoc, gnomeglade.Component):
                 if mod_times[j]:
                     lof.append( files[j] )
             all_same = Different
-            all_present_same = _files_same(lof, regexes)
+            all_present_same = _files_same(lof, regexes, self.options)
         different = 1
         one_isdir = [None for i in range(self.model.ntree)]
         for j in range(self.model.ntree):
diff --git a/meld/meldapp.py b/meld/meldapp.py
index 8974d07..038227e 100644
--- a/meld/meldapp.py
+++ b/meld/meldapp.py
@@ -40,6 +40,7 @@ class MeldApp(gobject.GObject):
     __gsignals__ = {
         'file-filters-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()),
         'text-filters-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()),
+        'dir-diff-options-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()),
     }
 
     def __init__(self):
@@ -58,7 +59,7 @@ class MeldApp(gobject.GObject):
     def create_window(self):
         self.window = meldwindow.MeldWindow()
         return self.window
-
+    
     def on_preference_changed(self, key, val):
         if key == "filters":
             self.file_filters = self._parse_filters(val,
@@ -68,6 +69,8 @@ class MeldApp(gobject.GObject):
             self.text_filters = self._parse_filters(val,
                                                     filters.FilterEntry.REGEX)
             self.emit('text-filters-changed')
+        elif key.startswith("dir_diff_"):
+            self.emit('dir-diff-options-changed')
 
     def _parse_filters(self, string, filt_type):
         filt = [filters.FilterEntry.parse(l, filt_type) for l
diff --git a/meld/preferences.py b/meld/preferences.py
index 4c61022..96b4cb0 100644
--- a/meld/preferences.py
+++ b/meld/preferences.py
@@ -19,7 +19,7 @@
 
 from gettext import gettext as _
 
-import gtk
+import copy, gtk
 
 from . import filters
 from . import misc
@@ -90,7 +90,7 @@ class PreferencesDialog(gnomeglade.Component):
 
     def __init__(self, parent, prefs):
         gnomeglade.Component.__init__(self, paths.ui_dir("preferences.ui"),
-                                      "preferencesdialog", ["adjustment1"])
+                                      "preferencesdialog", ["adjustment1","adjustment2"])
         self.widget.set_transient_for(parent)
         self.prefs = prefs
         if not self.prefs.use_custom_font:
@@ -144,8 +144,17 @@ class PreferencesDialog(gnomeglade.Component):
                                      filters.FilterEntry.REGEX)
         self.text_filters_tab.pack_start(self.textfilter.widget)
         self.checkbutton_ignore_blank_lines.set_active( self.prefs.ignore_blank_lines )
+        
         # encoding
         self.entry_text_codecs.set_text( self.prefs.text_codecs )
+        
+        # directory compare options   
+        self.checkbutton_superficial_compare.set_active(self.prefs.dir_diff_superficial_compare)
+        self.checkbutton_ignore_modes.set_active(self.prefs.dir_diff_ignore_mode_differences)
+        self.checkbutton_limit_time_resolution.set_active(self.prefs.dir_diff_limit_time_resolution)
+        self._enable_resolution_controls(self.prefs.dir_diff_limit_time_resolution)
+        self.spin_time_resolution.set_value(self.prefs.dir_diff_time_resolution)
+        
         self.widget.show()
 
     def on_fontpicker_font_set(self, picker):
@@ -203,6 +212,28 @@ class PreferencesDialog(gnomeglade.Component):
         # Called on "activate" and "focus-out-event"
         self.prefs.text_codecs = entry.props.text
 
+    #
+    # directory compare
+    #
+    def _enable_resolution_controls(self, enable):
+        self.label_resolution.set_sensitive(enable)
+        self.spin_time_resolution.set_sensitive(enable)
+        self.label_seconds.set_sensitive(enable)
+
+    def on_checkbutton_superficial_compare_toggled(self, button):
+        self.prefs.dir_diff_superficial_compare = button.get_active()
+
+    def on_checkbutton_ignore_modes_toggled(self, button):
+        self.prefs.dir_diff_ignore_mode_differences = button.get_active()
+
+    def on_checkbutton_limit_time_resolution_toggled(self, button):
+        self.prefs.dir_diff_limit_time_resolution = button.get_active()
+        self._enable_resolution_controls(button.get_active())
+        
+    def on_spin_time_resolution_value_changed(self, spin):
+        self.prefs.dir_diff_time_resolution = spin.get_value()
+
+    
     def on_response(self, dialog, response_id):
         self.widget.destroy()
 
@@ -254,6 +285,10 @@ class MeldPreferences(prefs.Preferences):
                                           ['normal', 'modified', 'new']),
         "vc_status_filters": prefs.Value(prefs.LIST,
                                          ['flatten', 'modified']),
+        "dir_diff_superficial_compare" : prefs.Value(prefs.BOOL, False),
+        "dir_diff_ignore_mode_differences" : prefs.Value(prefs.BOOL, False),
+        "dir_diff_limit_time_resolution":  prefs.Value(prefs.BOOL, False),
+        "dir_diff_time_resolution" : prefs.Value(prefs.FLOAT, 1.0),
     }
 
     def __init__(self):
@@ -304,3 +339,11 @@ class MeldPreferences(prefs.Preferences):
                 return argv
             else:
                 return [editor] + files
+    
+    def get_dir_diff_options(self):
+        options = dict()
+        for key in self._prefs:
+            if key.startswith('dir_diff_'):
+                options[key[len('dir_diff_'):]] = copy.copy(self.__getattr__(key))
+        
+        return options
-- 
1.7.10.4

