Add git-blame, a tool for assigning 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 def get_sha1_from_tags(line):
60         fp = os.popen("git cat-file -t " + line)
61         entry = string.strip(fp.readline())
62         fp.close()
63         if (entry == "commit"):
64                 return line
65         elif (entry == "tag"):
66                 fp = os.popen("git cat-file tag "+ line)
67                 entry = string.strip(fp.readline())
68                 fp.close()
69                 obj = re.split(" ", entry)
70                 if (obj[0] == "object"):
71                         return obj[1]
72         return None
73
74 class CellRendererGraph(gtk.GenericCellRenderer):
75         """Cell renderer for directed graph.
76
77         This module contains the implementation of a custom GtkCellRenderer that
78         draws part of the directed graph based on the lines suggested by the code
79         in graph.py.
80
81         Because we're shiny, we use Cairo to do this, and because we're naughty
82         we cheat and draw over the bits of the TreeViewColumn that are supposed to
83         just be for the background.
84
85         Properties:
86         node              (column, colour, [ names ]) tuple to draw revision node,
87         in_lines          (start, end, colour) tuple list to draw inward lines,
88         out_lines         (start, end, colour) tuple list to draw outward lines.
89         """
90
91         __gproperties__ = {
92         "node":         ( gobject.TYPE_PYOBJECT, "node",
93                           "revision node instruction",
94                           gobject.PARAM_WRITABLE
95                         ),
96         "in-lines":     ( gobject.TYPE_PYOBJECT, "in-lines",
97                           "instructions to draw lines into the cell",
98                           gobject.PARAM_WRITABLE
99                         ),
100         "out-lines":    ( gobject.TYPE_PYOBJECT, "out-lines",
101                           "instructions to draw lines out of the cell",
102                           gobject.PARAM_WRITABLE
103                         ),
104         }
105
106         def do_set_property(self, property, value):
107                 """Set properties from GObject properties."""
108                 if property.name == "node":
109                         self.node = value
110                 elif property.name == "in-lines":
111                         self.in_lines = value
112                 elif property.name == "out-lines":
113                         self.out_lines = value
114                 else:
115                         raise AttributeError, "no such property: '%s'" % property.name
116
117         def box_size(self, widget):
118                 """Calculate box size based on widget's font.
119
120                 Cache this as it's probably expensive to get.  It ensures that we
121                 draw the graph at least as large as the text.
122                 """
123                 try:
124                         return self._box_size
125                 except AttributeError:
126                         pango_ctx = widget.get_pango_context()
127                         font_desc = widget.get_style().font_desc
128                         metrics = pango_ctx.get_metrics(font_desc)
129
130                         ascent = pango.PIXELS(metrics.get_ascent())
131                         descent = pango.PIXELS(metrics.get_descent())
132
133                         self._box_size = ascent + descent + 6
134                         return self._box_size
135
136         def set_colour(self, ctx, colour, bg, fg):
137                 """Set the context source colour.
138
139                 Picks a distinct colour based on an internal wheel; the bg
140                 parameter provides the value that should be assigned to the 'zero'
141                 colours and the fg parameter provides the multiplier that should be
142                 applied to the foreground colours.
143                 """
144                 colours = [
145                     ( 1.0, 0.0, 0.0 ),
146                     ( 1.0, 1.0, 0.0 ),
147                     ( 0.0, 1.0, 0.0 ),
148                     ( 0.0, 1.0, 1.0 ),
149                     ( 0.0, 0.0, 1.0 ),
150                     ( 1.0, 0.0, 1.0 ),
151                     ]
152
153                 colour %= len(colours)
154                 red   = (colours[colour][0] * fg) or bg
155                 green = (colours[colour][1] * fg) or bg
156                 blue  = (colours[colour][2] * fg) or bg
157
158                 ctx.set_source_rgb(red, green, blue)
159
160         def on_get_size(self, widget, cell_area):
161                 """Return the size we need for this cell.
162
163                 Each cell is drawn individually and is only as wide as it needs
164                 to be, we let the TreeViewColumn take care of making them all
165                 line up.
166                 """
167                 box_size = self.box_size(widget)
168
169                 cols = self.node[0]
170                 for start, end, colour in self.in_lines + self.out_lines:
171                         cols = max(cols, start, end)
172
173                 (column, colour, names) = self.node
174                 names_len = 0
175                 if (len(names) != 0):
176                         for item in names:
177                                 names_len += len(item)/3
178
179                 width = box_size * (cols + 1 + names_len )
180                 height = box_size
181
182                 # FIXME I have no idea how to use cell_area properly
183                 return (0, 0, width, height)
184
185         def on_render(self, window, widget, bg_area, cell_area, exp_area, flags):
186                 """Render an individual cell.
187
188                 Draws the cell contents using cairo, taking care to clip what we
189                 do to within the background area so we don't draw over other cells.
190                 Note that we're a bit naughty there and should really be drawing
191                 in the cell_area (or even the exposed area), but we explicitly don't
192                 want any gutter.
193
194                 We try and be a little clever, if the line we need to draw is going
195                 to cross other columns we actually draw it as in the .---' style
196                 instead of a pure diagonal ... this reduces confusion by an
197                 incredible amount.
198                 """
199                 ctx = window.cairo_create()
200                 ctx.rectangle(bg_area.x, bg_area.y, bg_area.width, bg_area.height)
201                 ctx.clip()
202
203                 box_size = self.box_size(widget)
204
205                 ctx.set_line_width(box_size / 8)
206                 ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
207
208                 # Draw lines into the cell
209                 for start, end, colour in self.in_lines:
210                         ctx.move_to(cell_area.x + box_size * start + box_size / 2,
211                                         bg_area.y - bg_area.height / 2)
212
213                         if start - end > 1:
214                                 ctx.line_to(cell_area.x + box_size * start, bg_area.y)
215                                 ctx.line_to(cell_area.x + box_size * end + box_size, bg_area.y)
216                         elif start - end < -1:
217                                 ctx.line_to(cell_area.x + box_size * start + box_size,
218                                                 bg_area.y)
219                                 ctx.line_to(cell_area.x + box_size * end, bg_area.y)
220
221                         ctx.line_to(cell_area.x + box_size * end + box_size / 2,
222                                         bg_area.y + bg_area.height / 2)
223
224                         self.set_colour(ctx, colour, 0.0, 0.65)
225                         ctx.stroke()
226
227                 # Draw lines out of the cell
228                 for start, end, colour in self.out_lines:
229                         ctx.move_to(cell_area.x + box_size * start + box_size / 2,
230                                         bg_area.y + bg_area.height / 2)
231
232                         if start - end > 1:
233                                 ctx.line_to(cell_area.x + box_size * start,
234                                                 bg_area.y + bg_area.height)
235                                 ctx.line_to(cell_area.x + box_size * end + box_size,
236                                                 bg_area.y + bg_area.height)
237                         elif start - end < -1:
238                                 ctx.line_to(cell_area.x + box_size * start + box_size,
239                                                 bg_area.y + bg_area.height)
240                                 ctx.line_to(cell_area.x + box_size * end,
241                                                 bg_area.y + bg_area.height)
242
243                         ctx.line_to(cell_area.x + box_size * end + box_size / 2,
244                                         bg_area.y + bg_area.height / 2 + bg_area.height)
245
246                         self.set_colour(ctx, colour, 0.0, 0.65)
247                         ctx.stroke()
248
249                 # Draw the revision node in the right column
250                 (column, colour, names) = self.node
251                 ctx.arc(cell_area.x + box_size * column + box_size / 2,
252                                 cell_area.y + cell_area.height / 2,
253                                 box_size / 4, 0, 2 * math.pi)
254
255
256                 if (len(names) != 0):
257                         name = " "
258                         for item in names:
259                                 name = name + item + " "
260
261                         ctx.text_path(name)
262
263                 self.set_colour(ctx, colour, 0.0, 0.5)
264                 ctx.stroke_preserve()
265
266                 self.set_colour(ctx, colour, 0.5, 1.0)
267                 ctx.fill()
268
269 class Commit:
270         """ This represent a commit object obtained after parsing the git-rev-list
271         output """
272
273         children_sha1 = {}
274
275         def __init__(self, commit_lines):
276                 self.message            = ""
277                 self.author             = ""
278                 self.date               = ""
279                 self.committer          = ""
280                 self.commit_date        = ""
281                 self.commit_sha1        = ""
282                 self.parent_sha1        = [ ]
283                 self.parse_commit(commit_lines)
284
285
286         def parse_commit(self, commit_lines):
287
288                 # First line is the sha1 lines
289                 line = string.strip(commit_lines[0])
290                 sha1 = re.split(" ", line)
291                 self.commit_sha1 = sha1[0]
292                 self.parent_sha1 = sha1[1:]
293
294                 #build the child list
295                 for parent_id in self.parent_sha1:
296                         try:
297                                 Commit.children_sha1[parent_id].append(self.commit_sha1)
298                         except KeyError:
299                                 Commit.children_sha1[parent_id] = [self.commit_sha1]
300
301                 # IF we don't have parent
302                 if (len(self.parent_sha1) == 0):
303                         self.parent_sha1 = [0]
304
305                 for line in commit_lines[1:]:
306                         m = re.match("^ ", line)
307                         if (m != None):
308                                 # First line of the commit message used for short log
309                                 if self.message == "":
310                                         self.message = string.strip(line)
311                                 continue
312
313                         m = re.match("tree", line)
314                         if (m != None):
315                                 continue
316
317                         m = re.match("parent", line)
318                         if (m != None):
319                                 continue
320
321                         m = re_ident.match(line)
322                         if (m != None):
323                                 date = show_date(m.group('epoch'), m.group('tz'))
324                                 if m.group(1) == "author":
325                                         self.author = m.group('ident')
326                                         self.date = date
327                                 elif m.group(1) == "committer":
328                                         self.committer = m.group('ident')
329                                         self.commit_date = date
330
331                                 continue
332
333         def get_message(self, with_diff=0):
334                 if (with_diff == 1):
335                         message = self.diff_tree()
336                 else:
337                         fp = os.popen("git cat-file commit " + self.commit_sha1)
338                         message = fp.read()
339                         fp.close()
340
341                 return message
342
343         def diff_tree(self):
344                 fp = os.popen("git diff-tree --pretty --cc  -v -p --always " +  self.commit_sha1)
345                 diff = fp.read()
346                 fp.close()
347                 return diff
348
349 class DiffWindow:
350         """Diff window.
351         This object represents and manages a single window containing the
352         differences between two revisions on a branch.
353         """
354
355         def __init__(self):
356                 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
357                 self.window.set_border_width(0)
358                 self.window.set_title("Git repository browser diff window")
359
360                 # Use two thirds of the screen by default
361                 screen = self.window.get_screen()
362                 monitor = screen.get_monitor_geometry(0)
363                 width = int(monitor.width * 0.66)
364                 height = int(monitor.height * 0.66)
365                 self.window.set_default_size(width, height)
366
367                 self.construct()
368
369         def construct(self):
370                 """Construct the window contents."""
371                 vbox = gtk.VBox()
372                 self.window.add(vbox)
373                 vbox.show()
374
375                 menu_bar = gtk.MenuBar()
376                 save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
377                 save_menu.connect("activate", self.save_menu_response, "save")
378                 save_menu.show()
379                 menu_bar.append(save_menu)
380                 vbox.pack_start(menu_bar, False, False, 2)
381                 menu_bar.show()
382
383                 scrollwin = gtk.ScrolledWindow()
384                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
385                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
386                 vbox.pack_start(scrollwin, expand=True, fill=True)
387                 scrollwin.show()
388
389                 if have_gtksourceview:
390                         self.buffer = gtksourceview.SourceBuffer()
391                         slm = gtksourceview.SourceLanguagesManager()
392                         gsl = slm.get_language_from_mime_type("text/x-patch")
393                         self.buffer.set_highlight(True)
394                         self.buffer.set_language(gsl)
395                         sourceview = gtksourceview.SourceView(self.buffer)
396                 else:
397                         self.buffer = gtk.TextBuffer()
398                         sourceview = gtk.TextView(self.buffer)
399
400                 sourceview.set_editable(False)
401                 sourceview.modify_font(pango.FontDescription("Monospace"))
402                 scrollwin.add(sourceview)
403                 sourceview.show()
404
405
406         def set_diff(self, commit_sha1, parent_sha1):
407                 """Set the differences showed by this window.
408                 Compares the two trees and populates the window with the
409                 differences.
410                 """
411                 # Diff with the first commit or the last commit shows nothing
412                 if (commit_sha1 == 0 or parent_sha1 == 0 ):
413                         return
414
415                 fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
416                 self.buffer.set_text(fp.read())
417                 fp.close()
418                 self.window.show()
419
420         def save_menu_response(self, widget, string):
421                 dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
422                                 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
423                                         gtk.STOCK_SAVE, gtk.RESPONSE_OK))
424                 dialog.set_default_response(gtk.RESPONSE_OK)
425                 response = dialog.run()
426                 if response == gtk.RESPONSE_OK:
427                         patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
428                                         self.buffer.get_end_iter())
429                         fp = open(dialog.get_filename(), "w")
430                         fp.write(patch_buffer)
431                         fp.close()
432                 dialog.destroy()
433
434 class GitView:
435         """ This is the main class
436         """
437         version = "0.6"
438
439         def __init__(self, with_diff=0):
440                 self.with_diff = with_diff
441                 self.window =   gtk.Window(gtk.WINDOW_TOPLEVEL)
442                 self.window.set_border_width(0)
443                 self.window.set_title("Git repository browser")
444
445                 self.get_bt_sha1()
446
447                 # Use three-quarters of the screen by default
448                 screen = self.window.get_screen()
449                 monitor = screen.get_monitor_geometry(0)
450                 width = int(monitor.width * 0.75)
451                 height = int(monitor.height * 0.75)
452                 self.window.set_default_size(width, height)
453
454                 # FIXME AndyFitz!
455                 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
456                 self.window.set_icon(icon)
457
458                 self.accel_group = gtk.AccelGroup()
459                 self.window.add_accel_group(self.accel_group)
460
461                 self.construct()
462
463         def get_bt_sha1(self):
464                 """ Update the bt_sha1 dictionary with the
465                 respective sha1 details """
466
467                 self.bt_sha1 = { }
468                 git_dir = os.getenv("GIT_DIR")
469                 if (git_dir == None):
470                         git_dir = ".git"
471
472                 #FIXME the path seperator
473                 ref_files = os.listdir(git_dir + "/refs/tags")
474                 for file in ref_files:
475                         fp = open(git_dir + "/refs/tags/"+file)
476                         sha1 = get_sha1_from_tags(string.strip(fp.readline()))
477                         try:
478                                 self.bt_sha1[sha1].append(file)
479                         except KeyError:
480                                 self.bt_sha1[sha1] = [file]
481                         fp.close()
482
483
484                 #FIXME the path seperator
485                 ref_files = os.listdir(git_dir + "/refs/heads")
486                 for file in ref_files:
487                         fp = open(git_dir + "/refs/heads/" + file)
488                         sha1 = get_sha1_from_tags(string.strip(fp.readline()))
489                         try:
490                                 self.bt_sha1[sha1].append(file)
491                         except KeyError:
492                                 self.bt_sha1[sha1] = [file]
493                         fp.close()
494
495
496         def construct(self):
497                 """Construct the window contents."""
498                 paned = gtk.VPaned()
499                 paned.pack1(self.construct_top(), resize=False, shrink=True)
500                 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
501                 self.window.add(paned)
502                 paned.show()
503
504
505         def construct_top(self):
506                 """Construct the top-half of the window."""
507                 vbox = gtk.VBox(spacing=6)
508                 vbox.set_border_width(12)
509                 vbox.show()
510
511                 menu_bar = gtk.MenuBar()
512                 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
513                 help_menu = gtk.MenuItem("Help")
514                 menu = gtk.Menu()
515                 about_menu = gtk.MenuItem("About")
516                 menu.append(about_menu)
517                 about_menu.connect("activate", self.about_menu_response, "about")
518                 about_menu.show()
519                 help_menu.set_submenu(menu)
520                 help_menu.show()
521                 menu_bar.append(help_menu)
522                 vbox.pack_start(menu_bar, False, False, 2)
523                 menu_bar.show()
524
525                 scrollwin = gtk.ScrolledWindow()
526                 scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
527                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
528                 vbox.pack_start(scrollwin, expand=True, fill=True)
529                 scrollwin.show()
530
531                 self.treeview = gtk.TreeView()
532                 self.treeview.set_rules_hint(True)
533                 self.treeview.set_search_column(4)
534                 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
535                 scrollwin.add(self.treeview)
536                 self.treeview.show()
537
538                 cell = CellRendererGraph()
539                 column = gtk.TreeViewColumn()
540                 column.set_resizable(False)
541                 column.pack_start(cell, expand=False)
542                 column.add_attribute(cell, "node", 1)
543                 column.add_attribute(cell, "in-lines", 2)
544                 column.add_attribute(cell, "out-lines", 3)
545                 self.treeview.append_column(column)
546
547                 cell = gtk.CellRendererText()
548                 cell.set_property("width-chars", 65)
549                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
550                 column = gtk.TreeViewColumn("Message")
551                 column.set_resizable(True)
552                 column.pack_start(cell, expand=True)
553                 column.add_attribute(cell, "text", 4)
554                 self.treeview.append_column(column)
555
556                 cell = gtk.CellRendererText()
557                 cell.set_property("width-chars", 40)
558                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
559                 column = gtk.TreeViewColumn("Author")
560                 column.set_resizable(True)
561                 column.pack_start(cell, expand=True)
562                 column.add_attribute(cell, "text", 5)
563                 self.treeview.append_column(column)
564
565                 cell = gtk.CellRendererText()
566                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
567                 column = gtk.TreeViewColumn("Date")
568                 column.set_resizable(True)
569                 column.pack_start(cell, expand=True)
570                 column.add_attribute(cell, "text", 6)
571                 self.treeview.append_column(column)
572
573                 return vbox
574
575         def about_menu_response(self, widget, string):
576                 dialog = gtk.AboutDialog()
577                 dialog.set_name("Gitview")
578                 dialog.set_version(GitView.version)
579                 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
580                 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
581                 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
582                 dialog.set_wrap_license(True)
583                 dialog.run()
584                 dialog.destroy()
585
586
587         def construct_bottom(self):
588                 """Construct the bottom half of the window."""
589                 vbox = gtk.VBox(False, spacing=6)
590                 vbox.set_border_width(12)
591                 (width, height) = self.window.get_size()
592                 vbox.set_size_request(width, int(height / 2.5))
593                 vbox.show()
594
595                 self.table = gtk.Table(rows=4, columns=4)
596                 self.table.set_row_spacings(6)
597                 self.table.set_col_spacings(6)
598                 vbox.pack_start(self.table, expand=False, fill=True)
599                 self.table.show()
600
601                 align = gtk.Alignment(0.0, 0.5)
602                 label = gtk.Label()
603                 label.set_markup("<b>Revision:</b>")
604                 align.add(label)
605                 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
606                 label.show()
607                 align.show()
608
609                 align = gtk.Alignment(0.0, 0.5)
610                 self.revid_label = gtk.Label()
611                 self.revid_label.set_selectable(True)
612                 align.add(self.revid_label)
613                 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
614                 self.revid_label.show()
615                 align.show()
616
617                 align = gtk.Alignment(0.0, 0.5)
618                 label = gtk.Label()
619                 label.set_markup("<b>Committer:</b>")
620                 align.add(label)
621                 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
622                 label.show()
623                 align.show()
624
625                 align = gtk.Alignment(0.0, 0.5)
626                 self.committer_label = gtk.Label()
627                 self.committer_label.set_selectable(True)
628                 align.add(self.committer_label)
629                 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
630                 self.committer_label.show()
631                 align.show()
632
633                 align = gtk.Alignment(0.0, 0.5)
634                 label = gtk.Label()
635                 label.set_markup("<b>Timestamp:</b>")
636                 align.add(label)
637                 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
638                 label.show()
639                 align.show()
640
641                 align = gtk.Alignment(0.0, 0.5)
642                 self.timestamp_label = gtk.Label()
643                 self.timestamp_label.set_selectable(True)
644                 align.add(self.timestamp_label)
645                 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
646                 self.timestamp_label.show()
647                 align.show()
648
649                 align = gtk.Alignment(0.0, 0.5)
650                 label = gtk.Label()
651                 label.set_markup("<b>Parents:</b>")
652                 align.add(label)
653                 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
654                 label.show()
655                 align.show()
656                 self.parents_widgets = []
657
658                 align = gtk.Alignment(0.0, 0.5)
659                 label = gtk.Label()
660                 label.set_markup("<b>Children:</b>")
661                 align.add(label)
662                 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
663                 label.show()
664                 align.show()
665                 self.children_widgets = []
666
667                 scrollwin = gtk.ScrolledWindow()
668                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
669                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
670                 vbox.pack_start(scrollwin, expand=True, fill=True)
671                 scrollwin.show()
672
673                 if have_gtksourceview:
674                         self.message_buffer = gtksourceview.SourceBuffer()
675                         slm = gtksourceview.SourceLanguagesManager()
676                         gsl = slm.get_language_from_mime_type("text/x-patch")
677                         self.message_buffer.set_highlight(True)
678                         self.message_buffer.set_language(gsl)
679                         sourceview = gtksourceview.SourceView(self.message_buffer)
680                 else:
681                         self.message_buffer = gtk.TextBuffer()
682                         sourceview = gtk.TextView(self.message_buffer)
683
684                 sourceview.set_editable(False)
685                 sourceview.modify_font(pango.FontDescription("Monospace"))
686                 scrollwin.add(sourceview)
687                 sourceview.show()
688
689                 return vbox
690
691         def _treeview_cursor_cb(self, *args):
692                 """Callback for when the treeview cursor changes."""
693                 (path, col) = self.treeview.get_cursor()
694                 commit = self.model[path][0]
695
696                 if commit.committer is not None:
697                         committer = commit.committer
698                         timestamp = commit.commit_date
699                         message   =  commit.get_message(self.with_diff)
700                         revid_label = commit.commit_sha1
701                 else:
702                         committer = ""
703                         timestamp = ""
704                         message = ""
705                         revid_label = ""
706
707                 self.revid_label.set_text(revid_label)
708                 self.committer_label.set_text(committer)
709                 self.timestamp_label.set_text(timestamp)
710                 self.message_buffer.set_text(message)
711
712                 for widget in self.parents_widgets:
713                         self.table.remove(widget)
714
715                 self.parents_widgets = []
716                 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
717                 for idx, parent_id in enumerate(commit.parent_sha1):
718                         self.table.set_row_spacing(idx + 3, 0)
719
720                         align = gtk.Alignment(0.0, 0.0)
721                         self.parents_widgets.append(align)
722                         self.table.attach(align, 1, 2, idx + 3, idx + 4,
723                                         gtk.EXPAND | gtk.FILL, gtk.FILL)
724                         align.show()
725
726                         hbox = gtk.HBox(False, 0)
727                         align.add(hbox)
728                         hbox.show()
729
730                         label = gtk.Label(parent_id)
731                         label.set_selectable(True)
732                         hbox.pack_start(label, expand=False, fill=True)
733                         label.show()
734
735                         image = gtk.Image()
736                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
737                         image.show()
738
739                         button = gtk.Button()
740                         button.add(image)
741                         button.set_relief(gtk.RELIEF_NONE)
742                         button.connect("clicked", self._go_clicked_cb, parent_id)
743                         hbox.pack_start(button, expand=False, fill=True)
744                         button.show()
745
746                         image = gtk.Image()
747                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
748                         image.show()
749
750                         button = gtk.Button()
751                         button.add(image)
752                         button.set_relief(gtk.RELIEF_NONE)
753                         button.set_sensitive(True)
754                         button.connect("clicked", self._show_clicked_cb,
755                                         commit.commit_sha1, parent_id)
756                         hbox.pack_start(button, expand=False, fill=True)
757                         button.show()
758
759                 # Populate with child details
760                 for widget in self.children_widgets:
761                         self.table.remove(widget)
762
763                 self.children_widgets = []
764                 try:
765                         child_sha1 = Commit.children_sha1[commit.commit_sha1]
766                 except KeyError:
767                         # We don't have child
768                         child_sha1 = [ 0 ]
769
770                 if ( len(child_sha1) > len(commit.parent_sha1)):
771                         self.table.resize(4 + len(child_sha1) - 1, 4)
772
773                 for idx, child_id in enumerate(child_sha1):
774                         self.table.set_row_spacing(idx + 3, 0)
775
776                         align = gtk.Alignment(0.0, 0.0)
777                         self.children_widgets.append(align)
778                         self.table.attach(align, 3, 4, idx + 3, idx + 4,
779                                         gtk.EXPAND | gtk.FILL, gtk.FILL)
780                         align.show()
781
782                         hbox = gtk.HBox(False, 0)
783                         align.add(hbox)
784                         hbox.show()
785
786                         label = gtk.Label(child_id)
787                         label.set_selectable(True)
788                         hbox.pack_start(label, expand=False, fill=True)
789                         label.show()
790
791                         image = gtk.Image()
792                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
793                         image.show()
794
795                         button = gtk.Button()
796                         button.add(image)
797                         button.set_relief(gtk.RELIEF_NONE)
798                         button.connect("clicked", self._go_clicked_cb, child_id)
799                         hbox.pack_start(button, expand=False, fill=True)
800                         button.show()
801
802                         image = gtk.Image()
803                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
804                         image.show()
805
806                         button = gtk.Button()
807                         button.add(image)
808                         button.set_relief(gtk.RELIEF_NONE)
809                         button.set_sensitive(True)
810                         button.connect("clicked", self._show_clicked_cb,
811                                         child_id, commit.commit_sha1)
812                         hbox.pack_start(button, expand=False, fill=True)
813                         button.show()
814
815         def _destroy_cb(self, widget):
816                 """Callback for when a window we manage is destroyed."""
817                 self.quit()
818
819
820         def quit(self):
821                 """Stop the GTK+ main loop."""
822                 gtk.main_quit()
823
824         def run(self, args):
825                 self.set_branch(args)
826                 self.window.connect("destroy", self._destroy_cb)
827                 self.window.show()
828                 gtk.main()
829
830         def set_branch(self, args):
831                 """Fill in different windows with info from the reposiroty"""
832                 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
833                 git_rev_list_cmd = fp.read()
834                 fp.close()
835                 fp = os.popen("git rev-list  --header --topo-order --parents " + git_rev_list_cmd)
836                 self.update_window(fp)
837
838         def update_window(self, fp):
839                 commit_lines = []
840
841                 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
842                                 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
843
844                 # used for cursor positioning
845                 self.index = {}
846
847                 self.colours = {}
848                 self.nodepos = {}
849                 self.incomplete_line = {}
850
851                 index = 0
852                 last_colour = 0
853                 last_nodepos = -1
854                 out_line = []
855                 input_line = fp.readline()
856                 while (input_line != ""):
857                         # The commit header ends with '\0'
858                         # This NULL is immediately followed by the sha1 of the
859                         # next commit
860                         if (input_line[0] != '\0'):
861                                 commit_lines.append(input_line)
862                                 input_line = fp.readline()
863                                 continue;
864
865                         commit = Commit(commit_lines)
866                         if (commit != None ):
867                                 (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
868                                                                                 index, out_line,
869                                                                                 last_colour,
870                                                                                 last_nodepos)
871                                 self.index[commit.commit_sha1] = index
872                                 index += 1
873
874                         # Skip the '\0
875                         commit_lines = []
876                         commit_lines.append(input_line[1:])
877                         input_line = fp.readline()
878
879                 fp.close()
880
881                 self.treeview.set_model(self.model)
882                 self.treeview.show()
883
884         def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
885                 in_line=[]
886
887                 #   |   -> outline
888                 #   X
889                 #   |\  <- inline
890
891                 # Reset nodepostion
892                 if (last_nodepos > 5):
893                         last_nodepos = 0
894
895                 # Add the incomplete lines of the last cell in this
896                 for sha1 in self.incomplete_line.keys():
897                         if ( sha1 != commit.commit_sha1):
898                                 for pos in self.incomplete_line[sha1]:
899                                         in_line.append((pos, pos, self.colours[sha1]))
900                         else:
901                                 del self.incomplete_line[sha1]
902
903                 try:
904                         colour = self.colours[commit.commit_sha1]
905                 except KeyError:
906                         last_colour +=1
907                         self.colours[commit.commit_sha1] = last_colour
908                         colour =  last_colour
909                 try:
910                         node_pos = self.nodepos[commit.commit_sha1]
911                 except KeyError:
912                         last_nodepos +=1
913                         self.nodepos[commit.commit_sha1] = last_nodepos
914                         node_pos = last_nodepos
915
916                 #The first parent always continue on the same line
917                 try:
918                         # check we alreay have the value
919                         tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
920                 except KeyError:
921                         self.colours[commit.parent_sha1[0]] = colour
922                         self.nodepos[commit.parent_sha1[0]] = node_pos
923
924                 in_line.append((node_pos, self.nodepos[commit.parent_sha1[0]],
925                                         self.colours[commit.parent_sha1[0]]))
926
927                 self.add_incomplete_line(commit.parent_sha1[0], index+1)
928
929                 if (len(commit.parent_sha1) > 1):
930                         for parent_id in commit.parent_sha1[1:]:
931                                 try:
932                                         tmp_node_pos = self.nodepos[parent_id]
933                                 except KeyError:
934                                         last_colour += 1;
935                                         self.colours[parent_id] = last_colour
936                                         last_nodepos +=1
937                                         self.nodepos[parent_id] = last_nodepos
938
939                                 in_line.append((node_pos, self.nodepos[parent_id],
940                                                         self.colours[parent_id]))
941                                 self.add_incomplete_line(parent_id, index+1)
942
943
944                 try:
945                         branch_tag = self.bt_sha1[commit.commit_sha1]
946                 except KeyError:
947                         branch_tag = [ ]
948
949
950                 node = (node_pos, colour, branch_tag)
951
952                 self.model.append([commit, node, out_line, in_line,
953                                 commit.message, commit.author, commit.date])
954
955                 return (in_line, last_colour, last_nodepos)
956
957         def add_incomplete_line(self, sha1, index):
958                 try:
959                         self.incomplete_line[sha1].append(self.nodepos[sha1])
960                 except KeyError:
961                         self.incomplete_line[sha1] = [self.nodepos[sha1]]
962
963
964         def _go_clicked_cb(self, widget, revid):
965                 """Callback for when the go button for a parent is clicked."""
966                 try:
967                         self.treeview.set_cursor(self.index[revid])
968                 except KeyError:
969                         print "Revision %s not present in the list" % revid
970                         # revid == 0 is the parent of the first commit
971                         if (revid != 0 ):
972                                 print "Try running gitview without any options"
973
974                 self.treeview.grab_focus()
975
976         def _show_clicked_cb(self, widget,  commit_sha1, parent_sha1):
977                 """Callback for when the show button for a parent is clicked."""
978                 window = DiffWindow()
979                 window.set_diff(commit_sha1, parent_sha1)
980                 self.treeview.grab_focus()
981
982 if __name__ == "__main__":
983         without_diff = 0
984
985         if (len(sys.argv) > 1 ):
986                 if (sys.argv[1] == "--without-diff"):
987                         without_diff = 1
988
989         view = GitView( without_diff != 1)
990         view.run(sys.argv[without_diff:])
991
992