~ubuntu-branches/debian/sid/qbzr/sid

« back to all changes in this revision

Viewing changes to lib/diffwindow.py

  • Committer: Bazaar Package Importer
  • Author(s): Jelmer Vernooij
  • Date: 2009-12-05 01:20:38 UTC
  • Revision ID: james.westby@ubuntu.com-20091205012038-41f57s3ecv2r34lz
Tags: upstream-0.16
Import upstream version 0.16

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
#
 
3
# QBzr - Qt frontend to Bazaar commands
 
4
# Copyright (C) 2006 Lukáš Lalinský <lalinsky@gmail.com>
 
5
# Portions Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
 
6
# Portions Copyright (C) 2005 Canonical Ltd. (author: Scott James Remnant <scott@ubuntu.com>)
 
7
# Portions Copyright (C) 2004-2006 Christopher Lenz <cmlenz@gmx.de>
 
8
#
 
9
# This program is free software; you can redistribute it and/or
 
10
# modify it under the terms of the GNU General Public License
 
11
# as published by the Free Software Foundation; either version 2
 
12
# of the License, or (at your option) any later version.
 
13
#
 
14
# This program is distributed in the hope that it will be useful,
 
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
17
# GNU General Public License for more details.
 
18
#
 
19
# You should have received a copy of the GNU General Public License
 
20
# along with this program; if not, write to the Free Software
 
21
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 
22
 
 
23
import errno
 
24
import time
 
25
 
 
26
from PyQt4 import QtCore, QtGui
 
27
 
 
28
from bzrlib.errors import NoSuchRevision, PathsNotVersionedError
 
29
from bzrlib.mutabletree import MutableTree
 
30
from bzrlib.patiencediff import PatienceSequenceMatcher as SequenceMatcher
 
31
from bzrlib.revisiontree import RevisionTree
 
32
from bzrlib.transform import _PreviewTree
 
33
from bzrlib.workingtree import WorkingTree
 
34
from bzrlib.workingtree_4 import DirStateRevisionTree
 
35
from bzrlib import trace
 
36
 
 
37
from bzrlib.plugins.qbzr.lib.diffview import (
 
38
    SidebySideDiffView,
 
39
    SimpleDiffView,
 
40
    )
 
41
from bzrlib.plugins.qbzr.lib.diff import (
 
42
    show_diff,
 
43
    has_ext_diff,
 
44
    ExtDiffMenu,
 
45
    )
 
46
 
 
47
from bzrlib.plugins.qbzr.lib.i18n import gettext, ngettext, N_
 
48
from bzrlib.plugins.qbzr.lib.util import (
 
49
    BTN_CLOSE, BTN_REFRESH,
 
50
    FilterOptions,
 
51
    QBzrWindow,
 
52
    ThrobberWidget,
 
53
    StandardButton,
 
54
    get_set_encoding,
 
55
    is_binary_content,
 
56
    run_in_loading_queue,
 
57
    runs_in_loading_queue
 
58
    )
 
59
from bzrlib.plugins.qbzr.lib.uifactory import ui_current_widget
 
60
from bzrlib.plugins.qbzr.lib.trace import reports_exception
 
61
 
 
62
 
 
63
def get_file_lines_from_tree(tree, file_id):
 
64
    try:
 
65
        return tree.get_file_lines(file_id)
 
66
    except AttributeError:
 
67
        return tree.get_file(file_id).readlines()
 
68
 
 
69
def get_title_for_tree(tree, branch, other_branch):
 
70
    branch_title = ""
 
71
    if None not in (branch, other_branch) and branch.base != other_branch.base:
 
72
        branch_title = branch.nick
 
73
    
 
74
    if isinstance(tree, WorkingTree):
 
75
        if branch_title:
 
76
            return gettext("Working Tree for %s") % branch_title
 
77
        else:
 
78
            return gettext("Working Tree")
 
79
    
 
80
    elif isinstance(tree, (RevisionTree, DirStateRevisionTree)):
 
81
        # revision_id_to_revno is faster, but only works on mainline rev
 
82
        revid = tree.get_revision_id()
 
83
        try:
 
84
            revno = branch.revision_id_to_revno(revid)
 
85
        except NoSuchRevision:
 
86
            try:
 
87
                revno_map = branch.get_revision_id_to_revno_map()
 
88
                revno_tuple = revno_map[revid]      # this can raise KeyError is revision not in the branch
 
89
                revno = ".".join("%d" % i for i in revno_tuple)
 
90
            except KeyError:
 
91
                # this can happens when you try to diff against other branch
 
92
                # or pending merge
 
93
                revno = revid
 
94
 
 
95
        if revno is not None:
 
96
            if branch_title:
 
97
                return gettext("Rev %(rev)s for %(branch)s") % {"rev": revno, "branch": branch_title}
 
