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 Gtk
21
from gi.repository import Pango
24
from bzrlib import patiencediff
25
from bzrlib.errors import NoSuchRevision
26
from bzrlib.revision import NULL_REVISION, CURRENT_REVISION
28
from bzrlib.plugins.gtk.annotate.colormap import AnnotateColorSaturation
29
from bzrlib.plugins.gtk.revisionview import RevisionView
30
from bzrlib.plugins.gtk.window import Window
43
class GAnnotateWindow(Window):
44
"""Annotate window."""
46
def __init__(self, all=False, plain=False, parent=None, branch=None):
51
Window.__init__(self, parent)
53
self.set_icon(self.render_icon(Gtk.STOCK_FIND, Gtk.IconSize.BUTTON))
54
self.annotate_colormap = AnnotateColorSaturation()
61
def annotate(self, tree, branch, file_id):
65
self.file_id = file_id
66
self.revisionview.set_file_id(file_id)
67
self.revision_id = getattr(tree, 'get_revision_id',
68
lambda: CURRENT_REVISION)()
70
# [revision id, line number, author, revno, highlight color, line]
71
self.annomodel = Gtk.ListStore(GObject.TYPE_STRING,
81
branch.repository.lock_read()
83
revno_map = self.branch.get_revision_id_to_revno_map()
84
for revision_id, revno in revno_map.iteritems():
85
self.dotted[revision_id] = '.'.join(str(num) for num in revno)
86
for line_no, (revision, revno, line)\
87
in enumerate(self._annotate(tree, file_id)):
88
if revision.revision_id == last_seen and not self.all:
91
last_seen = revision.revision_id
92
author = ", ".join(revision.get_apparent_authors())
94
if revision.revision_id not in self.revisions:
95
self.revisions[revision.revision_id] = revision
97
self.annomodel.append([revision.revision_id,
104
self.annotations.append(revision)
108
self.annomodel.foreach(self._highlight_annotation, now)
110
branch.repository.unlock()
113
self.annoview.set_model(self.annomodel)
114
self.annoview.grab_focus()
115
my_revno = self.dotted.get(self.revision_id, 'current')
116
title = '%s (%s) - gannotate' % (self.tree.id2path(file_id), my_revno)
117
self.set_title(title)
119
def jump_to_line(self, lineno):
120
if lineno > len(self.annomodel) or lineno < 1:
122
# FIXME:should really deal with this in the gui. Perhaps a status
124
print("gannotate: Line number %d does't exist. Defaulting to "
130
self.annoview.set_cursor(row)
131
self.annoview.scroll_to_cell(row, use_align=True)
134
def _annotate(self, tree, file_id):
135
current_revision = FakeRevision(CURRENT_REVISION)
136
current_revision.committer = self.branch.get_config().username()
137
current_revision.timestamp = time.time()
138
current_revision.message = '[Not yet committed]'
139
current_revision.parent_ids = tree.get_parent_ids()
140
current_revision.properties['branch-nick'] = self.branch._get_nick(local=True)
141
current_revno = '%d?' % (self.branch.revno() + 1)
142
repository = self.branch.repository
143
if self.revision_id == CURRENT_REVISION:
144
revision_id = self.branch.last_revision()
146
revision_id = self.revision_id
147
revision_cache = RevisionCache(repository, self.revisions)
148
for origin, text in tree.annotate_iter(file_id):
150
if rev_id == CURRENT_REVISION:
151
revision = current_revision
152
revno = current_revno
155
revision = revision_cache.get_revision(rev_id)
156
revno = self.dotted.get(rev_id, 'merge')
159
except NoSuchRevision:
160
revision = FakeRevision(rev_id)
163
yield revision, revno, text
165
def _highlight_annotation(self, model, path, iter, now):
166
revision_id, = model.get(iter, REVISION_ID_COL)
167
revision = self.revisions[revision_id]
168
model.set(iter, HIGHLIGHT_COLOR_COL,
169
self.annotate_colormap.get_color(revision, now))
171
def _selected_revision(self):
172
(path, col) = self.annoview.get_cursor()
175
return self.annomodel[path][REVISION_ID_COL]
177
def _activate_selected_revision(self, w):
178
rev_id = self._selected_revision()
179
if not rev_id or rev_id == NULL_REVISION:
181
selected = self.revisions[rev_id]
182
self.revisionview.set_revision(selected)
183
if (len(selected.parent_ids) != 0 and selected.parent_ids[0] not in
188
self.back_button.set_sensitive(enable_back)
191
self.revisionview = self._create_log_view()
192
self.annoview = self._create_annotate_view()
194
vbox = Gtk.VBox(False)
197
sw = Gtk.ScrolledWindow()
198
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
199
sw.set_shadow_type(Gtk.ShadowType.IN)
200
sw.add(self.annoview)
201
self.annoview.gwindow = self
205
swbox.pack_start(sw, True, True, 0)
208
hbox = Gtk.HBox(False, 6)
209
self.back_button = self._create_back_button()
210
hbox.pack_start(self.back_button, expand=False, fill=True)
211
self.forward_button = self._create_forward_button()
212
hbox.pack_start(self.forward_button, expand=False, fill=True)
213
self.find_button = self._create_find_button()
214
hbox.pack_start(self.find_button, expand=False, fill=True)
215
self.goto_button = self._create_goto_button()
216
hbox.pack_start(self.goto_button, expand=False, fill=True)
218
vbox.pack_start(hbox, expand=False, fill=True)
220
self.pane = pane = Gtk.VPaned()
222
pane.add2(self.revisionview)
224
vbox.pack_start(pane, expand=True, fill=True)
226
self._search = SearchBox()
227
swbox.pack_start(self._search, expand=False, fill=True)
228
accels = Gtk.AccelGroup()
229
accels.connect_group(Gdk.KEY_f, Gdk.EventMask.CONTROL_MASK,
231
self._search_by_text)
232
accels.connect_group(Gdk.KEY_g, Gdk.EventMask.CONTROL_MASK,
234
self._search_by_line)
235
self.add_accel_group(accels)
239
def _search_by_text(self, *ignored): # (accel_group, window, key, modifiers):
240
self._search.show_for('text')
241
self._search.set_target(self.annoview, TEXT_LINE_COL)
243
def _search_by_line(self, *ignored): # accel_group, window, key, modifiers):
244
self._search.show_for('line')
245
self._search.set_target(self.annoview, LINE_NUM_COL)
247
def line_diff(self, tv, path, tvc):
249
revision = self.annotations[row]
250
repository = self.branch.repository
251
if revision.revision_id == CURRENT_REVISION:
253
tree2 = self.tree.basis_tree()
255
tree1 = repository.revision_tree(revision.revision_id)
256
if len(revision.parent_ids) > 0:
257
tree2 = repository.revision_tree(revision.parent_ids[0])
259
tree2 = repository.revision_tree(NULL_REVISION)
260
from bzrlib.plugins.gtk.diff import DiffWindow
261
window = DiffWindow(self)
262
window.set_diff("Diff for line %d" % (row+1), tree1, tree2)
263
window.set_file(tree1.id2path(self.file_id))
267
def _create_annotate_view(self):
269
tv.set_rules_hint(False)
270
tv.connect("cursor-changed", self._activate_selected_revision)
272
tv.connect("row-activated", self.line_diff)
274
cell = Gtk.CellRendererText()
275
cell.set_property("xalign", 1.0)
276
cell.set_property("ypad", 0)
277
cell.set_property("family", "Monospace")
278
cell.set_property("cell-background-gdk",
279
tv.get_style().bg[Gtk.StateType.NORMAL])
280
col = Gtk.TreeViewColumn()
281
col.set_resizable(False)
282
col.pack_start(cell, True, True, 0)
283
col.add_attribute(cell, "text", LINE_NUM_COL)
284
tv.append_column(col)
286
cell = Gtk.CellRendererText()
287
cell.set_property("ypad", 0)
288
cell.set_property("ellipsize", Pango.EllipsizeMode.END)
289
cell.set_property("cell-background-gdk",
290
self.get_style().bg[Gtk.StateType.NORMAL])
291
col = Gtk.TreeViewColumn("Committer")
292
col.set_resizable(True)
293
col.pack_start(cell, True, True, 0)
294
col.add_attribute(cell, "text", COMMITTER_COL)
295
tv.append_column(col)
297
cell = Gtk.CellRendererText()
298
cell.set_property("xalign", 1.0)
299
cell.set_property("ypad", 0)
300
cell.set_property("cell-background-gdk",
301
self.get_style().bg[Gtk.StateType.NORMAL])
302
col = Gtk.TreeViewColumn("Revno")
303
col.set_resizable(False)
304
col.pack_start(cell, True, True, 0)
305
col.add_attribute(cell, "markup", REVNO_COL)
306
tv.append_column(col)
308
cell = Gtk.CellRendererText()
309
cell.set_property("ypad", 0)
310
cell.set_property("family", "Monospace")
311
col = Gtk.TreeViewColumn()
312
col.set_resizable(False)
313
col.pack_start(cell, True, True, 0)
314
# col.add_attribute(cell, "foreground", HIGHLIGHT_COLOR_COL)
315
col.add_attribute(cell, "background", HIGHLIGHT_COLOR_COL)
316
col.add_attribute(cell, "text", TEXT_LINE_COL)
317
tv.append_column(col)
319
# interactive substring search
320
def search_equal_func(model, column, key, iter):
321
return model.get_value(iter, TEXT_LINE_COL).lower().find(key.lower()) == -1
323
tv.set_enable_search(True)
324
tv.set_search_equal_func(search_equal_func)
328
def _create_log_view(self):
329
lv = RevisionView(self._branch)
333
def _create_back_button(self):
334
button = Gtk.Button()
335
button.set_use_stock(True)
336
button.set_label("gtk-go-back")
337
button.connect("clicked", lambda w: self.go_back())
338
button.set_relief(Gtk.ReliefStyle.NONE)
342
def _create_forward_button(self):
343
button = Gtk.Button()
344
button.set_use_stock(True)
345
button.set_label("gtk-go-forward")
346
button.connect("clicked", lambda w: self.go_forward())
347
button.set_relief(Gtk.ReliefStyle.NONE)
349
button.set_sensitive(False)
352
def _create_find_button(self):
353
button = Gtk.Button()
354
button.set_use_stock(True)
355
button.set_label("gtk-find")
356
button.set_tooltip_text("Search for text (Ctrl+F)")
357
button.connect("clicked", self._search_by_text)
358
button.set_relief(Gtk.ReliefStyle.NONE)
360
button.set_sensitive(True)
363
def _create_goto_button(self):
364
button = Gtk.Button()
365
button.set_label("Goto Line")
366
button.set_tooltip_text("Scroll to a line by entering its number (Ctrl+G)")
367
button.connect("clicked", self._search_by_line)
368
button.set_relief(Gtk.ReliefStyle.NONE)
370
button.set_sensitive(True)
374
last_tree = self.tree
375
rev_id = self._selected_revision()
376
parent_id = self.revisions[rev_id].parent_ids[0]
377
target_tree = self.branch.repository.revision_tree(parent_id)
378
if self._go(target_tree):
379
self.history.append(last_tree)
380
self.forward_button.set_sensitive(True)
382
self._no_back.add(parent_id)
383
self.back_button.set_sensitive(False)
385
def go_forward(self):
386
if len(self.history) == 0:
388
target_tree = self.history.pop()
389
if len(self.history) == 0:
390
self.forward_button.set_sensitive(False)
391
self._go(target_tree)
393
def _go(self, target_tree):
394
rev_id = self._selected_revision()
395
if self.file_id in target_tree:
396
offset = self.get_scroll_offset(target_tree)
397
(row,), col = self.annoview.get_cursor()
398
self.annotate(target_tree, self.branch, self.file_id)
402
self.annoview.set_cursor(new_row)
407
def get_scroll_offset(self, tree):
408
old = self.tree.get_file(self.file_id)
409
new = tree.get_file(self.file_id)
410
(row,), col = self.annoview.get_cursor()
411
matcher = patiencediff.PatienceSequenceMatcher(None, old.readlines(),
413
for i, j, n in matcher.get_matching_blocks():
418
class FakeRevision(object):
421
For when a revision is referenced but not present.
424
def __init__(self, revision_id, committer='?', nick=None):
425
self.revision_id = revision_id
427
self.committer = committer
433
def get_apparent_authors(self):
434
return [self.committer]
437
class RevisionCache(object):
438
"""A caching revision source"""
440
def __init__(self, real_source, seed_cache=None):
441
self.__real_source = real_source
442
if seed_cache is None:
445
self.__cache = dict(seed_cache)
447
def get_revision(self, revision_id):
448
if revision_id not in self.__cache:
449
revision = self.__real_source.get_revision(revision_id)
450
self.__cache[revision_id] = revision
451
return self.__cache[revision_id]
453
class SearchBox(Gtk.HBox):
454
"""A button box for searching in text or lines of annotations"""
456
GObject.GObject.__init__(self, False, 6)
459
button = Gtk.Button()
461
image.set_from_stock('gtk-stop', Gtk.IconSize.BUTTON)
462
button.set_image(image)
463
button.set_relief(Gtk.ReliefStyle.NONE)
464
button.connect("clicked", lambda w: self.hide_all())
465
self.pack_start(button, expand=False, fill=False)
470
self.pack_start(label, expand=False, fill=False)
474
entry.connect("activate", lambda w, d: self._do_search(d),
476
self.pack_start(entry, expand=False, fill=False)
478
# Next/previous buttons
479
button = Gtk.Button('_Next')
481
image.set_from_stock('gtk-go-forward', Gtk.IconSize.BUTTON)
482
button.set_image(image)
483
button.connect("clicked", lambda w, d: self._do_search(d),
485
self.pack_start(button, expand=False, fill=False)
487
button = Gtk.Button('_Previous')
489
image.set_from_stock('gtk-go-back', Gtk.IconSize.BUTTON)
490
button.set_image(image)
491
button.connect("clicked", lambda w, d: self._do_search(d),
493
self.pack_start(button, expand=False, fill=False)
496
check = Gtk.CheckButton('Match case')
497
self._match_case = check
498
self.pack_start(check, expand=False, fill=False)
500
check = Gtk.CheckButton('Regexp')
501
check.connect("toggled", lambda w: self._set_label())
503
self.pack_start(check, expand=False, fill=False)
507
# Note that we stay hidden (we do not call self.show_all())
510
def show_for(self, kind):
514
# Hide unrelated buttons
516
self._match_case.hide()
519
self._entry.grab_focus()
521
def _set_label(self):
522
if self._kind == 'line':
523
self._label.set_text('Find Line: ')
525
if self._regexp.get_active():
526
self._label.set_text('Find Regexp: ')
528
self._label.set_text('Find Text: ')
530
def set_target(self, view,column):
532
self._column = column
534
def _match(self, model, iterator, column):
535
matching_case = self._match_case.get_active()
536
cell_value, = model.get(iterator, column)
537
key = self._entry.get_text()
538
if column == LINE_NUM_COL:
539
# FIXME: For goto-line there are faster algorithms than searching
540
# every line til we find the right one! -- mbp 2011-01-27
541
return key.strip() == str(cell_value)
542
elif self._regexp.get_active():
544
match = re.compile(key).search(cell_value, 1)
546
match = re.compile(key, re.I).search(cell_value, 1)
548
if not matching_case:
549
cell_value = cell_value.lower()
551
match = cell_value.find(key) != -1
555
def _iterate_rows_forward(self, model, start):
556
model_size = len(model)
558
while model_size != 0:
559
if current >= model_size: current = 0
560
yield model.get_iter_from_string('%d' % current)
561
if current == start: raise StopIteration
564
def _iterate_rows_backward(self, model, start):
565
model_size = len(model)
567
while model_size != 0:
568
if current < 0: current = model_size - 1
569
yield model.get_iter_from_string('%d' % current)
570
if current == start: raise StopIteration
573
def _do_search(self, direction):
574
if direction == 'forward':
575
iterate = self._iterate_rows_forward
577
iterate = self._iterate_rows_backward
579
model, sel = self._view.get_selection().get_selected()
583
path = model.get_string_from_iter(sel)
586
for row in iterate(model, start):
587
if self._match(model, row, self._column):
588
path = model.get_path(row)
589
self._view.set_cursor(path)
590
self._view.scroll_to_cell(path, use_align=True)