/b-gtk/fix-viz

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/b-gtk/fix-viz

« back to all changes in this revision

Viewing changes to annotate/gannotate.py

  • Committer: Daniel Schierbeck
  • Date: 2008-04-07 20:34:51 UTC
  • mfrom: (450.6.13 bugs)
  • mto: (463.2.1 bug.78765)
  • mto: This revision was merged to the branch mainline in revision 462.
  • Revision ID: daniel.schierbeck@gmail.com-20080407203451-2i6el7jf9t0k9y64
Merged bug page improvements.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 Dan Loda <danloda@gmail.com>
 
2
 
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
 
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
 
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
import time
 
18
 
 
19
import pygtk
 
20
pygtk.require("2.0")
 
21
import gobject
 
22
import gtk
 
23
import pango
 
24
import re
 
25
 
 
26
from bzrlib import patiencediff, tsort
 
27
from bzrlib.errors import NoSuchRevision
 
28
from bzrlib.revision import NULL_REVISION, CURRENT_REVISION
 
29
 
 
30
from colormap import AnnotateColorMap, AnnotateColorSaturation
 
31
from bzrlib.plugins.gtk.revisionview import RevisionView
 
32
from bzrlib.plugins.gtk.window import Window
 
33
 
 
34
 
 
35
(
 
36
    REVISION_ID_COL,
 
37
    LINE_NUM_COL,
 
38
    COMMITTER_COL,
 
39
    REVNO_COL,
 
40
    HIGHLIGHT_COLOR_COL,
 
41
    TEXT_LINE_COL
 
42
) = range(6)
 
43
 
 
44
 
 
45
class GAnnotateWindow(Window):
 
46
    """Annotate window."""
 
47
 
 
48
    def __init__(self, all=False, plain=False, parent=None):
 
49
        self.all = all
 
50
        self.plain = plain
 
51
        
 
52
        Window.__init__(self, parent)
 
53
        
 
54
        self.set_icon(self.render_icon(gtk.STOCK_FIND, gtk.ICON_SIZE_BUTTON))
 
55
        self.annotate_colormap = AnnotateColorSaturation()
 
56
 
 
57
        self._create()
 
58
        self.revisions = {}
 
59
        self.history = []
 
60
        self._no_back = set()
 
61
 
 
62
    def annotate(self, tree, branch, file_id):
 
63
        self.annotations = []
 
64
        self.branch = branch
 
65
        self.tree = tree
 
66
        self.file_id = file_id
 
67
        self.revisionview.set_file_id(file_id)
 
68
        self.revision_id = getattr(tree, 'get_revision_id', 
 
69
                                   lambda: CURRENT_REVISION)()
 
70
        
 
71
        # [revision id, line number, author, revno, highlight color, line]
 
72
        self.annomodel = gtk.ListStore(gobject.TYPE_STRING,
 
73
                                       gobject.TYPE_STRING,
 
74
                                       gobject.TYPE_STRING,
 
75
                                       gobject.TYPE_STRING,
 
76
                                       gobject.TYPE_STRING,
 
77
                                       gobject.TYPE_STRING)
 
78
        
 
79
        last_seen = None
 
80
        try:
 
81
            branch.lock_read()
 
82
            branch.repository.lock_read()
 
83
            self.dotted = {}
 
84
            revno_map = self.branch.get_revision_id_to_revno_map()
 
85
            for revision_id, revno in revno_map.iteritems():
 
86
                self.dotted[revision_id] = '.'.join(str(num) for num in revno)
 
87
            for line_no, (revision, revno, line)\
 
88
                    in enumerate(self._annotate(tree, file_id)):
 
89
                if revision.revision_id == last_seen and not self.all:
 
90
                    revno = author = ""
 
91
                else:
 
92
                    last_seen = revision.revision_id
 
93
                    author = revision.get_apparent_author()
 
94
 
 
95
                if revision.revision_id not in self.revisions:
 
96
                    self.revisions[revision.revision_id] = revision
 