98
            else:
 
99
                return gettext("Rev %s") % revno
 
100
        else:
 
101
            if branch_title:
 
102
                return gettext("Revid: %(revid)s for %(branch)s") %  {"revid": revid, "branch": branch_title}
 
103
            else:
 
104
                return gettext("Revid: %s") % revid
 
105
 
 
106
    elif isinstance(tree, _PreviewTree):
 
107
        return gettext('Merge Preview')
 
108
 
 
109
    # XXX I don't know what other cases we need to handle    
 
110
    return 'Unknown tree'
 
111
 
 
112
 
 
113
class DiffWindow(QBzrWindow):
 
114
 
 
115
    def __init__(self, arg_provider, parent=None,
 
116
                 complete=False, encoding=None,
 
117
                 filter_options=None, ui_mode=True):
 
118
 
 
119
        title = [gettext("Diff"), gettext("Loading...")]
 
120
        QBzrWindow.__init__(self, title, parent, ui_mode=ui_mode)
 
121
        self.restoreSize("diff", (780, 580))
 
122
 
 
123
        self.trees = None
 
124
        self.encoding = encoding
 
125
        self.arg_provider = arg_provider
 
126
        self.filter_options = filter_options
 
127
        if filter_options is None:
 
128
            self.filter_options = FilterOptions(all_enable=True)
 
129
        self.complete = complete
 
130
 
 
131
        self.throbber = ThrobberWidget(self)
 
132
        
 
133
        self.diffview = SidebySideDiffView(self)
 
134
        self.sdiffview = SimpleDiffView(self)
 
135
        self.views = (self.diffview, self.sdiffview)
 
136
 
 
137
        self.stack = QtGui.QStackedWidget(self.centralwidget)
 
138
        self.stack.addWidget(self.diffview)
 
139
        self.stack.addWidget(self.sdiffview)
 
140
 
 
141
        vbox = QtGui.QVBoxLayout(self.centralwidget)
 
142
        vbox.addWidget(self.throbber)
 
143
        vbox.addWidget(self.stack)
 
144
 
 
145
        diffsidebyside = QtGui.QRadioButton(gettext("Side by side"),
 
146
                                            self.centralwidget)
 
147
        self.connect(diffsidebyside,
 
148
                     QtCore.SIGNAL("clicked(bool)"),
 
149
                     self.click_diffsidebyside)
 
150
        diffsidebyside.setChecked(True);
 
151
 
 
152
        unidiff = QtGui.QRadioButton(gettext("Unidiff"), self.centralwidget)
 
153
        self.connect(unidiff,
 
154
                     QtCore.SIGNAL("clicked(bool)"),
 
155
                     self.click_unidiff)
 
156
 
 
157
        complete = QtGui.QCheckBox (gettext("Complete"),
 
158
                                            self.centralwidget)
 
159
        self.connect(complete,
 
160
                     QtCore.SIGNAL("clicked(bool)"),
 
161
                     self.click_complete)
 
162
        complete.setChecked(self.complete);
 
163
        
 
164
        if has_ext_diff():
 
165
            self.menu = ExtDiffMenu(include_builtin = False)
 
166
            ext_diff_button = QtGui.QPushButton(gettext('Using'), self)
 
167
            ext_diff_button.setMenu(self.menu)
 
168
            self.connect(self.menu, QtCore.SIGNAL("triggered(QString)"),
 
169
                         self.ext_diff_triggered)
 
170
 
 
171
        buttonbox = self.create_button_box(BTN_CLOSE)
 
172
 
 
173
        refresh = StandardButton(BTN_REFRESH)
 
174
        refresh.setEnabled(self.can_refresh())
 
175
        buttonbox.addButton(refresh, QtGui.QDialogButtonBox.ActionRole)
 
176
        self.connect(refresh,
 
177
                     QtCore.SIGNAL("clicked()"),
 
178
                     self.click_refresh)
 
179
        self.refresh_button = refresh
 
180
 
 
181
        hbox = QtGui.QHBoxLayout()
 
182
        hbox.addWidget(diffsidebyside)
 
183
        hbox.addWidget(unidiff)
 
184
        hbox.addWidget(complete)
 
185
        if has_ext_diff():
 
186
            hbox.addWidget(ext_diff_button)
 
187
        hbox.addWidget(buttonbox)
 
188
        vbox.addLayout(hbox)
 
189
 
 
190
    def show(self):
 
191
        QBzrWindow.show(self)
 
192
        QtCore.QTimer.singleShot(1, self.initial_load)
 
193
 
 
194
    @runs_in_loading_queue
 
195
    @ui_current_widget
 
196
    @reports_exception()
 
197
    def initial_load(self):
 
198
        """Called to perform the initial load of the form.  Enables a
 
199
        throbber window, then loads the branches etc if they weren't specified
 
200
        in our constructor.
 
201
        """
 
