Code

Merge fixes early for next maint series.
[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 = 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):
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(fp.read())
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.6"
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_bt_sha1()
435                 # Use three-quarters of the screen by default
436                 screen = self.window.get_screen()
437                 monitor = screen.get_monitor_geometry(0)
438                 width = int(monitor.width * 0.75)
439                 height = int(monitor.height * 0.75)
440                 self.window.set_default_size(width, height)
442                 # FIXME AndyFitz!
443                 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
444                 self.window.set_icon(icon)
446                 self.accel_group = gtk.AccelGroup()
447                 self.window.add_accel_group(self.accel_group)
449                 self.construct()
451         def get_bt_sha1(self):
452                 """ Update the bt_sha1 dictionary with the
453                 respective sha1 details """
455                 self.bt_sha1 = { }
456                 ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
457                 fp = os.popen('git ls-remote "${GIT_DIR-.git}"')
458                 while 1:
459                         line = string.strip(fp.readline())
460                         if line == '':
461                                 break
462                         m = ls_remote.match(line)
463                         if not m:
464                                 continue
465                         (sha1, name) = (m.group(1), m.group(2))
466                         if not self.bt_sha1.has_key(sha1):
467                                 self.bt_sha1[sha1] = []
468                         self.bt_sha1[sha1].append(name)
469                 fp.close()
472         def construct(self):
473                 """Construct the window contents."""
474                 paned = gtk.VPaned()
475                 paned.pack1(self.construct_top(), resize=False, shrink=True)
476                 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
477                 self.window.add(paned)
478                 paned.show()
481         def construct_top(self):
482                 """Construct the top-half of the window."""
483                 vbox = gtk.VBox(spacing=6)
484                 vbox.set_border_width(12)
485                 vbox.show()
487                 menu_bar = gtk.MenuBar()
488                 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
489                 help_menu = gtk.MenuItem("Help")
490                 menu = gtk.Menu()
491                 about_menu = gtk.MenuItem("About")
492                 menu.append(about_menu)
493                 about_menu.connect("activate", self.about_menu_response, "about")
494                 about_menu.show()
495                 help_menu.set_submenu(menu)
496                 help_menu.show()
497                 menu_bar.append(help_menu)
498                 vbox.pack_start(menu_bar, False, False, 2)
499                 menu_bar.show()
501                 scrollwin = gtk.ScrolledWindow()
502                 scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
503                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
504                 vbox.pack_start(scrollwin, expand=True, fill=True)
505                 scrollwin.show()
507                 self.treeview = gtk.TreeView()
508                 self.treeview.set_rules_hint(True)
509                 self.treeview.set_search_column(4)
510                 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
511                 scrollwin.add(self.treeview)
512                 self.treeview.show()
514                 cell = CellRendererGraph()
515                 column = gtk.TreeViewColumn()
516                 column.set_resizable(True)
517                 column.pack_start(cell, expand=True)
518                 column.add_attribute(cell, "node", 1)
519                 column.add_attribute(cell, "in-lines", 2)
520                 column.add_attribute(cell, "out-lines", 3)
521                 self.treeview.append_column(column)
523                 cell = gtk.CellRendererText()
524                 cell.set_property("width-chars", 65)
525                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
526                 column = gtk.TreeViewColumn("Message")
527                 column.set_resizable(True)
528                 column.pack_start(cell, expand=True)
529                 column.add_attribute(cell, "text", 4)
530                 self.treeview.append_column(column)
532                 cell = gtk.CellRendererText()
533                 cell.set_property("width-chars", 40)
534                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
535                 column = gtk.TreeViewColumn("Author")
536                 column.set_resizable(True)
537                 column.pack_start(cell, expand=True)
538                 column.add_attribute(cell, "text", 5)
539                 self.treeview.append_column(column)
541                 cell = gtk.CellRendererText()
542                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
543                 column = gtk.TreeViewColumn("Date")
544                 column.set_resizable(True)
545                 column.pack_start(cell, expand=True)
546                 column.add_attribute(cell, "text", 6)
547                 self.treeview.append_column(column)
549                 return vbox
551         def about_menu_response(self, widget, string):
552                 dialog = gtk.AboutDialog()
553                 dialog.set_name("Gitview")
554                 dialog.set_version(GitView.version)
555                 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
556                 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
557                 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
558                 dialog.set_wrap_license(True)
559                 dialog.run()
560                 dialog.destroy()
563         def construct_bottom(self):
564                 """Construct the bottom half of the window."""
565                 vbox = gtk.VBox(False, spacing=6)
566                 vbox.set_border_width(12)
567                 (width, height) = self.window.get_size()
568                 vbox.set_size_request(width, int(height / 2.5))
569                 vbox.show()
571                 self.table = gtk.Table(rows=4, columns=4)
572                 self.table.set_row_spacings(6)
573                 self.table.set_col_spacings(6)
574                 vbox.pack_start(self.table, expand=False, fill=True)
575                 self.table.show()
577                 align = gtk.Alignment(0.0, 0.5)
578                 label = gtk.Label()
579                 label.set_markup("<b>Revision:</b>")
580                 align.add(label)
581                 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
582                 label.show()
583                 align.show()
585                 align = gtk.Alignment(0.0, 0.5)
586                 self.revid_label = gtk.Label()
587                 self.revid_label.set_selectable(True)
588                 align.add(self.revid_label)
589                 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
590                 self.revid_label.show()
591                 align.show()
593                 align = gtk.Alignment(0.0, 0.5)
594                 label = gtk.Label()
595                 label.set_markup("<b>Committer:</b>")
596                 align.add(label)
597                 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
598                 label.show()
599                 align.show()
601                 align = gtk.Alignment(0.0, 0.5)
602                 self.committer_label = gtk.Label()
603                 self.committer_label.set_selectable(True)
604                 align.add(self.committer_label)
605                 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
606                 self.committer_label.show()
607                 align.show()
609                 align = gtk.Alignment(0.0, 0.5)
610                 label = gtk.Label()
611                 label.set_markup("<b>Timestamp:</b>")
612                 align.add(label)
613                 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
614                 label.show()
615                 align.show()
617                 align = gtk.Alignment(0.0, 0.5)
618                 self.timestamp_label = gtk.Label()
619                 self.timestamp_label.set_selectable(True)
620                 align.add(self.timestamp_label)
621                 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
622                 self.timestamp_label.show()
623                 align.show()
625                 align = gtk.Alignment(0.0, 0.5)
626                 label = gtk.Label()
627                 label.set_markup("<b>Parents:</b>")
628                 align.add(label)
629                 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
630                 label.show()
631                 align.show()
632                 self.parents_widgets = []
634                 align = gtk.Alignment(0.0, 0.5)
635                 label = gtk.Label()
636                 label.set_markup("<b>Children:</b>")
637                 align.add(label)
638                 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
639                 label.show()
640                 align.show()
641                 self.children_widgets = []
643                 scrollwin = gtk.ScrolledWindow()
644                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
645                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
646                 vbox.pack_start(scrollwin, expand=True, fill=True)
647                 scrollwin.show()
649                 if have_gtksourceview:
650                         self.message_buffer = gtksourceview.SourceBuffer()
651                         slm = gtksourceview.SourceLanguagesManager()
652                         gsl = slm.get_language_from_mime_type("text/x-patch")
653                         self.message_buffer.set_highlight(True)
654                         self.message_buffer.set_language(gsl)
655                         sourceview = gtksourceview.SourceView(self.message_buffer)
656                 else:
657                         self.message_buffer = gtk.TextBuffer()
658                         sourceview = gtk.TextView(self.message_buffer)
660                 sourceview.set_editable(False)
661                 sourceview.modify_font(pango.FontDescription("Monospace"))
662                 scrollwin.add(sourceview)
663                 sourceview.show()
665                 return vbox
667         def _treeview_cursor_cb(self, *args):
668                 """Callback for when the treeview cursor changes."""
669                 (path, col) = self.treeview.get_cursor()
670                 commit = self.model[path][0]
672                 if commit.committer is not None:
673                         committer = commit.committer
674                         timestamp = commit.commit_date
675                         message   =  commit.get_message(self.with_diff)
676                         revid_label = commit.commit_sha1
677                 else:
678                         committer = ""
679                         timestamp = ""
680                         message = ""
681                         revid_label = ""
683                 self.revid_label.set_text(revid_label)
684                 self.committer_label.set_text(committer)
685                 self.timestamp_label.set_text(timestamp)
686                 self.message_buffer.set_text(message)
688                 for widget in self.parents_widgets:
689                         self.table.remove(widget)
691                 self.parents_widgets = []
692                 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
693                 for idx, parent_id in enumerate(commit.parent_sha1):
694                         self.table.set_row_spacing(idx + 3, 0)
696                         align = gtk.Alignment(0.0, 0.0)
697                         self.parents_widgets.append(align)
698                         self.table.attach(align, 1, 2, idx + 3, idx + 4,
699                                         gtk.EXPAND | gtk.FILL, gtk.FILL)
700                         align.show()
702                         hbox = gtk.HBox(False, 0)
703                         align.add(hbox)
704                         hbox.show()
706                         label = gtk.Label(parent_id)
707                         label.set_selectable(True)
708                         hbox.pack_start(label, expand=False, fill=True)
709                         label.show()
711                         image = gtk.Image()
712                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
713                         image.show()
715                         button = gtk.Button()
716                         button.add(image)
717                         button.set_relief(gtk.RELIEF_NONE)
718                         button.connect("clicked", self._go_clicked_cb, parent_id)
719                         hbox.pack_start(button, expand=False, fill=True)
720                         button.show()
722                         image = gtk.Image()
723                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
724                         image.show()
726                         button = gtk.Button()
727                         button.add(image)
728                         button.set_relief(gtk.RELIEF_NONE)
729                         button.set_sensitive(True)
730                         button.connect("clicked", self._show_clicked_cb,
731                                         commit.commit_sha1, parent_id)
732                         hbox.pack_start(button, expand=False, fill=True)
733                         button.show()
735                 # Populate with child details
736                 for widget in self.children_widgets:
737                         self.table.remove(widget)
739                 self.children_widgets = []
740                 try:
741                         child_sha1 = Commit.children_sha1[commit.commit_sha1]
742                 except KeyError:
743                         # We don't have child
744                         child_sha1 = [ 0 ]
746                 if ( len(child_sha1) > len(commit.parent_sha1)):
747                         self.table.resize(4 + len(child_sha1) - 1, 4)
749                 for idx, child_id in enumerate(child_sha1):
750                         self.table.set_row_spacing(idx + 3, 0)
752                         align = gtk.Alignment(0.0, 0.0)
753                         self.children_widgets.append(align)
754                         self.table.attach(align, 3, 4, idx + 3, idx + 4,
755                                         gtk.EXPAND | gtk.FILL, gtk.FILL)
756                         align.show()
758                         hbox = gtk.HBox(False, 0)
759                         align.add(hbox)
760                         hbox.show()
762                         label = gtk.Label(child_id)
763                         label.set_selectable(True)
764                         hbox.pack_start(label, expand=False, fill=True)
765                         label.show()
767                         image = gtk.Image()
768                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
769                         image.show()
771                         button = gtk.Button()
772                         button.add(image)
773                         button.set_relief(gtk.RELIEF_NONE)
774                         button.connect("clicked", self._go_clicked_cb, child_id)
775                         hbox.pack_start(button, expand=False, fill=True)
776                         button.show()
778                         image = gtk.Image()
779                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
780                         image.show()
782                         button = gtk.Button()
783                         button.add(image)
784                         button.set_relief(gtk.RELIEF_NONE)
785                         button.set_sensitive(True)
786                         button.connect("clicked", self._show_clicked_cb,
787                                         child_id, commit.commit_sha1)
788                         hbox.pack_start(button, expand=False, fill=True)
789                         button.show()
791         def _destroy_cb(self, widget):
792                 """Callback for when a window we manage is destroyed."""
793                 self.quit()
796         def quit(self):
797                 """Stop the GTK+ main loop."""
798                 gtk.main_quit()
800         def run(self, args):
801                 self.set_branch(args)
802                 self.window.connect("destroy", self._destroy_cb)
803                 self.window.show()
804                 gtk.main()
806         def set_branch(self, args):
807                 """Fill in different windows with info from the reposiroty"""
808                 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
809                 git_rev_list_cmd = fp.read()
810                 fp.close()
811                 fp = os.popen("git rev-list  --header --topo-order --parents " + git_rev_list_cmd)
812                 self.update_window(fp)
814         def update_window(self, fp):
815                 commit_lines = []
817                 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
818                                 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
820                 # used for cursor positioning
821                 self.index = {}
823                 self.colours = {}
824                 self.nodepos = {}
825                 self.incomplete_line = {}
826                 self.commits = []
828                 index = 0
829                 last_colour = 0
830                 last_nodepos = -1
831                 out_line = []
832                 input_line = fp.readline()
833                 while (input_line != ""):
834                         # The commit header ends with '\0'
835                         # This NULL is immediately followed by the sha1 of the
836                         # next commit
837                         if (input_line[0] != '\0'):
838                                 commit_lines.append(input_line)
839                                 input_line = fp.readline()
840                                 continue;
842                         commit = Commit(commit_lines)
843                         if (commit != None ):
844                                 self.commits.append(commit)
846                         # Skip the '\0
847                         commit_lines = []
848                         commit_lines.append(input_line[1:])
849                         input_line = fp.readline()
851                 fp.close()
853                 for commit in self.commits:
854                         (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
855                                                                                 index, out_line,
856                                                                                 last_colour,
857                                                                                 last_nodepos)
858                         self.index[commit.commit_sha1] = index
859                         index += 1
861                 self.treeview.set_model(self.model)
862                 self.treeview.show()
864         def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
865                 in_line=[]
867                 #   |   -> outline
868                 #   X
869                 #   |\  <- inline
871                 # Reset nodepostion
872                 if (last_nodepos > 5):
873                         last_nodepos = 0
875                 # Add the incomplete lines of the last cell in this
876                 try:
877                         colour = self.colours[commit.commit_sha1]
878                 except KeyError:
879                         last_colour +=1
880                         self.colours[commit.commit_sha1] = last_colour
881                         colour =  last_colour
882                 try:
883                         node_pos = self.nodepos[commit.commit_sha1]
884                 except KeyError:
885                         last_nodepos +=1
886                         self.nodepos[commit.commit_sha1] = last_nodepos
887                         node_pos = last_nodepos
889                 #The first parent always continue on the same line
890                 try:
891                         # check we alreay have the value
892                         tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
893                 except KeyError:
894                         self.colours[commit.parent_sha1[0]] = colour
895                         self.nodepos[commit.parent_sha1[0]] = node_pos
897                 for sha1 in self.incomplete_line.keys():
898                         if ( sha1 != commit.commit_sha1):
899                                 self.draw_incomplete_line(sha1, node_pos,
900                                                 out_line, in_line, index)
901                         else:
902                                 del self.incomplete_line[sha1]
905                 in_line.append((node_pos, self.nodepos[commit.parent_sha1[0]],
906                                         self.colours[commit.parent_sha1[0]]))
908                 self.add_incomplete_line(commit.parent_sha1[0], index+1)
910                 if (len(commit.parent_sha1) > 1):
911                         for parent_id in commit.parent_sha1[1:]:
912                                 try:
913                                         tmp_node_pos = self.nodepos[parent_id]
914                                 except KeyError:
915                                         last_colour += 1;
916                                         self.colours[parent_id] = last_colour
917                                         last_nodepos +=1
918                                         self.nodepos[parent_id] = last_nodepos
920                                 in_line.append((node_pos, self.nodepos[parent_id],
921                                                         self.colours[parent_id]))
922                                 self.add_incomplete_line(parent_id, index+1)
925                 try:
926                         branch_tag = self.bt_sha1[commit.commit_sha1]
927                 except KeyError:
928                         branch_tag = [ ]
931                 node = (node_pos, colour, branch_tag)
933                 self.model.append([commit, node, out_line, in_line,
934                                 commit.message, commit.author, commit.date])
936                 return (in_line, last_colour, last_nodepos)
938         def add_incomplete_line(self, sha1, index):
939                 try:
940                         self.incomplete_line[sha1].append(self.nodepos[sha1])
941                 except KeyError:
942                         self.incomplete_line[sha1] = [self.nodepos[sha1]]
944         def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
945                 for idx, pos in enumerate(self.incomplete_line[sha1]):
946                         if(pos == node_pos):
947                                 out_line.append((pos,
948                                         pos+0.5, self.colours[sha1]))
949                                 self.incomplete_line[sha1][idx] = pos = pos+0.5
950                         try:
951                                 next_commit = self.commits[index+1]
952                                 if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
953                                 # join the line back to the node point 
954                                 # This need to be done only if we modified it
955                                         in_line.append((pos, pos-0.5, self.colours[sha1]))
956                                         continue;
957                         except IndexError:
958                                 pass
959                         in_line.append((pos, pos, self.colours[sha1]))
962         def _go_clicked_cb(self, widget, revid):
963                 """Callback for when the go button for a parent is clicked."""
964                 try:
965                         self.treeview.set_cursor(self.index[revid])
966                 except KeyError:
967                         print "Revision %s not present in the list" % revid
968                         # revid == 0 is the parent of the first commit
969                         if (revid != 0 ):
970                                 print "Try running gitview without any options"
972                 self.treeview.grab_focus()
974         def _show_clicked_cb(self, widget,  commit_sha1, parent_sha1):
975                 """Callback for when the show button for a parent is clicked."""
976                 window = DiffWindow()
977                 window.set_diff(commit_sha1, parent_sha1)
978                 self.treeview.grab_focus()
980 if __name__ == "__main__":
981         without_diff = 0
983         if (len(sys.argv) > 1 ):
984                 if (sys.argv[1] == "--without-diff"):
985                         without_diff = 1
987         view = GitView( without_diff != 1)
988         view.run(sys.argv[without_diff:])