gitview: Read tag and branch information using git ls-remote
[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)/3
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.text_path(name)
248
249                 self.set_colour(ctx, colour, 0.0, 0.5)
250                 ctx.stroke_preserve()
251
252                 self.set_colour(ctx, colour, 0.5, 1.0)
253                 ctx.fill()
254
255 class Commit:
256         """ This represent a commit object obtained after parsing the git-rev-list
257         output """
258
259         children_sha1 = {}
260
261         def __init__(self, commit_lines):
262                 self.message            = ""
263                 self.author             = ""
264                 self.date               = ""
265                 self.committer          = ""
266                 self.commit_date        = ""
267                 self.commit_sha1        = ""
268                 self.parent_sha1        = [ ]
269                 self.parse_commit(commit_lines)
270
271
272         def parse_commit(self, commit_lines):
273
274                 # First line is the sha1 lines
275                 line = string.strip(commit_lines[0])
276                 sha1 = re.split(" ", line)
277                 self.commit_sha1 = sha1[0]
278                 self.parent_sha1 = sha1[1:]
279
280                 #build the child list
281                 for parent_id in self.parent_sha1:
282                         try:
283                                 Commit.children_sha1[parent_id].append(self.commit_sha1)
284                         except KeyError:
285                                 Commit.children_sha1[parent_id] = [self.commit_sha1]
286
287                 # IF we don't have parent
288                 if (len(self.parent_sha1) == 0):
289                         self.parent_sha1 = [0]
290
291                 for line in commit_lines[1:]:
292                         m = re.match("^ ", line)
293                         if (m != None):
294                                 # First line of the commit message used for short log
295                                 if self.message == "":
296                                         self.message = string.strip(line)
297                                 continue
298
299                         m = re.match("tree", line)
300                         if (m != None):
301                                 continue
302
303                         m = re.match("parent", line)
304                         if (m != None):
305                                 continue
306
307                         m = re_ident.match(line)
308                         if (m != None):
309                                 date = show_date(m.group('epoch'), m.group('tz'))
310                                 if m.group(1) == "author":
311                                         self.author = m.group('ident')
312                                         self.date = date
313                                 elif m.group(1) == "committer":
314                                         self.committer = m.group('ident')
315                                         self.commit_date = date
316
317                                 continue
318
319         def get_message(self, with_diff=0):
320                 if (with_diff == 1):
321                         message = self.diff_tree()
322                 else:
323                         fp = os.popen("git cat-file commit " + self.commit_sha1)
324                         message = fp.read()
325                         fp.close()
326
327                 return message
328
329         def diff_tree(self):
330                 fp = os.popen("git diff-tree --pretty --cc  -v -p --always " +  self.commit_sha1)
331                 diff = fp.read()
332                 fp.close()
333                 return diff
334
335 class DiffWindow:
336         """Diff window.
337         This object represents and manages a single window containing the
338         differences between two revisions on a branch.
339         """
340
341         def __init__(self):
342                 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
343                 self.window.set_border_width(0)
344                 self.window.set_title("Git repository browser diff window")
345
346                 # Use two thirds of the screen by default
347                 screen = self.window.get_screen()
348                 monitor = screen.get_monitor_geometry(0)
349                 width = int(monitor.width * 0.66)
350                 height = int(monitor.height * 0.66)
351                 self.window.set_default_size(width, height)
352
353                 self.construct()
354
355         def construct(self):
356                 """Construct the window contents."""
357                 vbox = gtk.VBox()
358                 self.window.add(vbox)
359                 vbox.show()
360
361                 menu_bar = gtk.MenuBar()
362                 save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
363                 save_menu.connect("activate", self.save_menu_response, "save")
364                 save_menu.show()
365                 menu_bar.append(save_menu)
366                 vbox.pack_start(menu_bar, False, False, 2)
367                 menu_bar.show()
368
369                 scrollwin = gtk.ScrolledWindow()
370                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
371                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
372                 vbox.pack_start(scrollwin, expand=True, fill=True)
373                 scrollwin.show()
374
375                 if have_gtksourceview:
376                         self.buffer = gtksourceview.SourceBuffer()
377                         slm = gtksourceview.SourceLanguagesManager()
378                         gsl = slm.get_language_from_mime_type("text/x-patch")
379                         self.buffer.set_highlight(True)
380                         self.buffer.set_language(gsl)
381                         sourceview = gtksourceview.SourceView(self.buffer)
382                 else:
383                         self.buffer = gtk.TextBuffer()
384                         sourceview = gtk.TextView(self.buffer)
385
386                 sourceview.set_editable(False)
387                 sourceview.modify_font(pango.FontDescription("Monospace"))
388                 scrollwin.add(sourceview)
389                 sourceview.show()
390
391
392         def set_diff(self, commit_sha1, parent_sha1):
393                 """Set the differences showed by this window.
394                 Compares the two trees and populates the window with the
395                 differences.
396                 """
397                 # Diff with the first commit or the last commit shows nothing
398                 if (commit_sha1 == 0 or parent_sha1 == 0 ):
399                         return
400
401                 fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
402                 self.buffer.set_text(fp.read())
403                 fp.close()
404                 self.window.show()
405
406         def save_menu_response(self, widget, string):
407                 dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
408                                 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
409                                         gtk.STOCK_SAVE, gtk.RESPONSE_OK))
410                 dialog.set_default_response(gtk.RESPONSE_OK)
411                 response = dialog.run()
412                 if response == gtk.RESPONSE_OK:
413                         patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
414                                         self.buffer.get_end_iter())
415                         fp = open(dialog.get_filename(), "w")
416                         fp.write(patch_buffer)
417                         fp.close()
418                 dialog.destroy()
419
420 class GitView:
421         """ This is the main class
422         """
423         version = "0.6"
424
425         def __init__(self, with_diff=0):
426                 self.with_diff = with_diff
427                 self.window =   gtk.Window(gtk.WINDOW_TOPLEVEL)
428                 self.window.set_border_width(0)
429                 self.window.set_title("Git repository browser")
430
431                 self.get_bt_sha1()
432
433                 # Use three-quarters of the screen by default
434                 screen = self.window.get_screen()
435                 monitor = screen.get_monitor_geometry(0)
436                 width = int(monitor.width * 0.75)
437                 height = int(monitor.height * 0.75)
438                 self.window.set_default_size(width, height)
439
440                 # FIXME AndyFitz!
441                 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
442                 self.window.set_icon(icon)
443
444                 self.accel_group = gtk.AccelGroup()
445                 self.window.add_accel_group(self.accel_group)
446
447                 self.construct()
448
449         def get_bt_sha1(self):
450                 """ Update the bt_sha1 dictionary with the
451                 respective sha1 details """
452
453                 self.bt_sha1 = { }
454                 ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
455                 git_dir = os.getenv("GIT_DIR")
456                 if (git_dir == None):
457                         git_dir = ".git"
458
459                 fp = os.popen('git ls-remote ' + git_dir)
460                 while 1:
461                         line = string.strip(fp.readline())
462                         if line == '':
463                                 break
464                         m = ls_remote.match(line)
465                         if not m:
466                                 continue
467                         (sha1, name) = (m.group(1), m.group(2))
468                         if not self.bt_sha1.has_key(sha1):
469                                 self.bt_sha1[sha1] = []
470                         self.bt_sha1[sha1].append(name)
471                 fp.close()
472
473
474         def construct(self):
475                 """Construct the window contents."""
476                 paned = gtk.VPaned()
477                 paned.pack1(self.construct_top(), resize=False, shrink=True)
478                 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
479                 self.window.add(paned)
480                 paned.show()
481
482
483         def construct_top(self):
484                 """Construct the top-half of the window."""
485                 vbox = gtk.VBox(spacing=6)
486                 vbox.set_border_width(12)
487                 vbox.show()
488
489                 menu_bar = gtk.MenuBar()
490                 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
491                 help_menu = gtk.MenuItem("Help")
492                 menu = gtk.Menu()
493                 about_menu = gtk.MenuItem("About")
494                 menu.append(about_menu)
495                 about_menu.connect("activate", self.about_menu_response, "about")
496                 about_menu.show()
497                 help_menu.set_submenu(menu)
498                 help_menu.show()
499                 menu_bar.append(help_menu)
500                 vbox.pack_start(menu_bar, False, False, 2)
501                 menu_bar.show()
502
503                 scrollwin = gtk.ScrolledWindow()
504                 scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
505                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
506                 vbox.pack_start(scrollwin, expand=True, fill=True)
507                 scrollwin.show()
508
509                 self.treeview = gtk.TreeView()
510                 self.treeview.set_rules_hint(True)
511                 self.treeview.set_search_column(4)
512                 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
513                 scrollwin.add(self.treeview)
514                 self.treeview.show()
515
516                 cell = CellRendererGraph()
517                 column = gtk.TreeViewColumn()
518                 column.set_resizable(False)
519                 column.pack_start(cell, expand=False)
520                 column.add_attribute(cell, "node", 1)
521                 column.add_attribute(cell, "in-lines", 2)
522                 column.add_attribute(cell, "out-lines", 3)
523                 self.treeview.append_column(column)
524
525                 cell = gtk.CellRendererText()
526                 cell.set_property("width-chars", 65)
527                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
528                 column = gtk.TreeViewColumn("Message")
529                 column.set_resizable(True)
530                 column.pack_start(cell, expand=True)
531                 column.add_attribute(cell, "text", 4)
532                 self.treeview.append_column(column)
533
534                 cell = gtk.CellRendererText()
535                 cell.set_property("width-chars", 40)
536                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
537                 column = gtk.TreeViewColumn("Author")
538                 column.set_resizable(True)
539                 column.pack_start(cell, expand=True)
540                 column.add_attribute(cell, "text", 5)
541                 self.treeview.append_column(column)
542
543                 cell = gtk.CellRendererText()
544                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
545                 column = gtk.TreeViewColumn("Date")
546                 column.set_resizable(True)
547                 column.pack_start(cell, expand=True)
548                 column.add_attribute(cell, "text", 6)
549                 self.treeview.append_column(column)
550
551                 return vbox
552
553         def about_menu_response(self, widget, string):
554                 dialog = gtk.AboutDialog()
555                 dialog.set_name("Gitview")
556                 dialog.set_version(GitView.version)
557                 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
558                 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
559                 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
560                 dialog.set_wrap_license(True)
561                 dialog.run()
562                 dialog.destroy()
563
564
565         def construct_bottom(self):
566                 """Construct the bottom half of the window."""
567                 vbox = gtk.VBox(False, spacing=6)
568                 vbox.set_border_width(12)
569                 (width, height) = self.window.get_size()
570                 vbox.set_size_request(width, int(height / 2.5))
571                 vbox.show()
572
573                 self.table = gtk.Table(rows=4, columns=4)
574                 self.table.set_row_spacings(6)
575                 self.table.set_col_spacings(6)
576                 vbox.pack_start(self.table, expand=False, fill=True)
577                 self.table.show()
578
579                 align = gtk.Alignment(0.0, 0.5)
580                 label = gtk.Label()
581                 label.set_markup("<b>Revision:</b>")
582                 align.add(label)
583                 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
584                 label.show()
585                 align.show()
586
587                 align = gtk.Alignment(0.0, 0.5)
588                 self.revid_label = gtk.Label()
589                 self.revid_label.set_selectable(True)
590                 align.add(self.revid_label)
591                 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
592                 self.revid_label.show()
593                 align.show()
594
595                 align = gtk.Alignment(0.0, 0.5)
596                 label = gtk.Label()
597                 label.set_markup("<b>Committer:</b>")
598                 align.add(label)
599                 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
600                 label.show()
601                 align.show()
602
603                 align = gtk.Alignment(0.0, 0.5)
604                 self.committer_label = gtk.Label()
605                 self.committer_label.set_selectable(True)
606                 align.add(self.committer_label)
607                 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
608                 self.committer_label.show()
609                 align.show()
610
611                 align = gtk.Alignment(0.0, 0.5)
612                 label = gtk.Label()
613                 label.set_markup("<b>Timestamp:</b>")
614                 align.add(label)
615                 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
616                 label.show()
617                 align.show()
618
619                 align = gtk.Alignment(0.0, 0.5)
620                 self.timestamp_label = gtk.Label()
621                 self.timestamp_label.set_selectable(True)
622                 align.add(self.timestamp_label)
623                 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
624                 self.timestamp_label.show()
625                 align.show()
626
627                 align = gtk.Alignment(0.0, 0.5)
628                 label = gtk.Label()
629                 label.set_markup("<b>Parents:</b>")
630                 align.add(label)
631                 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
632                 label.show()
633                 align.show()
634                 self.parents_widgets = []
635
636                 align = gtk.Alignment(0.0, 0.5)
637                 label = gtk.Label()
638                 label.set_markup("<b>Children:</b>")
639                 align.add(label)
640                 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
641                 label.show()
642                 align.show()
643                 self.children_widgets = []
644
645                 scrollwin = gtk.ScrolledWindow()
646                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
647                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
648                 vbox.pack_start(scrollwin, expand=True, fill=True)
649                 scrollwin.show()
650
651                 if have_gtksourceview:
652                         self.message_buffer = gtksourceview.SourceBuffer()
653                         slm = gtksourceview.SourceLanguagesManager()
654                         gsl = slm.get_language_from_mime_type("text/x-patch")
655                         self.message_buffer.set_highlight(True)
656                         self.message_buffer.set_language(gsl)
657                         sourceview = gtksourceview.SourceView(self.message_buffer)
658                 else:
659                         self.message_buffer = gtk.TextBuffer()
660                         sourceview = gtk.TextView(self.message_buffer)
661
662                 sourceview.set_editable(False)
663                 sourceview.modify_font(pango.FontDescription("Monospace"))
664                 scrollwin.add(sourceview)
665                 sourceview.show()
666
667                 return vbox
668
669         def _treeview_cursor_cb(self, *args):
670                 """Callback for when the treeview cursor changes."""
671                 (path, col) = self.treeview.get_cursor()
672                 commit = self.model[path][0]
673
674                 if commit.committer is not None:
675                         committer = commit.committer
676                         timestamp = commit.commit_date
677                         message   =  commit.get_message(self.with_diff)
678                         revid_label = commit.commit_sha1
679                 else:
680                         committer = ""
681                         timestamp = ""
682                         message = ""
683                         revid_label = ""
684
685                 self.revid_label.set_text(revid_label)
686                 self.committer_label.set_text(committer)
687                 self.timestamp_label.set_text(timestamp)
688                 self.message_buffer.set_text(message)
689
690                 for widget in self.parents_widgets:
691                         self.table.remove(widget)
692
693                 self.parents_widgets = []
694                 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
695                 for idx, parent_id in enumerate(commit.parent_sha1):
696                         self.table.set_row_spacing(idx + 3, 0)
697
698                         align = gtk.Alignment(0.0, 0.0)
699                         self.parents_widgets.append(align)
700                         self.table.attach(align, 1, 2, idx + 3, idx + 4,
701                                         gtk.EXPAND | gtk.FILL, gtk.FILL)
702                         align.show()
703
704                         hbox = gtk.HBox(False, 0)
705                         align.add(hbox)
706                         hbox.show()
707
708                         label = gtk.Label(parent_id)
709                         label.set_selectable(True)
710                         hbox.pack_start(label, expand=False, fill=True)
711                         label.show()
712
713                         image = gtk.Image()
714                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
715                         image.show()
716
717                         button = gtk.Button()
718                         button.add(image)
719                         button.set_relief(gtk.RELIEF_NONE)
720                         button.connect("clicked", self._go_clicked_cb, parent_id)
721                         hbox.pack_start(button, expand=False, fill=True)
722                         button.show()
723
724                         image = gtk.Image()
725                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
726                         image.show()
727
728                         button = gtk.Button()
729                         button.add(image)
730                         button.set_relief(gtk.RELIEF_NONE)
731                         button.set_sensitive(True)
732                         button.connect("clicked", self._show_clicked_cb,
733                                         commit.commit_sha1, parent_id)
734                         hbox.pack_start(button, expand=False, fill=True)
735                         button.show()
736
737                 # Populate with child details
738                 for widget in self.children_widgets:
739                         self.table.remove(widget)
740
741                 self.children_widgets = []
742                 try:
743                         child_sha1 = Commit.children_sha1[commit.commit_sha1]
744                 except KeyError:
745                         # We don't have child
746                         child_sha1 = [ 0 ]
747
748                 if ( len(child_sha1) > len(commit.parent_sha1)):
749                         self.table.resize(4 + len(child_sha1) - 1, 4)
750
751                 for idx, child_id in enumerate(child_sha1):
752                         self.table.set_row_spacing(idx + 3, 0)
753
754                         align = gtk.Alignment(0.0, 0.0)
755                         self.children_widgets.append(align)
756                         self.table.attach(align, 3, 4, idx + 3, idx + 4,
757                                         gtk.EXPAND | gtk.FILL, gtk.FILL)
758                         align.show()
759
760                         hbox = gtk.HBox(False, 0)
761                         align.add(hbox)
762                         hbox.show()
763
764                         label = gtk.Label(child_id)
765                         label.set_selectable(True)
766                         hbox.pack_start(label, expand=False, fill=True)
767                         label.show()
768
769                         image = gtk.Image()
770                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
771                         image.show()
772
773                         button = gtk.Button()
774                         button.add(image)
775                         button.set_relief(gtk.RELIEF_NONE)
776                         button.connect("clicked", self._go_clicked_cb, child_id)
777                         hbox.pack_start(button, expand=False, fill=True)
778                         button.show()
779
780                         image = gtk.Image()
781                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
782                         image.show()
783
784                         button = gtk.Button()
785                         button.add(image)
786                         button.set_relief(gtk.RELIEF_NONE)
787                         button.set_sensitive(True)
788                         button.connect("clicked", self._show_clicked_cb,
789                                         child_id, commit.commit_sha1)
790                         hbox.pack_start(button, expand=False, fill=True)
791                         button.show()
792
793         def _destroy_cb(self, widget):
794                 """Callback for when a window we manage is destroyed."""
795                 self.quit()
796
797
798         def quit(self):
799                 """Stop the GTK+ main loop."""
800                 gtk.main_quit()
801
802         def run(self, args):
803                 self.set_branch(args)
804                 self.window.connect("destroy", self._destroy_cb)
805                 self.window.show()
806                 gtk.main()
807
808         def set_branch(self, args):
809                 """Fill in different windows with info from the reposiroty"""
810                 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
811                 git_rev_list_cmd = fp.read()
812                 fp.close()
813                 fp = os.popen("git rev-list  --header --topo-order --parents " + git_rev_list_cmd)
814                 self.update_window(fp)
815
816         def update_window(self, fp):
817                 commit_lines = []
818
819                 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
820                                 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
821
822                 # used for cursor positioning
823                 self.index = {}
824
825                 self.colours = {}
826                 self.nodepos = {}
827                 self.incomplete_line = {}
828
829                 index = 0
830                 last_colour = 0
831                 last_nodepos = -1
832                 out_line = []
833                 input_line = fp.readline()
834                 while (input_line != ""):
835                         # The commit header ends with '\0'
836                         # This NULL is immediately followed by the sha1 of the
837                         # next commit
838                         if (input_line[0] != '\0'):
839                                 commit_lines.append(input_line)
840                                 input_line = fp.readline()
841                                 continue;
842
843                         commit = Commit(commit_lines)
844                         if (commit != None ):
845                                 (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
846                                                                                 index, out_line,
847                                                                                 last_colour,
848                                                                                 last_nodepos)
849                                 self.index[commit.commit_sha1] = index
850                                 index += 1
851
852                         # Skip the '\0
853                         commit_lines = []
854                         commit_lines.append(input_line[1:])
855                         input_line = fp.readline()
856
857                 fp.close()
858
859                 self.treeview.set_model(self.model)
860                 self.treeview.show()
861
862         def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
863                 in_line=[]
864
865                 #   |   -> outline
866                 #   X
867                 #   |\  <- inline
868
869                 # Reset nodepostion
870                 if (last_nodepos > 5):
871                         last_nodepos = 0
872
873                 # Add the incomplete lines of the last cell in this
874                 for sha1 in self.incomplete_line.keys():
875                         if ( sha1 != commit.commit_sha1):
876                                 for pos in self.incomplete_line[sha1]:
877                                         in_line.append((pos, pos, self.colours[sha1]))
878                         else:
879                                 del self.incomplete_line[sha1]
880
881                 try:
882                         colour = self.colours[commit.commit_sha1]
883                 except KeyError:
884                         last_colour +=1
885                         self.colours[commit.commit_sha1] = last_colour
886                         colour =  last_colour
887                 try:
888                         node_pos = self.nodepos[commit.commit_sha1]
889                 except KeyError:
890                         last_nodepos +=1
891                         self.nodepos[commit.commit_sha1] = last_nodepos
892                         node_pos = last_nodepos
893
894                 #The first parent always continue on the same line
895                 try:
896                         # check we alreay have the value
897                         tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
898                 except KeyError:
899                         self.colours[commit.parent_sha1[0]] = colour
900                         self.nodepos[commit.parent_sha1[0]] = node_pos
901
902                 in_line.append((node_pos, self.nodepos[commit.parent_sha1[0]],
903                                         self.colours[commit.parent_sha1[0]]))
904
905                 self.add_incomplete_line(commit.parent_sha1[0], index+1)
906
907                 if (len(commit.parent_sha1) > 1):
908                         for parent_id in commit.parent_sha1[1:]:
909                                 try:
910                                         tmp_node_pos = self.nodepos[parent_id]
911                                 except KeyError:
912                                         last_colour += 1;
913                                         self.colours[parent_id] = last_colour
914                                         last_nodepos +=1
915                                         self.nodepos[parent_id] = last_nodepos
916
917                                 in_line.append((node_pos, self.nodepos[parent_id],
918                                                         self.colours[parent_id]))
919                                 self.add_incomplete_line(parent_id, index+1)
920
921
922                 try:
923                         branch_tag = self.bt_sha1[commit.commit_sha1]
924                 except KeyError:
925                         branch_tag = [ ]
926
927
928                 node = (node_pos, colour, branch_tag)
929
930                 self.model.append([commit, node, out_line, in_line,
931                                 commit.message, commit.author, commit.date])
932
933                 return (in_line, last_colour, last_nodepos)
934
935         def add_incomplete_line(self, sha1, index):
936                 try:
937                         self.incomplete_line[sha1].append(self.nodepos[sha1])
938                 except KeyError:
939                         self.incomplete_line[sha1] = [self.nodepos[sha1]]
940
941
942         def _go_clicked_cb(self, widget, revid):
943                 """Callback for when the go button for a parent is clicked."""
944                 try:
945                         self.treeview.set_cursor(self.index[revid])
946                 except KeyError:
947                         print "Revision %s not present in the list" % revid
948                         # revid == 0 is the parent of the first commit
949                         if (revid != 0 ):
950                                 print "Try running gitview without any options"
951
952                 self.treeview.grab_focus()
953
954         def _show_clicked_cb(self, widget,  commit_sha1, parent_sha1):
955                 """Callback for when the show button for a parent is clicked."""
956                 window = DiffWindow()
957                 window.set_diff(commit_sha1, parent_sha1)
958                 self.treeview.grab_focus()
959
960 if __name__ == "__main__":
961         without_diff = 0
962
963         if (len(sys.argv) > 1 ):
964                 if (sys.argv[1] == "--without-diff"):
965                         without_diff = 1
966
967         view = GitView( without_diff != 1)
968         view.run(sys.argv[without_diff:])
969
970