~ubuntu-branches/ubuntu/trusty/diffuse/trusty

« back to all changes in this revision

Viewing changes to src/usr/bin/diffuse

  • Committer: Bazaar Package Importer
  • Author(s): Philipp Huebner
  • Date: 2011-08-06 15:06:57 UTC
  • mfrom: (1.1.10 upstream)
  • Revision ID: james.westby@ubuntu.com-20110806150657-j30zn60xtm0cq51b
Tags: 0.4.5-1
* New upstream release (closes: #623991)
* Removed unnecessary use of python-support
* debian/copyright:
  - bumped up copyright years
  - updated maintainer's email address
* debian/rules:
  - added build-arch and build-indep targets
  - removed unnecessary calls of dh_pysupport, dh_makeshlibs and dh_shlibdeps
* Updated Standards-Version: 3.9.2 (no changes needed)
* Deleted deprecated README.Debian

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#!/usr/bin/env python
2
2
# -*- coding: utf-8 -*-
3
3
 
4
 
# Copyright (C) 2006-2010 Derrick Moser <derrick_moser@yahoo.com>
 
4
# Copyright (C) 2006-2011 Derrick Moser <derrick_moser@yahoo.com>
5
5
#
6
6
# This program is free software; you can redistribute it and/or modify it under
7
7
# the terms of the GNU General Public License as published by the Free Software
19
19
# (http://www.fsf.org/) or by writing to the Free Software Foundation, Inc.,
20
20
# 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
21
21
 
 
22
import codecs
22
23
import gettext
23
24
import locale
24
25
import os
29
30
if hasattr(sys, 'frozen'):
30
31
    app_path = sys.executable
31
32
else:
32
 
    app_path = sys.argv[0]
 
33
    app_path = os.path.realpath(sys.argv[0])
33
34
bin_dir = os.path.dirname(app_path)
34
35
 
35
36
# platform test
63
64
_ = gettext.gettext
64
65
 
65
66
APP_NAME = 'Diffuse'
66
 
VERSION = '0.4.4'
67
 
COPYRIGHT = _('Copyright © 2006-2010 Derrick Moser')
 
67
VERSION = '0.4.5'
 
68
COPYRIGHT = _('Copyright © 2006-2011 Derrick Moser')
68
69
WEBSITE = 'http://diffuse.sourceforge.net/'
69
70
 
 
71
# print a UTF-8 string using the host's native encoding
 
72
def printMessage(s):
 
73
    try:
 
74
        print codecs.encode(unicode(s, 'utf_8'), sys.getfilesystemencoding())
 
75
    except UnicodeEncodeError:
 
76
        pass
 
77
 
70
78
# process help options
71
79
if __name__ == '__main__':
72
80
    args = sys.argv
73
81
    argc = len(args)
74
82
    if argc == 2 and args[1] in [ '-v', '--version' ]:
75
 
        print '%s %s\n%s' % (APP_NAME, VERSION, COPYRIGHT)
 
83
        printMessage('%s %s\n%s' % (APP_NAME, VERSION, COPYRIGHT))
76
84
        sys.exit(0)
77
85
    if argc == 2 and args[1] in [ '-h', '-?', '--help' ]:
78
 
        print _("""Usage:
 
86
        printMessage(_('''Usage:
79
87
    diffuse [ [OPTION...] [FILE...] ]...
80
88
    diffuse ( -h | -? | --help | -v | --version )
81
89
 
97
105
  ( -c | --commit ) <rev>          File revisions <rev-1> and <rev>
98
106
  ( -D | --close-if-same )         Close all tabs with no differences
99
107
  ( -e | --encoding ) <codec>      Use <codec> to read and write files
 
108
  ( -L | --label ) <label>         Display <label> instead of the file name
100
109
  ( -m | --modified )              Create a new tab for each modified file
101
110
  ( -r | --revision ) <rev>        File revision <rev>
102
111
  ( -s | --separate )              Create a new tab for each file
103
112
  ( -t | --tab )                   Start a new tab
 
113
  --line <line>                    Start with line <line> selected
104
114
  --null-file                      Create a blank file comparison pane
105
115
 
106
116
Display Options:
108
118
  ( -B | --ignore-blank-lines )    Ignore changes in blank lines
109
119
  ( -E | --ignore-end-of-line )    Ignore end of line differences
110
120
  ( -i | --ignore-case )           Ignore case differences
111
 
  ( -w | --ignore-all-space )      Ignore white space differences""")
 
121
  ( -w | --ignore-all-space )      Ignore white space differences'''))
112
122
        sys.exit(0)
113
123
 
114
124
import pygtk
115
125
pygtk.require('2.0')
116
126
import gtk
117
127
 
118
 
import codecs
119
128
import difflib
120
129
import encodings
121
130
import glob
143
152
# associate our icon with all of our windows
144
153
if __name__ == '__main__':
145
154
    # handle platform specific icon
146
 
    if isWindows():
147
 
        iconpath = os.path.join(bin_dir, 'diffuse.ico')
148
 
    else:
 
155
    # icon will be provided by py2exe on Windows
 
156
    if not isWindows():
149
157
        iconpath = os.path.join(bin_dir, '../share/pixmaps/diffuse.png')
150
 
    if os.path.isfile(iconpath):
151
 
        gtk.window_set_default_icon(gtk.gdk.pixbuf_new_from_file(iconpath))
152
 
    else:
153
 
        # this can occur if we are run without a proper install
154
 
        # it is just cosmetic, don't bother the user
155
 
        logDebug('Missing icon %s' % (iconpath, ))
 
158
        if os.path.isfile(iconpath):
 
159
            gtk.window_set_default_icon(gtk.gdk.pixbuf_new_from_file(iconpath))
 
160
        else:
 
161
            # this can occur if we are run without a proper install
 
162
            # it is just cosmetic, don't bother the user
 
163
            logDebug('Missing icon %s' % (iconpath, ))
156
164
 
157
165
# convenience class for displaying a message dialogue
158
166
class MessageDialog(gtk.MessageDialog):
271
279
 
272
280
# also recognise old Mac OS line endings
273
281
def readlines(fd):
274
 
    return splitlines(fd.read())
 
282
    return strip_eols(splitlines(fd.read()))
275
283
 
276
284
# This class to hold all customisable behaviour not exposed in the preferences
277
285
# dialogue: hotkey assignment, colours, syntax highlighting, etc.
315
323
        set_binding('menu', 'previous_difference', 'Ctrl+Up')
316
324
        set_binding('menu', 'next_difference', 'Ctrl+Down')
317
325
        set_binding('menu', 'last_difference', 'Shift+Ctrl+Down')
 
326
        set_binding('menu', 'first_tab', 'Shift+Ctrl+Page_Up')
318
327
        set_binding('menu', 'previous_tab', 'Ctrl+Page_Up')
319
328
        set_binding('menu', 'next_tab', 'Ctrl+Page_Down')
 
329
        set_binding('menu', 'last_tab', 'Shift+Ctrl+Page_Down')
320
330
        set_binding('menu', 'shift_pane_right', 'Shift+Ctrl+parenright')
321
331
        set_binding('menu', 'shift_pane_left', 'Shift+Ctrl+parenleft')
322
332
        set_binding('menu', 'convert_to_upper_case', 'Ctrl+u')
419
429
            'line_number_background' : Colour(0.75, 0.75, 0.75),
420
430
            'line_selection' : Colour(0.7, 0.7, 1.0),
421
431
            'map_background' : Colour(0.6, 0.6, 0.6),
 
432
            'margin' : Colour(0.8, 0.8, 0.8),
422
433
            'edited' : Colour(0.5, 1.0, 0.5),
423
434
            'preedit' : Colour(0.0, 0.0, 0.0),
424
435
            'text' : Colour(0.0, 0.0, 0.0),
686
697
 
687
698
theResources = Resources()
688
699
 
689
 
def decodefilename(s):
 
700
# convert a string obtained from the file system or command line to unicode
 
701
def decode_fs_string(s):
690
702
    if s is not None:
691
703
        if isWindows():
692
704
            s = unicode(s, 'utf_8')
693
705
        else:
694
706
            s = unicode(s, sys.getfilesystemencoding())
695
707
    return s
696
 
 
 
708
 
 
709
# map an encoding name to its standard form
 
710
def norm_encoding(e):
 
711
    if e is not None:
 
712
        return e.replace('-', '_').lower()
 
713
 
 
714
# widget to help pick an encoding
 
715
class EncodingMenu(gtk.HBox):
 
716
    def __init__(self, prefs, autodetect=False):
 
717
        gtk.HBox.__init__(self)
 
718
        self.combobox = combobox = gtk.combo_box_new_text()
 
719
        self.encodings = prefs.getEncodings()[:]
 
720
        for e in self.encodings:
 
721
            combobox.append_text(e)
 
722
        if autodetect:
 
723
            self.encodings.insert(0, None)
 
724
            combobox.prepend_text(_('Auto Detect'))
 
725
        self.pack_start(combobox, False, False, 0)
 
726
        combobox.show()
 
727
 
 
728
    def set_text(self, encoding):
 
729
        encoding = norm_encoding(encoding)
 
730
        if encoding in self.encodings:
 
731
            self.combobox.set_active(self.encodings.index(encoding))
 
732
 
 
733
    def get_text(self):
 
734
        i = self.combobox.get_active()
 
735
        if i >= 0:
 
736
            return self.encodings[i]
 
737
 
697
738
# text entry widget with a button to help pick file names
698
739
class FileEntry(gtk.HBox):
699
740
    def __init__(self, parent, title):
752
793
        else:
753
794
            svk_bin = 'svk'
754
795
 
 
796
        auto_detect_codecs = [ 'utf_8', 'latin_1' ]
 
797
        e = norm_encoding(sys.getfilesystemencoding())
 
798
        if e not in auto_detect_codecs:
 
799
            # insert after UTF-8 as the default encoding may prevent UTF-8 from
 
800
            # being tried
 
801
            auto_detect_codecs.insert(1, e)
 
802
 
755
803
        # self.template describes how preference dialogue layout
756
804
        #
757
805
        # this will be traversed later to build the preferences dialogue and
773
821
            [ 'List',
774
822
              [ 'Font', 'display_font', 'Monospace 10', _('Font') ],
775
823
              [ 'Integer', 'display_tab_width', 8, _('Tab width'), 1, 1024 ],
 
824
              [ 'Boolean', 'display_show_right_margin', False, _('Show right margin') ],
 
825
              [ 'Integer', 'display_right_margin', 80, _('Right margin'), 1, 8192 ],
776
826
              [ 'Boolean', 'display_show_line_numbers', True, _('Show line numbers') ],
777
827
              [ 'Boolean', 'display_show_whitespace', False, _('Show white space characters') ],
 
828
              [ 'Boolean', 'display_ignore_case', False, _('Ignore case differences') ],
778
829
              [ 'Boolean', 'display_ignore_whitespace', False, _('Ignore white space differences') ],
779
830
              [ 'Boolean', 'display_ignore_whitespace_changes', False, _('Ignore changes to white space') ],
780
831
              [ 'Boolean', 'display_ignore_blanklines', False, _('Ignore blank line differences') ],
781
 
              [ 'Boolean', 'display_ignore_case', False, _('Ignore case differences') ],
782
832
              [ 'Boolean', 'display_ignore_endofline', False, _('Ignore end of line differences') ]
783
833
            ],
784
834
            _('Alignment'),
785
835
            [ 'List',
 
836
              [ 'Boolean', 'align_ignore_case', False, _('Ignore case') ],
786
837
              [ 'Boolean', 'align_ignore_whitespace', True, _('Ignore white space') ],
787
838
              [ 'Boolean', 'align_ignore_whitespace_changes', False, _('Ignore changes to white space') ],
788
839
              [ 'Boolean', 'align_ignore_blanklines', False, _('Ignore blank lines') ],
789
 
              [ 'Boolean', 'align_ignore_endofline', True, _('Ignore end of line characters') ],
790
 
              [ 'Boolean', 'align_ignore_case', False, _('Ignore case') ]
 
840
              [ 'Boolean', 'align_ignore_endofline', True, _('Ignore end of line characters') ]
791
841
            ],
792
842
            _('Editor'),
793
843
            [ 'List',
794
 
              [ 'Integer', 'editor_soft_tab_width', 8, _('Soft tab width'), 1, 1024 ],
 
844
              [ 'Boolean', 'editor_auto_indent', False, _('Auto indent') ],
795
845
              [ 'Boolean', 'editor_expand_tabs', False, _('Expand tabs to spaces') ],
796
 
              [ 'Boolean', 'editor_auto_indent', False, _('Auto indent') ]
 
846
              [ 'Integer', 'editor_soft_tab_width', 8, _('Soft tab width'), 1, 1024 ]
797
847
            ],
798
848
            _('Tabs'),
799
849
            [ 'List',
803
853
            ],
804
854
            _('Regional Settings'),
805
855
            [ 'List',
806
 
              [ 'String', 'encoding_auto_detect_codecs', ' '.join([sys.getfilesystemencoding(), 'latin_1']), _('Order of codecs used to identify encoding') ]
 
856
              [ 'Encoding', 'encoding_default_codec', sys.getfilesystemencoding(), _('Default codec') ],
 
857
              [ 'String', 'encoding_auto_detect_codecs', ' '.join(auto_detect_codecs), _('Order of codecs used to identify encoding') ]
807
858
            ],
808
859
        ]
 
860
        # conditions used to determine if a preference should be greyed out
 
861
        self.disable_when = {
 
862
            'display_right_margin': ('display_show_right_margin', False),
 
863
            'display_ignore_whitespace_changes': ('display_ignore_whitespace', True),
 
864
            'display_ignore_blanklines': ('display_ignore_whitespace', True),
 
865
            'display_ignore_endofline': ('display_ignore_whitespace', True),
 
866
            'align_ignore_whitespace_changes': ('align_ignore_whitespace', True),
 
867
            'align_ignore_blanklines': ('align_ignore_whitespace', True),
 
868
            'align_ignore_endofline': ('align_ignore_whitespace', True)
 
869
        }
809
870
        if isWindows():
810
871
            root = os.environ.get('SYSTEMDRIVE', None)
811
872
            if root is None:
849
910
 
850
911
        self.template.extend([ _('Version Control'), vcs_template ])
851
912
        self._initFromTemplate(self.template)
 
913
        self.default_bool_prefs = self.bool_prefs.copy()
 
914
        self.default_int_prefs = self.int_prefs.copy()
 
915
        self.default_string_prefs = self.string_prefs.copy()
852
916
        # load the user's preferences
853
917
        self.path = path
854
918
        if os.path.isfile(self.path):
860
924
                    try:
861
925
                        a = shlex.split(s, True)
862
926
                        if len(a) > 0:
863
 
                            if len(a) == 2 and self.bool_prefs.has_key(a[0]):
864
 
                                self.bool_prefs[a[0]] = (a[1] == 'True')
865
 
                            elif len(a) == 2 and self.int_prefs.has_key(a[0]):
866
 
                                self.int_prefs[a[0]] = max(self.int_prefs_min[a[0]], min(int(a[1]), self.int_prefs_max[a[0]]))
867
 
                            elif len(a) == 2 and self.string_prefs.has_key(a[0]):
868
 
                                self.string_prefs[a[0]] = a[1]
 
927
                            p = a[0]
 
928
                            if len(a) == 2 and self.bool_prefs.has_key(p):
 
929
                                self.bool_prefs[p] = (a[1] == 'True')
 
930
                            elif len(a) == 2 and self.int_prefs.has_key(p):
 
931
                                self.int_prefs[p] = max(self.int_prefs_min[p], min(int(a[1]), self.int_prefs_max[p]))
 
932
                            elif len(a) == 2 and self.string_prefs.has_key(p):
 
933
                                self.string_prefs[p] = a[1]
869
934
                            else:
870
935
                                raise ValueError()
871
936
                    except ValueError:
893
958
            self.int_prefs[template[1]] = template[2]
894
959
            self.int_prefs_min[template[1]] = template[4]
895
960
            self.int_prefs_max[template[1]] = template[5]
896
 
        elif template[0] in [ 'String', 'File', 'Font' ]:
 
961
        elif template[0] in [ 'String', 'File', 'Font', 'Encoding' ]:
897
962
            self.string_prefs[template[1]] = template[2]
898
963
 
 
964
    # callback used when a preference is toggled
 
965
    def _toggled_cb(self, widget, widgets, name):
 
966
        # disable any preferences than are no longer relevant
 
967
        for k, v in self.disable_when.items():
 
968
            p, t = v
 
969
            if p == name:
 
970
                widgets[k].set_sensitive(widgets[p].get_active() != t)
 
971
 
899
972
    # display the dialogue and update the preference values if the accept
900
973
    # button was pressed
901
974
    def runDialog(self, parent):
903
976
 
904
977
        widgets = {}
905
978
        w = self._buildPrefsDialog(parent, widgets, self.template)
 
979
        # disable any preferences than are not relevant
 
980
        for k, v in self.disable_when.items():
 
981
            p, t = v
 
982
            if widgets[p].get_active() == t:
 
983
                widgets[k].set_sensitive(False)
906
984
        dialog.vbox.add(w)
907
985
        w.show()
908
986
 
909
987
        accept = (dialog.run() == gtk.RESPONSE_OK)
910
988
        if accept:
911
 
            try:
912
 
                ss = []
913
 
                for k in self.bool_prefs.keys():
914
 
                    ss.append('%s %s\n' % (k, widgets[k].get_active()))
915
 
                for k in self.int_prefs.keys():
916
 
                    ss.append('%s %s\n' % (k, widgets[k].get_value_as_int()))
917
 
                for k in self.string_prefs.keys():
918
 
                    ss.append('%s "%s"\n' % (k, widgets[k].get_text().replace('\\', '\\\\').replace('"', '\\"')))
919
 
                ss.sort()
920
 
                f = open(self.path, 'w')
921
 
                f.write('# This prefs file was generated by %s %s.\n\n' % (APP_NAME, VERSION))
922
 
                for s in ss:
923
 
                    f.write(s)
924
 
                f.close()
925
 
            except IOError:
926
 
                m = MessageDialog(parent, gtk.MESSAGE_ERROR, _('Error writing %s.') % (self.path, ))
927
 
                m.run()
928
 
                m.destroy()
929
989
            for k in self.bool_prefs.keys():
930
990
                self.bool_prefs[k] = widgets[k].get_active()
931
991
            for k in self.int_prefs.keys():
932
992
                self.int_prefs[k] = widgets[k].get_value_as_int()
933
993
            for k in self.string_prefs.keys():
934
994
                self.string_prefs[k] = widgets[k].get_text()
 
995
            try:
 
996
                ss = []
 
997
                for k, v in self.bool_prefs.items():
 
998
                    if v != self.default_bool_prefs[k]:
 
999
                        ss.append('%s %s\n' % (k, v))
 
1000
                for k, v in self.int_prefs.items():
 
1001
                    if v != self.default_int_prefs[k]:
 
1002
                        ss.append('%s %s\n' % (k, v))
 
1003
                for k, v in self.string_prefs.items():
 
1004
                    if v != self.default_string_prefs[k]:
 
1005
                        ss.append('%s "%s"\n' % (k, v.replace('\\', '\\\\').replace('"', '\\"')))
 
1006
                ss.sort()
 
1007
                f = open(self.path, 'w')
 
1008
                f.write('# This prefs file was generated by %s %s.\n\n' % (APP_NAME, VERSION))
 
1009
                for s in ss:
 
1010
                    f.write(s)
 
1011
                f.close()
 
1012
            except IOError:
 
1013
                m = MessageDialog(parent, gtk.MESSAGE_ERROR, _('Error writing %s.') % (self.path, ))
 
1014
                m.run()
 
1015
                m.destroy()
935
1016
        dialog.destroy()
936
1017
        return accept
937
1018
 
968
1049
                    button.set_active(self.bool_prefs[tpl[1]])
969
1050
                    widgets[tpl[1]] = button
970
1051
                    table.attach(button, 1, 2, i, i + 1, gtk.FILL, gtk.FILL)
 
1052
                    button.connect('toggled', self._toggled_cb, widgets, tpl[1])
971
1053
                    button.show()
972
1054
                else:
973
1055
                    label = gtk.Label(tpl[3] + ': ')
983
1065
                            button = gtk.SpinButton()
984
1066
                            button.set_range(tpl[4], tpl[5])
985
1067
                            button.set_value(self.int_prefs[tpl[1]])
986
 
                            button.set_increments(1, 1)
987
1068
                        widgets[tpl[1]] = button
988
1069
                        entry.pack_start(button, False, False, 0)
989
1070
                        button.show()
990
1071
                    else:
991
 
                        if tpl[0] == 'File':
 
1072
                        if tpl[0] == 'Encoding':
 
1073
                            entry = EncodingMenu(self)
 
1074
                            entry.set_text(tpl[3])
 
1075
                        elif tpl[0] == 'File':
992
1076
                            entry = FileEntry(parent, tpl[3])
993
1077
                        else:
994
1078
                            entry = gtk.Entry()
1019
1103
        return self.string_prefs['encoding_auto_detect_codecs'].split()
1020
1104
 
1021
1105
    def getDefaultEncoding(self):
1022
 
        encodings = self._getDefaultEncodings()
1023
 
        if len(encodings) > 0:
1024
 
            return encodings[0]
1025
 
        return 'utf_8'
 
1106
        return self.string_prefs['encoding_default_codec']
1026
1107
 
1027
1108
    # attempt to convert a string to unicode from an unknown encoding
1028
 
    def convertToUnicode(self, ss):
 
1109
    def convertToUnicode(self, s):
1029
1110
        for encoding in self._getDefaultEncodings():
1030
1111
            try:
1031
 
                result = []
1032
 
                for s in ss:
1033
 
                    if s is not None:
1034
 
                        s = unicode(s, encoding)
1035
 
                    result.append(s)
1036
 
                return result, encoding
 
1112
                return unicode(s, encoding), encoding
1037
1113
            except (UnicodeDecodeError, LookupError):
1038
1114
                pass
1039
 
        result = []
1040
 
        for s in ss:
1041
 
            if s is not None:
1042
 
                s = ''.join([unichr(ord(c)) for c in s])
1043
 
            result.append(s)
1044
 
        return result, None
 
1115
        return u''.join([ unichr(ord(c)) for c in s ]), None
1045
1116
 
1046
1117
    # cygwin and native applications can be used on windows, use this method
1047
1118
    # to convert a path to the usual form expected on sys.platform
1048
1119
    def convertToNativePath(self, s):
1049
 
        s = unicode(s, sys.getfilesystemencoding())
 
1120
        s = unicode(s, sys.getfilesystemencoding())
1050
1121
        if isWindows() and s.find('/') >= 0:
1051
1122
            # treat as a cygwin path
1052
1123
            s = s.replace(os.sep, '/')
1260
1331
    return "'" + s.replace("'", "'\\''") + "'"
1261
1332
 
1262
1333
# use popen to read the output of a command
1263
 
def popenReadLines(dn, cmd, prefs, bash_pref, success_results=None):
 
1334
def popenRead(dn, cmd, prefs, bash_pref, success_results=None):
1264
1335
    if success_results is None:
1265
1336
        success_results = [ 0 ]
1266
1337
    if isWindows() and prefs.getBool(bash_pref):
1279
1350
    proc.stderr.close()
1280
1351
    fd = proc.stdout
1281
1352
    # read the command's output
1282
 
    ss = readlines(fd)
 
1353
    s = fd.read()
1283
1354
    fd.close()
1284
1355
    if proc.wait() not in success_results:
1285
1356
        raise IOError('Command failed.')
1286
 
    return ss
 
1357
    return s
 
1358
 
 
1359
# use popen to read the output of a command
 
1360
def popenReadLines(dn, cmd, prefs, bash_pref, success_results=None):
 
1361
    return strip_eols(splitlines(popenRead(dn, cmd, prefs, bash_pref, success_results)))
1287
1362
 
1288
1363
# simulate use of popen with xargs to read the output of a command
1289
 
def popenXArgsReadLines(dn, cmd, args, prefs, bash_pref, success_results=None):
 
1364
def popenXArgsReadLines(dn, cmd, args, prefs, bash_pref):
1290
1365
    # os.sysconf() is only available on Unix
1291
1366
    if hasattr(os, 'sysconf'):
1292
1367
        maxsize = os.sysconf('SC_ARG_MAX')
1311
1386
            s += len(args[i]) + 1
1312
1387
            i += 1
1313
1388
        if i == len(args) or not f:
1314
 
            ss.extend(popenReadLines(dn, a, prefs, bash_pref, success_results))
 
1389
            ss.extend(popenReadLines(dn, a, prefs, bash_pref))
1315
1390
            s, a = 0, []
1316
1391
    return ss
1317
1392
 
1369
1444
            return [ (name, prefs.getString('bzr_default_revision')), (name, None) ]
1370
1445
 
1371
1446
        def getFolderTemplate(self, prefs, names):
 
1447
            fs = _VcsFolderSet(names)
1372
1448
            result = []
1373
1449
            pwd, isabs = os.path.abspath(os.curdir), False
1374
1450
            args = [ prefs.getString('bzr_bin'), 'status', '-SV' ]
1378
1454
            # build list of interesting files
1379
1455
            # files will not appear in more than one set
1380
1456
            removed, modified, added, renamed = {}, {}, {}, {}
1381
 
            for s in strip_eols(popenReadLines(self.root, args, prefs, 'bzr_bash')):
 
1457
            for s in popenReadLines(self.root, args, prefs, 'bzr_bash'):
1382
1458
                if len(s) > 4:
1383
1459
                    k = s[4:]
1384
1460
                    if s[1] == 'D':
1386
1462
                        k = prefs.convertToNativePath(k)
1387
1463
                        if not k.endswith(os.sep):
1388
1464
                            k = os.path.join(self.root, k)
1389
 
                            if not isabs:
1390
 
                                k = relpath(pwd, k)
1391
 
                            removed[k] = [ (k, prefs.getString('bzr_default_revision')), (None, None) ]
 
1465
                            if fs.contains(k):
 
1466
                                if not isabs:
 
1467
                                    k = relpath(pwd, k)
 
1468
                                removed[k] = [ (k, prefs.getString('bzr_default_revision')), (None, None) ]
1392
1469
                    elif s[1] == 'N':
1393
1470
                        # new file
1394
1471
                        k = prefs.convertToNativePath(k)
1395
1472
                        if not k.endswith(os.sep):
1396
1473
                            k = os.path.join(self.root, k)
1397
 
                            if not isabs:
1398
 
                                k = relpath(pwd, k)
1399
 
                            added[k] = [ (None, None), (k, None) ]
 
1474
                            if fs.contains(k):
 
1475
                                if not isabs:
 
1476
                                    k = relpath(pwd, k)
 
1477
                                added[k] = [ (None, None), (k, None) ]
1400
1478
                    elif s[1] == 'M':
1401
1479
                        # modified file or merge conflict
1402
1480
                        k = prefs.convertToNativePath(k)
1403
1481
                        if not k.endswith(os.sep):
1404
1482
                            k = os.path.join(self.root, k)
1405
 
                            if not isabs:
1406
 
                                k = relpath(pwd, k)
1407
 
                            modified[k] = self.getFileTemplate(prefs, k)
 
1483
                            if fs.contains(k):
 
1484
                                if not isabs:
 
1485
                                    k = relpath(pwd, k)
 
1486
                                modified[k] = self.getFileTemplate(prefs, k)
1408
1487
                    elif s[0] == 'R':
1409
1488
                        # renamed file
1410
1489
                        k = k.split(' => ')
1414
1493
                            if not k0.endswith(os.sep) and not k1.endswith(os.sep):
1415
1494
                                k0 = os.path.join(self.root, k0)
1416
1495
                                k1 = os.path.join(self.root, k1)
1417
 
                                if not isabs:
1418
 
                                    k0 = relpath(pwd, k0)
1419
 
                                    k1 = relpath(pwd, k1)
1420
 
                                renamed[k1] = [ (k0, prefs.getString('bzr_default_revision')), (k1, None) ]
 
1496
                                if fs.contains(k0) or fs.contains(k1):
 
1497
                                    if not isabs:
 
1498
                                        k0 = relpath(pwd, k0)
 
1499
                                        k1 = relpath(pwd, k1)
 
1500
                                    renamed[k1] = [ (k0, prefs.getString('bzr_default_revision')), (k1, None) ]
1421
1501
            # sort the results
1422
1502
            r = set()
1423
1503
            for m in removed, modified, added, renamed:
1429
1509
            return result
1430
1510
 
1431
1511
        def getRevision(self, prefs, name, rev):
1432
 
            return popenReadLines(self.root, [ prefs.getString('bzr_bin'), 'cat', '--name-from-revision', '-r', rev, safeRelativePath(self.root, name, prefs, 'bzr_cygwin') ], prefs, 'bzr_bash')
 
1512
            return popenRead(self.root, [ prefs.getString('bzr_bin'), 'cat', '--name-from-revision', '-r', rev, safeRelativePath(self.root, name, prefs, 'bzr_cygwin') ], prefs, 'bzr_bash')
1433
1513
 
1434
1514
    # CVS support
1435
1515
    class Cvs:
1440
1520
            return [ (name, prefs.getString('cvs_default_revision')), (name, None) ]
1441
1521
 
1442
1522
        def getFolderTemplate(self, prefs, names):
 
1523
            fs = _VcsFolderSet(names)
1443
1524
            result = []
1444
1525
            pwd, isabs = os.path.abspath(os.curdir), False
1445
1526
            r = {}
1448
1529
                isabs |= os.path.isabs(name)
1449
1530
                args.append(safeRelativePath(self.root, name, prefs, 'cvs_cygwin'))
1450
1531
            # build list of interesting files
1451
 
            for s in strip_eols(popenReadLines(self.root, args, prefs, 'cvs_bash')):
 
1532
            for s in popenReadLines(self.root, args, prefs, 'cvs_bash'):
1452
1533
                if len(s) > 2 and s[0] in 'ACMR':
1453
1534
                    k = os.path.join(self.root, prefs.convertToNativePath(s[2:]))
1454
 
                    if not isabs:
1455
 
                        k = relpath(pwd, k)
1456
 
                    r[k] = s[0]
 
1535
                    if fs.contains(k):
 
1536
                        if not isabs:
 
1537
                            k = relpath(pwd, k)
 
1538
                        r[k] = s[0]
1457
1539
            # sort the results
1458
1540
            for k in sorted(r.keys()):
1459
1541
                v = r[k]
1469
1551
            return result
1470
1552
 
1471
1553
        def getRevision(self, prefs, name, rev):
1472
 
            return popenReadLines(self.root, [ prefs.getString('cvs_bin'), '-Q', 'update', '-p', '-r', rev, safeRelativePath(self.root, name, prefs, 'cvs_cygwin') ], prefs, 'cvs_bash')
 
1554
            if rev == 'BASE' and not os.path.exists(name):
 
1555
                # find revision for removed files
 
1556
                for s in popenReadLines(self.root, [ prefs.getString('cvs_bin'), 'status', safeRelativePath(self.root, name, prefs, 'cvs_cygwin') ], prefs, 'cvs_bash'):
 
1557
                    if s.startswith('   Working revision:\t-'):
 
1558
                        rev = s.split('\t')[1][1:]
 
1559
            return popenRead(self.root, [ prefs.getString('cvs_bin'), '-Q', 'update', '-p', '-r', rev, safeRelativePath(self.root, name, prefs, 'cvs_cygwin') ], prefs, 'cvs_bash')
1473
1560
 
1474
1561
    # Darcs support
1475
1562
    class Darcs:
1481
1568
            return [ (name, prefs.getString('darcs_default_revision')), (name, None) ]
1482
1569
 
1483
1570
        def getFolderTemplate(self, prefs, names):
 
1571
            fs = _VcsFolderSet(names)
1484
1572
            result = []
1485
1573
            pwd, isabs = os.path.abspath(os.curdir), False
1486
1574
            r = {}
1487
 
            args = [ prefs.getString('darcs_bin'), 'whatsnew', '-l' ]
 
1575
            args = [ prefs.getString('darcs_bin'), 'whatsnew', '-s' ]
1488
1576
            for name in names:
1489
1577
                isabs |= os.path.isabs(name)
1490
1578
                args.append(safeRelativePath(self.root, name, prefs, 'darcs_cygwin'))
1491
1579
            # build list of interesting files
1492
 
            for s in strip_eols(popenReadLines(self.root, args, prefs, 'darcs_bash')):
 
1580
            for s in popenReadLines(self.root, args, prefs, 'darcs_bash'):
1493
1581
                p = s.split(' ')
1494
1582
                if len(p) >= 2 and s[0] in 'AMR':
1495
1583
                    k = prefs.convertToNativePath(p[1])
1496
1584
                    if not k.endswith(os.sep):
1497
1585
                        k = os.path.join(self.root, k)
1498
 
                        if not isabs:
1499
 
                            k = relpath(pwd, k)
1500
 
                        r[k] = s[0]
 
1586
                        if fs.contains(k):
 
1587
                            if not isabs:
 
1588
                                k = relpath(pwd, k)
 
1589
                            r[k] = s[0]
1501
1590
            # sort the results
1502
1591
            for k in sorted(r.keys()):
1503
1592
                v = r[k]
1513
1602
            return result
1514
1603
 
1515
1604
        def getRevision(self, prefs, name, rev):
1516
 
            return popenReadLines(self.root, [ prefs.getString('darcs_bin'), 'show', 'contents', '-p', rev, safeRelativePath(self.root, name, prefs, 'darcs_cygwin') ], prefs, 'darcs_bash')
 
1605
            return popenRead(self.root, [ prefs.getString('darcs_bin'), 'show', 'contents', '-p', rev, safeRelativePath(self.root, name, prefs, 'darcs_cygwin') ], prefs, 'darcs_bash')
1517
1606
 
1518
1607
    # Git support
1519
1608
    class Git:
1527
1616
            return os.path.join(self.root, prefs.convertToNativePath(s.strip()))
1528
1617
 
1529
1618
        def getFolderTemplate(self, prefs, names):
1530
 
            fs = _VcsFolderSet(names)
1531
 
            result = []
 
1619
            # build command
 
1620
            args = [ prefs.getString('git_bin'), 'status', '--porcelain', '-s', '--untracked-files=no' ]
 
1621
            # build list of interesting files
1532
1622
            pwd, isabs = os.path.abspath(os.curdir), False
1533
 
            args = [ prefs.getString('git_bin'), 'status' ]
1534
1623
            for name in names:
1535
1624
                isabs |= os.path.isabs(name)
 
1625
            # run command
 
1626
            prev = prefs.getString('git_default_revision')
 
1627
            fs = _VcsFolderSet(names)
 
1628
            added, modified, removed, renamed = {}, {}, {}, {}
1536
1629
            # 'git status' will return 1 when a commit would fail
1537
 
            ss = strip_eols(popenReadLines(self.root, args, prefs, 'git_bash', [0, 1]))
1538
 
            # build list of interesting files
1539
 
            # files may appear in more than one set
1540
 
            i = 0
1541
 
            unmerged, removed, added, modified, renamed = {}, {}, {}, {}, {}
1542
 
            while i < len(ss):
1543
 
                s = ss[i]
1544
 
                i += 1
1545
 
                if s.startswith('# Untracked files:'):
1546
 
                    break
1547
 
                elif s.startswith('#\tunmerged:'):
 
1630
            for s in popenReadLines(self.root, args, prefs, 'git_bash', [0, 1]):
 
1631
                # parse response
 
1632
                if len(s) < 3:
 
1633
                    continue
 
1634
                x, y, k = s[0], s[1], s[2:]
 
1635
                if (y == 'D' and x not in 'UD') or (x == 'D' and y == ' '):
 
1636
                    # removed
 
1637
                    k = self._extractPath(k, prefs)
 
1638
                    if fs.contains(k):
 
1639
                        if not isabs:
 
1640
                            k = relpath(pwd, k)
 
1641
                        removed[k] = [ (k, prev), (None, None) ]
 
1642
                elif x == 'A' and y not in 'UA':
 
1643
                    # added
 
1644
                    k = self._extractPath(k, prefs)
 
1645
                    if fs.contains(k):
 
1646
                        if not isabs:
 
1647
                            k = relpath(pwd, k)
 
1648
                        added[k] = [ (None, None), (k, None) ]
 
1649
                elif x == 'R':
 
1650
                    # renamed
 
1651
                    k = k.split(' -> ')
 
1652
                    if len(k) == 2:
 
1653
                        k0 = self._extractPath(k[0], prefs)
 
1654
                        k1 = self._extractPath(k[1], prefs)
 
1655
                        if fs.contains(k0) or fs.contains(k1):
 
1656
                            if not isabs:
 
1657
                                k0 = relpath(pwd, k0)
 
1658
                                k1 = relpath(pwd, k1)
 
1659
                            renamed[k1] = [ (k0, prev), (k1, None) ]
 
1660
                elif x in 'CM' or y == 'M':
 
1661
                    # modified
 
1662
                    k = self._extractPath(k, prefs)
 
1663
                    if fs.contains(k):
 
1664
                        if not isabs:
 
1665
                            k = relpath(pwd, k)
 
1666
                        modified[k] = [ (k, prev), (k, None) ]
 
1667
                else:
1548
1668
                    # merge conflict
1549
 
                    k = self._extractPath(s[11:], prefs)
1550
 
                    if fs.contains(k):
1551
 
                        if not isabs:
1552
 
                            k = relpath(pwd, k)
1553
 
                        unmerged[k] = [ (k, prefs.getString('git_default_revision')), (k, None), (k, 'MERGE_HEAD') ]
1554
 
                elif s.startswith('#\tdeleted:'):
1555
 
                    # deleted file
1556
 
                    k = self._extractPath(s[10:], prefs)
1557
 
                    if fs.contains(k):
1558
 
                        if not isabs:
1559
 
                            k = relpath(pwd, k)
1560
 
                        removed[k] = [ (k, prefs.getString('git_default_revision')), (None, None) ]
1561
 
                elif s.startswith('#\tnew file:'):
1562
 
                    # new file
1563
 
                    k = self._extractPath(s[11:], prefs)
1564
 
                    if fs.contains(k):
1565
 
                        if not isabs:
1566
 
                            k = relpath(pwd, k)
1567
 
                        added[k] = [ (None, None), (k, None) ]
1568
 
                elif s.startswith('#\tmodified:'):
1569
 
                    # modified file
1570
 
                    k = self._extractPath(s[11:], prefs)
1571
 
                    if fs.contains(k):
1572
 
                        if not isabs:
1573
 
                            k = relpath(pwd, k)
1574
 
                        modified[k] = self.getFileTemplate(prefs, k)
1575
 
                elif s.startswith('#\tboth modified:'):
1576
 
                    # stash conflict
1577
 
                    k = self._extractPath(s[16:], prefs)
1578
 
                    if fs.contains(k):
1579
 
                        if not isabs:
1580
 
                            k = relpath(pwd, k)
1581
 
                        unmerged[k] = [ (k, prefs.getString('git_default_revision')), (k, None), (k, ':3') ]
1582
 
                elif s.startswith('#\trenamed:'):
1583
 
                    # renamed file
1584
 
                    keys = s[10:].split(' -> ')
1585
 
                    if len(keys) == 2:
1586
 
                        k0 = self._extractPath(keys[0], prefs)
1587
 
                        k1 = self._extractPath(keys[1], prefs)
1588
 
                        if fs.contains(k0) or fs.contains(k1):
1589
 
                            if not isabs:
1590
 
                                k0 = relpath(pwd, k0)
1591
 
                                k1 = relpath(pwd, k1)
1592
 
                            renamed[k1] = [ (k0, prefs.getString('git_default_revision')), (k1, None) ]
 
1669
                    k = self._extractPath(k, prefs)
 
1670
                    if fs.contains(k):
 
1671
                        if not isabs:
 
1672
                            k = relpath(pwd, k)
 
1673
                        if x == 'D':
 
1674
                            panes = [ (None, None) ]
 
1675
                        else:
 
1676
                            panes = [ (k, ':2') ]
 
1677
                        panes.append((k, None))
 
1678
                        if y == 'D':
 
1679
                            panes.append((None, None))
 
1680
                        else:
 
1681
                            panes.append((k, ':3'))
 
1682
                        if x != 'A' and y != 'A':
 
1683
                            panes.append((k, ':1'))
 
1684
                        modified[k] = panes
1593
1685
            # sort the results
1594
 
            r = set()
1595
 
            for m in unmerged, removed, added, modified, renamed:
 
1686
            result, r = [], set()
 
1687
            for m in added, modified, removed, renamed:
1596
1688
                r.update(m.keys())
1597
1689
            for k in sorted(r):
1598
 
                for m in unmerged, removed, added, modified, renamed:
 
1690
                for m in removed, added, modified, renamed:
1599
1691
                    if m.has_key(k):
1600
1692
                        result.append(m[k])
1601
 
                        # break so we open no more than one tab per file
1602
 
                        break
1603
1693
            return result
1604
1694
 
1605
1695
        def getRevision(self, prefs, name, rev):
1606
 
            return popenReadLines(self.root, [ prefs.getString('git_bin'), 'show', '%s:%s' % (rev, relpath(self.root, os.path.abspath(name)).replace(os.sep, '/')) ], prefs, 'git_bash')
 
1696
            return popenRead(self.root, [ prefs.getString('git_bin'), 'show', '%s:%s' % (rev, relpath(self.root, os.path.abspath(name)).replace(os.sep, '/')) ], prefs, 'git_bash')
1607
1697
 
1608
1698
    # Mercurial support
1609
1699
    class Hg:
1614
1704
            return [ (name, prefs.getString('hg_default_revision')), (name, None) ]
1615
1705
 
1616
1706
        def getFolderTemplate(self, prefs, names):
 
1707
            fs = _VcsFolderSet(names)
1617
1708
            result = []
1618
1709
            pwd, isabs = os.path.abspath(os.curdir), False
1619
1710
            r = {}
1622
1713
                isabs |= os.path.isabs(name)
1623
1714
                args.append(safeRelativePath(self.root, name, prefs, 'hg_cygwin'))
1624
1715
            # build list of interesting files
1625
 
            for s in strip_eols(popenReadLines(self.root, args, prefs, 'hg_bash')):
1626
 
                if len(s) > 2 and s[0] in 'ADM':
 
1716
            for s in popenReadLines(self.root, args, prefs, 'hg_bash'):
 
1717
                if len(s) > 2 and s[0] in 'AMR':
1627
1718
                    k = os.path.join(self.root, prefs.convertToNativePath(s[2:]))
1628
 
                    if not isabs:
1629
 
                        k = relpath(pwd, k)
1630
 
                    r[k] = s[0]
 
1719
                    if fs.contains(k):
 
1720
                        if not isabs:
 
1721
                            k = relpath(pwd, k)
 
1722
                        r[k] = s[0]
1631
1723
            # sort the results
1632
1724
            for k in sorted(r.keys()):
1633
1725
                v = r[k]
1634
 
                if v == 'D':
 
1726
                if v == 'R':
1635
1727
                    # deleted file
1636
1728
                    result.append( [ (k, prefs.getString('hg_default_revision')), (None, None) ] )
1637
1729
                elif v == 'A':
1643
1735
            return result
1644
1736
 
1645
1737
        def getRevision(self, prefs, name, rev):
1646
 
            return popenReadLines(self.root, [ prefs.getString('hg_bin'), 'cat', '-r', rev, safeRelativePath(self.root, name, prefs, 'hg_cygwin') ], prefs, 'hg_bash')
 
1738
            return popenRead(self.root, [ prefs.getString('hg_bin'), 'cat', '-r', rev, safeRelativePath(self.root, name, prefs, 'hg_cygwin') ], prefs, 'hg_bash')
1647
1739
 
1648
1740
    # Monotone support
1649
1741
    class Mtn:
1662
1754
            for name in names:
1663
1755
                isabs |= os.path.isabs(name)
1664
1756
            # build list of interesting files
1665
 
            ss = strip_eols(popenReadLines(self.root, args, prefs, 'mtn_bash'))
 
1757
            ss = popenReadLines(self.root, args, prefs, 'mtn_bash')
1666
1758
            removed, added, modified, renamed = {}, {}, {}, {}
1667
1759
            i = 0
1668
1760
            while i < len(ss):
1723
1815
            return result
1724
1816
 
1725
1817
        def getRevision(self, prefs, name, rev):
1726
 
            return popenReadLines(self.root, [ prefs.getString('mtn_bin'), 'cat', '--quiet', '-r', rev, safeRelativePath(self.root, name, prefs, 'mtn_cygwin') ], prefs, 'mtn_bash')
 
1818
            return popenRead(self.root, [ prefs.getString('mtn_bin'), 'cat', '--quiet', '-r', rev, safeRelativePath(self.root, name, prefs, 'mtn_cygwin') ], prefs, 'mtn_bash')
1727
1819
 
1728
1820
    # RCS support
1729
1821
    class Rcs:
1733
1825
        def getFileTemplate(self, prefs, name):
1734
1826
            args = [ prefs.getString('rcs_bin_rlog'), '-L', '-h', safeRelativePath(self.root, name, prefs, 'rcs_cygwin') ]
1735
1827
            rev = ''
1736
 
            for line in strip_eols(popenReadLines(self.root, args, prefs, 'rcs_bash')):
 
1828
            for line in popenReadLines(self.root, args, prefs, 'rcs_bash'):
1737
1829
                if line.startswith('head: '):
1738
1830
                    rev = line[6:]
1739
1831
            return [ (name, rev), (name, None) ]
1740
1832
 
1741
1833
        def getFolderTemplate(self, prefs, names):
1742
 
            result = []
 
1834
            # build command
 
1835
            cmd = [ prefs.getString('rcs_bin_rlog'), '-L', '-h' ]
 
1836
            # build list of interesting files
1743
1837
            pwd, isabs = os.path.abspath(os.curdir), False
1744
 
 
1745
1838
            r = []
1746
1839
            for k in names:
1747
1840
                if os.path.isdir(k):
1781
1874
                    s[-1] += ',v'
1782
1875
                    if os.path.isfile(os.sep.join(s)):
1783
1876
                        r.append(k)
1784
 
 
1785
 
            # build list of interesting files
1786
1877
            for k in r:
1787
1878
                isabs |= os.path.isabs(k)
1788
 
            cmd = [ prefs.getString('rcs_bin_rlog'), '-L', '-h' ]
1789
1879
            args = [ safeRelativePath(self.root, k, prefs, 'rcs_cygwin') for k in r ]
1790
 
 
 
1880
            # run command
1791
1881
            r, k = {}, ''
1792
 
            for line in strip_eols(popenXArgsReadLines(self.root, cmd, args, prefs, 'rcs_bash')):
 
1882
            for line in popenXArgsReadLines(self.root, cmd, args, prefs, 'rcs_bash'):
 
1883
                # parse response
1793
1884
                if line.startswith('Working file: '):
1794
1885
                    k = prefs.convertToNativePath(line[14:])
1795
1886
                    k = os.path.join(self.root, os.path.normpath(k))
1797
1888
                        k = relpath(pwd, k)
1798
1889
                elif line.startswith('head: '):
1799
1890
                    r[k] = line[6:]
1800
 
 
1801
1891
            # sort the results
1802
 
            for k in sorted(r.keys()):
1803
 
                result.append([ (k, r[k]), (k, None) ])
1804
 
            return result
 
1892
            return [ [ (k, r[k]), (k, None) ] for k in sorted(r.keys()) ]
1805
1893
 
1806
1894
        def getRevision(self, prefs, name, rev):
1807
 
            return popenReadLines(self.root, [ prefs.getString('rcs_bin_co'), '-p', '-q', '-r' + rev, safeRelativePath(self.root, name, prefs, 'rcs_cygwin') ], prefs, 'rcs_bash')
 
1895
            return popenRead(self.root, [ prefs.getString('rcs_bin_co'), '-p', '-q', '-r' + rev, safeRelativePath(self.root, name, prefs, 'rcs_cygwin') ], prefs, 'rcs_bash')
1808
1896
 
1809
1897
    # Subversion support
1810
1898
    class Svn:
1811
1899
        def __init__(self, root):
1812
1900
            self.root = root
 
1901
            self.url = None
 
1902
 
 
1903
        def getURL(self, prefs):
 
1904
            if self.url is None:
 
1905
                args = [ prefs.getString('svn_bin'), 'info' ]
 
1906
                for s in popenReadLines(self.root, args, prefs, 'svn_bash'):
 
1907
                    if s.startswith('URL: '):
 
1908
                        self.url = s[5:]
 
1909
                        break
 
1910
            return self.url
1813
1911
 
1814
1912
        def getFileTemplate(self, prefs, name):
1815
1913
            # merge conflict
1827
1925
            return [ (name, prefs.getString('svn_default_revision')), (name, None) ]
1828
1926
 
1829
1927
        def getFolderTemplate(self, prefs, names):
 
1928
            fs = _VcsFolderSet(names)
1830
1929
            result = []
1831
1930
            pwd, isabs = os.path.abspath(os.curdir), False
1832
1931
            r = {}
1835
1934
                isabs |= os.path.isabs(name)
1836
1935
                args.append(safeRelativePath(self.root, name, prefs, 'svn_cygwin'))
1837
1936
            # build list of interesting files
1838
 
            for s in strip_eols(popenReadLines(self.root, args, prefs, 'svn_bash')):
 
1937
            for s in popenReadLines(self.root, args, prefs, 'svn_bash'):
1839
1938
                if len(s) > 7 and s[0] in 'ACDMR':
1840
1939
                    # subversion 1.6 adds a new column
1841
1940
                    k = 7
1842
1941
                    if k < len(s) and s[k] == ' ':
1843
1942
                        k += 1
1844
1943
                    k = os.path.join(self.root, prefs.convertToNativePath(s[k:]))
1845
 
                    if not isabs:
1846
 
                        k = relpath(pwd, k)
1847
1944
                    # FIXME: prune dropped directories from the results
1848
 
                    if not os.path.isdir(k):
 
1945
                    if fs.contains(k) and not os.path.isdir(k):
 
1946
                        if not isabs:
 
1947
                            k = relpath(pwd, k)
1849
1948
                        r[k] = s[0]
1850
1949
            # sort the results
1851
1950
            for k in sorted(r.keys()):
1861
1960
                    result.append(self.getFileTemplate(prefs, k))
1862
1961
                elif v == 'R':
1863
1962
                    # renamed file
1864
 
                    result.append( [ (k + '@BASE', prefs.getString('svn_default_revision')), (None, None) ] )
 
1963
                    result.append( [ (k, prefs.getString('svn_default_revision')), (None, None) ] )
1865
1964
                    result.append( [ (None, None), (k, None) ] )
1866
1965
            return result
1867
1966
 
1868
1967
        def getRevision(self, prefs, name, rev):
1869
 
            return popenReadLines(self.root, [ prefs.getString('svn_bin'), 'cat', '-r', rev, safeRelativePath(self.root, name, prefs, 'svn_cygwin') ], prefs, 'svn_bash')
 
1968
            if rev in [ 'PREV', 'BASE', 'COMMITTED' ]:
 
1969
                return popenRead(self.root, [ prefs.getString('svn_bin'), 'cat', '%s@%s' % (safeRelativePath(self.root, name, prefs, 'svn_cygwin'), rev) ], prefs, 'svn_bash')
 
1970
            return popenRead(self.root, [ prefs.getString('svn_bin'), 'cat', '%s/%s@%s' % (self.getURL(prefs), relpath(self.root, os.path.abspath(name)).replace(os.sep, '/'), rev) ], prefs, 'svn_bash')
1870
1971
 
1871
1972
    # SVK support
1872
1973
    def isSvkManaged(self, name):
1878
1979
            try:
1879
1980
                # find working copies by parsing the config file
1880
1981
                f = open(self.svkconfig, 'r')
1881
 
                ss = strip_eols(readlines(f))
 
1982
                ss = readlines(f)
1882
1983
                f.close()
1883
1984
                projs, sep = [], os.sep
1884
1985
                # find the separator character
1935
2036
            return [ (name, prefs.getString('svk_default_revision')), (name, None) ]
1936
2037
 
1937
2038
        def getFolderTemplate(self, prefs, names):
 
2039
            fs = _VcsFolderSet(names)
1938
2040
            result = []
1939
2041
            pwd, isabs = os.path.abspath(os.curdir), False
1940
2042
            r = {}
1943
2045
                isabs |= os.path.isabs(name)
1944
2046
                args.append(safeRelativePath(self.root, name, prefs, 'svk_cygwin'))
1945
2047
            # build list of interesting files
1946
 
            for s in strip_eols(popenReadLines(self.root, args, prefs, 'svk_bash')):
 
2048
            for s in popenReadLines(self.root, args, prefs, 'svk_bash'):
1947
2049
                if len(s) > 4 and s[0] in 'ACDMR':
1948
2050
                    k = os.path.join(self.root, prefs.convertToNativePath(s[4:]))
1949
 
                    if not isabs:
1950
 
                        k = relpath(pwd, k)
1951
2051
                    # FIXME: prune dropped directories from the results
1952
 
                    if not os.path.isdir(k):
 
2052
                    if fs.contains(k) and not os.path.isdir(k):
 
2053
                        if not isabs:
 
2054
                            k = relpath(pwd, k)
1953
2055
                        r[k] = s[0]
1954
2056
            # sort the results
1955
2057
            for k in sorted(r.keys()):
1966
2068
            return result
1967
2069
 
1968
2070
        def getRevision(self, prefs, name, rev):
1969
 
            return popenReadLines(self.root, [ prefs.getString('svk_bin'), 'cat', '-r', rev, safeRelativePath(self.root, name, prefs, 'svk_cygwin') ], prefs, 'svk_bash')
 
2071
            return popenRead(self.root, [ prefs.getString('svk_bin'), 'cat', '-r', rev, safeRelativePath(self.root, name, prefs, 'svk_cygwin') ], prefs, 'svk_bash')
1970
2072
 
1971
2073
    def __init__(self):
1972
2074
        # initialise the VCS objects
1974
2076
        if svkroot is None:
1975
2077
            svkroot = os.path.expanduser('~/.svk')
1976
2078
        self.svkconfig = os.path.join(svkroot, 'config')
1977
 
        self.leaf_dir_repos = [('.svn', VCSs.Svn), ('CVS', VCSs.Cvs), ('RCS', VCSs.Rcs)]
1978
 
        self.common_dir_repos = [('.git', VCSs.Git), ('.hg', VCSs.Hg), ('.bzr', VCSs.Bzr), ('_darcs', VCSs.Darcs), ('_MTN', VCSs.Mtn)]
 
2079
        self.leaf_dir_repos = [ ('.svn', VCSs.Svn), ('CVS', VCSs.Cvs), ('RCS', VCSs.Rcs) ]
 
2080
        self.common_dir_repos = [ ('.git', VCSs.Git), ('.hg', VCSs.Hg), ('.bzr', VCSs.Bzr), ('_darcs', VCSs.Darcs), ('_MTN', VCSs.Mtn) ]
1979
2081
 
1980
2082
    # determines which VCS to use for the named file
1981
2083
    def findByFolder(self, path, prefs):
2321
2423
        return 1
2322
2424
    return 2
2323
2425
 
 
2426
# longest common subsequence of unique elements common to 'a' and 'b'
 
2427
def __patience_subsequence(a, b):
 
2428
    # value unique lines by their order in each list
 
2429
    value_a, value_b = {}, {}
 
2430
    # find unique values in 'a'
 
2431
    for i, s in enumerate(a):
 
2432
        if s in value_a:
 
2433
            value_a[s] = -1
 
2434
        else:
 
2435
            value_a[s] = i
 
2436
    # find unique values in 'b'
 
2437
    for i, s in enumerate(b):
 
2438
        if s in value_b:
 
2439
            value_b[s] = -1
 
2440
        else:
 
2441
            value_b[s] = i
 
2442
    # lay down items in 'b' as if playing patience if the item is unique in
 
2443
    # 'a' and 'b'
 
2444
    pile, pointers, atob = [], {}, {}
 
2445
    get, append = value_a.get, pile.append
 
2446
    for s in b:
 
2447
        v = get(s, -1)
 
2448
        if v != -1:
 
2449
            vb = value_b[s]
 
2450
            if vb != -1:
 
2451
                atob[v] = vb
 
2452
                # find appropriate pile for v
 
2453
                start, end = 0, len(pile)
 
2454
                # optimisation as values usually increase
 
2455
                if end and v > pile[-1]:
 
2456
                    start = end
 
2457
                else:
 
2458
                    while start < end:
 
2459
                        mid = (start + end) // 2
 
2460
                        if v < pile[mid]:
 
2461
                            end = mid
 
2462
                        else:
 
2463
                            start = mid + 1
 
2464
                if start < len(pile):
 
2465
                    pile[start] = v
 
2466
                else:
 
2467
                    append(v)
 
2468
                if start:
 
2469
                    pointers[v] = pile[start-1]
 
2470
    # examine our piles to determine the longest common subsequence
 
2471
    result = []
 
2472
    if pile:
 
2473
        v, append = pile[-1], result.append
 
2474
        append((v, atob[v]))
 
2475
        while v in pointers:
 
2476
            v = pointers[v]
 
2477
            append((v, atob[v]))
 
2478
        result.reverse()
 
2479
    return result
 
2480
 
 
2481
# difflib-style approximation of the longest common subsequence
 
2482
def __lcs_approx(a, b):
 
2483
    count1, lookup = {}, {}
 
2484
    # count occurances of each element in 'a'
 
2485
    for s in a:
 
2486
        count1[s] = count1.get(s, 0) + 1
 
2487
    # construct a mapping from a element to where it can be found in 'b'
 
2488
    for i, s in enumerate(b):
 
2489
        if s in lookup:
 
2490
            lookup[s].append(i)
 
2491
        else:
 
2492
            lookup[s] = [ i ]
 
2493
    if set(lookup).intersection(count1):
 
2494
        # we have some common elements
 
2495
        # identify popular entries
 
2496
        popular = {}
 
2497
        n = len(a)
 
2498
        if n > 200:
 
2499
            for k, v in count1.items():
 
2500
                if 100 * v > n:
 
2501
                    popular[k] = 1
 
2502
        n = len(b)
 
2503
        if n > 200:
 
2504
            for k, v in lookup.items():
 
2505
                if 100 * len(v) > n:
 
2506
                    popular[k] = 1
 
2507
        # while walk through entries in 'a', incrementally update the list of
 
2508
        # matching subsequences in 'b' and keep track of the longest match
 
2509
        # found
 
2510
        prev_matches, matches, max_length, max_indices = {}, {}, 0, []
 
2511
        for ai, s in enumerate(a):
 
2512
            if s in lookup:
 
2513
                if s in popular:
 
2514
                    # we only extend existing previously found matches to avoid
 
2515
                    # performance issues
 
2516
                    for bi in prev_matches:
 
2517
                        if bi + 1 < n and b[bi + 1] == s:
 
2518
                            matches[bi] = v = prev_matches[bi] + 1
 
2519
                            # check if this is now the longest match
 
2520
                            if v >= max_length:
 
2521
                                if v == max_length:
 
2522
                                    max_indices.append((ai, bi))
 
2523
                                else:
 
2524
                                    max_length = v
 
2525
                                    max_indices = [ (ai, bi) ]
 
2526
                else:
 
2527
                    prev_get = prev_matches.get
 
2528
                    for bi in lookup[s]:
 
2529
                        matches[bi] = v = prev_get(bi - 1, 0) + 1
 
2530
                        # check if this is now the longest match
 
2531
                        if v >= max_length:
 
2532
                            if v == max_length:
 
2533
                                max_indices.append((ai, bi))
 
2534
                            else:
 
2535
                                max_length = v
 
2536
                                max_indices = [ (ai, bi) ]
 
2537
            prev_matches, matches = matches, {}
 
2538
        if max_indices:
 
2539
            # include any popular entries at the beginning
 
2540
            aidx, bidx, nidx = 0, 0, 0
 
2541
            for ai, bi in max_indices:
 
2542
                n = max_length
 
2543
                ai += 1 - n
 
2544
                bi += 1 - n
 
2545
                while ai and bi and a[ai - 1] == b[bi - 1]:
 
2546
                    ai -= 1
 
2547
                    bi -= 1
 
2548
                    n += 1
 
2549
                if n > nidx:
 
2550
                    aidx, bidx, nidx = ai, bi, n
 
2551
            return aidx, bidx, nidx
 
2552
 
 
2553
# patinence diff with difflib-style fallback
 
2554
def patience_diff(a, b):
 
2555
    matches, len_a, len_b = [], len(a), len(b)
 
2556
    if len_a and len_b:
 
2557
        blocks = [ (0, len_a, 0, len_b, 0) ]
 
2558
        while blocks:
 
2559
            start_a, end_a, start_b, end_b, match_idx = blocks.pop()
 
2560
            aa, bb = a[start_a:end_a], b[start_b:end_b]
 
2561
            # try patience
 
2562
            pivots = __patience_subsequence(aa, bb)
 
2563
            if pivots:
 
2564
                offset_a, offset_b = start_a, start_b
 
2565
                for pivot_a, pivot_b in pivots:
 
2566
                    pivot_a += offset_a
 
2567
                    pivot_b += offset_b
 
2568
                    if start_a <= pivot_a:
 
2569
                        # extend before
 
2570
                        idx_a, idx_b = pivot_a, pivot_b
 
2571
                        while start_a < idx_a and start_b < idx_b and a[idx_a - 1] == b[idx_b - 1]:
 
2572
                            idx_a -= 1
 
2573
                            idx_b -= 1
 
2574
                        # if anything is before recurse on the section
 
2575
                        if start_a < idx_a and start_b < idx_b:
 
2576
                            blocks.append((start_a, idx_a, start_b, idx_b, match_idx))
 
2577
                        # extend after
 
2578
                        start_a, start_b = pivot_a + 1, pivot_b + 1
 
2579
                        while start_a < end_a and start_b < end_b and a[start_a] == b[start_b]:
 
2580
                            start_a += 1
 
2581
                            start_b += 1
 
2582
                        # record match
 
2583
                        matches.insert(match_idx, (idx_a, idx_b, start_a - idx_a))
 
2584
                        match_idx += 1
 
2585
                # if anything is after recurse on the section
 
2586
                if start_a < end_a and start_b < end_b:
 
2587
                    blocks.append((start_a, end_a, start_b, end_b, match_idx))
 
2588
            else:
 
2589
                # fallback if patience fails
 
2590
                pivots = __lcs_approx(aa, bb)
 
2591
                if pivots:
 
2592
                    idx_a, idx_b, n = pivots
 
2593
                    idx_a += start_a
 
2594
                    idx_b += start_b
 
2595
                    # if anything is before recurse on the section
 
2596
                    if start_a < idx_a and start_b < idx_b:
 
2597
                        blocks.append((start_a, idx_a, start_b, idx_b, match_idx))
 
2598
                    # record match
 
2599
                    matches.insert(match_idx, (idx_a, idx_b, n))
 
2600
                    match_idx += 1
 
2601
                    idx_a += n
 
2602
                    idx_b += n
 
2603
                    # if anything is after recurse on the section
 
2604
                    if idx_a < end_a and idx_b < end_b:
 
2605
                        blocks.append((idx_a, end_a, idx_b, end_b, match_idx))
 
2606
    # try matching from begining to first match block
 
2607
    if matches:
 
2608
        end_a, end_b = matches[0][:2]
 
2609
    else:
 
2610
        end_a, end_b = len_a, len_b
 
2611
    i = 0
 
2612
    while i < end_a and i < end_b and a[i] == b[i]:
 
2613
        i += 1
 
2614
    if i:
 
2615
        matches.insert(0, (0, 0, i))
 
2616
    # try matching from last match block to end
 
2617
    if matches:
 
2618
        start_a, start_b, n = matches[-1]
 
2619
        start_a += n
 
2620
        start_b += n
 
2621
    else:
 
2622
        start_a, start_b = 0, 0
 
2623
    end_a, end_b = len_a, len_b
 
2624
    while start_a < end_a and start_b < end_b and a[end_a - 1] == b[end_b - 1]:
 
2625
        end_a -= 1
 
2626
        end_b -= 1
 
2627
    if end_a < len_a:
 
2628
        matches.append((end_a, end_b, len_a - end_a))
 
2629
    # add a zero length block to the end
 
2630
    matches.append((len_a, len_b, 0))
 
2631
    return matches
 
2632
 
2324
2633
# widget used to compare and merge text files
2325
2634
class FileDiffViewer(gtk.Table):
2326
2635
    # class describing a text pane
2377
2686
        self.set_flags(gtk.CAN_FOCUS)
2378
2687
        self.prefs = prefs
2379
2688
        self.string_width_cache = {}
 
2689
        self.options = {}
2380
2690
 
2381
2691
        # diff blocks
2382
2692
        self.blocks = []
2545
2855
    # are known and the scroll bar can be moved to the first difference
2546
2856
    def _realise_cb(self, widget):
2547
2857
        self.im_context.set_client_window(self.window)
2548
 
        self.first_difference()
 
2858
        try:
 
2859
            self.go_to_line(self.options['line'])
 
2860
        except KeyError:
 
2861
            self.first_difference()
2549
2862
 
2550
2863
    # callback for most menu items and buttons
2551
2864
    def button_cb(self, widget, data):
2553
2866
        self._button_actions[data]()
2554
2867
        self.closeUndoBlock()
2555
2868
 
 
2869
    # set startup options
 
2870
    def setOptions(self, options):
 
2871
        self.options = options
 
2872
 
2556
2873
    # updates the display font and resizes viewports as necessary
2557
2874
    def setFont(self, font):
2558
2875
        self.font = font
2572
2889
        get = char_width_cache.get
2573
2890
        for c in s:
2574
2891
            w = get(c, 0)
2575
 
            if w == 0: 
 
2892
            if w == 0:
2576
2893
                v = ord(c)
2577
2894
                if v < 32:
2578
2895
                    if c == '\t':
3283
3600
        # align s1 and s2 by inserting spacer lines
3284
3601
        # this will be used to determine which lines from the inner lists of
3285
3602
        # lines should be neighbours
3286
 
        for block in difflib.SequenceMatcher(None, t1, t2).get_matching_blocks():
 
3603
        for block in patience_diff(t1, t2):
3287
3604
            delta = (n1 + block[0]) - (n2 + block[1])
3288
3605
            if delta < 0:
3289
3606
                # insert spacer lines in s1
4283
4600
                                cr.rectangle(x_start + pixels(x_temp), y_start, pixels(w), h)
4284
4601
                                cr.fill()
4285
4602
 
 
4603
                if self.prefs.getBool('display_show_right_margin'):
 
4604
                    # draw margin
 
4605
                    x_temp = line_number_width + pixels(self.prefs.getInt('display_right_margin') * self.digit_width)
 
4606
                    if x_temp >= x and x_temp < maxx:
 
4607
                        colour = theResources.getColour('margin')
 
4608
                        cr.set_source_rgb(colour.red, colour.green, colour.blue)
 
4609
                        cr.set_line_width(1)
 
4610
                        cr.move_to(x_temp, y_start)
 
4611
                        cr.rel_line_to(0, h)
 
4612
                        cr.stroke()
 
4613
 
4286
4614
                if text is None:
4287
4615
                    # draw hatching
4288
4616
                    colour = theResources.getColour('hatch')
4696
5024
    def im_commit_cb(self, im, s):
4697
5025
        if self.mode == CHAR_MODE:
4698
5026
            self.openUndoBlock()
4699
 
            self.replaceText(self.prefs.convertToUnicode([ s ])[0][0])
 
5027
            self.replaceText(unicode(s, 'utf_8'))
4700
5028
            self.closeUndoBlock()
4701
5029
 
4702
5030
    # update the cached preedit text
4717
5045
            s, a, c = self.im_context.get_preedit_string()
4718
5046
            if len(s) > 0:
4719
5047
                # we have preedit text, draw that instead
4720
 
                s = self.prefs.convertToUnicode([ s ])[0][0]
 
5048
                s = unicode(s, 'utf_8')
4721
5049
                p = (s, a, c)
4722
5050
            else:
4723
5051
                p = None
5000
5328
            needs_block = (self.undoblock is None)
5001
5329
            if needs_block:
5002
5330
                self.openUndoBlock()
5003
 
            self.replaceText(self.prefs.convertToUnicode([ text ])[0][0])
 
5331
            self.replaceText(unicode(nullToEmpty(text), 'utf_8'))
5004
5332
            if needs_block:
5005
5333
                self.closeUndoBlock()
5006
5334
 
5232
5560
        vadj.set_value(y)
5233
5561
 
5234
5562
    # move the cursor from line 'i' to the next difference in direction 'delta'
5235
 
    def goto_difference(self, i, delta):
 
5563
    def go_to_difference(self, i, delta):
5236
5564
        f = self.current_pane
5237
5565
        nlines = len(self.panes[f].lines)
5238
5566
        # back up to beginning of difference
5260
5588
    # 'first_difference' action
5261
5589
    def first_difference(self):
5262
5590
        self.setLineMode()
5263
 
        self.goto_difference(0, 1)
 
5591
        self.go_to_difference(0, 1)
5264
5592
 
5265
5593
    # 'previous_difference' action
5266
5594
    def previous_difference(self):
5267
5595
        self.setLineMode()
5268
5596
        i = min(self.current_line, self.selection_line) - 1
5269
 
        self.goto_difference(i, -1)
 
5597
        self.go_to_difference(i, -1)
5270
5598
 
5271
5599
    # 'next_difference' action
5272
5600
    def next_difference(self):
5273
5601
        self.setLineMode()
5274
5602
        i = max(self.current_line, self.selection_line) + 1
5275
 
        self.goto_difference(i, 1)
 
5603
        self.go_to_difference(i, 1)
5276
5604
 
5277
5605
    # 'last_difference' action
5278
5606
    def last_difference(self):
5279
5607
        self.setLineMode()
5280
5608
        i = len(self.panes[self.current_pane].lines)
5281
 
        self.goto_difference(i, -1)
 
5609
        self.go_to_difference(i, -1)
5282
5610
 
5283
5611
    # Undo for changes to the pane ordering
5284
5612
    class SwapPanesUndo:
5818
6146
        label = gtk.Label(_('Encoding: '))
5819
6147
        hbox.pack_start(label, False, False, 0)
5820
6148
        label.show()
5821
 
        self.combobox = combobox = gtk.combo_box_new_text()
5822
 
        self.encodings = prefs.getEncodings()
5823
 
        for e in self.encodings:
5824
 
            combobox.append_text(e)
5825
 
        if action in [ gtk.FILE_CHOOSER_ACTION_OPEN, gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER ]:
5826
 
            self.encodings = self.encodings[:]
5827
 
            self.encodings.insert(0, None)
5828
 
            combobox.prepend_text(_('Auto Detect'))
5829
 
            combobox.set_active(0)
5830
 
        hbox.pack_start(combobox, False, False, 5)
5831
 
        combobox.show()
 
6149
        self.encoding = entry = EncodingMenu(prefs, action in [ gtk.FILE_CHOOSER_ACTION_OPEN, gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER ])
 
6150
        hbox.pack_start(entry, False, False, 5)
 
6151
        entry.show()
5832
6152
        if action == gtk.FILE_CHOOSER_ACTION_OPEN:
5833
6153
            self.revision = entry = gtk.Entry()
5834
6154
            hbox.pack_end(entry, False, False, 0)
5842
6162
        self.set_current_folder(unicode(os.path.realpath(os.curdir), sys.getfilesystemencoding()))
5843
6163
 
5844
6164
    def set_encoding(self, encoding):
5845
 
        if encoding not in self.encodings:
5846
 
            encoding = self.prefs.getDefaultEncoding()
5847
 
        if encoding in self.encodings:
5848
 
            self.combobox.set_active(self.encodings.index(encoding))
 
6165
        self.encoding.set_text(encoding)
5849
6166
 
5850
6167
    def get_encoding(self):
5851
 
        i = self.combobox.get_active()
5852
 
        if i >= 0:
5853
 
            return self.encodings[i]
 
6168
        return self.encoding.get_text()
5854
6169
 
5855
6170
    def get_revision(self):
5856
6171
        return self.revision.get_text()
5857
6172
 
5858
6173
    def get_filename(self):
5859
6174
        # convert from UTF-8 string to unicode
5860
 
        return decodefilename(gtk.FileChooserDialog.get_filename(self))
 
6175
        return decode_fs_string(gtk.FileChooserDialog.get_filename(self))
5861
6176
 
5862
6177
# dialogue used to search for text
5863
6178
class GoToLineDialog(gtk.Dialog):
5954
6269
 
5955
6270
# contains information about a file
5956
6271
class FileInfo:
5957
 
    def __init__(self, name=None, encoding=None, vcs=None, revision=None):
 
6272
    def __init__(self, name=None, encoding=None, vcs=None, revision=None, label=None):
5958
6273
        # file name
5959
6274
        self.name = name
5960
6275
        # name of codec used to translate the file contents to unicode text
5963
6278
        self.vcs = vcs
5964
6279
        # revision used to retrieve file from the VCS
5965
6280
        self.revision = revision
 
6281
        # alternate text to display instead of the actual file name
 
6282
        self.label = label
5966
6283
        # 'stat' for files read from disk -- used to warn about changes to the
5967
6284
        # file on disk before saving
5968
6285
        self.stat = None
5970
6287
        # to warn about changes to file on disk
5971
6288
        self.last_stat = None
5972
6289
 
 
6290
# assign user specified labels to the corresponding files
 
6291
def assign_file_labels(items, labels):
 
6292
    new_items = []
 
6293
    ss = labels[::-1]
 
6294
    for name, data in items:
 
6295
        if ss:
 
6296
            s = ss.pop()
 
6297
        else:
 
6298
            s = None
 
6299
        new_items.append((name, data, s))
 
6300
    return new_items
 
6301
 
5973
6302
# the main application class containing a set of file viewers
5974
6303
# this class displays tab for switching between viewers and dispatches menu
5975
6304
# commands to the current viewer
6005
6334
            def updateTitle(self):
6006
6335
                ss = []
6007
6336
                info = self.info
6008
 
                if info.name is not None:
6009
 
                    ss.append(info.name)
6010
 
                if info.revision is not None:
6011
 
                    ss.append('(' + info.revision + ')')
 
6337
                if info.label is not None:
 
6338
                    # show the provided label instead of the file name
 
6339
                    ss.append(info.label)
 
6340
                else:
 
6341
                    if info.name is not None:
 
6342
                        ss.append(info.name)
 
6343
                    if info.revision is not None:
 
6344
                        ss.append('(' + info.revision + ')')
6012
6345
                if self.has_edits:
6013
6346
                    ss.append('*')
6014
6347
                s = ' '.join(ss)
6193
6526
            unique_names = set()
6194
6527
            for header in self.headers:
6195
6528
                has_edits |= header.has_edits
6196
 
                s = header.info.name
 
6529
                s = header.info.label
 
6530
                if s is None:
 
6531
                    # no label provided, show the file name instead
 
6532
                    s = header.info.name
 
6533
                    if s is not None:
 
6534
                        s = os.path.basename(s)
6197
6535
                if s is not None:
6198
 
                    s = os.path.basename(s)
6199
6536
                    names.append(s)
6200
6537
                    unique_names.add(s)
6201
6538
 
6230
6567
                    if rev is None:
6231
6568
                        # load the contents of a plain file
6232
6569
                        fd = open(name, 'rb')
6233
 
                        ss = readlines(fd)
 
6570
                        s = fd.read()
6234
6571
                        fd.close()
6235
6572
                        # get the file's modification times so we can detect changes
6236
6573
                        stat = os.stat(name)
6239
6576
                            raise IOError('Not under version control.')
6240
6577
                        fullname = os.path.abspath(name)
6241
6578
                        # retrieve the revision from the version control system
6242
 
                        ss = info.vcs.getRevision(self.prefs, fullname, rev)
 
6579
                        s = info.vcs.getRevision(self.prefs, fullname, rev)
6243
6580
                    # convert file contents to unicode
6244
6581
                    if encoding is None:
6245
 
                        ss, encoding = self.prefs.convertToUnicode(ss)
 
6582
                        s, encoding = self.prefs.convertToUnicode(s)
6246
6583
                    else:
6247
 
                        ss = [ unicode(s, encoding) for s in ss ]
 
6584
                        s = unicode(s, encoding)
 
6585
                    ss = splitlines(s)
6248
6586
                except (IOError, OSError, UnicodeDecodeError, WindowsError, LookupError):
6249
6587
                    # FIXME: this can occur before the toplevel window is drawn
6250
6588
                    if rev is not None:
6319
6657
                        if info.last_stat[stat.ST_MTIME] < new_stat[stat.ST_MTIME]:
6320
6658
                            # update our notion of the most recent modification
6321
6659
                            info.last_stat = new_stat
6322
 
                            msg = _('The file %s changed on disk.  Do you want to reload the file?') % (info.name, )
 
6660
                            if info.label is not None:
 
6661
                                s = info.label
 
6662
                            else:
 
6663
                                s = info.name
 
6664
                            msg = _('The file %s changed on disk.  Do you want to reload the file?') % (s, )
6323
6665
                            dialog = MessageDialog(self.get_toplevel(), gtk.MESSAGE_QUESTION, msg)
6324
6666
                            ok = (dialog.run() == gtk.RESPONSE_OK)
6325
6667
                            dialog.destroy()
6332
6674
        def save_file(self, f, save_as=False):
6333
6675
            h = self.headers[f]
6334
6676
            info = h.info
6335
 
            name, encoding, rev = info.name, info.encoding, info.revision
 
6677
            name, encoding, rev, label = info.name, info.encoding, info.revision, info.label
6336
6678
            if name is None or rev is not None:
6337
6679
                # we need to prompt for a file name the current contents were
6338
6680
                # not loaded from a regular file
6342
6684
                dialog = FileChooserDialog(_('Save %(title)s Pane %(pane)d') % { 'title': self.title, 'pane': f + 1 }, self.get_toplevel(), self.prefs, gtk.FILE_CHOOSER_ACTION_SAVE, gtk.STOCK_SAVE)
6343
6685
                if name is not None:
6344
6686
                    dialog.set_filename(os.path.abspath(name))
 
6687
                if encoding is None:
 
6688
                    encoding = self.prefs.getDefaultEncoding()
6345
6689
                dialog.set_encoding(encoding)
6346
 
                name = None
 
6690
                name, label = None, None
6347
6691
                dialog.set_default_response(gtk.RESPONSE_OK)
6348
6692
                if dialog.run() == gtk.RESPONSE_OK:
6349
6693
                    name = dialog.get_filename()
6350
6694
                    encoding = dialog.get_encoding()
 
6695
                    if encoding is None:
 
6696
                        if info.encoding is not None:
 
6697
                            # this case can occur if the user provided the
 
6698
                            # encoding and it is not an encoding we know about
 
6699
                            encoding = info.encoding
 
6700
                        else:
 
6701
                            encoding = self.prefs.getDefaultEncoding()
6351
6702
                dialog.destroy()
6352
6703
            if name is None:
6353
6704
                return False
6393
6744
                self.bakeEdits(f)
6394
6745
                self.closeUndoBlock()
6395
6746
                # update the pane file info
6396
 
                info.name, info.encoding, info.revision = name, encoding, None
 
6747
                info.name, info.encoding, info.revision, info.label = name, encoding, None, label
6397
6748
                info.last_stat = info.stat = os.stat(name)
6398
6749
                self.setFileInfo(f, info)
6399
6750
                # update the syntax highlighting incase we changed the file
6627
6978
                     [_('_Next Difference'), self.button_cb, 'next_difference', gtk.STOCK_GO_DOWN, 'next_difference'],
6628
6979
                     [_('_Last Difference'), self.button_cb, 'last_difference', gtk.STOCK_GOTO_BOTTOM, 'last_difference'],
6629
6980
                     [],
 
6981
                     [_('Fir_st Tab'), self.first_tab_cb, None, None, 'first_tab'],
6630
6982
                     [_('Pre_vious Tab'), self.previous_tab_cb, None, None, 'previous_tab'],
6631
6983
                     [_('Next _Tab'), self.next_tab_cb, None, None, 'next_tab'],
 
6984
                     [_('Las_t Tab'), self.last_tab_cb, None, None, 'last_tab'],
6632
6985
                     [],
6633
6986
                     [_('Shift Pane _Right'), self.button_cb, 'shift_pane_right', None, 'shift_pane_right'],
6634
6987
                     [_('Shift Pane _Left'), self.button_cb, 'shift_pane_left', None, 'shift_pane_left'] ] ])
6951
7304
        elif len(items) == 1 and len(items[0][1]) == 1:
6952
7305
            # one file specified
6953
7306
            # determine which other files to compare it with
6954
 
            name, data = items[0]
 
7307
            name, data, label = items[0]
6955
7308
            rev, encoding = data[0]
6956
7309
            vcs = theVCSs.findByFilename(name, self.prefs)
6957
7310
            if vcs is None:
6958
7311
                # shift the existing file so it will be in the second pane
6959
7312
                specs.append(FileInfo())
6960
 
                specs.append(FileInfo(name, encoding))
 
7313
                specs.append(FileInfo(name, encoding, None, None, label))
6961
7314
            else:
6962
7315
                if rev is None:
6963
7316
                    # no revision specified assume defaults
6964
7317
                    for name, rev in vcs.getFileTemplate(self.prefs, name):
6965
 
                        specs.append(FileInfo(name, encoding, vcs, rev))
 
7318
                        if rev is None:
 
7319
                            s = label
 
7320
                        else:
 
7321
                            s = None
 
7322
                        specs.append(FileInfo(name, encoding, vcs, rev, s))
6966
7323
                else:
6967
7324
                    # single revision specified
6968
7325
                    specs.append(FileInfo(name, encoding, vcs, rev))
6969
 
                    specs.append(FileInfo(name, encoding))
 
7326
                    specs.append(FileInfo(name, encoding, None, None, label))
6970
7327
        else:
6971
7328
            # multiple files specified, use one pane for each file
6972
 
            for name, data in items:
 
7329
            for name, data, label in items:
6973
7330
                for rev, encoding in data:
6974
7331
                    if rev is None:
6975
 
                        vcs = None
 
7332
                        vcs, s = None, label
6976
7333
                    else:
6977
 
                        vcs = theVCSs.findByFilename(name, self.prefs)
6978
 
                    specs.append(FileInfo(name, encoding, vcs, rev))
 
7334
                        vcs, s = theVCSs.findByFilename(name, self.prefs), None
 
7335
                    specs.append(FileInfo(name, encoding, vcs, rev, s))
6979
7336
 
6980
7337
        # open a new viewer
6981
7338
        viewer = self.newFileDiffViewer(max(2, len(specs)))
6986
7343
        return viewer
6987
7344
 
6988
7345
    # create a new viewer for 'items'
6989
 
    def createSingleTab(self, items):
 
7346
    def createSingleTab(self, items, labels, options):
6990
7347
        if len(items) > 0:
6991
 
            self.newLoadedFileDiffViewer(items)
 
7348
            self.newLoadedFileDiffViewer(assign_file_labels(items, labels)).setOptions(options)
6992
7349
 
6993
7350
    # create a new viewer for each item in 'items'
6994
 
    def createSeparateTabs(self, items):
6995
 
        for item in items:
6996
 
            self.newLoadedFileDiffViewer([ item ])
 
7351
    def createSeparateTabs(self, items, labels, options):
 
7352
        # all tabs inherit the first tab's revision and encoding specifications
 
7353
        items = [ (name, items[0][1]) for name, data in items ]
 
7354
        for item in assign_file_labels(items, labels):
 
7355
            self.newLoadedFileDiffViewer([ item ]).setOptions(options)
6997
7356
 
6998
7357
    # create a new viewer for each modified file found in 'items'
6999
 
    def createModifiedFileTabs(self, items):
 
7358
    def createModifiedFileTabs(self, items, labels, options):
7000
7359
        new_items = []
7001
7360
        for item in items:
7002
 
            s, r = item
7003
 
            dn = s
7004
 
            if os.path.isfile(dn):
7005
 
                dn = os.path.dirname(dn)
 
7361
            name, data = item
 
7362
            # get full path to an existing ancessor directory
 
7363
            dn = os.path.abspath(name)
 
7364
            while not os.path.isdir(dn):
 
7365
                dn, old_dn = os.path.dirname(dn), dn
 
7366
                if dn == old_dn:
 
7367
                    break
7006
7368
            if len(new_items) == 0 or dn != new_items[-1][0]:
7007
7369
                new_items.append([ dn, None, [] ])
7008
7370
            dst = new_items[-1]
7009
 
            dst[1] = r[-1][1]
7010
 
            dst[2].append(s)
 
7371
            dst[1] = data[-1][1]
 
7372
            dst[2].append(name)
7011
7373
        for dn, encoding, names in new_items:
7012
7374
            vcs = theVCSs.findByFolder(dn, self.prefs)
7013
7375
            if vcs is not None:
7017
7379
                        for i, spec in enumerate(specs):
7018
7380
                            name, rev = spec
7019
7381
                            viewer.load(i, FileInfo(name, encoding, vcs, rev))
 
7382
                        viewer.setOptions(options)
7020
7383
                except (IOError, OSError, WindowsError):
7021
7384
                    dialog = MessageDialog(self.get_toplevel(), gtk.MESSAGE_ERROR, _('Error retrieving modifications for %s.') % (dn, ))
7022
7385
                    dialog.run()
7060
7423
            rev = None
7061
7424
        dialog.destroy()
7062
7425
        if accept:
7063
 
            viewer = self.newLoadedFileDiffViewer([ (name, [ (rev, encoding) ]) ])
 
7426
            viewer = self.newLoadedFileDiffViewer([ (name, [ (rev, encoding) ], None) ])
7064
7427
            self.notebook.set_current_page(self.notebook.get_n_pages() - 1)
7065
7428
            viewer.grab_focus()
7066
7429
 
7074
7437
        dialog.destroy()
7075
7438
        if accept:
7076
7439
            n = self.notebook.get_n_pages()
7077
 
            self.createModifiedFileTabs([ (name, [ (None, encoding) ]) ])
 
7440
            self.createModifiedFileTabs([ (name, [ (None, encoding) ]) ], [], {})
7078
7441
            if self.notebook.get_n_pages() > n:
7079
7442
                # we added some new tabs, focus on the first one
7080
7443
                self.notebook.set_current_page(n)
7213
7576
        if self.menu_update_depth == 0 and widget.get_active():
7214
7577
            self.getCurrentViewer().setSyntax(data)
7215
7578
 
 
7579
    # callback for the first tab menu item
 
7580
    def first_tab_cb(self, widget, data):
 
7581
        self.notebook.set_current_page(0)
 
7582
 
7216
7583
    # callback for the previous tab menu item
7217
7584
    def previous_tab_cb(self, widget, data):
7218
 
        i = self.notebook.get_current_page() - 1
7219
 
        if i >= 0:
7220
 
            self.notebook.set_current_page(i)
 
7585
        i, n = self.notebook.get_current_page(), self.notebook.get_n_pages()
 
7586
        self.notebook.set_current_page((n + i - 1) % n)
7221
7587
 
7222
7588
    # callback for the next tab menu item
7223
7589
    def next_tab_cb(self, widget, data):
7224
 
        i = self.notebook.get_current_page() + 1
7225
 
        n = self.notebook.get_n_pages()
7226
 
        if i < n:
7227
 
            self.notebook.set_current_page(i)
 
7590
        i, n = self.notebook.get_current_page(), self.notebook.get_n_pages()
 
7591
        self.notebook.set_current_page((i + 1) % n)
 
7592
 
 
7593
    # callback for the last tab menu item
 
7594
    def last_tab_cb(self, widget, data):
 
7595
        self.notebook.set_current_page(self.notebook.get_n_pages() - 1)
7228
7596
 
7229
7597
    # callback for most menu items and buttons
7230
7598
    def button_cb(self, widget, data):
7302
7670
gobject.signal_new('save', Diffuse.FileDiffViewer.PaneHeader, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ())
7303
7671
gobject.signal_new('save_as', Diffuse.FileDiffViewer.PaneHeader, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ())
7304
7672
 
 
7673
# create nested subdirectories and return the complete path
 
7674
def make_subdirs(p, ss):
 
7675
    for s in ss:
 
7676
        p = os.path.join(p, s)
 
7677
        if not os.path.exists(p):
 
7678
            try:
 
7679
                os.mkdir(p)
 
7680
            except IOError:
 
7681
                pass
 
7682
    return p
 
7683
 
7305
7684
# process the command line arguments
7306
7685
if __name__ == '__main__':
7307
 
    # find the config directory
7308
 
    rc_dir = os.environ.get('XDG_CONFIG_HOME', None)
 
7686
    # find the config directory and create it if it didn't exist
 
7687
    rc_dir, subdirs = os.environ.get('XDG_CONFIG_HOME', None), ['diffuse']
7309
7688
    if rc_dir is None:
7310
 
        rc_dir = os.path.join(os.path.expanduser('~'), '.config')
7311
 
    for rc_dir in rc_dir, os.path.join(rc_dir, 'diffuse'):
7312
 
        # create the directory if it didn't exist
7313
 
        if not os.path.exists(rc_dir):
7314
 
            try:
7315
 
                os.mkdir(rc_dir)
7316
 
            except IOError:
7317
 
                pass
7318
 
 
 
7689
        rc_dir = os.path.expanduser('~')
 
7690
        subdirs.insert(0, '.config')
 
7691
    rc_dir = make_subdirs(rc_dir, subdirs)
 
7692
    # find the local data directory and create it if it didn't exist
 
7693
    data_dir, subdirs = os.environ.get('XDG_DATA_HOME', None), ['diffuse']
 
7694
    if data_dir is None:
 
7695
        data_dir = os.path.expanduser('~')
 
7696
        subdirs[:0] = [ '.local', 'share' ]
 
7697
    data_dir = make_subdirs(data_dir, subdirs)
7319
7698
    # load resource files
7320
 
    i = 1
7321
 
    rc_files = []
 
7699
    i, rc_files = 1, []
7322
7700
    if argc == 2 and args[1] == '--no-rcfile':
7323
7701
        i += 1
7324
7702
    elif argc == 3 and args[1] == '--rcfile':
7345
7723
 
7346
7724
    diff = Diffuse(rc_dir)
7347
7725
    # load state
7348
 
    statepath = os.path.join(rc_dir, 'state')
 
7726
    statepath = os.path.join(data_dir, 'state')
7349
7727
    diff.loadState(statepath)
7350
7728
 
7351
7729
    # process remaining command line arguments
7352
7730
    encoding, revs, close_on_same = None, [], False
7353
 
    specs, had_specs = [], False
 
7731
    specs, had_specs, labels = [], False, []
7354
7732
    funcs = { 'modified': diff.createModifiedFileTabs,
7355
7733
              'separate': diff.createSeparateTabs,
7356
7734
              'single': diff.createSingleTab }
7357
 
    mode = 'single'
 
7735
    mode, options = 'single', {}
7358
7736
    while i < argc:
7359
7737
        arg = args[i]
7360
7738
        if len(arg) > 0 and arg[0] == '-':
7379
7757
                encoding = args[i]
7380
7758
                encoding = encodings.aliases.aliases.get(encoding, encoding)
7381
7759
            elif arg in [ '-m', '--modified' ]:
7382
 
                funcs[mode](specs)
7383
 
                specs = []
 
7760
                funcs[mode](specs, labels, options)
 
7761
                specs, labels, options = [], [], {}
7384
7762
                mode = 'modified'
7385
7763
            elif i + 1 < argc and arg in [ '-r', '--revision' ]:
7386
7764
                # specified revision
7387
7765
                i += 1
7388
 
                revs.append((args[i], encoding))
 
7766
                revs.append((decode_fs_string(args[i]), encoding))
7389
7767
            elif arg in [ '-s', '--separate' ]:
7390
 
                funcs[mode](specs)
7391
 
                specs = []
 
7768
                funcs[mode](specs, labels, options)
 
7769
                specs, labels, options = [], [], {}
7392
7770
                # open items in separate tabs
7393
7771
                mode = 'separate'
7394
 
            elif i + 1 < argc and arg in [ '-t', '--tab' ]:
7395
 
                funcs[mode](specs)
7396
 
                specs = []
 
7772
            elif arg in [ '-t', '--tab' ]:
 
7773
                funcs[mode](specs, labels, options)
 
7774
                specs, labels, options = [], [], {}
7397
7775
                # start a new tab
7398
7776
                mode = 'single'
7399
7777
            elif arg in [ '-b', '--ignore-space-change' ]:
7416
7794
                diff.prefs.setBool('display_ignore_whitespace', True)
7417
7795
                diff.prefs.setBool('align_ignore_whitespace', True)
7418
7796
                diff.preferences_updated()
 
7797
            elif i + 1 < argc and arg == '-L':
 
7798
                i += 1
 
7799
                labels.append(decode_fs_string(args[i]))
 
7800
            elif i + 1 < argc and arg == '--line':
 
7801
                i += 1
 
7802
                try:
 
7803
                    options['line'] = int(args[i])
 
7804
                except ValueError:
 
7805
                    logError(_('Error parsing line number.'))
7419
7806
            elif arg == '--null-file':
7420
7807
                # add a blank file pane
7421
7808
                if mode == 'single' or mode == 'separate':
7444
7831
    if mode == 'modified' and len(specs) == 0:
7445
7832
        specs.append((os.curdir, [ (None, encoding) ]))
7446
7833
        had_specs = True
7447
 
    funcs[mode](specs)
 
7834
    funcs[mode](specs, labels, options)
7448
7835
 
7449
7836
    # create a file diff viewer if the command line arguments haven't
7450
7837
    # implicitly created any