Code

aded7ede7028fc8dd3298fce81e4ad0da9d54a02
[git.git] / contrib / gitview / gitview
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                 self.set_colour(ctx, colour, 0.0, 0.5)
243                 ctx.stroke_preserve()
245                 self.set_colour(ctx, colour, 0.5, 1.0)
246                 ctx.fill_preserve()
248                 if (len(names) != 0):
249                         name = " "
250                         for item in names:
251                                 name = name + item + " "
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)
260 class Commit:
261         """ This represent a commit object obtained after parsing the git-rev-list
262         output """
264         children_sha1 = {}
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)
277         def parse_commit(self, commit_lines):
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:]
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]
292                 # IF we don't have parent
293                 if (len(self.parent_sha1) == 0):
294                         self.parent_sha1 = [0]
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
304                         m = re.match("tree", line)
305                         if (m != None):
306                                 continue
308                         m = re.match("parent", line)
309                         if (m != None):
310                                 continue
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
322                                 continue
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()
332                 return message
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
340 class DiffWindow:
341         """Diff window.
342         This object represents and manages a single window containing the
343         differences between two revisions on a branch.
344         """
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")
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)
358                 self.construct()
360         def construct(self):
361                 """Construct the window contents."""
362                 vbox = gtk.VBox()
363                 self.window.add(vbox)
364                 vbox.show()
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, False, False, 2)
372                 menu_bar.show()
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()
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)
391                 sourceview.set_editable(False)
392                 sourceview.modify_font(pango.FontDescription("Monospace"))
393                 scrollwin.add(sourceview)
394                 sourceview.show()
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
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()
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()
425 class GitView:
426         """ This is the main class
427         """
428         version = "0.7"
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")
436                 self.get_encoding()
437                 self.get_bt_sha1()
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)
446                 # FIXME AndyFitz!
447                 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
448                 self.window.set_icon(icon)
450                 self.accel_group = gtk.AccelGroup()
451                 self.window.add_accel_group(self.accel_group)
453                 self.construct()
455         def get_bt_sha1(self):
456                 """ Update the bt_sha1 dictionary with the
457                 respective sha1 details """
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()
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"
483         def construct(self):
484                 """Construct the window contents."""
485                 paned = gtk.VPaned()
486                 paned.pack1(self.construct_top(), resize=False, shrink=True)
487                 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
488                 self.window.add(paned)
489                 paned.show()
492         def construct_top(self):
493                 """Construct the top-half of the window."""
494                 vbox = gtk.VBox(spacing=6)
495                 vbox.set_border_width(12)
496                 vbox.show()
498                 menu_bar = gtk.MenuBar()
499                 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
500                 help_menu = gtk.MenuItem("Help")
501                 menu = gtk.Menu()
502                 about_menu = gtk.MenuItem("About")
503                 menu.append(about_menu)
504                 about_menu.connect("activate", self.about_menu_response, "about")
505                 about_menu.show()
506                 help_menu.set_submenu(menu)
507                 help_menu.show()
508                 menu_bar.append(help_menu)
509                 vbox.pack_start(menu_bar, False, False, 2)
510                 menu_bar.show()
512                 scrollwin = gtk.ScrolledWindow()
513                 scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
514                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
515                 vbox.pack_start(scrollwin, expand=True, fill=True)
516                 scrollwin.show()
518                 self.treeview = gtk.TreeView()
519                 self.treeview.set_rules_hint(True)
520                 self.treeview.set_search_column(4)
521                 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
522                 scrollwin.add(self.treeview)
523                 self.treeview.show()
525                 cell = CellRendererGraph()
526                 column = gtk.TreeViewColumn()
527                 column.set_resizable(True)
528                 column.pack_start(cell, expand=True)
529                 column.add_attribute(cell, "node", 1)
530                 column.add_attribute(cell, "in-lines", 2)
531                 column.add_attribute(cell, "out-lines", 3)
532                 self.treeview.append_column(column)
534                 cell = gtk.CellRendererText()
535                 cell.set_property("width-chars", 65)
536                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
537                 column = gtk.TreeViewColumn("Message")
538                 column.set_resizable(True)
539                 column.pack_start(cell, expand=True)
540                 column.add_attribute(cell, "text", 4)
541                 self.treeview.append_column(column)
543                 cell = gtk.CellRendererText()
544                 cell.set_property("width-chars", 40)
545                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
546                 column = gtk.TreeViewColumn("Author")
547                 column.set_resizable(True)
548                 column.pack_start(cell, expand=True)
549                 column.add_attribute(cell, "text", 5)
550                 self.treeview.append_column(column)
552                 cell = gtk.CellRendererText()
553                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
554                 column = gtk.TreeViewColumn("Date")
555                 column.set_resizable(True)
556                 column.pack_start(cell, expand=True)
557                 column.add_attribute(cell, "text", 6)
558                 self.treeview.append_column(column)
560                 return vbox
562         def about_menu_response(self, widget, string):
563                 dialog = gtk.AboutDialog()
564                 dialog.set_name("Gitview")
565                 dialog.set_version(GitView.version)
566                 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
567                 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
568                 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
569                 dialog.set_wrap_license(True)
570                 dialog.run()
571                 dialog.destroy()
574         def construct_bottom(self):
575                 """Construct the bottom half of the window."""
576                 vbox = gtk.VBox(False, spacing=6)
577                 vbox.set_border_width(12)
578                 (width, height) = self.window.get_size()
579                 vbox.set_size_request(width, int(height / 2.5))
580                 vbox.show()
582                 self.table = gtk.Table(rows=4, columns=4)
583                 self.table.set_row_spacings(6)
584                 self.table.set_col_spacings(6)
585                 vbox.pack_start(self.table, expand=False, fill=True)
586                 self.table.show()
588                 align = gtk.Alignment(0.0, 0.5)
589                 label = gtk.Label()
590                 label.set_markup("<b>Revision:</b>")
591                 align.add(label)
592                 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
593                 label.show()
594                 align.show()
596                 align = gtk.Alignment(0.0, 0.5)
597                 self.revid_label = gtk.Label()
598                 self.revid_label.set_selectable(True)
599                 align.add(self.revid_label)
600                 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
601                 self.revid_label.show()
602                 align.show()
604                 align = gtk.Alignment(0.0, 0.5)
605                 label = gtk.Label()
606                 label.set_markup("<b>Committer:</b>")
607                 align.add(label)
608                 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
609                 label.show()
610                 align.show()
612                 align = gtk.Alignment(0.0, 0.5)
613                 self.committer_label = gtk.Label()
614                 self.committer_label.set_selectable(True)
615                 align.add(self.committer_label)
616                 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
617                 self.committer_label.show()
618                 align.show()
620                 align = gtk.Alignment(0.0, 0.5)
621                 label = gtk.Label()
622                 label.set_markup("<b>Timestamp:</b>")
623                 align.add(label)
624                 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
625                 label.show()
626                 align.show()
628                 align = gtk.Alignment(0.0, 0.5)
629                 self.timestamp_label = gtk.Label()
630                 self.timestamp_label.set_selectable(True)
631                 align.add(self.timestamp_label)
632                 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
633                 self.timestamp_label.show()
634                 align.show()
636                 align = gtk.Alignment(0.0, 0.5)
637                 label = gtk.Label()
638                 label.set_markup("<b>Parents:</b>")
639                 align.add(label)
640                 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
641                 label.show()
642                 align.show()
643                 self.parents_widgets = []
645                 align = gtk.Alignment(0.0, 0.5)
646                 label = gtk.Label()
647                 label.set_markup("<b>Children:</b>")
648                 align.add(label)
649                 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
650                 label.show()
651                 align.show()
652                 self.children_widgets = []
654                 scrollwin = gtk.ScrolledWindow()
655                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
656                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
657                 vbox.pack_start(scrollwin, expand=True, fill=True)
658                 scrollwin.show()
660                 if have_gtksourceview:
661                         self.message_buffer = gtksourceview.SourceBuffer()
662                         slm = gtksourceview.SourceLanguagesManager()
663                         gsl = slm.get_language_from_mime_type("text/x-patch")
664                         self.message_buffer.set_highlight(True)
665                         self.message_buffer.set_language(gsl)
666                         sourceview = gtksourceview.SourceView(self.message_buffer)
667                 else:
668                         self.message_buffer = gtk.TextBuffer()
669                         sourceview = gtk.TextView(self.message_buffer)
671                 sourceview.set_editable(False)
672                 sourceview.modify_font(pango.FontDescription("Monospace"))
673                 scrollwin.add(sourceview)
674                 sourceview.show()
676                 return vbox
678         def _treeview_cursor_cb(self, *args):
679                 """Callback for when the treeview cursor changes."""
680                 (path, col) = self.treeview.get_cursor()
681                 commit = self.model[path][0]
683                 if commit.committer is not None:
684                         committer = commit.committer
685                         timestamp = commit.commit_date
686                         message   =  commit.get_message(self.with_diff)
687                         revid_label = commit.commit_sha1
688                 else:
689                         committer = ""
690                         timestamp = ""
691                         message = ""
692                         revid_label = ""
694                 self.revid_label.set_text(revid_label)
695                 self.committer_label.set_text(committer)
696                 self.timestamp_label.set_text(timestamp)
697                 self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
699                 for widget in self.parents_widgets:
700                         self.table.remove(widget)
702                 self.parents_widgets = []
703                 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
704                 for idx, parent_id in enumerate(commit.parent_sha1):
705                         self.table.set_row_spacing(idx + 3, 0)
707                         align = gtk.Alignment(0.0, 0.0)
708                         self.parents_widgets.append(align)
709                         self.table.attach(align, 1, 2, idx + 3, idx + 4,
710                                         gtk.EXPAND | gtk.FILL, gtk.FILL)
711                         align.show()
713                         hbox = gtk.HBox(False, 0)
714                         align.add(hbox)
715                         hbox.show()
717                         label = gtk.Label(parent_id)
718                         label.set_selectable(True)
719                         hbox.pack_start(label, expand=False, fill=True)
720                         label.show()
722                         image = gtk.Image()
723                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
724                         image.show()
726                         button = gtk.Button()
727                         button.add(image)
728                         button.set_relief(gtk.RELIEF_NONE)
729                         button.connect("clicked", self._go_clicked_cb, parent_id)
730                         hbox.pack_start(button, expand=False, fill=True)
731                         button.show()
733                         image = gtk.Image()
734                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
735                         image.show()
737                         button = gtk.Button()
738                         button.add(image)
739                         button.set_relief(gtk.RELIEF_NONE)
740                         button.set_sensitive(True)
741                         button.connect("clicked", self._show_clicked_cb,
742                                         commit.commit_sha1, parent_id, self.encoding)
743                         hbox.pack_start(button, expand=False, fill=True)
744                         button.show()
746                 # Populate with child details
747                 for widget in self.children_widgets:
748                         self.table.remove(widget)
750                 self.children_widgets = []
751                 try:
752                         child_sha1 = Commit.children_sha1[commit.commit_sha1]
753                 except KeyError:
754                         # We don't have child
755                         child_sha1 = [ 0 ]
757                 if ( len(child_sha1) > len(commit.parent_sha1)):
758                         self.table.resize(4 + len(child_sha1) - 1, 4)
760                 for idx, child_id in enumerate(child_sha1):
761                         self.table.set_row_spacing(idx + 3, 0)
763                         align = gtk.Alignment(0.0, 0.0)
764                         self.children_widgets.append(align)
765                         self.table.attach(align, 3, 4, idx + 3, idx + 4,
766                                         gtk.EXPAND | gtk.FILL, gtk.FILL)
767                         align.show()
769                         hbox = gtk.HBox(False, 0)
770                         align.add(hbox)
771                         hbox.show()
773                         label = gtk.Label(child_id)
774                         label.set_selectable(True)
775                         hbox.pack_start(label, expand=False, fill=True)
776                         label.show()
778                         image = gtk.Image()
779                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
780                         image.show()
782                         button = gtk.Button()
783                         button.add(image)
784                         button.set_relief(gtk.RELIEF_NONE)
785                         button.connect("clicked", self._go_clicked_cb, child_id)
786                         hbox.pack_start(button, expand=False, fill=True)
787                         button.show()
789                         image = gtk.Image()
790                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
791                         image.show()
793                         button = gtk.Button()
794                         button.add(image)
795                         button.set_relief(gtk.RELIEF_NONE)
796                         button.set_sensitive(True)
797                         button.connect("clicked", self._show_clicked_cb,
798                                         child_id, commit.commit_sha1)
799                         hbox.pack_start(button, expand=False, fill=True)
800                         button.show()
802         def _destroy_cb(self, widget):
803                 """Callback for when a window we manage is destroyed."""
804                 self.quit()
807         def quit(self):
808                 """Stop the GTK+ main loop."""
809                 gtk.main_quit()
811         def run(self, args):
812                 self.set_branch(args)
813                 self.window.connect("destroy", self._destroy_cb)
814                 self.window.show()
815                 gtk.main()
817         def set_branch(self, args):
818                 """Fill in different windows with info from the reposiroty"""
819                 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
820                 git_rev_list_cmd = fp.read()
821                 fp.close()
822                 fp = os.popen("git rev-list  --header --topo-order --parents " + git_rev_list_cmd)
823                 self.update_window(fp)
825         def update_window(self, fp):
826                 commit_lines = []
828                 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
829                                 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
831                 # used for cursor positioning
832                 self.index = {}
834                 self.colours = {}
835                 self.nodepos = {}
836                 self.incomplete_line = {}
837                 self.commits = []
839                 index = 0
840                 last_colour = 0
841                 last_nodepos = -1
842                 out_line = []
843                 input_line = fp.readline()
844                 while (input_line != ""):
845                         # The commit header ends with '\0'
846                         # This NULL is immediately followed by the sha1 of the
847                         # next commit
848                         if (input_line[0] != '\0'):
849                                 commit_lines.append(input_line)
850                                 input_line = fp.readline()
851                                 continue;
853                         commit = Commit(commit_lines)
854                         if (commit != None ):
855                                 self.commits.append(commit)
857                         # Skip the '\0
858                         commit_lines = []
859                         commit_lines.append(input_line[1:])
860                         input_line = fp.readline()
862                 fp.close()
864                 for commit in self.commits:
865                         (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
866                                                                                 index, out_line,
867                                                                                 last_colour,
868                                                                                 last_nodepos)
869                         self.index[commit.commit_sha1] = index
870                         index += 1
872                 self.treeview.set_model(self.model)
873                 self.treeview.show()
875         def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
876                 in_line=[]
878                 #   |   -> outline
879                 #   X
880                 #   |\  <- inline
882                 # Reset nodepostion
883                 if (last_nodepos > 5):
884                         last_nodepos = -1
886                 # Add the incomplete lines of the last cell in this
887                 try:
888                         colour = self.colours[commit.commit_sha1]
889                 except KeyError:
890                         self.colours[commit.commit_sha1] = last_colour+1
891                         last_colour = self.colours[commit.commit_sha1]
892                         colour =   self.colours[commit.commit_sha1]
894                 try:
895                         node_pos = self.nodepos[commit.commit_sha1]
896                 except KeyError:
897                         self.nodepos[commit.commit_sha1] = last_nodepos+1
898                         last_nodepos = self.nodepos[commit.commit_sha1]
899                         node_pos =  self.nodepos[commit.commit_sha1]
901                 #The first parent always continue on the same line
902                 try:
903                         # check we alreay have the value
904                         tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
905                 except KeyError:
906                         self.colours[commit.parent_sha1[0]] = colour
907                         self.nodepos[commit.parent_sha1[0]] = node_pos
909                 for sha1 in self.incomplete_line.keys():
910                         if (sha1 != commit.commit_sha1):
911                                 self.draw_incomplete_line(sha1, node_pos,
912                                                 out_line, in_line, index)
913                         else:
914                                 del self.incomplete_line[sha1]
917                 for parent_id in commit.parent_sha1:
918                         try:
919                                 tmp_node_pos = self.nodepos[parent_id]
920                         except KeyError:
921                                 self.colours[parent_id] = last_colour+1
922                                 last_colour = self.colours[parent_id]
923                                 self.nodepos[parent_id] = last_nodepos+1
924                                 last_nodepos = self.nodepos[parent_id]
926                         in_line.append((node_pos, self.nodepos[parent_id],
927                                                 self.colours[parent_id]))
928                         self.add_incomplete_line(parent_id)
930                 try:
931                         branch_tag = self.bt_sha1[commit.commit_sha1]
932                 except KeyError:
933                         branch_tag = [ ]
936                 node = (node_pos, colour, branch_tag)
938                 self.model.append([commit, node, out_line, in_line,
939                                 commit.message, commit.author, commit.date])
941                 return (in_line, last_colour, last_nodepos)
943         def add_incomplete_line(self, sha1):
944                 try:
945                         self.incomplete_line[sha1].append(self.nodepos[sha1])
946                 except KeyError:
947                         self.incomplete_line[sha1] = [self.nodepos[sha1]]
949         def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
950                 for idx, pos in enumerate(self.incomplete_line[sha1]):
951                         if(pos == node_pos):
952                                 #remove the straight line and add a slash
953                                 if ((pos, pos, self.colours[sha1]) in out_line):
954                                         out_line.remove((pos, pos, self.colours[sha1]))
955                                 out_line.append((pos, pos+0.5, self.colours[sha1]))
956                                 self.incomplete_line[sha1][idx] = pos = pos+0.5
957                         try:
958                                 next_commit = self.commits[index+1]
959                                 if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
960                                 # join the line back to the node point
961                                 # This need to be done only if we modified it
962                                         in_line.append((pos, pos-0.5, self.colours[sha1]))
963                                         continue;
964                         except IndexError:
965                                 pass
966                         in_line.append((pos, pos, self.colours[sha1]))
969         def _go_clicked_cb(self, widget, revid):
970                 """Callback for when the go button for a parent is clicked."""
971                 try:
972                         self.treeview.set_cursor(self.index[revid])
973                 except KeyError:
974                         print "Revision %s not present in the list" % revid
975                         # revid == 0 is the parent of the first commit
976                         if (revid != 0 ):
977                                 print "Try running gitview without any options"
979                 self.treeview.grab_focus()
981         def _show_clicked_cb(self, widget,  commit_sha1, parent_sha1, encoding):
982                 """Callback for when the show button for a parent is clicked."""
983                 window = DiffWindow()
984                 window.set_diff(commit_sha1, parent_sha1, encoding)
985                 self.treeview.grab_focus()
987 if __name__ == "__main__":
988         without_diff = 0
990         if (len(sys.argv) > 1 ):
991                 if (sys.argv[1] == "--without-diff"):
992                         without_diff = 1
994         view = GitView( without_diff != 1)
995         view.run(sys.argv[without_diff:])