97
 
 
98
                self.annomodel.append([revision.revision_id,
 
99
                                       line_no + 1,
 
100
                                       author,
 
101
                                       revno,
 
102
                                       None,
 
103
                                       line.rstrip("\r\n")
 
104
                                      ])
 
105
                self.annotations.append(revision)
 
106
 
 
107
            if not self.plain:
 
108
                now = time.time()
 
109
                self.annomodel.foreach(self._highlight_annotation, now)
 
110
        finally:
 
111
            branch.repository.unlock()
 
112
            branch.unlock()
 
113
 
 
114
        self.annoview.set_model(self.annomodel)
 
115
        self.annoview.grab_focus()
 
116
        my_revno = self.dotted.get(self.revision_id, 'current')
 
117
        title = '%s (%s) - gannotate' % (self.tree.id2path(file_id), my_revno)
 
118
        self.set_title(title)
 
119
 
 
120
    def jump_to_line(self, lineno):
 
121
        if lineno > len(self.annomodel) or lineno < 1:
 
122
            row = 0
 
123
            # FIXME:should really deal with this in the gui. Perhaps a status
 
124
            # bar?
 
125
            print("gannotate: Line number %d does't exist. Defaulting to "
 
126
                  "line 1." % lineno)
 
127
            return
 
128
        else:
 
129
            row = lineno - 1
 
130
 
 
131
        self.annoview.set_cursor(row)
 
132
        self.annoview.scroll_to_cell(row, use_align=True)
 
133
 
 
134
 
 
135
    def _annotate(self, tree, file_id):
 
136
        current_revision = FakeRevision(CURRENT_REVISION)
 
137
        current_revision.committer = self.branch.get_config().username()
 
138
        current_revision.timestamp = time.time()
 
139
        current_revision.message = '[Not yet committed]'
 
140
        current_revision.parent_ids = tree.get_parent_ids()
 
141
        current_revision.properties['branch-nick'] = self.branch.nick
 
142
        current_revno = '%d?' % (self.branch.revno() + 1)
 
143
        repository = self.branch.repository
 
144
        if self.revision_id == CURRENT_REVISION:
 
145
            revision_id = self.branch.last_revision()
 
146
        else:
 
147
            revision_id = self.revision_id
 
148
        revision_cache = RevisionCache(repository, self.revisions)
 
149
        for origin, text in tree.annotate_iter(file_id):
 
150
            rev_id = origin
 
151
            if rev_id == CURRENT_REVISION:
 
152
                revision = current_revision
 
153
                revno = current_revno
 
154
            else:
 
155
                try:
 
156
                    revision = revision_cache.get_revision(rev_id)
 
157
                    revno = self.dotted.get(rev_id, 'merge')
 
158
                    if len(revno) > 15:
 
159
                        revno = 'merge'
 
160
                except NoSuchRevision:
 
161
                    revision = FakeRevision(rev_id)
 
162
                    revno = "?"
 
163
 
 
164
            yield revision, revno, text
 
165
 
 
166
    def _highlight_annotation(self, model, path, iter, now):
 
167
        revision_id, = model.get(iter, REVISION_ID_COL)
 
168
        revision = self.revisions[revision_id]
 
169
        model.set(iter, HIGHLIGHT_COLOR_COL,
 
170
                  self.annotate_colormap.get_color(revision, now))
 
171
 
 
172
    def _selected_revision(self):
 
173
        (path, col) = self.annoview.get_cursor()
 
174
        if path is None:
 
175
            return None
 
176
        return self.annomodel[path][REVISION_ID_COL]
 
177
 
 
178
    def _activate_selected_revision(self, w):
 
179
        rev_id = self._selected_revision()
 
180
        if rev_id is None:
 
181
            return
 
182
        selected = self.revisions[rev_id]
 
183
        self.revisionview.set_revision(selected)
 
184
        if (len(selected.parent_ids) != 0 and selected.parent_ids[0] not in
 
185
            self._no_back):
 
186
            enable_back = True
 
