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
Window.__init__(self, 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.new_from_string(str(row))
133
self.annoview.set_cursor(tree_path, None, None)
134
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
model.set(iter, HIGHLIGHT_COLOR_COL,
172
self.annotate_colormap.get_color(revision, now))
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.VPaned()
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):
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
(row,), col = self.annoview.get_cursor()
401
self.annotate(target_tree, self.branch, self.file_id)
405
self.annoview.set_cursor(new_row)
410
def get_scroll_offset(self, tree):
411
old = self.tree.get_file(self.file_id)
412
new = tree.get_file(self.file_id)
413
(row,), col = self.annoview.get_cursor()
414
matcher = patiencediff.PatienceSequenceMatcher(None, old.readlines(),
416
for i, j, n in matcher.get_matching_blocks():
421
class FakeRevision(object):
424
For when a revision is referenced but not present.
427
def __init__(self, revision_id, committer='?', nick=None):
428
self.revision_id = revision_id
430
self.committer = committer
436
def get_apparent_authors(self):
437
return [self.committer]
440
class RevisionCache(object):
441
"""A caching revision source"""
443
def __init__(self, real_source, seed_cache=None):
444
self.__real_source = real_source
445
if seed_cache is None:
448
self.__cache = dict(seed_cache)
450
def get_revision(self, revision_id):
451
if revision_id not in self.__cache:
452
revision = self.__real_source.get_revision(revision_id)
453
self.__cache[revision_id] = revision
454
return self.__cache[revision_id]
456
class SearchBox(Gtk.HBox):
457
"""A button box for searching in text or lines of annotations"""
459
Gtk.HBox.__init__(self, homogeneous=False, spacing=6)
462
button = Gtk.Button()
464
image.set_from_stock('gtk-stop', Gtk.IconSize.BUTTON)
465
button.set_image(image)
466
button.set_relief(Gtk.ReliefStyle.NONE)
467
button.connect("clicked", lambda w: self.hide_all())
468
self.pack_start(button, False, False, 0)
473
self.pack_start(label, False, False, 0)
477
entry.connect("activate", lambda w, d: self._do_search(d),
479
self.pack_start(entry, False, False, 0)
481
# Next/previous buttons
482
button = Gtk.Button('_Next')
484
image.set_from_stock('gtk-go-forward', Gtk.IconSize.BUTTON)
485
button.set_image(image)
486
button.connect("clicked", lambda w, d: self._do_search(d),
488
self.pack_start(button, False, False, 0)
490
button = Gtk.Button('_Previous')
492
image.set_from_stock('gtk-go-back', Gtk.IconSize.BUTTON)
493
button.set_image(image)
494
button.connect("clicked", lambda w, d: self._do_search(d),
496
self.pack_start(button, False, False, 0)
499
check = Gtk.CheckButton('Match case')
500
self._match_case = check
501
self.pack_start(check, False, False, 0)
503
check = Gtk.CheckButton('Regexp')
504
check.connect("toggled", lambda w: self._set_label())
506
self.pack_start(check, False, False, 0)
510
# Note that we stay hidden (we do not call self.show_all())
513
def show_for(self, kind):
517
# Hide unrelated buttons
519
self._match_case.hide()
522
self._entry.grab_focus()
524
def _set_label(self):
525
if self._kind == 'line':
526
self._label.set_text('Find Line: ')
528
if self._regexp.get_active():
529
self._label.set_text('Find Regexp: ')
531
self._label.set_text('Find Text: ')
533
def set_target(self, view,column):
535
self._column = column
537
def _match(self, model, iterator, column):
538
matching_case = self._match_case.get_active()
539
cell_value, = model.get(iterator, column)
540
key = self._entry.get_text()
541
if column == LINE_NUM_COL:
542
# FIXME: For goto-line there are faster algorithms than searching
543
# every line til we find the right one! -- mbp 2011-01-27
544
return key.strip() == str(cell_value)
545
elif self._regexp.get_active():
547
match = re.compile(key).search(cell_value, 1)
549
match = re.compile(key, re.I).search(cell_value, 1)
551
if not matching_case:
552
cell_value = cell_value.lower()
554
match = cell_value.find(key) != -1
558
def _iterate_rows_forward(self, model, start):
559
model_size = len(model)
561
while model_size != 0:
562
if current >= model_size: current = 0
563
yield model.get_iter_from_string('%d' % current)
564
if current == start: raise StopIteration
567
def _iterate_rows_backward(self, model, start):
568
model_size = len(model)
570
while model_size != 0:
571
if current < 0: current = model_size - 1
572
yield model.get_iter_from_string('%d' % current)
573
if current == start: raise StopIteration
576
def _do_search(self, direction):
577
if direction == 'forward':
578
iterate = self._iterate_rows_forward
580
iterate = self._iterate_rows_backward
582
model, sel = self._view.get_selection().get_selected()
586
path = model.get_string_from_iter(sel)
589
for row in iterate(model, start):
590
if self._match(model, row, self._column):
591
path = model.get_path(row)
592
self._view.set_cursor(path)
593
self._view.scroll_to_cell(path, use_align=True)