Merge part of 'lt/rev-list' into 'fk/blame'
[git.git] / contrib / gitview / gitview
1 #! /usr/bin/env python
2
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
7
8 """ gitview
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>"
14
15
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
27
28 try:
29     import gtksourceview
30     have_gtksourceview = True
31 except ImportError:
32     have_gtksourceview = False
33     print "Running without gtksourceview module"
34
35 re_ident = re.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
36
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
45
46         return str_arg
47
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
56
57         return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs))
58
59
60 class CellRendererGraph(gtk.GenericCellRenderer):
61         """Cell renderer for directed graph.
62
63         This module contains the implementation of a custom GtkCellRenderer that
64         draws part of the directed graph based on the lines suggested by the code
65         in graph.py.
66
67         Because we're shiny, we use Cairo to do this, and because we're naughty
68         we cheat and draw over the bits of the TreeViewColumn that are supposed to
69         just be for the background.
70
71         Properties:
72         node              (column, colour, [ names ]) tuple to draw revision node,
73         in_lines          (start, end, colour) tuple list to draw inward lines,
74         out_lines         (start, end, colour) tuple list to draw outward lines.
75         """
76
77         __gproperties__ = {
78         "node":         ( gobject.TYPE_PYOBJECT, "node",
79                           "revision node instruction",
80                           gobject.PARAM_WRITABLE
81                         ),
82         "in-lines":     ( gobject.TYPE_PYOBJECT, "in-lines",
83                           "instructions to draw lines into the cell",
84                           gobject.PARAM_WRITABLE
85                         ),
86         "out-lines":    ( gobject.TYPE_PYOBJECT, "out-lines",
87                           "instructions to draw lines out of the cell",
88                           gobject.PARAM_WRITABLE
89                         ),
90         }
91
92         def do_set_property(self, property, value):
93                 """Set properties from GObject properties."""
94                 if property.name == "node":
95                         self.node = value
96                 elif property.name == "in-lines":
97                         self.in_lines = value
98                 elif property.name == "out-lines":
99                         self.out_lines = value
100                 else:
101                         raise AttributeError, "no such property: '%s'" % property.name
102
103         def box_size(self, widget):
104                 """Calculate box size based on widget's font.
105
106                 Cache this as it's probably expensive to get.  It ensures that we
107                 draw the graph at least as large as the text.
108                 """
109                 try:
110                         return self._box_size
111                 except AttributeError:
112                         pango_ctx = widget.get_pango_context()
113                         font_desc = widget.get_style().font_desc
114                         metrics = pango_ctx.get_metrics(font_desc)
115
116                         ascent = pango.PIXELS(metrics.get_ascent())
117                         descent = pango.PIXELS(metrics.get_descent())
118
119                         self._box_size = ascent + descent + 6
120                         return self._box_size
121
122         def set_colour(self, ctx, colour, bg, fg):
123                 """Set the context source colour.
124
125                 Picks a distinct colour based on an internal wheel; the bg
126                 parameter provides the value that should be assigned to the 'zero'
127                 colours and the fg parameter provides the multiplier that should be
128                 applied to the foreground colours.
129                 """
130                 colours = [
131                     ( 1.0, 0.0, 0.0 ),
132                     ( 1.0, 1.0, 0.0 ),
133                     ( 0.0, 1.0, 0.0 ),
134                     ( 0.0, 1.0, 1.0 ),
135                     ( 0.0, 0.0, 1.0 ),
136                     ( 1.0, 0.0, 1.0 ),
137                     ]
138
139                 colour %= len(colours)
140                 red   = (colours[colour][0] * fg) or bg
141                 green = (colours[colour][1] * fg) or bg
142                 blue  = (colours[colour][2] * fg) or bg
143
144                 ctx.set_source_rgb(red, green, blue)
145
146         def on_get_size(self, widget, cell_area):
147                 """Return the size we need for this cell.
148
149                 Each cell is drawn individually and is only as wide as it needs
150                 to be, we let the TreeViewColumn take care of making them all
151                 line up.
152                 """
153                 box_size = self.box_size(widget)
154
155                 cols = self.node[0]
156                 for start, end, colour in self.in_lines + self.out_lines:
157                         cols = int(max(cols, start, end))
158
159                 (column, colour, names) = self.node
160                 names_len = 0
161                 if (len(names) != 0):
162                         for item in names:
163                                 names_len += len(item)
164
165                 width = box_size * (cols + 1 ) + names_len 
166                 height = box_size
167
168                 # FIXME I have no idea how to use cell_area properly
169                 return (0, 0, width, height)
170
171         def on_render(self, window, widget, bg_area, cell_area, exp_area, flags):
172                 """Render an individual cell.
173
174                 Draws the cell contents using cairo, taking care to clip what we
175                 do to within the background area so we don't draw over other cells.
176                 Note that we're a bit naughty there and should really be drawing
177                 in the cell_area (or even the exposed area), but we explicitly don't
178                 want any gutter.
179
180                 We try and be a little clever, if the line we need to draw is going
181                 to cross other columns we actually draw it as in the .---' style
182                 instead of a pure diagonal ... this reduces confusion by an
183                 incredible amount.
184                 """
185                 ctx = window.cairo_create()
186                 ctx.rectangle(bg_area.x, bg_area.y, bg_area.width, bg_area.height)
187                 ctx.clip()
188
189                 box_size = self.box_size(widget)
190
191                 ctx.set_line_width(box_size / 8)
192                 ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
193
194                 # Draw lines into the cell
195                 for start, end, colour in self.in_lines:
196                         ctx.move_to(cell_area.x + box_size * start + box_size / 2,
197                                         bg_area.y - bg_area.height / 2)
198
199                         if start - end > 1:
200                                 ctx.line_to(cell_area.x + box_size * start, bg_area.y)
201                                 ctx.line_to(cell_area.x + box_size * end + box_size, bg_area.y)
202                         elif start - end < -1:
203                                 ctx.line_to(cell_area.x + box_size * start + box_size,
204                                                 bg_area.y)
205                                 ctx.line_to(cell_area.x + box_size * end, bg_area.y)
206
207                         ctx.line_to(cell_area.x + box_size * end + box_size / 2,
208                                         bg_area.y + bg_area.height / 2)
209
210                         self.set_colour(ctx, colour, 0.0, 0.65)
211                         ctx.stroke()
212
213                 # Draw lines out of the cell
214                 for start, end, colour in self.out_lines:
215                         ctx.move_to(cell_area.x + box_size * start + box_size / 2,
216                                         bg_area.y + bg_area.height / 2)
217
218                         if start - end > 1:
219                                 ctx.line_to(cell_area.x + box_size * start,
220                                                 bg_area.y + bg_area.height)
221                                 ctx.line_to(cell_area.x + box_size * end + box_size,
222                                                 bg_area.y + bg_area.height)
223                         elif start - end < -1:
224                                 ctx.line_to(cell_area.x + box_size * start + box_size,
225                                                 bg_area.y + bg_area.height)
226                                 ctx.line_to(cell_area.x + box_size * end,
227                                                 bg_area.y + bg_area.height)
228
229                         ctx.line_to(cell_area.x + box_size * end + box_size / 2,
230                                         bg_area.y + bg_area.height / 2 + bg_area.height)
231
232                         self.set_colour(ctx, colour, 0.0, 0.65)
233                         ctx.stroke()
234
235                 # Draw the revision node in the right column
236                 (column, colour, names) = self.node
237                 ctx.arc(cell_area.x + box_size * column + box_size / 2,
238                                 cell_area.y + cell_area.height / 2,
239                                 box_size / 4, 0, 2 * math.pi)
240
241
242                 if (len(names) != 0):
243                         name = " "
244                         for item in names:
245                                 name = name + item + " "
246
247                         ctx.select_font_face("Monospace")
248                         ctx.set_font_size(13)
249                         ctx.text_path(name)
250
251                 self.set_colour(ctx, colour, 0.0, 0.5)
252                 ctx.stroke_preserve()
253
254                 self.set_colour(ctx, colour, 0.5, 1.0)
255                 ctx.fill()
256
257 class Commit:
258         """ This represent a commit object obtained after parsing the git-rev-list
259         output """
260
261         children_sha1 = {}
262
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)
272
273
274         def parse_commit(self, commit_lines):
275
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:]
281
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]
288
289                 # IF we don't have parent
290                 if (len(self.parent_sha1) == 0):
291                         self.parent_sha1 = [0]
292
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
300
301                         m = re.match("tree", line)
302                         if (m != None):
303                                 continue
304
305                         m = re.match("parent", line)
306                         if (m != None):
307                                 continue
308
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
318
319                                 continue
320
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()
328
329                 return message
330
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
336
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         """
342
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")
347
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)
354
355                 self.construct()
356
357         def construct(self):
358                 """Construct the window contents."""
359                 vbox = gtk.VBox()
360                 self.window.add(vbox)
361                 vbox.show()
362
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()
370
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()
376
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)
387
388                 sourceview.set_editable(False)
389                 sourceview.modify_font(pango.FontDescription("Monospace"))
390                 scrollwin.add(sourceview)
391                 sourceview.show()
392
393
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
402
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()
407
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()
421
422 class GitView:
423         """ This is the main class
424         """
425         version = "0.7"
426
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")
432
433                 self.get_bt_sha1()
434
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)
441
442                 # FIXME AndyFitz!
443                 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
444                 self.window.set_icon(icon)
445
446                 self.accel_group = gtk.AccelGroup()
447                 self.window.add_accel_group(self.accel_group)
448
449                 self.construct()
450
451         def get_bt_sha1(self):
452                 """ Update the bt_sha1 dictionary with the
453                 respective sha1 details """
454
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()
470
471
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()
479
480
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()
486
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()
500
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()
506
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()
513
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)
522
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)
531
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)
540
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)
548
549                 return vbox
550
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()
561
562
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()
570
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()
576
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()
584
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()
592
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()
600
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()
608
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()
616
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()
624
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 = []
633
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 = []
642
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()
648
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)
659
660                 sourceview.set_editable(False)
661                 sourceview.modify_font(pango.FontDescription("Monospace"))
662                 scrollwin.add(sourceview)
663                 sourceview.show()
664
665                 return vbox
666
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]
671
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 = ""
682
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)
687
688                 for widget in self.parents_widgets:
689                         self.table.remove(widget)
690
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)
695
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()
701
702                         hbox = gtk.HBox(False, 0)
703                         align.add(hbox)
704                         hbox.show()
705
706                         label = gtk.Label(parent_id)
707                         label.set_selectable(True)
708                         hbox.pack_start(label, expand=False, fill=True)
709                         label.show()
710
711                         image = gtk.Image()
712                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
713                         image.show()
714
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()
721
722                         image = gtk.Image()
723                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
724                         image.show()
725
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()
734
735                 # Populate with child details
736                 for widget in self.children_widgets:
737                         self.table.remove(widget)
738
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 ]
745
746                 if ( len(child_sha1) > len(commit.parent_sha1)):
747                         self.table.resize(4 + len(child_sha1) - 1, 4)
748
749                 for idx, child_id in enumerate(child_sha1):
750                         self.table.set_row_spacing(idx + 3, 0)
751
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()
757
758                         hbox = gtk.HBox(False, 0)
759                         align.add(hbox)
760                         hbox.show()
761
762                         label = gtk.Label(child_id)
763                         label.set_selectable(True)
764                         hbox.pack_start(label, expand=False, fill=True)
765                         label.show()
766
767                         image = gtk.Image()
768                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
769                         image.show()
770
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()
777
778                         image = gtk.Image()
779                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
780                         image.show()
781
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()
790
791         def _destroy_cb(self, widget):
792                 """Callback for when a window we manage is destroyed."""
793                 self.quit()
794
795
796         def quit(self):
797                 """Stop the GTK+ main loop."""
798                 gtk.main_quit()
799
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()
805
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)
813
814         def update_window(self, fp):
815                 commit_lines = []
816
817                 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
818                                 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
819
820                 # used for cursor positioning
821                 self.index = {}
822
823                 self.colours = {}
824                 self.nodepos = {}
825                 self.incomplete_line = {}
826                 self.commits = []
827
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;
841
842                         commit = Commit(commit_lines)
843                         if (commit != None ):
844                                 self.commits.append(commit)
845
846                         # Skip the '\0
847                         commit_lines = []
848                         commit_lines.append(input_line[1:])
849                         input_line = fp.readline()
850
851                 fp.close()
852
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
860
861                 self.treeview.set_model(self.model)
862                 self.treeview.show()
863
864         def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
865                 in_line=[]
866
867                 #   |   -> outline
868                 #   X
869                 #   |\  <- inline
870
871                 # Reset nodepostion
872                 if (last_nodepos > 5):
873                         last_nodepos = -1 
874
875                 # Add the incomplete lines of the last cell in this
876                 try:
877                         colour = self.colours[commit.commit_sha1]
878                 except KeyError:
879                         self.colours[commit.commit_sha1] = last_colour+1
880                         last_colour = self.colours[commit.commit_sha1] 
881                         colour =   self.colours[commit.commit_sha1] 
882
883                 try:
884                         node_pos = self.nodepos[commit.commit_sha1]
885                 except KeyError:
886                         self.nodepos[commit.commit_sha1] = last_nodepos+1
887                         last_nodepos = self.nodepos[commit.commit_sha1]
888                         node_pos =  self.nodepos[commit.commit_sha1]
889
890                 #The first parent always continue on the same line
891                 try:
892                         # check we alreay have the value
893                         tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
894                 except KeyError:
895                         self.colours[commit.parent_sha1[0]] = colour
896                         self.nodepos[commit.parent_sha1[0]] = node_pos
897
898                 for sha1 in self.incomplete_line.keys():
899                         if (sha1 != commit.commit_sha1):
900                                 self.draw_incomplete_line(sha1, node_pos,
901                                                 out_line, in_line, index)
902                         else:
903                                 del self.incomplete_line[sha1]
904
905
906                 for parent_id in commit.parent_sha1:
907                         try:
908                                 tmp_node_pos = self.nodepos[parent_id]
909                         except KeyError:
910                                 self.colours[parent_id] = last_colour+1
911                                 last_colour = self.colours[parent_id]
912                                 self.nodepos[parent_id] = last_nodepos+1
913                                 last_nodepos = self.nodepos[parent_id] 
914
915                         in_line.append((node_pos, self.nodepos[parent_id],
916                                                 self.colours[parent_id]))
917                         self.add_incomplete_line(parent_id)
918
919                 try:
920                         branch_tag = self.bt_sha1[commit.commit_sha1]
921                 except KeyError:
922                         branch_tag = [ ]
923
924
925                 node = (node_pos, colour, branch_tag)
926
927                 self.model.append([commit, node, out_line, in_line,
928                                 commit.message, commit.author, commit.date])
929
930                 return (in_line, last_colour, last_nodepos)
931
932         def add_incomplete_line(self, sha1):
933                 try:
934                         self.incomplete_line[sha1].append(self.nodepos[sha1])
935                 except KeyError:
936                         self.incomplete_line[sha1] = [self.nodepos[sha1]]
937
938         def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
939                 for idx, pos in enumerate(self.incomplete_line[sha1]):
940                         if(pos == node_pos):
941                                 #remove the straight line and add a slash
942                                 if ((pos, pos, self.colours[sha1]) in out_line):
943                                         out_line.remove((pos, pos, self.colours[sha1]))
944                                 out_line.append((pos, pos+0.5, self.colours[sha1]))
945                                 self.incomplete_line[sha1][idx] = pos = pos+0.5
946                         try:
947                                 next_commit = self.commits[index+1]
948                                 if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
949                                 # join the line back to the node point 
950                                 # This need to be done only if we modified it
951                                         in_line.append((pos, pos-0.5, self.colours[sha1]))
952                                         continue;
953                         except IndexError:
954                                 pass
955                         in_line.append((pos, pos, self.colours[sha1]))
956
957
958         def _go_clicked_cb(self, widget, revid):
959                 """Callback for when the go button for a parent is clicked."""
960                 try:
961                         self.treeview.set_cursor(self.index[revid])
962                 except KeyError:
963                         print "Revision %s not present in the list" % revid
964                         # revid == 0 is the parent of the first commit
965                         if (revid != 0 ):
966                                 print "Try running gitview without any options"
967
968                 self.treeview.grab_focus()
969
970         def _show_clicked_cb(self, widget,  commit_sha1, parent_sha1):
971                 """Callback for when the show button for a parent is clicked."""
972                 window = DiffWindow()
973                 window.set_diff(commit_sha1, parent_sha1)
974                 self.treeview.grab_focus()
975
976 if __name__ == "__main__":
977         without_diff = 0
978
979         if (len(sys.argv) > 1 ):
980                 if (sys.argv[1] == "--without-diff"):
981                         without_diff = 1
982
983         view = GitView( without_diff != 1)
984         view.run(sys.argv[without_diff:])
985
986