1 #! /usr/bin/env python
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 """ gitview
9 GUI browser for git repository
10 This 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>"
16 import sys
17 import os
18 import gtk
19 import pygtk
20 import pango
21 import re
22 import time
23 import gobject
24 import cairo
25 import math
26 import string
28 try:
29 import gtksourceview
30 have_gtksourceview = True
31 except ImportError:
32 have_gtksourceview = False
33 print "Running without gtksourceview module"
35 re_ident = re.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
37 def 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
46 return str_arg
48 def 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
57 return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs))
60 class CellRendererGraph(gtk.GenericCellRenderer):
61 """Cell renderer for directed graph.
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.
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.
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 """
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 }
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
103 def box_size(self, widget):
104 """Calculate box size based on widget's font.
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)
116 ascent = pango.PIXELS(metrics.get_ascent())
117 descent = pango.PIXELS(metrics.get_descent())
119 self._box_size = ascent + descent + 6
120 return self._box_size
122 def set_colour(self, ctx, colour, bg, fg):
123 """Set the context source colour.
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 ]
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
144 ctx.set_source_rgb(red, green, blue)
146 def on_get_size(self, widget, cell_area):
147 """Return the size we need for this cell.
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)
155 cols = self.node[0]
156 for start, end, colour in self.in_lines + self.out_lines:
157 cols = int(max(cols, start, end))
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)
165 width = box_size * (cols + 1 ) + names_len
166 height = box_size
168 # FIXME I have no idea how to use cell_area properly
169 return (0, 0, width, height)
171 def on_render(self, window, widget, bg_area, cell_area, exp_area, flags):
172 """Render an individual cell.
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.
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()
189 box_size = self.box_size(widget)
191 ctx.set_line_width(box_size / 8)
192 ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
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)
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)
207 ctx.line_to(cell_area.x + box_size * end + box_size / 2,
208 bg_area.y + bg_area.height / 2)
210 self.set_colour(ctx, colour, 0.0, 0.65)
211 ctx.stroke()
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)
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)
229 ctx.line_to(cell_area.x + box_size * end + box_size / 2,
230 bg_area.y + bg_area.height / 2 + bg_area.height)
232 self.set_colour(ctx, colour, 0.0, 0.65)
233 ctx.stroke()
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)
242 if (len(names) != 0):
243 name = " "
244 for item in names:
245 name = name + item + " "
247 ctx.select_font_face("Monospace")
248 ctx.set_font_size(13)
249 ctx.text_path(name)
251 self.set_colour(ctx, colour, 0.0, 0.5)
252 ctx.stroke_preserve()
254 self.set_colour(ctx, colour, 0.5, 1.0)
255 ctx.fill()
257 class Commit:
258 """ This represent a commit object obtained after parsing the git-rev-list
259 output """
261 children_sha1 = {}
263 def __init__(self, commit_lines):
264 self.message = ""
265 self.author = ""
266 self.date = ""
267 self.committer = ""
268 self.commit_date = ""
269 self.commit_sha1 = ""
270 self.parent_sha1 = [ ]
271 self.parse_commit(commit_lines)
274 def parse_commit(self, commit_lines):
276 # First line is the sha1 lines
277 line = string.strip(commit_lines[0])
278 sha1 = re.split(" ", line)
279 self.commit_sha1 = sha1[0]
280 self.parent_sha1 = sha1[1:]
282 #build the child list
283 for parent_id in self.parent_sha1:
284 try:
285 Commit.children_sha1[parent_id].append(self.commit_sha1)
286 except KeyError:
287 Commit.children_sha1[parent_id] = [self.commit_sha1]
289 # IF we don't have parent
290 if (len(self.parent_sha1) == 0):
291 self.parent_sha1 = [0]
293 for line in commit_lines[1:]:
294 m = re.match("^ ", line)
295 if (m != None):
296 # First line of the commit message used for short log
297 if self.message == "":
298 self.message = string.strip(line)
299 continue
301 m = re.match("tree", line)
302 if (m != None):
303 continue
305 m = re.match("parent", line)
306 if (m != None):
307 continue
309 m = re_ident.match(line)
310 if (m != None):
311 date = show_date(m.group('epoch'), m.group('tz'))
312 if m.group(1) == "author":
313 self.author = m.group('ident')
314 self.date = date
315 elif m.group(1) == "committer":
316 self.committer = m.group('ident')
317 self.commit_date = date
319 continue
321 def get_message(self, with_diff=0):
322 if (with_diff == 1):
323 message = self.diff_tree()
324 else:
325 fp = os.popen("git cat-file commit " + self.commit_sha1)
326 message = fp.read()
327 fp.close()
329 return message
331 def diff_tree(self):
332 fp = os.popen("git diff-tree --pretty --cc -v -p --always " + self.commit_sha1)
333 diff = fp.read()
334 fp.close()
335 return diff
337 class DiffWindow:
338 """Diff window.
339 This object represents and manages a single window containing the
340 differences between two revisions on a branch.
341 """
343 def __init__(self):
344 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
345 self.window.set_border_width(0)
346 self.window.set_title("Git repository browser diff window")
348 # Use two thirds of the screen by default
349 screen = self.window.get_screen()
350 monitor = screen.get_monitor_geometry(0)
351 width = int(monitor.width * 0.66)
352 height = int(monitor.height * 0.66)
353 self.window.set_default_size(width, height)
355 self.construct()
357 def construct(self):
358 """Construct the window contents."""
359 vbox = gtk.VBox()
360 self.window.add(vbox)
361 vbox.show()
363 menu_bar = gtk.MenuBar()
364 save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
365 save_menu.connect("activate", self.save_menu_response, "save")
366 save_menu.show()
367 menu_bar.append(save_menu)
368 vbox.pack_start(menu_bar, False, False, 2)
369 menu_bar.show()
371 scrollwin = gtk.ScrolledWindow()
372 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
373 scrollwin.set_shadow_type(gtk.SHADOW_IN)
374 vbox.pack_start(scrollwin, expand=True, fill=True)
375 scrollwin.show()
377 if have_gtksourceview:
378 self.buffer = gtksourceview.SourceBuffer()
379 slm = gtksourceview.SourceLanguagesManager()
380 gsl = slm.get_language_from_mime_type("text/x-patch")
381 self.buffer.set_highlight(True)
382 self.buffer.set_language(gsl)
383 sourceview = gtksourceview.SourceView(self.buffer)
384 else:
385 self.buffer = gtk.TextBuffer()
386 sourceview = gtk.TextView(self.buffer)
388 sourceview.set_editable(False)
389 sourceview.modify_font(pango.FontDescription("Monospace"))
390 scrollwin.add(sourceview)
391 sourceview.show()
394 def set_diff(self, commit_sha1, parent_sha1, encoding):
395 """Set the differences showed by this window.
396 Compares the two trees and populates the window with the
397 differences.
398 """
399 # Diff with the first commit or the last commit shows nothing
400 if (commit_sha1 == 0 or parent_sha1 == 0 ):
401 return
403 fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
404 self.buffer.set_text(unicode(fp.read(), encoding).encode('utf-8'))
405 fp.close()
406 self.window.show()
408 def save_menu_response(self, widget, string):
409 dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
410 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
411 gtk.STOCK_SAVE, gtk.RESPONSE_OK))
412 dialog.set_default_response(gtk.RESPONSE_OK)
413 response = dialog.run()
414 if response == gtk.RESPONSE_OK:
415 patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
416 self.buffer.get_end_iter())
417 fp = open(dialog.get_filename(), "w")
418 fp.write(patch_buffer)
419 fp.close()
420 dialog.destroy()
422 class GitView:
423 """ This is the main class
424 """
425 version = "0.7"
427 def __init__(self, with_diff=0):
428 self.with_diff = with_diff
429 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
430 self.window.set_border_width(0)
431 self.window.set_title("Git repository browser")
433 self.get_encoding()
434 self.get_bt_sha1()
436 # Use three-quarters of the screen by default
437 screen = self.window.get_screen()
438 monitor = screen.get_monitor_geometry(0)
439 width = int(monitor.width * 0.75)
440 height = int(monitor.height * 0.75)
441 self.window.set_default_size(width, height)
443 # FIXME AndyFitz!
444 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
445 self.window.set_icon(icon)
447 self.accel_group = gtk.AccelGroup()
448 self.window.add_accel_group(self.accel_group)
450 self.construct()
452 def get_bt_sha1(self):
453 """ Update the bt_sha1 dictionary with the
454 respective sha1 details """
456 self.bt_sha1 = { }
457 ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
458 fp = os.popen('git ls-remote "${GIT_DIR-.git}"')
459 while 1:
460 line = string.strip(fp.readline())
461 if line == '':
462 break
463 m = ls_remote.match(line)
464 if not m:
465 continue
466 (sha1, name) = (m.group(1), m.group(2))
467 if not self.bt_sha1.has_key(sha1):
468 self.bt_sha1[sha1] = []
469 self.bt_sha1[sha1].append(name)
470 fp.close()
472 def get_encoding(self):
473 fp = os.popen("git repo-config --get i18n.commitencoding")
474 self.encoding=string.strip(fp.readline())
475 fp.close()
476 if (self.encoding == ""):
477 self.encoding = "utf-8"
480 def construct(self):
481 """Construct the window contents."""
482 paned = gtk.VPaned()
483 paned.pack1(self.construct_top(), resize=False, shrink=True)
484 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
485 self.window.add(paned)
486 paned.show()
489 def construct_top(self):
490 """Construct the top-half of the window."""
491 vbox = gtk.VBox(spacing=6)
492 vbox.set_border_width(12)
493 vbox.show()
495 menu_bar = gtk.MenuBar()
496 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
497 help_menu = gtk.MenuItem("Help")
498 menu = gtk.Menu()
499 about_menu = gtk.MenuItem("About")
500 menu.append(about_menu)
501 about_menu.connect("activate", self.about_menu_response, "about")
502 about_menu.show()
503 help_menu.set_submenu(menu)
504 help_menu.show()
505 menu_bar.append(help_menu)
506 vbox.pack_start(menu_bar, False, False, 2)
507 menu_bar.show()
509 scrollwin = gtk.ScrolledWindow()
510 scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
511 scrollwin.set_shadow_type(gtk.SHADOW_IN)
512 vbox.pack_start(scrollwin, expand=True, fill=True)
513 scrollwin.show()
515 self.treeview = gtk.TreeView()
516 self.treeview.set_rules_hint(True)
517 self.treeview.set_search_column(4)
518 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
519 scrollwin.add(self.treeview)
520 self.treeview.show()
522 cell = CellRendererGraph()
523 column = gtk.TreeViewColumn()
524 column.set_resizable(True)
525 column.pack_start(cell, expand=True)
526 column.add_attribute(cell, "node", 1)
527 column.add_attribute(cell, "in-lines", 2)
528 column.add_attribute(cell, "out-lines", 3)
529 self.treeview.append_column(column)
531 cell = gtk.CellRendererText()
532 cell.set_property("width-chars", 65)
533 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
534 column = gtk.TreeViewColumn("Message")
535 column.set_resizable(True)
536 column.pack_start(cell, expand=True)
537 column.add_attribute(cell, "text", 4)
538 self.treeview.append_column(column)
540 cell = gtk.CellRendererText()
541 cell.set_property("width-chars", 40)
542 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
543 column = gtk.TreeViewColumn("Author")
544 column.set_resizable(True)
545 column.pack_start(cell, expand=True)
546 column.add_attribute(cell, "text", 5)
547 self.treeview.append_column(column)
549 cell = gtk.CellRendererText()
550 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
551 column = gtk.TreeViewColumn("Date")
552 column.set_resizable(True)
553 column.pack_start(cell, expand=True)
554 column.add_attribute(cell, "text", 6)
555 self.treeview.append_column(column)
557 return vbox
559 def about_menu_response(self, widget, string):
560 dialog = gtk.AboutDialog()
561 dialog.set_name("Gitview")
562 dialog.set_version(GitView.version)
563 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
564 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
565 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
566 dialog.set_wrap_license(True)
567 dialog.run()
568 dialog.destroy()
571 def construct_bottom(self):
572 """Construct the bottom half of the window."""
573 vbox = gtk.VBox(False, spacing=6)
574 vbox.set_border_width(12)
575 (width, height) = self.window.get_size()
576 vbox.set_size_request(width, int(height / 2.5))
577 vbox.show()
579 self.table = gtk.Table(rows=4, columns=4)
580 self.table.set_row_spacings(6)
581 self.table.set_col_spacings(6)
582 vbox.pack_start(self.table, expand=False, fill=True)
583 self.table.show()
585 align = gtk.Alignment(0.0, 0.5)
586 label = gtk.Label()
587 label.set_markup("<b>Revision:</b>")
588 align.add(label)
589 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
590 label.show()
591 align.show()
593 align = gtk.Alignment(0.0, 0.5)
594 self.revid_label = gtk.Label()
595 self.revid_label.set_selectable(True)
596 align.add(self.revid_label)
597 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
598 self.revid_label.show()
599 align.show()
601 align = gtk.Alignment(0.0, 0.5)
602 label = gtk.Label()
603 label.set_markup("<b>Committer:</b>")
604 align.add(label)
605 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
606 label.show()
607 align.show()
609 align = gtk.Alignment(0.0, 0.5)
610 self.committer_label = gtk.Label()
611 self.committer_label.set_selectable(True)
612 align.add(self.committer_label)
613 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
614 self.committer_label.show()
615 align.show()
617 align = gtk.Alignment(0.0, 0.5)
618 label = gtk.Label()
619 label.set_markup("<b>Timestamp:</b>")
620 align.add(label)
621 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
622 label.show()
623 align.show()
625 align = gtk.Alignment(0.0, 0.5)
626 self.timestamp_label = gtk.Label()
627 self.timestamp_label.set_selectable(True)
628 align.add(self.timestamp_label)
629 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
630 self.timestamp_label.show()
631 align.show()
633 align = gtk.Alignment(0.0, 0.5)
634 label = gtk.Label()
635 label.set_markup("<b>Parents:</b>")
636 align.add(label)
637 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
638 label.show()
639 align.show()
640 self.parents_widgets = []
642 align = gtk.Alignment(0.0, 0.5)
643 label = gtk.Label()
644 label.set_markup("<b>Children:</b>")
645 align.add(label)
646 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
647 label.show()
648 align.show()
649 self.children_widgets = []
651 scrollwin = gtk.ScrolledWindow()
652 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
653 scrollwin.set_shadow_type(gtk.SHADOW_IN)
654 vbox.pack_start(scrollwin, expand=True, fill=True)
655 scrollwin.show()
657 if have_gtksourceview:
658 self.message_buffer = gtksourceview.SourceBuffer()
659 slm = gtksourceview.SourceLanguagesManager()
660 gsl = slm.get_language_from_mime_type("text/x-patch")
661 self.message_buffer.set_highlight(True)
662 self.message_buffer.set_language(gsl)
663 sourceview = gtksourceview.SourceView(self.message_buffer)
664 else:
665 self.message_buffer = gtk.TextBuffer()
666 sourceview = gtk.TextView(self.message_buffer)
668 sourceview.set_editable(False)
669 sourceview.modify_font(pango.FontDescription("Monospace"))
670 scrollwin.add(sourceview)
671 sourceview.show()
673 return vbox
675 def _treeview_cursor_cb(self, *args):
676 """Callback for when the treeview cursor changes."""
677 (path, col) = self.treeview.get_cursor()
678 commit = self.model[path][0]
680 if commit.committer is not None:
681 committer = commit.committer
682 timestamp = commit.commit_date
683 message = commit.get_message(self.with_diff)
684 revid_label = commit.commit_sha1
685 else:
686 committer = ""
687 timestamp = ""
688 message = ""
689 revid_label = ""
691 self.revid_label.set_text(revid_label)
692 self.committer_label.set_text(committer)
693 self.timestamp_label.set_text(timestamp)
694 self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
696 for widget in self.parents_widgets:
697 self.table.remove(widget)
699 self.parents_widgets = []
700 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
701 for idx, parent_id in enumerate(commit.parent_sha1):
702 self.table.set_row_spacing(idx + 3, 0)
704 align = gtk.Alignment(0.0, 0.0)
705 self.parents_widgets.append(align)
706 self.table.attach(align, 1, 2, idx + 3, idx + 4,
707 gtk.EXPAND | gtk.FILL, gtk.FILL)
708 align.show()
710 hbox = gtk.HBox(False, 0)
711 align.add(hbox)
712 hbox.show()
714 label = gtk.Label(parent_id)
715 label.set_selectable(True)
716 hbox.pack_start(label, expand=False, fill=True)
717 label.show()
719 image = gtk.Image()
720 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
721 image.show()
723 button = gtk.Button()
724 button.add(image)
725 button.set_relief(gtk.RELIEF_NONE)
726 button.connect("clicked", self._go_clicked_cb, parent_id)
727 hbox.pack_start(button, expand=False, fill=True)
728 button.show()
730 image = gtk.Image()
731 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
732 image.show()
734 button = gtk.Button()
735 button.add(image)
736 button.set_relief(gtk.RELIEF_NONE)
737 button.set_sensitive(True)
738 button.connect("clicked", self._show_clicked_cb,
739 commit.commit_sha1, parent_id, self.encoding)
740 hbox.pack_start(button, expand=False, fill=True)
741 button.show()
743 # Populate with child details
744 for widget in self.children_widgets:
745 self.table.remove(widget)
747 self.children_widgets = []
748 try:
749 child_sha1 = Commit.children_sha1[commit.commit_sha1]
750 except KeyError:
751 # We don't have child
752 child_sha1 = [ 0 ]
754 if ( len(child_sha1) > len(commit.parent_sha1)):
755 self.table.resize(4 + len(child_sha1) - 1, 4)
757 for idx, child_id in enumerate(child_sha1):
758 self.table.set_row_spacing(idx + 3, 0)
760 align = gtk.Alignment(0.0, 0.0)
761 self.children_widgets.append(align)
762 self.table.attach(align, 3, 4, idx + 3, idx + 4,
763 gtk.EXPAND | gtk.FILL, gtk.FILL)
764 align.show()
766 hbox = gtk.HBox(False, 0)
767 align.add(hbox)
768 hbox.show()
770 label = gtk.Label(child_id)
771 label.set_selectable(True)
772 hbox.pack_start(label, expand=False, fill=True)
773 label.show()
775 image = gtk.Image()
776 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
777 image.show()
779 button = gtk.Button()
780 button.add(image)
781 button.set_relief(gtk.RELIEF_NONE)
782 button.connect("clicked", self._go_clicked_cb, child_id)
783 hbox.pack_start(button, expand=False, fill=True)
784 button.show()
786 image = gtk.Image()
787 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
788 image.show()
790 button = gtk.Button()
791 button.add(image)
792 button.set_relief(gtk.RELIEF_NONE)
793 button.set_sensitive(True)
794 button.connect("clicked", self._show_clicked_cb,
795 child_id, commit.commit_sha1)
796 hbox.pack_start(button, expand=False, fill=True)
797 button.show()
799 def _destroy_cb(self, widget):
800 """Callback for when a window we manage is destroyed."""
801 self.quit()
804 def quit(self):
805 """Stop the GTK+ main loop."""
806 gtk.main_quit()
808 def run(self, args):
809 self.set_branch(args)
810 self.window.connect("destroy", self._destroy_cb)
811 self.window.show()
812 gtk.main()
814 def set_branch(self, args):
815 """Fill in different windows with info from the reposiroty"""
816 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
817 git_rev_list_cmd = fp.read()
818 fp.close()
819 fp = os.popen("git rev-list --header --topo-order --parents " + git_rev_list_cmd)
820 self.update_window(fp)
822 def update_window(self, fp):
823 commit_lines = []
825 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
826 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
828 # used for cursor positioning
829 self.index = {}
831 self.colours = {}
832 self.nodepos = {}
833 self.incomplete_line = {}
834 self.commits = []
836 index = 0
837 last_colour = 0
838 last_nodepos = -1
839 out_line = []
840 input_line = fp.readline()
841 while (input_line != ""):
842 # The commit header ends with '\0'
843 # This NULL is immediately followed by the sha1 of the
844 # next commit
845 if (input_line[0] != '\0'):
846 commit_lines.append(input_line)
847 input_line = fp.readline()
848 continue;
850 commit = Commit(commit_lines)
851 if (commit != None ):
852 self.commits.append(commit)
854 # Skip the '\0
855 commit_lines = []
856 commit_lines.append(input_line[1:])
857 input_line = fp.readline()
859 fp.close()
861 for commit in self.commits:
862 (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
863 index, out_line,
864 last_colour,
865 last_nodepos)
866 self.index[commit.commit_sha1] = index
867 index += 1
869 self.treeview.set_model(self.model)
870 self.treeview.show()
872 def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
873 in_line=[]
875 # | -> outline
876 # X
877 # |\ <- inline
879 # Reset nodepostion
880 if (last_nodepos > 5):
881 last_nodepos = -1
883 # Add the incomplete lines of the last cell in this
884 try:
885 colour = self.colours[commit.commit_sha1]
886 except KeyError:
887 self.colours[commit.commit_sha1] = last_colour+1
888 last_colour = self.colours[commit.commit_sha1]
889 colour = self.colours[commit.commit_sha1]
891 try:
892 node_pos = self.nodepos[commit.commit_sha1]
893 except KeyError:
894 self.nodepos[commit.commit_sha1] = last_nodepos+1
895 last_nodepos = self.nodepos[commit.commit_sha1]
896 node_pos = self.nodepos[commit.commit_sha1]
898 #The first parent always continue on the same line
899 try:
900 # check we alreay have the value
901 tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
902 except KeyError:
903 self.colours[commit.parent_sha1[0]] = colour
904 self.nodepos[commit.parent_sha1[0]] = node_pos
906 for sha1 in self.incomplete_line.keys():
907 if (sha1 != commit.commit_sha1):
908 self.draw_incomplete_line(sha1, node_pos,
909 out_line, in_line, index)
910 else:
911 del self.incomplete_line[sha1]
914 for parent_id in commit.parent_sha1:
915 try:
916 tmp_node_pos = self.nodepos[parent_id]
917 except KeyError:
918 self.colours[parent_id] = last_colour+1
919 last_colour = self.colours[parent_id]
920 self.nodepos[parent_id] = last_nodepos+1
921 last_nodepos = self.nodepos[parent_id]
923 in_line.append((node_pos, self.nodepos[parent_id],
924 self.colours[parent_id]))
925 self.add_incomplete_line(parent_id)
927 try:
928 branch_tag = self.bt_sha1[commit.commit_sha1]
929 except KeyError:
930 branch_tag = [ ]
933 node = (node_pos, colour, branch_tag)
935 self.model.append([commit, node, out_line, in_line,
936 commit.message, commit.author, commit.date])
938 return (in_line, last_colour, last_nodepos)
940 def add_incomplete_line(self, sha1):
941 try:
942 self.incomplete_line[sha1].append(self.nodepos[sha1])
943 except KeyError:
944 self.incomplete_line[sha1] = [self.nodepos[sha1]]
946 def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
947 for idx, pos in enumerate(self.incomplete_line[sha1]):
948 if(pos == node_pos):
949 #remove the straight line and add a slash
950 if ((pos, pos, self.colours[sha1]) in out_line):
951 out_line.remove((pos, pos, self.colours[sha1]))
952 out_line.append((pos, pos+0.5, self.colours[sha1]))
953 self.incomplete_line[sha1][idx] = pos = pos+0.5
954 try:
955 next_commit = self.commits[index+1]
956 if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
957 # join the line back to the node point
958 # This need to be done only if we modified it
959 in_line.append((pos, pos-0.5, self.colours[sha1]))
960 continue;
961 except IndexError:
962 pass
963 in_line.append((pos, pos, self.colours[sha1]))
966 def _go_clicked_cb(self, widget, revid):
967 """Callback for when the go button for a parent is clicked."""
968 try:
969 self.treeview.set_cursor(self.index[revid])
970 except KeyError:
971 print "Revision %s not present in the list" % revid
972 # revid == 0 is the parent of the first commit
973 if (revid != 0 ):
974 print "Try running gitview without any options"
976 self.treeview.grab_focus()
978 def _show_clicked_cb(self, widget, commit_sha1, parent_sha1, encoding):
979 """Callback for when the show button for a parent is clicked."""
980 window = DiffWindow()
981 window.set_diff(commit_sha1, parent_sha1, encoding)
982 self.treeview.grab_focus()
984 if __name__ == "__main__":
985 without_diff = 0
987 if (len(sys.argv) > 1 ):
988 if (sys.argv[1] == "--without-diff"):
989 without_diff = 1
991 view = GitView( without_diff != 1)
992 view.run(sys.argv[without_diff:])