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