187
        else:
 
188
            enable_back = False
 
189
        self.back_button.set_sensitive(enable_back)
 
190
 
 
191
    def _create(self):
 
192
        self.revisionview = self._create_log_view()
 
193
        self.annoview = self._create_annotate_view()
 
194
 
 
195
        vbox = gtk.VBox(False)
 
196
        vbox.show()
 
197
 
 
198
        sw = gtk.ScrolledWindow()
 
199
        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 
200
        sw.set_shadow_type(gtk.SHADOW_IN)
 
201
        sw.add(self.annoview)
 
202
        self.annoview.gwindow = self
 
203
        sw.show()
 
204
 
 
205
        swbox = gtk.VBox()
 
206
        swbox.pack_start(sw)
 
207
        swbox.show()
 
208
 
 
209
        hbox = gtk.HBox(False, 6)
 
210
        self.back_button = self._create_back_button()
 
211
        hbox.pack_start(self.back_button, expand=False, fill=True)
 
212
        self.forward_button = self._create_forward_button()
 
213
        hbox.pack_start(self.forward_button, expand=False, fill=True)
 
214
        hbox.show()
 
215
        vbox.pack_start(hbox, expand=False, fill=True)
 
216
        
 
217
        self.pane = pane = gtk.VPaned()
 
218
        pane.add1(swbox)
 
219
        pane.add2(self.revisionview)
 
220
        pane.show()
 
221
        vbox.pack_start(pane, expand=True, fill=True)
 
222
 
 
223
        self._search = SearchBox()
 
224
        swbox.pack_start(self._search, expand=False, fill=True)
 
225
        accels = gtk.AccelGroup()
 
226
        accels.connect_group(gtk.keysyms.f, gtk.gdk.CONTROL_MASK,
 
227
                             gtk.ACCEL_LOCKED,
 
228
                             self._search_by_text)
 
229
        accels.connect_group(gtk.keysyms.g, gtk.gdk.CONTROL_MASK,
 
230
                             gtk.ACCEL_LOCKED,
 
231
                             self._search_by_line)
 
232
        self.add_accel_group(accels)
 
233
 
 
234
        self.add(vbox)
 
235
 
 
236
    def _search_by_text(self, accel_group, window, key, modifiers):
 
237
        self._search.show_for('text')
 
238
        self._search.set_target(self.annoview, TEXT_LINE_COL)
 
239
 
 
240
    def _search_by_line(self, accel_group, window, key, modifiers):
 
241
        self._search.show_for('line')
 
242
        self._search.set_target(self.annoview, LINE_NUM_COL)
 
243
 
 
244
    def line_diff(self, tv, path, tvc):
 
245
        row = path[0]
 
246
        revision = self.annotations[row]
 
247
        repository = self.branch.repository
 
248
        if revision.revision_id == CURRENT_REVISION:
 
249
            tree1 = self.tree
 
250
            tree2 = self.tree.basis_tree()
 
251
        else:
 
252
            tree1 = repository.revision_tree(revision.revision_id)
 
253
            if len(revision.parent_ids) > 0:
 
254
                tree2 = repository.revision_tree(revision.parent_ids[0])
 
255
            else:
 
256
                tree2 = repository.revision_tree(NULL_REVISION)
 
257
        from bzrlib.plugins.gtk.diff import DiffWindow
 
258
        window = DiffWindow()
 
259
        window.set_diff("Diff for line %d" % (row+1), tree1, tree2)
 
260
        window.set_file(tree1.id2path(self.file_id))
 
261
        window.show()
 
262
 
 
263
 
 
264
    def _create_annotate_view(self):
 
265
        tv = gtk.TreeView()
 
266
        tv.set_rules_hint(False)
 
267
        tv.connect("cursor-changed", self._activate_selected_revision)
 
268
        tv.show()
 
269
        tv.connect("row-activated", self.line_diff)
 
270
 
 
271
        cell = gtk.CellRendererText()
 
272
        cell.set_property("xalign", 1.0)
 
