1
# Copyright (C) 2005 Dan Loda <danloda@gmail.com>
2
# Copyright (C) 2007 Jelmer Vernooij <jelmer@samba.org>
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25
from bzrlib.plugins.gtk import icon_path
26
from bzrlib.osutils import format_date
27
from bzrlib.util.bencode import bdecode
29
def _open_link(widget, uri):
30
subprocess.Popen(['sensible-browser', uri], close_fds=True)
32
gtk.link_button_set_uri_hook(_open_link)
34
class BugsTab(gtk.VBox):
36
super(BugsTab, self).__init__(False, 6)
38
table = gtk.Table(rows=2, columns=2)
40
table.set_row_spacings(6)
43
image.set_from_file(icon_path("bug.png"))
44
table.attach(image, 0, 1, 0, 1, gtk.FILL)
46
align = gtk.Alignment(0.0, 0.5)
48
label.set_markup("<b>Bugs</b>\nThis revision has one or more bug associations.")
50
table.attach(align, 1, 2, 0, 1, gtk.FILL)
52
treeview = self.construct_treeview()
53
table.attach(treeview, 1, 2, 1, 2, gtk.FILL | gtk.EXPAND)
55
self.set_border_width(6)
56
self.pack_start(table, expand=False)
60
def construct_treeview(self):
61
self.bugs = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
62
self.treeview = gtk.TreeView(self.bugs)
64
uri_column = gtk.TreeViewColumn('Bug URI', gtk.CellRendererText(), text=0)
65
status_column = gtk.TreeViewColumn('Status', gtk.CellRendererText(), text=1)
67
self.treeview.append_column(uri_column)
68
self.treeview.append_column(status_column)
70
win = gtk.ScrolledWindow()
71
win.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
72
win.set_shadow_type(gtk.SHADOW_IN)
73
win.add(self.treeview)
79
self.hide_all() # Only shown when there are bugs
81
def add_bug(self, url, status):
82
self.bugs.append([url, status])
86
class SignatureTab(gtk.VBox):
88
from gpg import GPGSubprocess
89
self.gpg = GPGSubprocess()
90
super(SignatureTab, self).__init__(False, 6)
91
signature_box = gtk.Table(rows=1, columns=2)
92
signature_box.set_col_spacing(0, 12)
94
self.signature_image = gtk.Image()
95
signature_box.attach(self.signature_image, 0, 1, 0, 1, gtk.FILL)
97
self.signature_label = gtk.Label()
98
signature_box.attach(self.signature_label, 1, 2, 0, 1, gtk.FILL)
100
signature_info = gtk.Table(rows=1, columns=2)
101
signature_info.set_row_spacings(6)
102
signature_info.set_col_spacings(6)
104
align = gtk.Alignment(1.0, 0.5)
106
label.set_markup("<b>Key Id:</b>")
108
signature_info.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
110
align = gtk.Alignment(0.0, 0.5)
111
self.signature_key_id = gtk.Label()
112
self.signature_key_id.set_selectable(True)
113
align.add(self.signature_key_id)
114
signature_info.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
116
self.set_border_width(6)
117
self.pack_start(signature_box, expand=False)
118
self.pack_start(signature_info, expand=False)
121
def show_no_signature(self):
122
self.signature_key_id.set_text("")
123
self.signature_image.set_from_file(icon_path("sign-unknown.png"))
124
self.signature_label.set_text("This revision has not been signed.")
126
def show_signature(self, text):
127
signature = self.gpg.verify(text)
129
if signature.key_id is not None:
130
self.signature_key_id.set_text(signature.key_id)
132
if signature.is_valid():
133
self.signature_image.set_from_file(icon_path("sign-ok.png"))
134
self.signature_label.set_text("This revision has been signed.")
136
self.signature_image.set_from_file(icon_path("sign-bad.png"))
137
self.signature_label.set_text("This revision has been signed, " +
138
"but the authenticity of the signature cannot be verified.")
141
class RevisionView(gtk.Notebook):
142
""" Custom widget for commit log details.
144
A variety of bzr tools may need to implement such a thing. This is a
150
gobject.TYPE_PYOBJECT,
152
'The branch holding the revision being displayed',
153
gobject.PARAM_CONSTRUCT_ONLY | gobject.PARAM_WRITABLE
157
gobject.TYPE_PYOBJECT,
159
'The revision being displayed',
160
gobject.PARAM_READWRITE
164
gobject.TYPE_PYOBJECT,
167
gobject.PARAM_READWRITE
171
gobject.TYPE_PYOBJECT,
174
gobject.PARAM_READWRITE
179
def __init__(self, branch=None):
180
gtk.Notebook.__init__(self)
182
self._create_general()
183
self._create_relations()
184
self._create_signature()
185
self._create_file_info_view()
188
self.set_current_page(0)
190
self._show_callback = None
191
self._clicked_callback = None
193
self._revision = None
194
self._branch = branch
198
self.set_file_id(None)
200
def do_get_property(self, property):
201
if property.name == 'branch':
203
elif property.name == 'revision':
204
return self._revision
205
elif property.name == 'children':
206
return self._children
207
elif property.name == 'file-id':
210
raise AttributeError, 'unknown property %s' % property.name
212
def do_set_property(self, property, value):
213
if property.name == 'branch':
215
elif property.name == 'revision':
216
self._set_revision(value)
217
elif property.name == 'children':
218
self.set_children(value)
219
elif property.name == 'file-id':
220
self._file_id = value
222
raise AttributeError, 'unknown property %s' % property.name
224
def set_show_callback(self, callback):
225
self._show_callback = callback
227
def set_file_id(self, file_id):
228
"""Set a specific file id that we want to track.
230
This just effects the display of a per-file commit message.
231
If it is set to None, then all commit messages will be shown.
233
self.set_property('file-id', file_id)
235
def set_revision(self, revision):
236
if revision != self._revision:
237
self.set_property('revision', revision)
239
def get_revision(self):
240
return self.get_property('revision')
242
def _set_revision(self, revision):
243
if revision is None: return
245
self._revision = revision
246
if revision.committer is not None:
247
self.committer.set_text(revision.committer)
249
self.committer.set_text("")
250
author = revision.properties.get('author', '')
252
self.author.set_text(author)
254
self.author_label.show()
257
self.author_label.hide()
259
if revision.timestamp is not None:
260
self.timestamp.set_text(format_date(revision.timestamp,
263
self.branchnick_label.set_text(revision.properties['branch-nick'])
265
self.branchnick_label.set_text("")
267
self._add_parents_or_children(revision.parent_ids,
268
self.parents_widgets,
271
file_info = revision.properties.get('file-info', None)
272
if file_info is not None:
273
file_info = bdecode(file_info.encode('UTF-8'))
276
if self._file_id is None:
279
text.append('%(path)s\n%(message)s' % fi)
280
self.file_info_buffer.set_text('\n'.join(text))
281
self.file_info_box.show()
285
if fi['file_id'] == self._file_id:
286
text.append(fi['message'])
288
self.file_info_buffer.set_text('\n'.join(text))
289
self.file_info_box.show()
291
self.file_info_box.hide()
293
self.file_info_box.hide()
295
self.bugs_table.clear()
296
bugs_text = revision.properties.get('bugs', None)
298
for bugline in bugs_text.splitlines():
299
(url, status) = bugline.split(" ")
300
self.bugs_table.add_bug(url, status)
302
def update_tags(self):
303
if self._branch is not None and self._branch.supports_tags():
304
self._tagdict = self._branch.tags.get_reverse_tag_dict()
310
def _update_signature(self, widget, param):
311
revid = self._revision.revision_id
313
if self._branch.repository.has_signature_for_revision_id(revid):
314
signature_text = self._branch.repository.get_signature_text(revid)
315
self.signature_table.show_signature(signature_text)
317
self.signature_table.show_no_signature()
319
def set_children(self, children):
320
self._add_parents_or_children(children,
321
self.children_widgets,
324
def _show_clicked_cb(self, widget, revid, parentid):
325
"""Callback for when the show button for a parent is clicked."""
326
self._show_callback(revid, parentid)
328
def _go_clicked_cb(self, widget, revid):
329
"""Callback for when the go button for a parent is clicked."""
331
def _add_tags(self, *args):
332
if self._revision is None:
335
if self._tagdict.has_key(self._revision.revision_id):
336
tags = self._tagdict[self._revision.revision_id]
341
self.tags_list.hide()
342
self.tags_label.hide()
345
self.tags_list.set_text(", ".join(tags))
347
self.tags_list.show_all()
348
self.tags_label.show_all()
350
def _add_parents_or_children(self, revids, widgets, table):
351
while len(widgets) > 0:
352
widget = widgets.pop()
355
table.resize(max(len(revids), 1), 2)
357
for idx, revid in enumerate(revids):
358
align = gtk.Alignment(0.0, 0.0)
359
widgets.append(align)
360
table.attach(align, 1, 2, idx, idx + 1,
361
gtk.EXPAND | gtk.FILL, gtk.FILL)
364
hbox = gtk.HBox(False, spacing=6)
369
image.set_from_stock(
370
gtk.STOCK_FIND, gtk.ICON_SIZE_SMALL_TOOLBAR)
373
if self._show_callback is not None:
374
button = gtk.Button()
376
button.connect("clicked", self._show_clicked_cb,
377
self._revision.revision_id, revid)
378
hbox.pack_start(button, expand=False, fill=True)
381
button = gtk.Button(revid)
382
button.connect("clicked",
383
lambda w, r: self.set_revision(self._branch.repository.get_revision(r)), revid)
384
button.set_use_underline(False)
385
hbox.pack_start(button, expand=False, fill=True)
388
def _create_general(self):
389
vbox = gtk.VBox(False, 6)
390
vbox.set_border_width(6)
391
vbox.pack_start(self._create_headers(), expand=False, fill=True)
392
vbox.pack_start(self._create_message_view())
393
self.append_page(vbox, tab_label=gtk.Label("General"))
396
def _create_relations(self):
397
vbox = gtk.VBox(False, 6)
398
vbox.set_border_width(6)
399
vbox.pack_start(self._create_parents(), expand=False, fill=True)
400
vbox.pack_start(self._create_children(), expand=False, fill=True)
401
self.append_page(vbox, tab_label=gtk.Label("Relations"))
404
def _create_signature(self):
406
self.signature_table = SignatureTab()
407
except ValueError: # No GPG found
408
self.signature_table = None
410
self.append_page(self.signature_table, tab_label=gtk.Label('Signature'))
411
self.connect_after('notify::revision', self._update_signature)
413
def _create_headers(self):
414
self.table = gtk.Table(rows=5, columns=2)
415
self.table.set_row_spacings(6)
416
self.table.set_col_spacings(6)
419
align = gtk.Alignment(1.0, 0.5)
421
label.set_markup("<b>Revision Id:</b>")
423
self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
427
align = gtk.Alignment(0.0, 0.5)
428
revision_id = gtk.Label()
429
revision_id.set_selectable(True)
430
self.connect('notify::revision',
431
lambda w, p: revision_id.set_text(self._revision.revision_id))
432
align.add(revision_id)
433
self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
437
align = gtk.Alignment(1.0, 0.5)
438
self.author_label = gtk.Label()
439
self.author_label.set_markup("<b>Author:</b>")
440
align.add(self.author_label)
441
self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
443
self.author_label.show()
445
align = gtk.Alignment(0.0, 0.5)
446
self.author = gtk.Label()
447
self.author.set_selectable(True)
448
align.add(self.author)
449
self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
454
align = gtk.Alignment(1.0, 0.5)
456
label.set_markup("<b>Committer:</b>")
458
self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
462
align = gtk.Alignment(0.0, 0.5)
463
self.committer = gtk.Label()
464
self.committer.set_selectable(True)
465
align.add(self.committer)
466
self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
468
self.committer.show()
470
align = gtk.Alignment(0.0, 0.5)
472
label.set_markup("<b>Branch nick:</b>")
474
self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
478
align = gtk.Alignment(0.0, 0.5)
479
self.branchnick_label = gtk.Label()
480
self.branchnick_label.set_selectable(True)
481
align.add(self.branchnick_label)
482
self.table.attach(align, 1, 2, 3, 4, gtk.EXPAND | gtk.FILL, gtk.FILL)
483
self.branchnick_label.show()
486
align = gtk.Alignment(1.0, 0.5)
488
label.set_markup("<b>Timestamp:</b>")
490
self.table.attach(align, 0, 1, 4, 5, gtk.FILL, gtk.FILL)
494
align = gtk.Alignment(0.0, 0.5)
495
self.timestamp = gtk.Label()
496
self.timestamp.set_selectable(True)
497
align.add(self.timestamp)
498
self.table.attach(align, 1, 2, 4, 5, gtk.EXPAND | gtk.FILL, gtk.FILL)
500
self.timestamp.show()
502
align = gtk.Alignment(1.0, 0.5)
503
self.tags_label = gtk.Label()
504
self.tags_label.set_markup("<b>Tags:</b>")
505
align.add(self.tags_label)
507
self.table.attach(align, 0, 1, 5, 6, gtk.FILL, gtk.FILL)
508
self.tags_label.show()
510
align = gtk.Alignment(0.0, 0.5)
511
self.tags_list = gtk.Label()
512
align.add(self.tags_list)
513
self.table.attach(align, 1, 2, 5, 6, gtk.EXPAND | gtk.FILL, gtk.FILL)
515
self.tags_list.show()
517
self.connect('notify::revision', self._add_tags)
521
def _create_parents(self):
522
hbox = gtk.HBox(True, 3)
524
self.parents_table = self._create_parents_or_children_table(
526
self.parents_widgets = []
527
hbox.pack_start(self.parents_table)
532
def _create_children(self):
533
hbox = gtk.HBox(True, 3)
534
self.children_table = self._create_parents_or_children_table(
536
self.children_widgets = []
537
hbox.pack_start(self.children_table)
541
def _create_parents_or_children_table(self, text):
542
table = gtk.Table(rows=1, columns=2)
543
table.set_row_spacings(3)
544
table.set_col_spacings(6)
548
label.set_markup(text)
549
align = gtk.Alignment(0.0, 0.5)
551
table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
557
def _create_message_view(self):
558
msg_buffer = gtk.TextBuffer()
559
self.connect('notify::revision',
560
lambda w, p: msg_buffer.set_text(self._revision.message))
561
window = gtk.ScrolledWindow()
562
window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
563
window.set_shadow_type(gtk.SHADOW_IN)
564
tv = gtk.TextView(msg_buffer)
565
tv.set_editable(False)
566
tv.set_wrap_mode(gtk.WRAP_WORD)
568
tv.modify_font(pango.FontDescription("Monospace"))
574
def _create_bugs(self):
575
self.bugs_table = BugsTab()
576
self.append_page(self.bugs_table, tab_label=gtk.Label('Bugs'))
578
def _create_file_info_view(self):
579
self.file_info_box = gtk.VBox(False, 6)
580
self.file_info_box.set_border_width(6)
581
self.file_info_buffer = gtk.TextBuffer()
582
window = gtk.ScrolledWindow()
583
window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
584
window.set_shadow_type(gtk.SHADOW_IN)
585
tv = gtk.TextView(self.file_info_buffer)
586
tv.set_editable(False)
587
tv.set_wrap_mode(gtk.WRAP_WORD)
588
tv.modify_font(pango.FontDescription("Monospace"))
592
self.file_info_box.pack_start(window)
593
self.file_info_box.hide() # Only shown when there are per-file messages
594
self.append_page(self.file_info_box, tab_label=gtk.Label('Per-file'))