1
# Copyright (C) 2005 Dan Loda <danloda@gmail.com>
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.
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.
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
19
from gi.repository import GObject
20
from gi.repository import Gdk
21
from gi.repository import Gtk
22
from gi.repository import Pango
25
from bzrlib import patiencediff
26
from bzrlib.errors import NoSuchRevision
27
from bzrlib.revision import NULL_REVISION, CURRENT_REVISION
29
from bzrlib.plugins.gtk.annotate.colormap import AnnotateColorSaturation
30
from bzrlib.plugins.gtk.i18n import _i18n
31
from bzrlib.plugins.gtk.revisionview import RevisionView
32
from bzrlib.plugins.gtk.window import Window
45
class GAnnotateWindow(Window):
46
"""Annotate window."""
48
def __init__(self, all=False, plain=False, parent=None, branch=None):
53
super(GAnnotateWindow, self).__init__(parent=parent)
56
self.render_icon_pixbuf(Gtk.STOCK_FIND, Gtk.IconSize.BUTTON))
57
self.annotate_colormap = AnnotateColorSaturation()
64
def annotate(self, tree, branch, file_id):
68
self.file_id = file_id
69
self.revisionview.set_file_id(file_id)
70
self.revision_id = getattr(tree, 'get_revision_id',
71
lambda: CURRENT_REVISION)()
73
# [revision id, line number, author, revno, highlight color, line]
74
self.annomodel = Gtk.ListStore(GObject.TYPE_STRING,
84
branch.repository.lock_read()
86
revno_map = self.branch.get_revision_id_to_revno_map()
87
for revision_id, revno in revno_map.iteritems():
88
self.dotted[revision_id] = '.'.join(str(num) for num in revno)
89
for line_no, (revision, revno, line)\
90
in enumerate(self._annotate(tree, file_id)):
91
if revision.revision_id == last_seen and not self.all:
94
last_seen = revision.revision_id
95
author = ", ".join(revision.get_apparent_authors())
97
if revision.revision_id not in self.revisions:
98
self.revisions[revision.revision_id] = revision
100
self.annomodel.append([revision.revision_id,
107
self.annotations.append(revision)
111
self.annomodel.foreach(self._highlight_annotation, now)
113
branch.repository.unlock()
116
self.annoview.set_model(self.annomodel)
117
self.annoview.grab_focus()
118
my_revno = self.dotted.get(self.revision_id, 'current')
119
title = '%s (%s) - gannotate' % (self.tree.id2path(file_id), my_revno)
120
self.set_title(title)
122
def jump_to_line(self, lineno):
123
if lineno > len(self.annomodel) or lineno < 1:
125
# FIXME:should really deal with this in the gui. Perhaps a status
127
print("gannotate: Line number %d does't exist. Defaulting to "
133
tree_path = Gtk.TreePath(path=row)
134
self.annoview.set_cursor(tree_path, None, False)
135
self.annoview.scroll_to_cell(tree_path, use_align=True)
137
def _annotate(self, tree, file_id):
138
current_revision = FakeRevision(CURRENT_REVISION)
139
current_revision.committer = self.branch.get_config().username()
140
current_revision.timestamp = time.time()
141
current_revision.message = '[Not yet committed]'
142
current_revision.parent_ids = tree.get_parent_ids()
143
current_revision.properties['branch-nick'] = self.branch._get_nick(local=True)
144
current_revno = '%d?' % (self.branch.revno() + 1)
145
repository = self.branch.repository
146
if self.revision_id == CURRENT_REVISION:
147
revision_id = self.branch.last_revision()
149
revision_id = self.revision_id
150
revision_cache = RevisionCache(repository, self.revisions)
151
for origin, text in tree.annotate_iter(file_id):
153
if rev_id == CURRENT_REVISION:
154
revision = current_revision
155
revno = current_revno
158
revision = revision_cache.get_revision(rev_id)
159
revno = self.dotted.get(rev_id, 'merge')
162
except NoSuchRevision:
163
revision = FakeRevision(rev_id)
166
yield revision, revno, text
168
def _highlight_annotation(self, model, path, iter, now):
169
revision_id, = model.get(iter, REVISION_ID_COL)
170
revision = self.revisions[revision_id]
171
# XXX sinzui 2011-08-12: What does get_color return?
172
color = self.annotate_colormap.get_color(revision, now)
173
model.set_value(iter, HIGHLIGHT_COLOR_COL, color)
175
def _selected_revision(self):
176
(path, col) = self.annoview.get_cursor()
179
return self.annomodel[path][REVISION_ID_COL]
181
def _activate_selected_revision(self, w):
182
rev_id = self._selected_revision()
183
if not rev_id or rev_id == NULL_REVISION:
185
selected = self.revisions[rev_id]
186
self.revisionview.set_revision(selected)
187
if (len(selected.parent_ids) != 0 and selected.parent_ids[0] not in
192
self.back_button.set_sensitive(enable_back)
195
self.revisionview = self._create_log_view()
196
self.annoview = self._create_annotate_view()
198
vbox = Gtk.VBox(homogeneous=False, spacing=0)
201
sw = Gtk.ScrolledWindow()
202
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
203
sw.set_shadow_type(Gtk.ShadowType.IN)
204
sw.add(self.annoview)
205
self.annoview.gwindow = self
209
swbox.pack_start(sw, True, True, 0)
212
hbox = Gtk.HBox(homogeneous=False, spacing=6)
213
self.back_button = self._create_back_button()
214
hbox.pack_start(self.back_button, False, True, 0)
215
self.forward_button = self._create_forward_button()
216
hbox.pack_start(self.forward_button, False, True, 0)
217
self.find_button = self._create_find_button()
218
hbox.pack_start(self.find_button, False, True, 0)
219
self.goto_button = self._create_goto_button()
220
hbox.pack_start(self.goto_button, False, True, 0)
222
vbox.pack_start(hbox, False, True, 0)
224
self.pane = pane = Gtk.Paned.new(Gtk.Orientation.VERTICAL)
226
pane.add2(self.revisionview)
228
vbox.pack_start(pane, True, True, 0)
230
self._search = SearchBox()
231
swbox.pack_start(self._search, False, True, 0)
232
accels = Gtk.AccelGroup()
233
accels.connect(Gdk.KEY_f, Gdk.ModifierType.CONTROL_MASK,
234
Gtk.AccelFlags.LOCKED,
235
self._search_by_text)
236
accels.connect(Gdk.KEY_g, Gdk.ModifierType.CONTROL_MASK,
237
Gtk.AccelFlags.LOCKED,
238
self._search_by_line)
239
self.add_accel_group(accels)
243
def _search_by_text(self, *ignored): # (accel_group, window, key, modifiers):
244
self._search.show_for('text')
245
self._search.set_target(self.annoview, TEXT_LINE_COL)
247
def _search_by_line(self, *ignored): # accel_group, window, key, modifiers):
248
self._search.show_for('line')
249
self._search.set_target(self.annoview, LINE_NUM_COL)
251
def line_diff(self, tv, path, tvc):
252
row = path.get_indices()[0]
253
revision = self.annotations[row]
254
repository = self.branch.repository
255
if revision.revision_id == CURRENT_REVISION:
257
tree2 = self.tree.basis_tree()
259
tree1 = repository.revision_tree(revision.revision_id)
260
if len(revision.parent_ids) > 0:
261
tree2 = repository.revision_tree(revision.parent_ids[0])
263
tree2 = repository.revision_tree(NULL_REVISION)
264
from bzrlib.plugins.gtk.diff import DiffWindow
265
window = DiffWindow(self)
266
window.set_diff("Diff for line %d" % (row+1), tree1, tree2)
267
window.set_file(tree1.id2path(self.file_id))
271
def _create_annotate_view(self):
273
tv.set_rules_hint(False)
274
tv.connect("cursor-changed", self._activate_selected_revision)
276
tv.connect("row-activated", self.line_diff)
278
cell = Gtk.CellRendererText()
279
cell.set_property("xalign", 1.0)
280
cell.set_property("ypad", 0)
281
cell.set_property("family", "Monospace")
282
cell.set_property("cell-background-gdk",
283
tv.get_style().bg[Gtk.StateType.NORMAL])
284
col = Gtk.TreeViewColumn()
285
col.set_resizable(False)
286
col.pack_start(cell, True)
287
col.add_attribute(cell, "text", LINE_NUM_COL)
288
tv.append_column(col)
290
cell = Gtk.CellRendererText()
291
cell.set_property("ypad", 0)
292
cell.set_property("ellipsize", Pango.EllipsizeMode.END)
293
cell.set_property("cell-background-gdk",
294
self.get_style().bg[Gtk.StateType.NORMAL])
295
col = Gtk.TreeViewColumn("Committer")
296
col.set_resizable(True)
297
col.pack_start(cell, True)
298
col.add_attribute(cell, "text", COMMITTER_COL)
299
tv.append_column(col)
301
cell = Gtk.CellRendererText()
302
cell.set_property("xalign", 1.0)
303
cell.set_property("ypad", 0)
304
cell.set_property("cell-background-gdk",
305
self.get_style().bg[Gtk.StateType.NORMAL])
306
col = Gtk.TreeViewColumn("Revno")
307
col.set_resizable(False)
308
col.pack_start(cell, True)
309
col.add_attribute(cell, "markup", REVNO_COL)
310
tv.append_column(col)
312
cell = Gtk.CellRendererText()
313
cell.set_property("ypad", 0)
314
cell.set_property("family", "Monospace")
315
col = Gtk.TreeViewColumn()
316
col.set_resizable(False)
317
col.pack_start(cell, True)
318
# col.add_attribute(cell, "foreground", HIGHLIGHT_COLOR_COL)
319
col.add_attribute(cell, "background", HIGHLIGHT_COLOR_COL)
320
col.add_attribute(cell, "text", TEXT_LINE_COL)
321
tv.append_column(col)
323
# interactive substring search
324
def search_equal_func(model, column, key, iter):
325
return model.get_value(iter, TEXT_LINE_COL).lower().find(key.lower()) == -1
327
tv.set_enable_search(True)
328
tv.set_search_equal_func(search_equal_func, None)
332
def _create_log_view(self):
333
lv = RevisionView(self._branch)
337
def _create_back_button(self):
338
button = Gtk.Button()
339
button.set_use_stock(True)
340
button.set_label("gtk-go-back")
341
button.connect("clicked", lambda w: self.go_back())
342
button.set_relief(Gtk.ReliefStyle.NONE)
346
def _create_forward_button(self):
347
button = Gtk.Button()
348
button.set_use_stock(True)
349
button.set_label("gtk-go-forward")
350
button.connect("clicked", lambda w: self.go_forward())
351
button.set_relief(Gtk.ReliefStyle.NONE)
353
button.set_sensitive(False)
356
def _create_find_button(self):
357
button = Gtk.Button()
358
button.set_use_stock(True)
359
button.set_label("gtk-find")
360
button.set_tooltip_text("Search for text (Ctrl+F)")
361
button.connect("clicked", self._search_by_text)
362
button.set_relief(Gtk.ReliefStyle.NONE)
364
button.set_sensitive(True)
367
def _create_goto_button(self):
368
button = Gtk.Button()
369
button.set_label("Goto Line")
370
button.set_tooltip_text("Scroll to a line by entering its number (Ctrl+G)")
371
button.connect("clicked", self._search_by_line)
372
button.set_relief(Gtk.ReliefStyle.NONE)
374
button.set_sensitive(True)
378
last_tree = self.tree
379
rev_id = self._selected_revision()
380
parent_id = self.revisions[rev_id].parent_ids[0]
381
target_tree = self.branch.repository.revision_tree(parent_id)
382
if self._go(target_tree):
383
self.history.append(last_tree)
384
self.forward_button.set_sensitive(True)
386
self._no_back.add(parent_id)
387
self.back_button.set_sensitive(False)
389
def go_forward(self):
390
if len(self.history) == 0:
392
target_tree = self.history.pop()
393
if len(self.history) == 0:
394
self.forward_button.set_sensitive(False)
395
self._go(target_tree)
397
def _go(self, target_tree):
398
rev_id = self._selected_revision()
399
if self.file_id in target_tree:
400
offset = self.get_scroll_offset(target_tree)
401
path, col = self.annoview.get_cursor()
402
(row,) = path.get_indices()
403
self.annotate(target_tree, self.branch, self.file_id)
407
new_path = Gtk.TreePath(path=new_row)
408
self.annoview.set_cursor(new_path, None, False)
413
def get_scroll_offset(self, tree):
414
old = self.tree.get_file(self.file_id)
415
new = tree.get_file(self.file_id)
416
path, col = self.annoview.get_cursor()
417
(row,) = path.get_indices()
418
matcher = patiencediff.PatienceSequenceMatcher(None, old.readlines(),
420
for i, j, n in matcher.get_matching_blocks():
425
class FakeRevision(object):
428
For when a revision is referenced but not present.
431
def __init__(self, revision_id, committer='?', nick=None):
432
self.revision_id = revision_id
434
self.committer = committer
440
def get_apparent_authors(self):
441
return [self.committer]
444
class RevisionCache(object):
445
"""A caching revision source"""
447
def __init__(self, real_source, seed_cache=None):
448
self.__real_source = real_source
449
if seed_cache is None:
452
self.__cache = dict(seed_cache)
454
def get_revision(self, revision_id):
455
if revision_id not in self.__cache:
456
revision = self.__real_source.get_revision(revision_id)
457
self.__cache[revision_id] = revision
458
return self.__cache[revision_id]
461
class SearchBox(Gtk.HBox):
462
"""A button box for searching in text or lines of annotations"""
464
super(SearchBox, self).__init__(homogeneous=False, spacing=6)
467
button = Gtk.Button()
469
image.set_from_stock('gtk-stop', Gtk.IconSize.BUTTON)
470
button.set_image(image)
471
button.set_relief(Gtk.ReliefStyle.NONE)
472
button.connect("clicked", lambda w: self.hide())
473
self.pack_start(button, False, False, 0)
478
self.pack_start(label, False, False, 0)
482
entry.connect("activate", lambda w, d: self._do_search(d),
484
self.pack_start(entry, False, False, 0)
486
# Next/previous buttons
487
button = Gtk.Button(_i18n('_Next'), use_underline=True)
489
image.set_from_stock('gtk-go-forward', Gtk.IconSize.BUTTON)
490
button.set_image(image)
491
button.connect("clicked", lambda w, d: self._do_search(d),
493
self.pack_start(button, False, False, 0)
495
button = Gtk.Button(_i18n('_Previous'), use_underline=True)
497
image.set_from_stock('gtk-go-back', Gtk.IconSize.BUTTON)
498
button.set_image(image)
499
button.connect("clicked", lambda w, d: self._do_search(d),
501
self.pack_start(button, False, False, 0)
504
check = Gtk.CheckButton('Match case')
505
self._match_case = check
506
self.pack_start(check, False, False, 0)
508
check = Gtk.CheckButton('Regexp')
509
check.connect("toggled", lambda w: self._set_label())
511
self.pack_start(check, False, False, 0)
515
# Note that we stay hidden (we do not call self.show_all())
518
def show_for(self, kind):
522
# Hide unrelated buttons
524
self._match_case.hide()
527
self._entry.grab_focus()
529
def _set_label(self):
530
if self._kind == 'line':
531
self._label.set_text('Find Line: ')
533
if self._regexp.get_active():
534
self._label.set_text('Find Regexp: ')
536
self._label.set_text('Find Text: ')
538
def set_target(self, view,column):
540
self._column = column
542
def _match(self, model, iterator, column):
543
matching_case = self._match_case.get_active()
544
cell_value, = model.get(iterator, column)
545
key = self._entry.get_text()
546
if column == LINE_NUM_COL:
547
# FIXME: For goto-line there are faster algorithms than searching
548
# every line til we find the right one! -- mbp 2011-01-27
549
return key.strip() == str(cell_value)
550
elif self._regexp.get_active():
552
match = re.compile(key).search(cell_value, 1)
554
match = re.compile(key, re.I).search(cell_value, 1)
556
if not matching_case:
557
cell_value = cell_value.lower()
559
match = cell_value.find(key) != -1
563
def _iterate_rows_forward(self, model, start):
564
model_size = len(model)
566
while model_size != 0:
567
if current >= model_size: current = 0
568
yield model.get_iter_from_string('%d' % current)
569
if current == start: raise StopIteration
572
def _iterate_rows_backward(self, model, start):
573
model_size = len(model)
575
while model_size != 0:
576
if current < 0: current = model_size - 1
577
yield model.get_iter_from_string('%d' % current)
578
if current == start: raise StopIteration
581
def _do_search(self, direction):
582
if direction == 'forward':
583
iterate = self._iterate_rows_forward
585
iterate = self._iterate_rows_backward
587
model, sel = self._view.get_selection().get_selected()
591
path = model.get_string_from_iter(sel)
594
for row in iterate(model, start):
595
if self._match(model, row, self._column):
596
path = model.get_path(row)
597
self._view.set_cursor(path, None, False)
598
self._view.scroll_to_cell(path, use_align=True)