273
        cell.set_property("ypad", 0)
 
274
        cell.set_property("family", "Monospace")
 
275
        cell.set_property("cell-background-gdk",
 
276
                          tv.get_style().bg[gtk.STATE_NORMAL])
 
277
        col = gtk.TreeViewColumn()
 
278
        col.set_resizable(False)
 
279
        col.pack_start(cell, expand=True)
 
280
        col.add_attribute(cell, "text", LINE_NUM_COL)
 
281
        tv.append_column(col)
 
282
 
 
283
        cell = gtk.CellRendererText()
 
284
        cell.set_property("ypad", 0)
 
285
        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 
286
        cell.set_property("cell-background-gdk",
 
287
                          self.get_style().bg[gtk.STATE_NORMAL])
 
288
        col = gtk.TreeViewColumn("Committer")
 
289
        col.set_resizable(True)
 
290
        col.pack_start(cell, expand=True)
 
291
        col.add_attribute(cell, "text", COMMITTER_COL)
 
292
        tv.append_column(col)
 
293
 
 
294
        cell = gtk.CellRendererText()
 
295
        cell.set_property("xalign", 1.0)
 
296
        cell.set_property("ypad", 0)
 
297
        cell.set_property("cell-background-gdk",
 
298
                          self.get_style().bg[gtk.STATE_NORMAL])
 
299
        col = gtk.TreeViewColumn("Revno")
 
300
        col.set_resizable(False)
 
301
        col.pack_start(cell, expand=True)
 
302
        col.add_attribute(cell, "markup", REVNO_COL)
 
303
        tv.append_column(col)
 
304
 
 
305
        cell = gtk.CellRendererText()
 
306
        cell.set_property("ypad", 0)
 
307
        cell.set_property("family", "Monospace")
 
308
        col = gtk.TreeViewColumn()
 
309
        col.set_resizable(False)
 
310
        col.pack_start(cell, expand=True)
 
311
#        col.add_attribute(cell, "foreground", HIGHLIGHT_COLOR_COL)
 
312
        col.add_attribute(cell, "background", HIGHLIGHT_COLOR_COL)
 
313
        col.add_attribute(cell, "text", TEXT_LINE_COL)
 
314
        tv.append_column(col)
 
315
 
 
316
        # FIXME: Now that C-f is now used for search by text we
 
317
        # may as well disable the auto search.
 
318
        tv.set_search_column(LINE_NUM_COL)
 
319
 
 
320
        return tv
 
321
 
 
322
    def _create_log_view(self):
 
323
        lv = RevisionView()
 
324
        lv.show()
 
325
        return lv
 
326
 
 
327
    def _create_back_button(self):
 
328
        button = gtk.Button()
 
329
        button.set_use_stock(True)
 
330
        button.set_label("gtk-go-back")
 
331
        button.connect("clicked", lambda w: self.go_back())
 
332
        button.set_relief(gtk.RELIEF_NONE)
 
333
        button.show()
 
334
        return button
 
335
 
 
336
    def _create_forward_button(self):
 
337
        button = gtk.Button()
 
338
        button.set_use_stock(True)
 
339
        button.set_label("gtk-go-forward")
 
340
        button.connect("clicked", lambda w: self.go_forward())
 
341
        button.set_relief(gtk.RELIEF_NONE)
 
342
        button.show()
 
343
        button.set_sensitive(False)
 
344
        return button
 
345
 
 
346
    def go_back(self):
 
347
        last_tree = self.tree
 
348
        rev_id = self._selected_revision()
 
349
        parent_id = self.revisions[rev_id].parent_ids[0]
 
350
        target_tree = self.branch.repository.revision_tree(parent_id)
 
351
        if self._go(target_tree):
 
352
            self.history.append(last_tree)
 
353
            self.forward_button.set_sensitive(True)
 
354
        else:
 
355
            self._no_back.add(parent_id)
 
356
            self.back_button.set_sensitive(False)
 
357
 
 
358
    def go_forward(self):
 
