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.revisionview import RevisionView
31
from bzrlib.plugins.gtk.window import Window
44
class GAnnotateWindow(Window):
45
"""Annotate window."""
47
def __init__(self, all=False, plain=False, parent=None, branch=None):
52
super(GAnnotateWindow, self).__init__(parent=parent)
55
self.render_icon_pixbuf(Gtk.STOCK_FIND, Gtk.IconSize.BUTTON))
56
self.annotate_colormap = AnnotateColorSaturation()
63
def annotate(self, tree, branch, file_id):
67
self.file_id = file_id
68
self.revisionview.set_file_id(file_id)
69
self.revision_id = getattr(tree, 'get_revision_id',
70
lambda: CURRENT_REVISION)()
72
# [revision id, line number, author, revno, highlight color, line]
73
self.annomodel = Gtk.ListStore(GObject.TYPE_STRING,
83
branch.repository.lock_read()
85
revno_map = self.branch.get_revision_id_to_revno_map()
86
for revision_id, revno in revno_map.iteritems():
87
self.dotted[revision_id] = '.'.join(str(num) for num in revno)
88
for line_no, (revision, revno, line)\
89
in enumerate(self._annotate(tree, file_id)):
90
if revision.revision_id == last_seen and not self.all:
93
last_seen = revision.revision_id
94
author = ", ".join(revision.get_apparent_authors())
96
if revision.revision_id not in self.revisions:
97
self.revisions[revision.revision_id] = revision
99
self.annomodel.append([revision.revision_id,
106
self.annotations.append(revision)
110
self.annomodel.foreach(self._highlight_annotation, now)
112
branch.repository.unlock()
115
self.annoview.set_model(self.annomodel)
116
self.annoview.grab_focus()
117
my_revno = self.dotted.get(self.revision_id, 'current')
118
title = '%s (%s) - gannotate' % (self.tree.id2path(file_id), my_revno)
119
self.set_title(title)
121
def jump_to_line(self, lineno):
122
if lineno > len(self.annomodel) or lineno < 1:
124
# FIXME:should really deal with this in the gui. Perhaps a status
126
print("gannotate: Line number %d does't exist. Defaulting to "
132
tree_path = Gtk.TreePath(path=row)
133
self.annoview.set_cursor(tree_path, None, False)
134
self.annoview.scroll_to_cell(tree_path, use_align=True)
136
def _annotate(self, tree, file_id):
137
current_revision = FakeRevision(CURRENT_REVISION)
138
current_revision.committer = self.branch.get_config().username()
139
current_revision.timestamp = time.time()
140
current_revision.message = '[Not yet committed]'
141
current_revision.parent_ids = tree.get_parent_ids()
142
current_revision.properties['branch-nick'] = self.branch._get_nick(local=True)
143
current_revno = '%d?' % (self.branch.revno() + 1)
144
repository = self.branch.repository
145
if self.revision_id == CURRENT_REVISION:
146
revision_id = self.branch.last_revision()
148
revision_id = self.revision_id
149
revision_cache = RevisionCache(repository, self.revisions)
150
for origin, text in tree.annotate_iter(file_id):
152
if rev_id == CURRENT_REVISION:
153
revision = current_revision
154
revno = current_revno
157
revision = revision_cache.get_revision(rev_id)
158
revno = self.dotted.get(rev_id, 'merge')
161
except NoSuchRevision:
162
revision = FakeRevision(rev_id)
165
yield revision, revno, text
167
def _highlight_annotation(self, model, path, iter, now):
168
revision_id, = model.get(iter, REVISION_ID_COL)
169
revision = self.revisions[revision_id]
170
# XXX sinzui 2011-08-12: What does get_color return?
171
color = self.annotate_colormap.get_color(revision, now)
172
model.set_value(iter, HIGHLIGHT_COLOR_COL, color)
174
def _selected_revision(self):
175
(path, col) = self.annoview.get_cursor()
178
return self.annomodel[path][REVISION_ID_COL]
180
def _activate_selected_revision(self, w):
181
rev_id = self._selected_revision()
182
if not rev_id or rev_id == NULL_REVISION:
184
selected = self.revisions[rev_id]
185
self.revisionview.set_revision(selected)
186
if (len(selected.parent_ids) != 0 and selected.parent_ids[0] not in
191
self.back_button.set_sensitive(enable_back)
194
self.revisionview = self._create_log_view()
195
self.annoview = self._create_annotate_view()
197
vbox = Gtk.VBox(homogeneous=False, spacing=0)
200
sw = Gtk.ScrolledWindow()
201
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
202
sw.set_shadow_type(Gtk.ShadowType.IN)
203
sw.add(self.annoview)
204
self.annoview.gwindow = self
208
swbox.pack_start(sw, True, True, 0)
211
hbox = Gtk.HBox(homogeneous=False, spacing=6)
212
self.back_button = self._create_back_button()
213
hbox.pack_start(self.back_button, False, True, 0)
214
self.forward_button = self._create_forward_button()
215
hbox.pack_start(self.forward_button, False, True, 0)
216
self.find_button = self._create_find_button()
217
hbox.pack_start(self.find_button, False, True, 0)
218
self.goto_button = self._create_goto_button()
219
hbox.pack_start(self.goto_button, False, True, 0)
221
vbox.pack_start(hbox, False, True, 0)
223
self.pane = pane = Gtk.Paned.new(Gtk.Orientation.VERTICAL)
225
pane.add2(self.revisionview)
227
vbox.pack_start(pane, True, True, 0)
229
self._search = SearchBox()
230
swbox.pack_start(self._search, False, True, 0)
231
accels = Gtk.AccelGroup()
232
accels.connect(Gdk.KEY_f, Gdk.ModifierType.CONTROL_MASK,
233
Gtk.AccelFlags.LOCKED,
234
self._search_by_text)
235
accels.connect(Gdk.KEY_g, Gdk.ModifierType.CONTROL_MASK,
236
Gtk.AccelFlags.LOCKED,
237
self._search_by_line)
238
self.add_accel_group(accels)
242
def _search_by_text(self, *ignored): # (accel_group, window, key, modifiers):
243
self._search.show_for('text')
244
self._search.set_target(self.annoview, TEXT_LINE_COL)
246
def _search_by_line(self, *ignored): # accel_group, window, key, modifiers):
247
self._search.show_for('line')
248
self._search.set_target(self.annoview, LINE_NUM_COL)
250
def line_diff(self, tv, path, tvc):
251
row = path.get_indices()[0]
252
revision = self.annotations[row]
253
repository = self.branch.repository
254
if revision.revision_id == CURRENT_REVISION:
256
tree2 = self.tree.basis_tree()
258
tree1 = repository.revision_tree(revision.revision_id)
259
if len(revision.parent_ids) > 0:
260
tree2 = repository.revision_tree(revision.parent_ids[0])
262
tree2 = repository.revision_tree(NULL_REVISION)
263
from bzrlib.plugins.gtk.diff import DiffWindow
264
window = DiffWindow(self)
265
window.set_diff("Diff for line %d" % (row+1), tree1, tree2)
266
window.set_file(tree1.id2path(self.file_id))
270
def _create_annotate_view(self):
272
tv.set_rules_hint(False)
273
tv.connect("cursor-changed", self._activate_selected_revision)
275
tv.connect("row-activated", self.line_diff)
277
cell = Gtk.CellRendererText()
278
cell.set_property("xalign", 1.0)
279
cell.set_property("ypad", 0)
280
cell.set_property("family", "Monospace")
281
cell.set_property("cell-background-gdk",
282
tv.get_style().bg[Gtk.StateType.NORMAL])
283
col = Gtk.TreeViewColumn()
284
col.set_resizable(False)
285
col.pack_start(cell, True)
286
col.add_attribute(cell, "text", LINE_NUM_COL)
287
tv.append_column(col)
289
cell = Gtk.CellRendererText()
290
cell.set_property("ypad", 0)
291
cell.set_property("ellipsize", Pango.EllipsizeMode.END)
292
cell.set_property("cell-background-gdk",
293
self.get_style().bg[Gtk.StateType.NORMAL])
294
col = Gtk.TreeViewColumn("Committer")
295
col.set_resizable(True)
296
col.pack_start(cell, True)
297
col.add_attribute(cell, "text", COMMITTER_COL)
298
tv.append_column(col)
300
cell = Gtk.CellRendererText()
301
cell.set_property("xalign", 1.0)
302
cell.set_property("ypad", 0)
303
cell.set_property("cell-background-gdk",
304
self.get_style().bg[Gtk.StateType.NORMAL])
305
col = Gtk.TreeViewColumn("Revno")
306
col.set_resizable(False)
307
col.pack_start(cell, True)
308
col.add_attribute(cell, "markup", REVNO_COL)
309
tv.append_column(col)
311
cell = Gtk.CellRendererText()
312
cell.set_property("ypad", 0)
313
cell.set_property("family", "Monospace")
314
col = Gtk.TreeViewColumn()
315
col.set_resizable(False)
316
col.pack_start(cell, True)
317
# col.add_attribute(cell, "foreground", HIGHLIGHT_COLOR_COL)
318
col.add_attribute(cell, "background", HIGHLIGHT_COLOR_COL)
319
col.add_attribute(cell, "text", TEXT_LINE_COL)
320
tv.append_column(col)
322
# interactive substring search
323
def search_equal_func(model, column, key, iter):
324
return model.get_value(iter, TEXT_LINE_COL).lower().find(key.lower()) == -1
326
tv.set_enable_search(True)
327
tv.set_search_equal_func(search_equal_func, None)
331
def _create_log_view(self):
332
lv = RevisionView(self._branch)
336
def _create_back_button(self):
337
button = Gtk.Button()
338
button.set_use_stock(True)
339
button.set_label("gtk-go-back")
340
button.connect("clicked", lambda w: self.go_back())
341
button.set_relief(Gtk.ReliefStyle.NONE)
345
def _create_forward_button(self):
346
button = Gtk.Button()
347
button.set_use_stock(True)
348
button.set_label("gtk-go-forward")
349
button.connect("clicked", lambda w: self.go_forward())
350
button.set_relief(Gtk.ReliefStyle.NONE)
352
button.set_sensitive(False)
355
def _create_find_button(self):
356
button = Gtk.Button()
357
button.set_use_stock(True)
358
button.set_label("gtk-find")
359
button.set_tooltip_text("Search for text (Ctrl+F)")
360
button.connect("clicked", self._search_by_text)
361
button.set_relief(Gtk.ReliefStyle.NONE)
363
button.set_sensitive(True)
366
def _create_goto_button(self):
367
button = Gtk.Button()
368
button.set_label("Goto Line")
369
button.set_tooltip_text("Scroll to a line by entering its number (Ctrl+G)")
370
button.connect("clicked", self._search_by_line)
371
button.set_relief(Gtk.ReliefStyle.NONE)
373
button.set_sensitive(True)
377
last_tree = self.tree
378
rev_id = self._selected_revision()
379
parent_id = self.revisions[rev_id].parent_ids[0]
380
target_tree = self.branch.repository.revision_tree(parent_id)
381
if self._go(target_tree):
382
self.history.append(last_tree)
383
self.forward_button.set_sensitive(True)
385
self._no_back.add(parent_id)
386
self.back_button.set_sensitive(False)
388
def go_forward(self):
389
if len(self.history) == 0:
391
target_tree = self.history.pop()
392
if len(self.history) == 0:
393
self.forward_button.set_sensitive(False)
394
self._go(target_tree)
396
def _go(self, target_tree):
397
rev_id = self._selected_revision()
398
if self.file_id in target_tree:
399
offset = self.get_scroll_offset(target_tree)
400
path, col = self.annoview.get_cursor()
401
(row,) = path.get_indices()
402
self.annotate(target_tree, self.branch, self.file_id)
406
new_path = Gtk.TreePath(path=new_row)
407
self.annoview.set_cursor(new_path, None, False)
412
def get_scroll_offset(self, tree):
413
old = self.tree.get_file(self.file_id)
414
new = tree.get_file(self.file_id)
415
path, col = self.annoview.get_cursor()
416
(row,) = path.get_indices()
417
matcher = patiencediff.PatienceSequenceMatcher(None, old.readlines(),
419
for i, j, n in matcher.get_matching_blocks():
424
class FakeRevision(object):
427
For when a revision is referenced but not present.
430
def __init__(self, revision_id, committer='?', nick=None):
431
self.revision_id = revision_id
433
self.committer = committer
439
def get_apparent_authors(self):
440
return [self.committer]
443
class RevisionCache(object):
444
"""A caching revision source"""
446
def __init__(self, real_source, seed_cache=None):
447
self.__real_source = real_source
448
if seed_cache is None:
451
self.__cache = dict(seed_cache)
453
def get_revision(self, revision_id):
454
if revision_id not in self.__cache:
455
revision = self.__real_source.get_revision(revision_id)
456
self.__cache[revision_id] = revision
457
return self.__cache[revision_id]
460
class SearchBox(Gtk.HBox):
461
"""A button box for searching in text or lines of annotations"""
463
super(SearchBox, self).__init__(homogeneous=False, spacing=6)
466
button = Gtk.Button()
468
image.set_from_stock('gtk-stop', Gtk.IconSize.BUTTON)
469
button.set_image(image)
470
button.set_relief(Gtk.ReliefStyle.NONE)
471
button.connect("clicked", lambda w: self.hide())
472
self.pack_start(button, False, False, 0)
477
self.pack_start(label, False, False, 0)
481
entry.connect("activate", lambda w, d: self._do_search(d),
483
self.pack_start(entry, False, False, 0)
485
# Next/previous buttons
486
button = Gtk.Button('_Next')
488
image.set_from_stock('gtk-go-forward', Gtk.IconSize.BUTTON)
489
button.set_image(image)
490
button.connect("clicked", lambda w, d: self._do_search(d),
492
self.pack_start(button, False, False, 0)
494
button = Gtk.Button('_Previous')
496
image.set_from_stock('gtk-go-back', Gtk.IconSize.BUTTON)
497
button.set_image(image)
498
button.connect("clicked", lambda w, d: self._do_search(d),
500
self.pack_start(button, False, False, 0)
503
check = Gtk.CheckButton('Match case')
504
self._match_case = check
505
self.pack_start(check, False, False, 0)
507
check = Gtk.CheckButton('Regexp')
508
check.connect("toggled", lambda w: self._set_label())
510
self.pack_start(check, False, False, 0)
514
# Note that we stay hidden (we do not call self.show_all())
517
def show_for(self, kind):
521
# Hide unrelated buttons
523
self._match_case.hide()
526
self._entry.grab_focus()
528
def _set_label(self):
529
if self._kind == 'line':
530
self._label.set_text('Find Line: ')
532
if self._regexp.get_active():
533
self._label.set_text('Find Regexp: ')
535
self._label.set_text('Find Text: ')
537
def set_target(self, view,column):
539
self._column = column
541
def _match(self, model, iterator, column):
542
matching_case = self._match_case.get_active()
543
cell_value, = model.get(iterator, column)
544
key = self._entry.get_text()
545
if column == LINE_NUM_COL:
546
# FIXME: For goto-line there are faster algorithms than searching
547
# every line til we find the right one! -- mbp 2011-01-27
548
return key.strip() == str(cell_value)
549
elif self._regexp.get_active():
551
match = re.compile(key).search(cell_value, 1)
553
match = re.compile(key, re.I).search(cell_value, 1)
555
if not matching_case:
556
cell_value = cell_value.lower()
558
match = cell_value.find(key) != -1
562
def _iterate_rows_forward(self, model, start):
563
model_size = len(model)
565
while model_size != 0:
566
if current >= model_size: current = 0
567
yield model.get_iter_from_string('%d' % current)
568
if current == start: raise StopIteration
571
def _iterate_rows_backward(self, model, start):
572
model_size = len(model)
574
while model_size != 0:
575
if current < 0: current = model_size - 1
576
yield model.get_iter_from_string('%d' % current)
577
if current == start: raise StopIteration
580
def _do_search(self, direction):
581
if direction == 'forward':
582
iterate = self._iterate_rows_forward
584
iterate = self._iterate_rows_backward
586
model, sel = self._view.get_selection().get_selected()
590
path = model.get_string_from_iter(sel)
593
for row in iterate(model, start):
594
if self._match(model, row, self._column):
595
path = model.get_path(row)
596
self._view.set_cursor(path, None, False)
597
self._view.scroll_to_cell(path, use_align=True)