202
        # we only open the branch using the throbber
 
203
        self.throbber.show()
 
204
        try:
 
205
            self.load_branch_info()
 
206
            self.load_diff()
 
207
        finally:
 
208
            self.throbber.hide()
 
209
 
 
210
    def load_branch_info(self):
 
211
        
 
212
        (tree1, tree2,
 
213
         branch1, branch2,
 
214
         specific_files) = self.arg_provider.get_diff_window_args(self.processEvents)
 
215
        
 
216
        self.trees = (tree1, tree2)
 
217
        self.branches = (branch1, branch2)
 
218
        self.specific_files = specific_files
 
219
        
 
220
        self.set_diff_title()
 
221
        
 
222
        self.encodings = (get_set_encoding(self.encoding, branch1),
 
223
                          get_set_encoding(self.encoding, branch2))
 
224
        self.processEvents()
 
225
    
 
226
    def set_diff_title(self):
 
227
        rev1_title = get_title_for_tree(self.trees[0], self.branches[0],
 
228
                                        self.branches[1])
 
229
        rev2_title = get_title_for_tree(self.trees[1], self.branches[1],
 
230
                                        self.branches[0])
 
231
        
 
232
        title = [gettext("Diff"), "%s..%s" % (rev1_title, rev2_title)]
 
233
 
 
234
        if self.specific_files:
 
235
            nfiles = len(self.specific_files)
 
236
            if nfiles > 2:
 
237
                title.append(
 
238
                    ngettext("%d file", "%d files", nfiles) % nfiles)
 
239
            else:
 
240
                title.append(", ".join(self.specific_files))
 
241
        else:
 
242
            if self.filter_options and not self.filter_options.is_all_enable():
 
243
                title.append(self.filter_options.to_str())
 
244
 
 
245
        self.set_title_and_icon(title)
 
246
        self.processEvents()        
 
247
 
 
248
    def load_diff(self):
 
249
        self.refresh_button.setEnabled(False)
 
250
        for tree in self.trees: tree.lock_read()
 
251
        self.processEvents()
 
252
        try:
 
253
            changes = self.trees[1].iter_changes(self.trees[0],
 
254
                                                 specific_files=self.specific_files,
 
255
                                                 require_versioned=True)
 
256
            def changes_key(change):
 
257
                old_path, new_path = change[1]
 
258
                path = new_path
 
259
                if path is None:
 
260
                    path = old_path
 
261
                return path
 
262
 
 
263
            try:
 
264
                no_changes = True   # if there is no changes found we need to inform the user
 
265
                for (file_id, paths, changed_content, versioned, parent, name, kind,
 
266
                     executable) in sorted(changes, key=changes_key):
 
267
                    # file_id         -> ascii string
 
268
                    # paths           -> 2-tuple (old, new) fullpaths unicode/None
 
269
                    # changed_content -> bool
 
270
                    # versioned       -> 2-tuple (bool, bool)
 
271
                    # parent          -> 2-tuple
 
272
                    # name            -> 2-tuple (old_name, new_name) utf-8?/None
 
273
                    # kind            -> 2-tuple (string/None, string/None)
 
274
                    # executable      -> 2-tuple (bool/None, bool/None)
 
275
                    # NOTE: None value used for non-existing entry in corresponding
 
276
                    #       tree, e.g. for added/deleted file
 
277
 
 
278
                    self.processEvents()
 
279
 
 
280
                    if parent == (None, None):  # filter out TREE_ROOT (?)
 
281
                        continue
 
282
 
 
283
                    # check for manually deleted files (w/o using bzr rm commands)
 
284
                    if kind[1] is None:
 
285
                        if versioned == (False, True):
 
286
                            # added and missed
 
287
                            continue
 
288
                        if versioned == (True, True):
 
289
                            versioned = (True, False)
 
290
                            paths = (paths[0], None)
 
291
 
 
292
                    renamed = (parent[0], name[0]) != (parent[1], name[1])
 
293
 
 
294
                    dates = [None, None]
 
295
                    for ix in range(2):
 
296
                        if versioned[ix]:
 
297
                            try:
 
298
                                dates[ix] = self.trees[ix].get_file_mtime(file_id, paths[ix])
 
299
                            except OSError, e:
 
300
                                if not renamed or e.errno != errno.ENOENT:
 
301
                                    raise
 
302
                                # If we get ENOENT error then probably we trigger
 
303
                                # bug #251532 in bzrlib. Take current time instead
 
304
                                dates[ix] = time.time()
 
305
 
 
306
                    properties_changed = [] 
 
307
                    if bool(executable[0]) != bool(executable[1]):
 
308
                        descr = {True: "+x", False: "-x", None: None}
 
