ea05cd42401a92777daba721a6f78c55f2e10848
1#! /usr/bin/env python
2
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.
7
8""" gitview
9GUI browser for git repository
10This program is based on bzrk by Scott James Remnant <scott@ubuntu.com>
11"""
12__copyright__ = "Copyright (C) 2006 Hewlett-Packard Development Company, L.P."
13__author__ = "Aneesh Kumar K.V <aneesh.kumar@hp.com>"
14
15
16import sys
17import os
18import gtk
19import pygtk
20import pango
21import re
22import time
23import gobject
24import cairo
25import math
26import string
27
28try:
29 import gtksourceview
30 have_gtksourceview = True
31except ImportError:
32 have_gtksourceview = False
33 print "Running without gtksourceview module"
34
35re_ident = re.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
36
37def list_to_string(args, skip):
38 count = len(args)
39 i = skip
40 str_arg=" "
41 while (i < count ):
42 str_arg = str_arg + args[i]
43 str_arg = str_arg + " "
44 i = i+1
45
46 return str_arg
47
48def show_date(epoch, tz):
49 secs = float(epoch)
50 tzsecs = float(tz[1:3]) * 3600
51 tzsecs += float(tz[3:5]) * 60
52 if (tz[0] == "+"):
53 secs += tzsecs
54 else:
55 secs -= tzsecs
56
57 return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs))
58
59
60class CellRendererGraph(gtk.GenericCellRenderer):
61 """Cell renderer for directed graph.
62
63 This module contains the implementation of a custom GtkCellRenderer that
64 draws part of the directed graph based on the lines suggested by the code
65 in graph.py.
66
67 Because we're shiny, we use Cairo to do this, and because we're naughty
68 we cheat and draw over the bits of the TreeViewColumn that are supposed to
69 just be for the background.
70
71 Properties:
72 node (column, colour, [ names ]) tuple to draw revision node,
73 in_lines (start, end, colour) tuple list to draw inward lines,
74 out_lines (start, end, colour) tuple list to draw outward lines.
75 """
76
77 __gproperties__ = {
78 "node": ( gobject.TYPE_PYOBJECT, "node",
79 "revision node instruction",
80 gobject.PARAM_WRITABLE
81 ),
82 "in-lines": ( gobject.TYPE_PYOBJECT, "in-lines",
83 "instructions to draw lines into the cell",
84 gobject.PARAM_WRITABLE
85 ),
86 "out-lines": ( gobject.TYPE_PYOBJECT, "out-lines",
87 "instructions to draw lines out of the cell",
88 gobject.PARAM_WRITABLE
89 ),
90 }
91
92 def do_set_property(self, property, value):
93 """Set properties from GObject properties."""
94 if property.name == "node":
95 self.node = value
96 elif property.name == "in-lines":
97 self.in_lines = value
98 elif property.name == "out-lines":
99 self.out_lines = value
100 else:
101 raise AttributeError, "no such property: '%s'" % property.name
102
103 def box_size(self, widget):
104 """Calculate box size based on widget's font.
105
106 Cache this as it's probably expensive to get. It ensures that we
107 draw the graph at least as large as the text.
108 """
109 try:
110 return self._box_size
111 except AttributeError:
112 pango_ctx = widget.get_pango_context()
113 font_desc = widget.get_style().font_desc
114 metrics = pango_ctx.get_metrics(font_desc)
115
116 ascent = pango.PIXELS(metrics.get_ascent())
117 descent = pango.PIXELS(metrics.get_descent())
118
119 self._box_size = ascent + descent + 6
120 return self._box_size
121
122 def set_colour(self, ctx, colour, bg, fg):
123 """Set the context source colour.
124
125 Picks a distinct colour based on an internal wheel; the bg
126 parameter provides the value that should be assigned to the 'zero'
127 colours and the fg parameter provides the multiplier that should be
128 applied to the foreground colours.
129 """
130 colours = [
131 ( 1.0, 0.0, 0.0 ),
132 ( 1.0, 1.0, 0.0 ),
133 ( 0.0, 1.0, 0.0 ),
134 ( 0.0, 1.0, 1.0 ),
135 ( 0.0, 0.0, 1.0 ),
136 ( 1.0, 0.0, 1.0 ),
137 ]
138
139 colour %= len(colours)
140 red = (colours[colour][0] * fg) or bg
141 green = (colours[colour][1] * fg) or bg
142 blue = (colours[colour][2] * fg) or bg
143
144 ctx.set_source_rgb(red, green, blue)
145
146 def on_get_size(self, widget, cell_area):
147 """Return the size we need for this cell.
148
149 Each cell is drawn individually and is only as wide as it needs
150 to be, we let the TreeViewColumn take care of making them all
151 line up.
152 """
153 box_size = self.box_size(widget)
154
155 cols = self.node[0]
156 for start, end, colour in self.in_lines + self.out_lines:
157 cols = int(max(cols, start, end))
158
159 (column, colour, names) = self.node
160 names_len = 0
161 if (len(names) != 0):
162 for item in names:
163 names_len += len(item)
164
165 width = box_size * (cols + 1 ) + names_len
166 height = box_size
167
168 # FIXME I have no idea how to use cell_area properly
169 return (0, 0, width, height)
170
171 def on_render(self, window, widget, bg_area, cell_area, exp_area, flags):
172 """Render an individual cell.
173
174 Draws the cell contents using cairo, taking care to clip what we
175 do to within the background area so we don't draw over other cells.
176 Note that we're a bit naughty there and should really be drawing
177 in the cell_area (or even the exposed area), but we explicitly don't
178 want any gutter.
179
180 We try and be a little clever, if the line we need to draw is going
181 to cross other columns we actually draw it as in the .---' style
182 instead of a pure diagonal ... this reduces confusion by an
183 incredible amount.
184 """
185 ctx = window.cairo_create()
186 ctx.rectangle(bg_area.x, bg_area.y, bg_area.width, bg_area.height)
187 ctx.clip()
188
189 box_size = self.box_size(widget)
190
191 ctx.set_line_width(box_size / 8)
192 ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
193
194 # Draw lines into the cell
195 for start, end, colour in self.in_lines:
196 ctx.move_to(cell_area.x + box_size * start + box_size / 2,
197 bg_area.y - bg_area.height / 2)
198
199 if start - end > 1:
200 ctx.line_to(cell_area.x + box_size * start, bg_area.y)
201 ctx.line_to(cell_area.x + box_size * end + box_size, bg_area.y)
202 elif start - end < -1:
203 ctx.line_to(cell_area.x + box_size * start + box_size,
204 bg_area.y)
205 ctx.line_to(cell_area.x + box_size * end, bg_area.y)
206
207 ctx.line_to(cell_area.x + box_size * end + box_size / 2,
208 bg_area.y + bg_area.height / 2)
209
210 self.set_colour(ctx, colour, 0.0, 0.65)
211 ctx.stroke()
212
213 # Draw lines out of the cell
214 for start, end, colour in self.out_lines:
215 ctx.move_to(cell_area.x + box_size * start + box_size / 2,
216 bg_area.y + bg_area.height / 2)
217
218 if start - end > 1:
219 ctx.line_to(cell_area.x + box_size * start,
220 bg_area.y + bg_area.height)
221 ctx.line_to(cell_area.x + box_size * end + box_size,
222 bg_area.y + bg_area.height)
223 elif start - end < -1:
224 ctx.line_to(cell_area.x + box_size * start + box_size,
225 bg_area.y + bg_area.height)
226 ctx.line_to(cell_area.x + box_size * end,
227 bg_area.y + bg_area.height)
228
229 ctx.line_to(cell_area.x + box_size * end + box_size / 2,
230 bg_area.y + bg_area.height / 2 + bg_area.height)
231
232 self.set_colour(ctx, colour, 0.0, 0.65)
233 ctx.stroke()
234
235 # Draw the revision node in the right column
236 (column, colour, names) = self.node
237 ctx.arc(cell_area.x + box_size * column + box_size / 2,
238 cell_area.y + cell_area.height / 2,
239 box_size / 4, 0, 2 * math.pi)
240
241
242 self.set_colour(ctx, colour, 0.0, 0.5)
243 ctx.stroke_preserve()
244
245 self.set_colour(ctx, colour, 0.5, 1.0)
246 ctx.fill_preserve()
247
248 if (len(names) != 0):
249 name = " "
250 for item in names:
251 name = name + item + " "
252
253 ctx.set_font_size(13)
254 if (flags & 1):
255 self.set_colour(ctx, colour, 0.5, 1.0)
256 else:
257 self.set_colour(ctx, colour, 0.0, 0.5)
258 ctx.show_text(name)
259
260class Commit:
261 """ This represent a commit object obtained after parsing the git-rev-list
262 output """
263
264 children_sha1 = {}
265
266 def __init__(self, commit_lines):
267 self.message = ""
268 self.author = ""
269 self.date = ""
270 self.committer = ""
271 self.commit_date = ""
272 self.commit_sha1 = ""
273 self.parent_sha1 = [ ]
274 self.parse_commit(commit_lines)
275
276
277 def parse_commit(self, commit_lines):
278
279 # First line is the sha1 lines
280 line = string.strip(commit_lines[0])
281 sha1 = re.split(" ", line)
282 self.commit_sha1 = sha1[0]
283 self.parent_sha1 = sha1[1:]
284
285 #build the child list
286 for parent_id in self.parent_sha1:
287 try:
288 Commit.children_sha1[parent_id].append(self.commit_sha1)
289 except KeyError:
290 Commit.children_sha1[parent_id] = [self.commit_sha1]
291
292 # IF we don't have parent
293 if (len(self.parent_sha1) == 0):
294 self.parent_sha1 = [0]
295
296 for line in commit_lines[1:]:
297 m = re.match("^ ", line)
298 if (m != None):
299 # First line of the commit message used for short log
300 if self.message == "":
301 self.message = string.strip(line)
302 continue
303
304 m = re.match("tree", line)
305 if (m != None):
306 continue
307
308 m = re.match("parent", line)
309 if (m != None):
310 continue
311
312 m = re_ident.match(line)
313 if (m != None):
314 date = show_date(m.group('epoch'), m.group('tz'))
315 if m.group(1) == "author":
316 self.author = m.group('ident')
317 self.date = date
318 elif m.group(1) == "committer":
319 self.committer = m.group('ident')
320 self.commit_date = date
321
322 continue
323
324 def get_message(self, with_diff=0):
325 if (with_diff == 1):
326 message = self.diff_tree()
327 else:
328 fp = os.popen("git cat-file commit " + self.commit_sha1)
329 message = fp.read()
330 fp.close()
331
332 return message
333
334 def diff_tree(self):
335 fp = os.popen("git diff-tree --pretty --cc -v -p --always " + self.commit_sha1)
336 diff = fp.read()
337 fp.close()
338 return diff
339
340class DiffWindow:
341 """Diff window.
342 This object represents and manages a single window containing the
343 differences between two revisions on a branch.
344 """
345
346 def __init__(self):
347 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
348 self.window.set_border_width(0)
349 self.window.set_title("Git repository browser diff window")
350
351 # Use two thirds of the screen by default
352 screen = self.window.get_screen()
353 monitor = screen.get_monitor_geometry(0)
354 width = int(monitor.width * 0.66)
355 height = int(monitor.height * 0.66)
356 self.window.set_default_size(width, height)
357
358 self.construct()
359
360 def construct(self):
361 """Construct the window contents."""
362 vbox = gtk.VBox()
363 self.window.add(vbox)
364 vbox.show()
365
366 menu_bar = gtk.MenuBar()
367 save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
368 save_menu.connect("activate", self.save_menu_response, "save")
369 save_menu.show()
370 menu_bar.append(save_menu)
371 vbox.pack_start(menu_bar, expand=False, fill=True)
372 menu_bar.show()
373
374 scrollwin = gtk.ScrolledWindow()
375 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
376 scrollwin.set_shadow_type(gtk.SHADOW_IN)
377 vbox.pack_start(scrollwin, expand=True, fill=True)
378 scrollwin.show()
379
380 if have_gtksourceview:
381 self.buffer = gtksourceview.SourceBuffer()
382 slm = gtksourceview.SourceLanguagesManager()
383 gsl = slm.get_language_from_mime_type("text/x-patch")
384 self.buffer.set_highlight(True)
385 self.buffer.set_language(gsl)
386 sourceview = gtksourceview.SourceView(self.buffer)
387 else:
388 self.buffer = gtk.TextBuffer()
389 sourceview = gtk.TextView(self.buffer)
390
391 sourceview.set_editable(False)
392 sourceview.modify_font(pango.FontDescription("Monospace"))
393 scrollwin.add(sourceview)
394 sourceview.show()
395
396
397 def set_diff(self, commit_sha1, parent_sha1, encoding):
398 """Set the differences showed by this window.
399 Compares the two trees and populates the window with the
400 differences.
401 """
402 # Diff with the first commit or the last commit shows nothing
403 if (commit_sha1 == 0 or parent_sha1 == 0 ):
404 return
405
406 fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
407 self.buffer.set_text(unicode(fp.read(), encoding).encode('utf-8'))
408 fp.close()
409 self.window.show()
410
411 def save_menu_response(self, widget, string):
412 dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
413 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
414 gtk.STOCK_SAVE, gtk.RESPONSE_OK))
415 dialog.set_default_response(gtk.RESPONSE_OK)
416 response = dialog.run()
417 if response == gtk.RESPONSE_OK:
418 patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
419 self.buffer.get_end_iter())
420 fp = open(dialog.get_filename(), "w")
421 fp.write(patch_buffer)
422 fp.close()
423 dialog.destroy()
424
425class GitView:
426 """ This is the main class
427 """
428 version = "0.7"
429
430 def __init__(self, with_diff=0):
431 self.with_diff = with_diff
432 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
433 self.window.set_border_width(0)
434 self.window.set_title("Git repository browser")
435
436 self.get_encoding()
437 self.get_bt_sha1()
438
439 # Use three-quarters of the screen by default
440 screen = self.window.get_screen()
441 monitor = screen.get_monitor_geometry(0)
442 width = int(monitor.width * 0.75)
443 height = int(monitor.height * 0.75)
444 self.window.set_default_size(width, height)
445
446 # FIXME AndyFitz!
447 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
448 self.window.set_icon(icon)
449
450 self.accel_group = gtk.AccelGroup()
451 self.window.add_accel_group(self.accel_group)
452
453 self.construct()
454
455 def get_bt_sha1(self):
456 """ Update the bt_sha1 dictionary with the
457 respective sha1 details """
458
459 self.bt_sha1 = { }
460 ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
461 fp = os.popen('git ls-remote "${GIT_DIR-.git}"')
462 while 1:
463 line = string.strip(fp.readline())
464 if line == '':
465 break
466 m = ls_remote.match(line)
467 if not m:
468 continue
469 (sha1, name) = (m.group(1), m.group(2))
470 if not self.bt_sha1.has_key(sha1):
471 self.bt_sha1[sha1] = []
472 self.bt_sha1[sha1].append(name)
473 fp.close()
474
475 def get_encoding(self):
476 fp = os.popen("git repo-config --get i18n.commitencoding")
477 self.encoding=string.strip(fp.readline())
478 fp.close()
479 if (self.encoding == ""):
480 self.encoding = "utf-8"
481
482
483 def construct(self):
484 """Construct the window contents."""
485 vbox = gtk.VBox()
486 paned = gtk.VPaned()
487 paned.pack1(self.construct_top(), resize=False, shrink=True)
488 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
489 menu_bar = gtk.MenuBar()
490 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
491 help_menu = gtk.MenuItem("Help")
492 menu = gtk.Menu()
493 about_menu = gtk.MenuItem("About")
494 menu.append(about_menu)
495 about_menu.connect("activate", self.about_menu_response, "about")
496 about_menu.show()
497 help_menu.set_submenu(menu)
498 help_menu.show()
499 menu_bar.append(help_menu)
500 menu_bar.show()
501 vbox.pack_start(menu_bar, expand=False, fill=True)
502 vbox.pack_start(paned, expand=True, fill=True)
503 self.window.add(vbox)
504 paned.show()
505 vbox.show()
506
507
508 def construct_top(self):
509 """Construct the top-half of the window."""
510 vbox = gtk.VBox(spacing=6)
511 vbox.set_border_width(12)
512 vbox.show()
513
514
515 scrollwin = gtk.ScrolledWindow()
516 scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
517 scrollwin.set_shadow_type(gtk.SHADOW_IN)
518 vbox.pack_start(scrollwin, expand=True, fill=True)
519 scrollwin.show()
520
521 self.treeview = gtk.TreeView()
522 self.treeview.set_rules_hint(True)
523 self.treeview.set_search_column(4)
524 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
525 scrollwin.add(self.treeview)
526 self.treeview.show()
527
528 cell = CellRendererGraph()
529 # Set the default width to 265
530 # This make sure that we have nice display with large tag names
531 cell.set_property("width", 265)
532 column = gtk.TreeViewColumn()
533 column.set_resizable(True)
534 column.pack_start(cell, expand=True)
535 column.add_attribute(cell, "node", 1)
536 column.add_attribute(cell, "in-lines", 2)
537 column.add_attribute(cell, "out-lines", 3)
538 self.treeview.append_column(column)
539
540 cell = gtk.CellRendererText()
541 cell.set_property("width-chars", 65)
542 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
543 column = gtk.TreeViewColumn("Message")
544 column.set_resizable(True)
545 column.pack_start(cell, expand=True)
546 column.add_attribute(cell, "text", 4)
547 self.treeview.append_column(column)
548
549 cell = gtk.CellRendererText()
550 cell.set_property("width-chars", 40)
551 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
552 column = gtk.TreeViewColumn("Author")
553 column.set_resizable(True)
554 column.pack_start(cell, expand=True)
555 column.add_attribute(cell, "text", 5)
556 self.treeview.append_column(column)
557
558 cell = gtk.CellRendererText()
559 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
560 column = gtk.TreeViewColumn("Date")
561 column.set_resizable(True)
562 column.pack_start(cell, expand=True)
563 column.add_attribute(cell, "text", 6)
564 self.treeview.append_column(column)
565
566 return vbox
567
568 def about_menu_response(self, widget, string):
569 dialog = gtk.AboutDialog()
570 dialog.set_name("Gitview")
571 dialog.set_version(GitView.version)
572 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
573 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
574 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
575 dialog.set_wrap_license(True)
576 dialog.run()
577 dialog.destroy()
578
579
580 def construct_bottom(self):
581 """Construct the bottom half of the window."""
582 vbox = gtk.VBox(False, spacing=6)
583 vbox.set_border_width(12)
584 (width, height) = self.window.get_size()
585 vbox.set_size_request(width, int(height / 2.5))
586 vbox.show()
587
588 self.table = gtk.Table(rows=4, columns=4)
589 self.table.set_row_spacings(6)
590 self.table.set_col_spacings(6)
591 vbox.pack_start(self.table, expand=False, fill=True)
592 self.table.show()
593
594 align = gtk.Alignment(0.0, 0.5)
595 label = gtk.Label()
596 label.set_markup("<b>Revision:</b>")
597 align.add(label)
598 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
599 label.show()
600 align.show()
601
602 align = gtk.Alignment(0.0, 0.5)
603 self.revid_label = gtk.Label()
604 self.revid_label.set_selectable(True)
605 align.add(self.revid_label)
606 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
607 self.revid_label.show()
608 align.show()
609
610 align = gtk.Alignment(0.0, 0.5)
611 label = gtk.Label()
612 label.set_markup("<b>Committer:</b>")
613 align.add(label)
614 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
615 label.show()
616 align.show()
617
618 align = gtk.Alignment(0.0, 0.5)
619 self.committer_label = gtk.Label()
620 self.committer_label.set_selectable(True)
621 align.add(self.committer_label)
622 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
623 self.committer_label.show()
624 align.show()
625
626 align = gtk.Alignment(0.0, 0.5)
627 label = gtk.Label()
628 label.set_markup("<b>Timestamp:</b>")
629 align.add(label)
630 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
631 label.show()
632 align.show()
633
634 align = gtk.Alignment(0.0, 0.5)
635 self.timestamp_label = gtk.Label()
636 self.timestamp_label.set_selectable(True)
637 align.add(self.timestamp_label)
638 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
639 self.timestamp_label.show()
640 align.show()
641
642 align = gtk.Alignment(0.0, 0.5)
643 label = gtk.Label()
644 label.set_markup("<b>Parents:</b>")
645 align.add(label)
646 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
647 label.show()
648 align.show()
649 self.parents_widgets = []
650
651 align = gtk.Alignment(0.0, 0.5)
652 label = gtk.Label()
653 label.set_markup("<b>Children:</b>")
654 align.add(label)
655 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
656 label.show()
657 align.show()
658 self.children_widgets = []
659
660 scrollwin = gtk.ScrolledWindow()
661 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
662 scrollwin.set_shadow_type(gtk.SHADOW_IN)
663 vbox.pack_start(scrollwin, expand=True, fill=True)
664 scrollwin.show()
665
666 if have_gtksourceview:
667 self.message_buffer = gtksourceview.SourceBuffer()
668 slm = gtksourceview.SourceLanguagesManager()
669 gsl = slm.get_language_from_mime_type("text/x-patch")
670 self.message_buffer.set_highlight(True)
671 self.message_buffer.set_language(gsl)
672 sourceview = gtksourceview.SourceView(self.message_buffer)
673 else:
674 self.message_buffer = gtk.TextBuffer()
675 sourceview = gtk.TextView(self.message_buffer)
676
677 sourceview.set_editable(False)
678 sourceview.modify_font(pango.FontDescription("Monospace"))
679 scrollwin.add(sourceview)
680 sourceview.show()
681
682 return vbox
683
684 def _treeview_cursor_cb(self, *args):
685 """Callback for when the treeview cursor changes."""
686 (path, col) = self.treeview.get_cursor()
687 commit = self.model[path][0]
688
689 if commit.committer is not None:
690 committer = commit.committer
691 timestamp = commit.commit_date
692 message = commit.get_message(self.with_diff)
693 revid_label = commit.commit_sha1
694 else:
695 committer = ""
696 timestamp = ""
697 message = ""
698 revid_label = ""
699
700 self.revid_label.set_text(revid_label)
701 self.committer_label.set_text(committer)
702 self.timestamp_label.set_text(timestamp)
703 self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
704
705 for widget in self.parents_widgets:
706 self.table.remove(widget)
707
708 self.parents_widgets = []
709 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
710 for idx, parent_id in enumerate(commit.parent_sha1):
711 self.table.set_row_spacing(idx + 3, 0)
712
713 align = gtk.Alignment(0.0, 0.0)
714 self.parents_widgets.append(align)
715 self.table.attach(align, 1, 2, idx + 3, idx + 4,
716 gtk.EXPAND | gtk.FILL, gtk.FILL)
717 align.show()
718
719 hbox = gtk.HBox(False, 0)
720 align.add(hbox)
721 hbox.show()
722
723 label = gtk.Label(parent_id)
724 label.set_selectable(True)
725 hbox.pack_start(label, expand=False, fill=True)
726 label.show()
727
728 image = gtk.Image()
729 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
730 image.show()
731
732 button = gtk.Button()
733 button.add(image)
734 button.set_relief(gtk.RELIEF_NONE)
735 button.connect("clicked", self._go_clicked_cb, parent_id)
736 hbox.pack_start(button, expand=False, fill=True)
737 button.show()
738
739 image = gtk.Image()
740 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
741 image.show()
742
743 button = gtk.Button()
744 button.add(image)
745 button.set_relief(gtk.RELIEF_NONE)
746 button.set_sensitive(True)
747 button.connect("clicked", self._show_clicked_cb,
748 commit.commit_sha1, parent_id, self.encoding)
749 hbox.pack_start(button, expand=False, fill=True)
750 button.show()
751
752 # Populate with child details
753 for widget in self.children_widgets:
754 self.table.remove(widget)
755
756 self.children_widgets = []
757 try:
758 child_sha1 = Commit.children_sha1[commit.commit_sha1]
759 except KeyError:
760 # We don't have child
761 child_sha1 = [ 0 ]
762
763 if ( len(child_sha1) > len(commit.parent_sha1)):
764 self.table.resize(4 + len(child_sha1) - 1, 4)
765
766 for idx, child_id in enumerate(child_sha1):
767 self.table.set_row_spacing(idx + 3, 0)
768
769 align = gtk.Alignment(0.0, 0.0)
770 self.children_widgets.append(align)
771 self.table.attach(align, 3, 4, idx + 3, idx + 4,
772 gtk.EXPAND | gtk.FILL, gtk.FILL)
773 align.show()
774
775 hbox = gtk.HBox(False, 0)
776 align.add(hbox)
777 hbox.show()
778
779 label = gtk.Label(child_id)
780 label.set_selectable(True)
781 hbox.pack_start(label, expand=False, fill=True)
782 label.show()
783
784 image = gtk.Image()
785 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
786 image.show()
787
788 button = gtk.Button()
789 button.add(image)
790 button.set_relief(gtk.RELIEF_NONE)
791 button.connect("clicked", self._go_clicked_cb, child_id)
792 hbox.pack_start(button, expand=False, fill=True)
793 button.show()
794
795 image = gtk.Image()
796 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
797 image.show()
798
799 button = gtk.Button()
800 button.add(image)
801 button.set_relief(gtk.RELIEF_NONE)
802 button.set_sensitive(True)
803 button.connect("clicked", self._show_clicked_cb,
804 child_id, commit.commit_sha1)
805 hbox.pack_start(button, expand=False, fill=True)
806 button.show()
807
808 def _destroy_cb(self, widget):
809 """Callback for when a window we manage is destroyed."""
810 self.quit()
811
812
813 def quit(self):
814 """Stop the GTK+ main loop."""
815 gtk.main_quit()
816
817 def run(self, args):
818 self.set_branch(args)
819 self.window.connect("destroy", self._destroy_cb)
820 self.window.show()
821 gtk.main()
822
823 def set_branch(self, args):
824 """Fill in different windows with info from the reposiroty"""
825 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
826 git_rev_list_cmd = fp.read()
827 fp.close()
828 fp = os.popen("git rev-list --header --topo-order --parents " + git_rev_list_cmd)
829 self.update_window(fp)
830
831 def update_window(self, fp):
832 commit_lines = []
833
834 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
835 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
836
837 # used for cursor positioning
838 self.index = {}
839
840 self.colours = {}
841 self.nodepos = {}
842 self.incomplete_line = {}
843 self.commits = []
844
845 index = 0
846 last_colour = 0
847 last_nodepos = -1
848 out_line = []
849 input_line = fp.readline()
850 while (input_line != ""):
851 # The commit header ends with '\0'
852 # This NULL is immediately followed by the sha1 of the
853 # next commit
854 if (input_line[0] != '\0'):
855 commit_lines.append(input_line)
856 input_line = fp.readline()
857 continue;
858
859 commit = Commit(commit_lines)
860 if (commit != None ):
861 self.commits.append(commit)
862
863 # Skip the '\0
864 commit_lines = []
865 commit_lines.append(input_line[1:])
866 input_line = fp.readline()
867
868 fp.close()
869
870 for commit in self.commits:
871 (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
872 index, out_line,
873 last_colour,
874 last_nodepos)
875 self.index[commit.commit_sha1] = index
876 index += 1
877
878 self.treeview.set_model(self.model)
879 self.treeview.show()
880
881 def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
882 in_line=[]
883
884 # | -> outline
885 # X
886 # |\ <- inline
887
888 # Reset nodepostion
889 if (last_nodepos > 5):
890 last_nodepos = -1
891
892 # Add the incomplete lines of the last cell in this
893 try:
894 colour = self.colours[commit.commit_sha1]
895 except KeyError:
896 self.colours[commit.commit_sha1] = last_colour+1
897 last_colour = self.colours[commit.commit_sha1]
898 colour = self.colours[commit.commit_sha1]
899
900 try:
901 node_pos = self.nodepos[commit.commit_sha1]
902 except KeyError:
903 self.nodepos[commit.commit_sha1] = last_nodepos+1
904 last_nodepos = self.nodepos[commit.commit_sha1]
905 node_pos = self.nodepos[commit.commit_sha1]
906
907 #The first parent always continue on the same line
908 try:
909 # check we alreay have the value
910 tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
911 except KeyError:
912 self.colours[commit.parent_sha1[0]] = colour
913 self.nodepos[commit.parent_sha1[0]] = node_pos
914
915 for sha1 in self.incomplete_line.keys():
916 if (sha1 != commit.commit_sha1):
917 self.draw_incomplete_line(sha1, node_pos,
918 out_line, in_line, index)
919 else:
920 del self.incomplete_line[sha1]
921
922
923 for parent_id in commit.parent_sha1:
924 try:
925 tmp_node_pos = self.nodepos[parent_id]
926 except KeyError:
927 self.colours[parent_id] = last_colour+1
928 last_colour = self.colours[parent_id]
929 self.nodepos[parent_id] = last_nodepos+1
930 last_nodepos = self.nodepos[parent_id]
931
932 in_line.append((node_pos, self.nodepos[parent_id],
933 self.colours[parent_id]))
934 self.add_incomplete_line(parent_id)
935
936 try:
937 branch_tag = self.bt_sha1[commit.commit_sha1]
938 except KeyError:
939 branch_tag = [ ]
940
941
942 node = (node_pos, colour, branch_tag)
943
944 self.model.append([commit, node, out_line, in_line,
945 commit.message, commit.author, commit.date])
946
947 return (in_line, last_colour, last_nodepos)
948
949 def add_incomplete_line(self, sha1):
950 try:
951 self.incomplete_line[sha1].append(self.nodepos[sha1])
952 except KeyError:
953 self.incomplete_line[sha1] = [self.nodepos[sha1]]
954
955 def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
956 for idx, pos in enumerate(self.incomplete_line[sha1]):
957 if(pos == node_pos):
958 #remove the straight line and add a slash
959 if ((pos, pos, self.colours[sha1]) in out_line):
960 out_line.remove((pos, pos, self.colours[sha1]))
961 out_line.append((pos, pos+0.5, self.colours[sha1]))
962 self.incomplete_line[sha1][idx] = pos = pos+0.5
963 try:
964 next_commit = self.commits[index+1]
965 if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
966 # join the line back to the node point
967 # This need to be done only if we modified it
968 in_line.append((pos, pos-0.5, self.colours[sha1]))
969 continue;
970 except IndexError:
971 pass
972 in_line.append((pos, pos, self.colours[sha1]))
973
974
975 def _go_clicked_cb(self, widget, revid):
976 """Callback for when the go button for a parent is clicked."""
977 try:
978 self.treeview.set_cursor(self.index[revid])
979 except KeyError:
980 print "Revision %s not present in the list" % revid
981 # revid == 0 is the parent of the first commit
982 if (revid != 0 ):
983 print "Try running gitview without any options"
984
985 self.treeview.grab_focus()
986
987 def _show_clicked_cb(self, widget, commit_sha1, parent_sha1, encoding):
988 """Callback for when the show button for a parent is clicked."""
989 window = DiffWindow()
990 window.set_diff(commit_sha1, parent_sha1, encoding)
991 self.treeview.grab_focus()
992
993if __name__ == "__main__":
994 without_diff = 0
995
996 if (len(sys.argv) > 1 ):
997 if (sys.argv[1] == "--without-diff"):
998 without_diff = 1
999
1000 view = GitView( without_diff != 1)
1001 view.run(sys.argv[without_diff:])
1002
1003