359
        if len(self.history) == 0:
 
360
            return
 
361
        target_tree = self.history.pop()
 
362
        if len(self.history) == 0:
 
363
            self.forward_button.set_sensitive(False)
 
364
        self._go(target_tree)
 
365
 
 
366
    def _go(self, target_tree):
 
367
        rev_id = self._selected_revision()
 
368
        if self.file_id in target_tree:
 
369
            offset = self.get_scroll_offset(target_tree)
 
370
            (row,), col = self.annoview.get_cursor()
 
371
            self.annotate(target_tree, self.branch, self.file_id)
 
372
            new_row = row+offset
 
373
            if new_row < 0:
 
374
                new_row = 0
 
375
            self.annoview.set_cursor(new_row)
 
376
            return True
 
377
        else:
 
378
            return False
 
379
 
 
380
    def get_scroll_offset(self, tree):
 
381
        old = self.tree.get_file(self.file_id)
 
382
        new = tree.get_file(self.file_id)
 
383
        (row,), col = self.annoview.get_cursor()
 
384
        matcher = patiencediff.PatienceSequenceMatcher(None, old.readlines(),
 
385
                                                       new.readlines())
 
386
        for i, j, n in matcher.get_matching_blocks():
 
387
            if i + n >= row:
 
388
                return j - i
 
389
 
 
390
 
 
391
class FakeRevision:
 
392
    """ A fake revision.
 
393
 
 
394
    For when a revision is referenced but not present.
 
395
    """
 
396
 
 
397
    def __init__(self, revision_id, committer='?', nick=None):
 
398
        self.revision_id = revision_id
 
399
        self.parent_ids = []
 
400
        self.committer = committer
 
401
        self.message = "?"
 
402
        self.timestamp = 0.0
 
403
        self.timezone = 0
 
404
        self.properties = {}
 
405
 
 
406
    def get_apparent_author(self):
 
407
        return self.committer
 
408
 
 
409
 
 
410
class RevisionCache(object):
 
411
    """A caching revision source"""
 
412
    def __init__(self, real_source, seed_cache=None):
 
413
        self.__real_source = real_source
 
414
        if seed_cache is None:
 
415
            self.__cache = {}
 
416
        else:
 
417
            self.__cache = dict(seed_cache)
 
418
 
 
419
    def get_revision(self, revision_id):
 
420
        if revision_id not in self.__cache:
 
421
            revision = self.__real_source.get_revision(revision_id)
 
422
            self.__cache[revision_id] = revision
 
423
        return self.__cache[revision_id]
 
424
 
 
425
class SearchBox(gtk.HBox):
 
426
    """A button box for searching in text or lines of annotations"""
 
427
    def __init__(self):
 
428
        gtk.HBox.__init__(self, False, 6)
 
429
 
 
430
        # Close button
 
431
        button = gtk.Button()
 
432
        image = gtk.Image()
 
433
        image.set_from_stock('gtk-stop', gtk.ICON_SIZE_BUTTON)
 
434
        button.set_image(image)
 
435
        button.set_relief(gtk.RELIEF_NONE)
 
436
        button.connect("clicked", lambda w: self.hide_all())
 
437
        self.pack_start(button, expand=False, fill=False)
 
438
 
 
439
        # Search entry
 
440
        label = gtk.Label()
 
441
        self._label = label
 
442
        self.pack_start(label, expand=False, fill=False)
 
443
 
 
444
        entry = gtk.Entry()
 
445
        self._entry = entry
 
446
        entry.connect("activate", lambda w, d: self._do_search(d),
 
447
                      'forward')
 
448
        self.pack_start(entry, expand=False, fill=False)
 
449
 
 
450
        # Next/previous buttons
 
451
        button = gtk.Button('_Next')
 
452
        image = gtk.Image()
 
453
        image.set_from_stock('gtk-go-forward', gtk.ICON_SIZE_BUTTON)
 
454
        button.set_image(image)
 
455
        button.connect("clicked", lambda w, d: self._do_search(d),
 
456
                       'forward')
 