309
                        properties_changed.append((descr[executable[0]],
 
310
                                                   descr[executable[1]]))
 
311
 
 
312
                    if versioned == (True, False):
 
313
                        status = N_('removed')
 
314
                    elif versioned == (False, True):
 
315
                        status = N_('added')
 
316
                    elif renamed and changed_content:
 
317
                        status = N_('renamed and modified')
 
318
                    elif renamed:
 
319
                        status = N_('renamed')
 
320
                    else:
 
321
                        status = N_('modified')
 
322
                    # check filter options
 
323
                    if not self.filter_options.check(status):
 
324
                        continue
 
325
 
 
326
                    if ((versioned[0] != versioned[1] or changed_content)
 
327
                        and (kind[0] == 'file' or kind[1] == 'file')):
 
328
                        lines = []
 
329
                        binary = False
 
330
                        for ix, tree in enumerate(self.trees):
 
331
                            content = ()
 
332
                            if versioned[ix] and kind[ix] == 'file':
 
333
                                content = get_file_lines_from_tree(tree, file_id)
 
334
                            lines.append(content)
 
335
                            binary = binary or is_binary_content(content)
 
336
                            self.processEvents()
 
337
                        if not binary:
 
338
                            if versioned == (True, False):
 
339
                                groups = [[('delete', 0, len(lines[0]), 0, 0)]]
 
340
                            elif versioned == (False, True):
 
341
                                groups = [[('insert', 0, 0, 0, len(lines[1]))]]
 
342
                            else:
 
343
                                matcher = SequenceMatcher(None, lines[0], lines[1])
 
344
                                self.processEvents()
 
345
                                if self.complete:
 
346
                                    groups = list([matcher.get_opcodes()])
 
347
                                else:
 
348
                                    groups = list(matcher.get_grouped_opcodes())
 
349
                            try:
 
350
                                lines = [[i.decode(encoding) for i in l]
 
351
                                        for l, encoding in zip(lines, self.encodings)]
 
352
                            except UnicodeDecodeError, e:
 
353
                                trace.note('Failed to decode using %s, falling back to latin1' % e.encoding)
 
354
                                lines = [[i.decode('latin1') for i in l] for l in lines]
 
355
                            data = ((),())
 
356
                        else:
 
357
                            groups = []
 
358
                        data = [''.join(l) for l in lines]
 
359
                    else:
 
360
                        binary = False
 
361
                        lines = ((),())
 
362
                        groups = ()
 
363
                        data = ("", "")
 
364
                    for view in self.views:
 
365
                        view.append_diff(list(paths), file_id, kind, status,
 
366
                                         dates, versioned, binary, lines, groups,
 
367
                                         data, properties_changed)
 
368
                        self.processEvents()
 
369
                    no_changes = False
 
370
            except PathsNotVersionedError, e:
 
371
                    QtGui.QMessageBox.critical(self, gettext('Diff'),
 
372
                        gettext(u'File %s is not versioned.\n'
 
373
                            'Operation aborted.') % e.paths_as_string,
 
374
                        gettext('&Close'))
 
375
                    self.close()
 
376
        finally:
 
377
            for tree in self.trees: tree.unlock()
 
378
        if no_changes:
 
379
            QtGui.QMessageBox.information(self, gettext('Diff'),
 
380
                gettext('No changes found.'),
 
381
                gettext('&OK'))
 
382
        self.refresh_button.setEnabled(self.can_refresh())
 
383
 
 
384
    def click_unidiff(self, checked):
 
385
        if checked:
 
386
            self.sdiffview.rewind()
 
387
            self.stack.setCurrentIndex(1)
 
388
 
 
389
    def click_diffsidebyside(self, checked):
 
390
        if checked:
 
391
            self.diffview.rewind()
 
392
            self.stack.setCurrentIndex(0)
 
393
    
 
394
    def click_complete(self, checked ):
 
395
        self.complete = checked
 
396
        #Has the side effect of refreshing...
 
397
        self.diffview.clear()
 
398
        self.sdiffview.clear()
 
399
        run_in_loading_queue(self.load_diff)
 
400
 
 
401
    def click_refresh(self):
 
402
        self.diffview.clear()
 
403
        self.sdiffview.clear()
 
404
        run_in_loading_queue(self.load_diff)
 
405
 
 
406
    def can_refresh(self):
 
407
        """Does any of tree is Mutanble/Working tree."""
 
408
        if self.trees is None: # we might still be loading...
 
409
            return False
 
410
        tree1, tree2 = self.trees
 
411
        if isinstance(tree1, MutableTree) or isinstance(tree2, MutableTree):
 
412
            return True
 
413
        return False
 
414
    
 
415
    def ext_diff_triggered(self, ext_diff):
 
416
        show_diff(self.arg_provider, ext_diff=ext_diff, parent_window = self)