457
        self.pack_start(button, expand=False, fill=False)
 
458
 
 
459
        button = gtk.Button('_Previous')
 
460
        image = gtk.Image()
 
461
        image.set_from_stock('gtk-go-back', gtk.ICON_SIZE_BUTTON)
 
462
        button.set_image(image)
 
463
        button.connect("clicked", lambda w, d: self._do_search(d),
 
464
                       'backward')
 
465
        self.pack_start(button, expand=False, fill=False)
 
466
 
 
467
        # Search options
 
468
        check = gtk.CheckButton('Match case')
 
469
        self._match_case = check
 
470
        self.pack_start(check, expand=False, fill=False)
 
471
 
 
472
        check = gtk.CheckButton('Regexp')
 
473
        check.connect("toggled", lambda w: self._set_label())
 
474
        self._regexp = check
 
475
        self.pack_start(check, expand=False, fill=False)
 
476
 
 
477
        self._view = None
 
478
        self._column = None
 
479
        # Note that we stay hidden (we do not call self.show_all())
 
480
 
 
481
 
 
482
    def show_for(self, kind):
 
483
        self._kind = kind
 
484
        self.show_all()
 
485
        self._set_label()
 
486
        # Hide unrelated buttons
 
487
        if kind == 'line':
 
488
            self._match_case.hide()
 
489
            self._regexp.hide()
 
490
        # Be ready
 
491
        self._entry.grab_focus()
 
492
 
 
493
    def _set_label(self):
 
494
        if self._kind == 'line':
 
495
            self._label.set_text('Find Line: ')
 
496
        else:
 
497
            if self._regexp.get_active():
 
498
                self._label.set_text('Find Regexp: ')
 
499
            else:
 
500
                self._label.set_text('Find Text: ')
 
501
 
 
502
    def set_target(self, view,column):
 
503
        self._view = view
 
504
        self._column = column
 
505
 
 
506
    def _match(self, model, iterator, column):
 
507
        matching_case = self._match_case.get_active()
 
508
        string, = model.get(iterator, column)
 
509
        key = self._entry.get_text()
 
510
        if self._regexp.get_active():
 
511
            if matching_case:
 
512
                match = re.compile(key).search(string, 1)
 
513
            else:
 
514
                match = re.compile(key, re.I).search(string, 1)
 
515
        else:
 
516
            if not matching_case:
 
517
                string = string.lower()
 
518
                key = key.lower()
 
519
            match = string.find(key) != -1
 
520
 
 
521
        return match
 
522
 
 
523
    def _iterate_rows_forward(self, model, start):
 
524
        model_size = len(model)
 
525
        current = start + 1
 
526
        while model_size != 0:
 
527
            if current >= model_size: current =  0
 
528
            yield model.get_iter_from_string('%d' % current)
 
529
            if current == start: raise StopIteration
 
530
            current += 1
 
531
 
 
532
    def _iterate_rows_backward(self, model, start):
 
533
        model_size = len(model)
 
534
        current = start - 1
 
535
        while model_size != 0:
 
536
            if current < 0: current = model_size - 1
 
537
            yield model.get_iter_from_string('%d' % current)
 
538
            if current == start: raise StopIteration
 
539
            current -= 1
 
540
 
 
541
    def _do_search(self, direction):
 
542
        if direction == 'forward':
 
543
            iterate = self._iterate_rows_forward
 
544
        else:
 
545
            iterate = self._iterate_rows_backward
 
546
 
 
547
        model, sel = self._view.get_selection().get_selected()
 
548
        if sel is None:
 
549
            start = 0
 
550
        else:
 
551
            path = model.get_string_from_iter(sel)
 
552
            start = int(path)
 
553
 
 
554
        for row in iterate(model, start):
 
555
            if self._match(model, row, self._column):
 
556
                path = model.get_path(row)
 
557
                self._view.set_cursor(path)
 
558
                self._view.scroll_to_cell(path, use_align=True)
 
559
                break