Merge branch 'maint'
[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 __copyright__ = "Copyright (C) 2007 Aneesh Kumar K.V <aneesh.kumar@gmail.com"
14 __author__    = "Aneesh Kumar K.V <aneesh.kumar@gmail.com>"
15
16
17 import sys
18 import os
19 import gtk
20 import pygtk
21 import pango
22 import re
23 import time
24 import gobject
25 import cairo
26 import math
27 import string
28 import fcntl
29
30 try:
31     import gtksourceview2
32     have_gtksourceview2 = True
33 except ImportError:
34     have_gtksourceview2 = False
35
36 try:
37     import gtksourceview
38     have_gtksourceview = True
39 except ImportError:
40     have_gtksourceview = False
41
42 if not have_gtksourceview2 and not have_gtksourceview:
43     print "Running without gtksourceview2 or gtksourceview module"
44
45 re_ident = re.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
46
47 def list_to_string(args, skip):
48         count = len(args)
49         i = skip
50         str_arg=" "
51         while (i < count ):
52                 str_arg = str_arg + args[i]
53                 str_arg = str_arg + " "
54                 i = i+1
55
56         return str_arg
57
58 def show_date(epoch, tz):
59         secs = float(epoch)
60         tzsecs = float(tz[1:3]) * 3600
61         tzsecs += float(tz[3:5]) * 60
62         if (tz[0] == "+"):
63                 secs += tzsecs
64         else:
65                 secs -= tzsecs
66
67         return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs))
68
69 def get_source_buffer_and_view():
70         if have_gtksourceview2:
71                 buffer = gtksourceview2.Buffer()
72                 slm = gtksourceview2.LanguageManager()
73                 gsl = slm.get_language("diff")
74                 buffer.set_highlight_syntax(True)
75                 buffer.set_language(gsl)
76                 view = gtksourceview2.View(buffer)
77         elif have_gtksourceview:
78                 buffer = gtksourceview.SourceBuffer()
79                 slm = gtksourceview.SourceLanguagesManager()
80                 gsl = slm.get_language_from_mime_type("text/x-patch")
81                 buffer.set_highlight(True)
82                 buffer.set_language(gsl)
83                 view = gtksourceview.SourceView(buffer)
84         else:
85                 buffer = gtk.TextBuffer()
86                 view = gtk.TextView(buffer)
87         return (buffer, view)
88
89
90 class CellRendererGraph(gtk.GenericCellRenderer):
91         """Cell renderer for directed graph.
92
93         This module contains the implementation of a custom GtkCellRenderer that
94         draws part of the directed graph based on the lines suggested by the code
95         in graph.py.
96
97         Because we're shiny, we use Cairo to do this, and because we're naughty
98         we cheat and draw over the bits of the TreeViewColumn that are supposed to
99         just be for the background.
100
101         Properties:
102         node              (column, colour, [ names ]) tuple to draw revision node,
103         in_lines          (start, end, colour) tuple list to draw inward lines,
104         out_lines         (start, end, colour) tuple list to draw outward lines.
105         """
106
107         __gproperties__ = {
108         "node":         ( gobject.TYPE_PYOBJECT, "node",
109                           "revision node instruction",
110                           gobject.PARAM_WRITABLE
111                         ),
112         "in-lines":     ( gobject.TYPE_PYOBJECT, "in-lines",
113                           "instructions to draw lines into the cell",
114                           gobject.PARAM_WRITABLE
115                         ),
116         "out-lines":    ( gobject.TYPE_PYOBJECT, "out-lines",
117                           "instructions to draw lines out of the cell",
118                           gobject.PARAM_WRITABLE
119                         ),
120         }
121
122         def do_set_property(self, property, value):
123                 """Set properties from GObject properties."""
124                 if property.name == "node":
125                         self.node = value
126                 elif property.name == "in-lines":
127                         self.in_lines = value
128                 elif property.name == "out-lines":
129                         self.out_lines = value
130                 else:
131                         raise AttributeError, "no such property: '%s'" % property.name
132
133         def box_size(self, widget):
134                 """Calculate box size based on widget's font.
135
136                 Cache this as it's probably expensive to get.  It ensures that we
137                 draw the graph at least as large as the text.
138                 """
139                 try:
140                         return self._box_size
141                 except AttributeError:
142                         pango_ctx = widget.get_pango_context()
143                         font_desc = widget.get_style().font_desc
144                         metrics = pango_ctx.get_metrics(font_desc)
145
146                         ascent = pango.PIXELS(metrics.get_ascent())
147                         descent = pango.PIXELS(metrics.get_descent())
148
149                         self._box_size = ascent + descent + 6
150                         return self._box_size
151
152         def set_colour(self, ctx, colour, bg, fg):
153                 """Set the context source colour.
154
155                 Picks a distinct colour based on an internal wheel; the bg
156                 parameter provides the value that should be assigned to the 'zero'
157                 colours and the fg parameter provides the multiplier that should be
158                 applied to the foreground colours.
159                 """
160                 colours = [
161                     ( 1.0, 0.0, 0.0 ),
162                     ( 1.0, 1.0, 0.0 ),
163                     ( 0.0, 1.0, 0.0 ),
164                     ( 0.0, 1.0, 1.0 ),
165                     ( 0.0, 0.0, 1.0 ),
166                     ( 1.0, 0.0, 1.0 ),
167                     ]
168
169                 colour %= len(colours)
170                 red   = (colours[colour][0] * fg) or bg
171                 green = (colours[colour][1] * fg) or bg
172                 blue  = (colours[colour][2] * fg) or bg
173
174                 ctx.set_source_rgb(red, green, blue)
175
176         def on_get_size(self, widget, cell_area):
177                 """Return the size we need for this cell.
178
179                 Each cell is drawn individually and is only as wide as it needs
180                 to be, we let the TreeViewColumn take care of making them all
181                 line up.
182                 """
183                 box_size = self.box_size(widget)
184
185                 cols = self.node[0]
186                 for start, end, colour in self.in_lines + self.out_lines:
187                         cols = int(max(cols, start, end))
188
189                 (column, colour, names) = self.node
190                 names_len = 0
191                 if (len(names) != 0):
192                         for item in names:
193                                 names_len += len(item)
194
195                 width = box_size * (cols + 1 ) + names_len
196                 height = box_size
197
198                 # FIXME I have no idea how to use cell_area properly
199                 return (0, 0, width, height)
200
201         def on_render(self, window, widget, bg_area, cell_area, exp_area, flags):
202                 """Render an individual cell.
203
204                 Draws the cell contents using cairo, taking care to clip what we
205                 do to within the background area so we don't draw over other cells.
206                 Note that we're a bit naughty there and should really be drawing
207                 in the cell_area (or even the exposed area), but we explicitly don't
208                 want any gutter.
209
210                 We try and be a little clever, if the line we need to draw is going
211                 to cross other columns we actually draw it as in the .---' style
212                 instead of a pure diagonal ... this reduces confusion by an
213                 incredible amount.
214                 """
215                 ctx = window.cairo_create()
216                 ctx.rectangle(bg_area.x, bg_area.y, bg_area.width, bg_area.height)
217                 ctx.clip()
218
219                 box_size = self.box_size(widget)
220
221                 ctx.set_line_width(box_size / 8)
222                 ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
223
224                 # Draw lines into the cell
225                 for start, end, colour in self.in_lines:
226                         ctx.move_to(cell_area.x + box_size * start + box_size / 2,
227                                         bg_area.y - bg_area.height / 2)
228
229                         if start - end > 1:
230                                 ctx.line_to(cell_area.x + box_size * start, bg_area.y)
231                                 ctx.line_to(cell_area.x + box_size * end + box_size, bg_area.y)
232                         elif start - end < -1:
233                                 ctx.line_to(cell_area.x + box_size * start + box_size,
234                                                 bg_area.y)
235                                 ctx.line_to(cell_area.x + box_size * end, bg_area.y)
236
237                         ctx.line_to(cell_area.x + box_size * end + box_size / 2,
238                                         bg_area.y + bg_area.height / 2)
239
240                         self.set_colour(ctx, colour, 0.0, 0.65)
241                         ctx.stroke()
242
243                 # Draw lines out of the cell
244                 for start, end, colour in self.out_lines:
245                         ctx.move_to(cell_area.x + box_size * start + box_size / 2,
246                                         bg_area.y + bg_area.height / 2)
247
248                         if start - end > 1:
249                                 ctx.line_to(cell_area.x + box_size * start,
250                                                 bg_area.y + bg_area.height)
251                                 ctx.line_to(cell_area.x + box_size * end + box_size,
252                                                 bg_area.y + bg_area.height)
253                         elif start - end < -1:
254                                 ctx.line_to(cell_area.x + box_size * start + box_size,
255                                                 bg_area.y + bg_area.height)
256                                 ctx.line_to(cell_area.x + box_size * end,
257                                                 bg_area.y + bg_area.height)
258
259                         ctx.line_to(cell_area.x + box_size * end + box_size / 2,
260                                         bg_area.y + bg_area.height / 2 + bg_area.height)
261
262                         self.set_colour(ctx, colour, 0.0, 0.65)
263                         ctx.stroke()
264
265                 # Draw the revision node in the right column
266                 (column, colour, names) = self.node
267                 ctx.arc(cell_area.x + box_size * column + box_size / 2,
268                                 cell_area.y + cell_area.height / 2,
269                                 box_size / 4, 0, 2 * math.pi)
270
271
272                 self.set_colour(ctx, colour, 0.0, 0.5)
273                 ctx.stroke_preserve()
274
275                 self.set_colour(ctx, colour, 0.5, 1.0)
276                 ctx.fill_preserve()
277
278                 if (len(names) != 0):
279                         name = " "
280                         for item in names:
281                                 name = name + item + " "
282
283                         ctx.set_font_size(13)
284                         if (flags & 1):
285                                 self.set_colour(ctx, colour, 0.5, 1.0)
286                         else:
287                                 self.set_colour(ctx, colour, 0.0, 0.5)
288                         ctx.show_text(name)
289
290 class Commit(object):
291         """ This represent a commit object obtained after parsing the git-rev-list
292         output """
293
294         __slots__ = ['children_sha1', 'message', 'author', 'date', 'committer',
295                                  'commit_date', 'commit_sha1', 'parent_sha1']
296
297         children_sha1 = {}
298
299         def __init__(self, commit_lines):
300                 self.message            = ""
301                 self.author             = ""
302                 self.date               = ""
303                 self.committer          = ""
304                 self.commit_date        = ""
305                 self.commit_sha1        = ""
306                 self.parent_sha1        = [ ]
307                 self.parse_commit(commit_lines)
308
309
310         def parse_commit(self, commit_lines):
311
312                 # First line is the sha1 lines
313                 line = string.strip(commit_lines[0])
314                 sha1 = re.split(" ", line)
315                 self.commit_sha1 = sha1[0]
316                 self.parent_sha1 = sha1[1:]
317
318                 #build the child list
319                 for parent_id in self.parent_sha1:
320                         try:
321                                 Commit.children_sha1[parent_id].append(self.commit_sha1)
322                         except KeyError:
323                                 Commit.children_sha1[parent_id] = [self.commit_sha1]
324
325                 # IF we don't have parent
326                 if (len(self.parent_sha1) == 0):
327                         self.parent_sha1 = [0]
328
329                 for line in commit_lines[1:]:
330                         m = re.match("^ ", line)
331                         if (m != None):
332                                 # First line of the commit message used for short log
333                                 if self.message == "":
334                                         self.message = string.strip(line)
335                                 continue
336
337                         m = re.match("tree", line)
338                         if (m != None):
339                                 continue
340
341                         m = re.match("parent", line)
342                         if (m != None):
343                                 continue
344
345                         m = re_ident.match(line)
346                         if (m != None):
347                                 date = show_date(m.group('epoch'), m.group('tz'))
348                                 if m.group(1) == "author":
349                                         self.author = m.group('ident')
350                                         self.date = date
351                                 elif m.group(1) == "committer":
352                                         self.committer = m.group('ident')
353                                         self.commit_date = date
354
355                                 continue
356
357         def get_message(self, with_diff=0):
358                 if (with_diff == 1):
359                         message = self.diff_tree()
360                 else:
361                         fp = os.popen("git cat-file commit " + self.commit_sha1)
362                         message = fp.read()
363                         fp.close()
364
365                 return message
366
367         def diff_tree(self):
368                 fp = os.popen("git diff-tree --pretty --cc  -v -p --always " +  self.commit_sha1)
369                 diff = fp.read()
370                 fp.close()
371                 return diff
372
373 class AnnotateWindow(object):
374         """Annotate window.
375         This object represents and manages a single window containing the
376         annotate information of the file
377         """
378
379         def __init__(self):
380                 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
381                 self.window.set_border_width(0)
382                 self.window.set_title("Git repository browser annotation window")
383                 self.prev_read = ""
384
385                 # Use two thirds of the screen by default
386                 screen = self.window.get_screen()
387                 monitor = screen.get_monitor_geometry(0)
388                 width = int(monitor.width * 0.66)
389                 height = int(monitor.height * 0.66)
390                 self.window.set_default_size(width, height)
391
392         def add_file_data(self, filename, commit_sha1, line_num):
393                 fp = os.popen("git cat-file blob " + commit_sha1 +":"+filename)
394                 i = 1;
395                 for line in fp.readlines():
396                         line = string.rstrip(line)
397                         self.model.append(None, ["HEAD", filename, line, i])
398                         i = i+1
399                 fp.close()
400
401                 # now set the cursor position
402                 self.treeview.set_cursor(line_num-1)
403                 self.treeview.grab_focus()
404
405         def _treeview_cursor_cb(self, *args):
406                 """Callback for when the treeview cursor changes."""
407                 (path, col) = self.treeview.get_cursor()
408                 commit_sha1 = self.model[path][0]
409                 commit_msg = ""
410                 fp = os.popen("git cat-file commit " + commit_sha1)
411                 for line in fp.readlines():
412                         commit_msg =  commit_msg + line
413                 fp.close()
414
415                 self.commit_buffer.set_text(commit_msg)
416
417         def _treeview_row_activated(self, *args):
418                 """Callback for when the treeview row gets selected."""
419                 (path, col) = self.treeview.get_cursor()
420                 commit_sha1 = self.model[path][0]
421                 filename    = self.model[path][1]
422                 line_num    = self.model[path][3]
423
424                 window = AnnotateWindow();
425                 fp = os.popen("git rev-parse "+ commit_sha1 + "~1")
426                 commit_sha1 = string.strip(fp.readline())
427                 fp.close()
428                 window.annotate(filename, commit_sha1, line_num)
429
430         def data_ready(self, source, condition):
431                 while (1):
432                         try :
433                                 # A simple readline doesn't work
434                                 # a readline bug ??
435                                 buffer = source.read(100)
436
437                         except:
438                                 # resource temporary not available
439                                 return True
440
441                         if (len(buffer) == 0):
442                                 gobject.source_remove(self.io_watch_tag)
443                                 source.close()
444                                 return False
445
446                         if (self.prev_read != ""):
447                                 buffer = self.prev_read + buffer
448                                 self.prev_read = ""
449
450                         if (buffer[len(buffer) -1] != '\n'):
451                                 try:
452                                         newline_index = buffer.rindex("\n")
453                                 except ValueError:
454                                         newline_index = 0
455
456                                 self.prev_read = buffer[newline_index:(len(buffer))]
457                                 buffer = buffer[0:newline_index]
458
459                         for buff in buffer.split("\n"):
460                                 annotate_line = re.compile('^([0-9a-f]{40}) (.+) (.+) (.+)$')
461                                 m = annotate_line.match(buff)
462                                 if not m:
463                                         annotate_line = re.compile('^(filename) (.+)$')
464                                         m = annotate_line.match(buff)
465                                         if not m:
466                                                 continue
467                                         filename = m.group(2)
468                                 else:
469                                         self.commit_sha1 = m.group(1)
470                                         self.source_line = int(m.group(2))
471                                         self.result_line = int(m.group(3))
472                                         self.count          = int(m.group(4))
473                                         #set the details only when we have the file name
474                                         continue
475
476                                 while (self.count > 0):
477                                         # set at result_line + count-1 the sha1 as commit_sha1
478                                         self.count = self.count - 1
479                                         iter = self.model.iter_nth_child(None, self.result_line + self.count-1)
480                                         self.model.set(iter, 0, self.commit_sha1, 1, filename, 3, self.source_line)
481
482
483         def annotate(self, filename, commit_sha1, line_num):
484                 # verify the commit_sha1 specified has this filename
485
486                 fp = os.popen("git ls-tree "+ commit_sha1 + " -- " + filename)
487                 line = string.strip(fp.readline())
488                 if line == '':
489                         # pop up the message the file is not there as a part of the commit
490                         fp.close()
491                         dialog = gtk.MessageDialog(parent=None, flags=0,
492                                         type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
493                                         message_format=None)
494                         dialog.set_markup("The file %s is not present in the parent commit %s" % (filename, commit_sha1))
495                         dialog.run()
496                         dialog.destroy()
497                         return
498
499                 fp.close()
500
501                 vpan = gtk.VPaned();
502                 self.window.add(vpan);
503                 vpan.show()
504
505                 scrollwin = gtk.ScrolledWindow()
506                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
507                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
508                 vpan.pack1(scrollwin, True, True);
509                 scrollwin.show()
510
511                 self.model = gtk.TreeStore(str, str, str, int)
512                 self.treeview = gtk.TreeView(self.model)
513                 self.treeview.set_rules_hint(True)
514                 self.treeview.set_search_column(0)
515                 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
516                 self.treeview.connect("row-activated", self._treeview_row_activated)
517                 scrollwin.add(self.treeview)
518                 self.treeview.show()
519
520                 cell = gtk.CellRendererText()
521                 cell.set_property("width-chars", 10)
522                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
523                 column = gtk.TreeViewColumn("Commit")
524                 column.set_resizable(True)
525                 column.pack_start(cell, expand=True)
526                 column.add_attribute(cell, "text", 0)
527                 self.treeview.append_column(column)
528
529                 cell = gtk.CellRendererText()
530                 cell.set_property("width-chars", 20)
531                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
532                 column = gtk.TreeViewColumn("File Name")
533                 column.set_resizable(True)
534                 column.pack_start(cell, expand=True)
535                 column.add_attribute(cell, "text", 1)
536                 self.treeview.append_column(column)
537
538                 cell = gtk.CellRendererText()
539                 cell.set_property("width-chars", 20)
540                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
541                 column = gtk.TreeViewColumn("Data")
542                 column.set_resizable(True)
543                 column.pack_start(cell, expand=True)
544                 column.add_attribute(cell, "text", 2)
545                 self.treeview.append_column(column)
546
547                 # The commit message window
548                 scrollwin = gtk.ScrolledWindow()
549                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
550                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
551                 vpan.pack2(scrollwin, True, True);
552                 scrollwin.show()
553
554                 commit_text = gtk.TextView()
555                 self.commit_buffer = gtk.TextBuffer()
556                 commit_text.set_buffer(self.commit_buffer)
557                 scrollwin.add(commit_text)
558                 commit_text.show()
559
560                 self.window.show()
561
562                 self.add_file_data(filename, commit_sha1, line_num)
563
564                 fp = os.popen("git blame --incremental -C -C -- " + filename + " " + commit_sha1)
565                 flags = fcntl.fcntl(fp.fileno(), fcntl.F_GETFL)
566                 fcntl.fcntl(fp.fileno(), fcntl.F_SETFL, flags | os.O_NONBLOCK)
567                 self.io_watch_tag = gobject.io_add_watch(fp, gobject.IO_IN, self.data_ready)
568
569
570 class DiffWindow(object):
571         """Diff window.
572         This object represents and manages a single window containing the
573         differences between two revisions on a branch.
574         """
575
576         def __init__(self):
577                 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
578                 self.window.set_border_width(0)
579                 self.window.set_title("Git repository browser diff window")
580
581                 # Use two thirds of the screen by default
582                 screen = self.window.get_screen()
583                 monitor = screen.get_monitor_geometry(0)
584                 width = int(monitor.width * 0.66)
585                 height = int(monitor.height * 0.66)
586                 self.window.set_default_size(width, height)
587
588
589                 self.construct()
590
591         def construct(self):
592                 """Construct the window contents."""
593                 vbox = gtk.VBox()
594                 self.window.add(vbox)
595                 vbox.show()
596
597                 menu_bar = gtk.MenuBar()
598                 save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
599                 save_menu.connect("activate", self.save_menu_response, "save")
600                 save_menu.show()
601                 menu_bar.append(save_menu)
602                 vbox.pack_start(menu_bar, expand=False, fill=True)
603                 menu_bar.show()
604
605                 hpan = gtk.HPaned()
606
607                 scrollwin = gtk.ScrolledWindow()
608                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
609                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
610                 hpan.pack1(scrollwin, True, True)
611                 scrollwin.show()
612
613                 (self.buffer, sourceview) = get_source_buffer_and_view()
614
615                 sourceview.set_editable(False)
616                 sourceview.modify_font(pango.FontDescription("Monospace"))
617                 scrollwin.add(sourceview)
618                 sourceview.show()
619
620                 # The file hierarchy: a scrollable treeview
621                 scrollwin = gtk.ScrolledWindow()
622                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
623                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
624                 scrollwin.set_size_request(20, -1)
625                 hpan.pack2(scrollwin, True, True)
626                 scrollwin.show()
627
628                 self.model = gtk.TreeStore(str, str, str)
629                 self.treeview = gtk.TreeView(self.model)
630                 self.treeview.set_search_column(1)
631                 self.treeview.connect("cursor-changed", self._treeview_clicked)
632                 scrollwin.add(self.treeview)
633                 self.treeview.show()
634
635                 cell = gtk.CellRendererText()
636                 cell.set_property("width-chars", 20)
637                 column = gtk.TreeViewColumn("Select to annotate")
638                 column.pack_start(cell, expand=True)
639                 column.add_attribute(cell, "text", 0)
640                 self.treeview.append_column(column)
641
642                 vbox.pack_start(hpan, expand=True, fill=True)
643                 hpan.show()
644
645         def _treeview_clicked(self, *args):
646                 """Callback for when the treeview cursor changes."""
647                 (path, col) = self.treeview.get_cursor()
648                 specific_file = self.model[path][1]
649                 commit_sha1 =  self.model[path][2]
650                 if specific_file ==  None :
651                         return
652                 elif specific_file ==  "" :
653                         specific_file =  None
654
655                 window = AnnotateWindow();
656                 window.annotate(specific_file, commit_sha1, 1)
657
658
659         def commit_files(self, commit_sha1, parent_sha1):
660                 self.model.clear()
661                 add  = self.model.append(None, [ "Added", None, None])
662                 dele = self.model.append(None, [ "Deleted", None, None])
663                 mod  = self.model.append(None, [ "Modified", None, None])
664                 diff_tree = re.compile('^(:.{6}) (.{6}) (.{40}) (.{40}) (A|D|M)\s(.+)$')
665                 fp = os.popen("git diff-tree -r --no-commit-id " + parent_sha1 + " " + commit_sha1)
666                 while 1:
667                         line = string.strip(fp.readline())
668                         if line == '':
669                                 break
670                         m = diff_tree.match(line)
671                         if not m:
672                                 continue
673
674                         attr = m.group(5)
675                         filename = m.group(6)
676                         if attr == "A":
677                                 self.model.append(add,  [filename, filename, commit_sha1])
678                         elif attr == "D":
679                                 self.model.append(dele, [filename, filename, commit_sha1])
680                         elif attr == "M":
681                                 self.model.append(mod,  [filename, filename, commit_sha1])
682                 fp.close()
683
684                 self.treeview.expand_all()
685
686         def set_diff(self, commit_sha1, parent_sha1, encoding):
687                 """Set the differences showed by this window.
688                 Compares the two trees and populates the window with the
689                 differences.
690                 """
691                 # Diff with the first commit or the last commit shows nothing
692                 if (commit_sha1 == 0 or parent_sha1 == 0 ):
693                         return
694
695                 fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
696                 self.buffer.set_text(unicode(fp.read(), encoding).encode('utf-8'))
697                 fp.close()
698                 self.commit_files(commit_sha1, parent_sha1)
699                 self.window.show()
700
701         def save_menu_response(self, widget, string):
702                 dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
703                                 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
704                                         gtk.STOCK_SAVE, gtk.RESPONSE_OK))
705                 dialog.set_default_response(gtk.RESPONSE_OK)
706                 response = dialog.run()
707                 if response == gtk.RESPONSE_OK:
708                         patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
709                                         self.buffer.get_end_iter())
710                         fp = open(dialog.get_filename(), "w")
711                         fp.write(patch_buffer)
712                         fp.close()
713                 dialog.destroy()
714
715 class GitView(object):
716         """ This is the main class
717         """
718         version = "0.9"
719
720         def __init__(self, with_diff=0):
721                 self.with_diff = with_diff
722                 self.window =   gtk.Window(gtk.WINDOW_TOPLEVEL)
723                 self.window.set_border_width(0)
724                 self.window.set_title("Git repository browser")
725
726                 self.get_encoding()
727                 self.get_bt_sha1()
728
729                 # Use three-quarters of the screen by default
730                 screen = self.window.get_screen()
731                 monitor = screen.get_monitor_geometry(0)
732                 width = int(monitor.width * 0.75)
733                 height = int(monitor.height * 0.75)
734                 self.window.set_default_size(width, height)
735
736                 # FIXME AndyFitz!
737                 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
738                 self.window.set_icon(icon)
739
740                 self.accel_group = gtk.AccelGroup()
741                 self.window.add_accel_group(self.accel_group)
742                 self.accel_group.connect_group(0xffc2, 0, gtk.ACCEL_LOCKED, self.refresh);
743                 self.accel_group.connect_group(0xffc1, 0, gtk.ACCEL_LOCKED, self.maximize);
744                 self.accel_group.connect_group(0xffc8, 0, gtk.ACCEL_LOCKED, self.fullscreen);
745                 self.accel_group.connect_group(0xffc9, 0, gtk.ACCEL_LOCKED, self.unfullscreen);
746
747                 self.window.add(self.construct())
748
749         def refresh(self, widget, event=None, *arguments, **keywords):
750                 self.get_encoding()
751                 self.get_bt_sha1()
752                 Commit.children_sha1 = {}
753                 self.set_branch(sys.argv[without_diff:])
754                 self.window.show()
755                 return True
756
757         def maximize(self, widget, event=None, *arguments, **keywords):
758                 self.window.maximize()
759                 return True
760
761         def fullscreen(self, widget, event=None, *arguments, **keywords):
762                 self.window.fullscreen()
763                 return True
764
765         def unfullscreen(self, widget, event=None, *arguments, **keywords):
766                 self.window.unfullscreen()
767                 return True
768
769         def get_bt_sha1(self):
770                 """ Update the bt_sha1 dictionary with the
771                 respective sha1 details """
772
773                 self.bt_sha1 = { }
774                 ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
775                 fp = os.popen('git ls-remote "${GIT_DIR-.git}"')
776                 while 1:
777                         line = string.strip(fp.readline())
778                         if line == '':
779                                 break
780                         m = ls_remote.match(line)
781                         if not m:
782                                 continue
783                         (sha1, name) = (m.group(1), m.group(2))
784                         if not self.bt_sha1.has_key(sha1):
785                                 self.bt_sha1[sha1] = []
786                         self.bt_sha1[sha1].append(name)
787                 fp.close()
788
789         def get_encoding(self):
790                 fp = os.popen("git config --get i18n.commitencoding")
791                 self.encoding=string.strip(fp.readline())
792                 fp.close()
793                 if (self.encoding == ""):
794                         self.encoding = "utf-8"
795
796
797         def construct(self):
798                 """Construct the window contents."""
799                 vbox = gtk.VBox()
800                 paned = gtk.VPaned()
801                 paned.pack1(self.construct_top(), resize=False, shrink=True)
802                 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
803                 menu_bar = gtk.MenuBar()
804                 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
805                 help_menu = gtk.MenuItem("Help")
806                 menu = gtk.Menu()
807                 about_menu = gtk.MenuItem("About")
808                 menu.append(about_menu)
809                 about_menu.connect("activate", self.about_menu_response, "about")
810                 about_menu.show()
811                 help_menu.set_submenu(menu)
812                 help_menu.show()
813                 menu_bar.append(help_menu)
814                 menu_bar.show()
815                 vbox.pack_start(menu_bar, expand=False, fill=True)
816                 vbox.pack_start(paned, expand=True, fill=True)
817                 paned.show()
818                 vbox.show()
819                 return vbox
820
821
822         def construct_top(self):
823                 """Construct the top-half of the window."""
824                 vbox = gtk.VBox(spacing=6)
825                 vbox.set_border_width(12)
826                 vbox.show()
827
828
829                 scrollwin = gtk.ScrolledWindow()
830                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
831                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
832                 vbox.pack_start(scrollwin, expand=True, fill=True)
833                 scrollwin.show()
834
835                 self.treeview = gtk.TreeView()
836                 self.treeview.set_rules_hint(True)
837                 self.treeview.set_search_column(4)
838                 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
839                 scrollwin.add(self.treeview)
840                 self.treeview.show()
841
842                 cell = CellRendererGraph()
843                 column = gtk.TreeViewColumn()
844                 column.set_resizable(True)
845                 column.pack_start(cell, expand=True)
846                 column.add_attribute(cell, "node", 1)
847                 column.add_attribute(cell, "in-lines", 2)
848                 column.add_attribute(cell, "out-lines", 3)
849                 self.treeview.append_column(column)
850
851                 cell = gtk.CellRendererText()
852                 cell.set_property("width-chars", 65)
853                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
854                 column = gtk.TreeViewColumn("Message")
855                 column.set_resizable(True)
856                 column.pack_start(cell, expand=True)
857                 column.add_attribute(cell, "text", 4)
858                 self.treeview.append_column(column)
859
860                 cell = gtk.CellRendererText()
861                 cell.set_property("width-chars", 40)
862                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
863                 column = gtk.TreeViewColumn("Author")
864                 column.set_resizable(True)
865                 column.pack_start(cell, expand=True)
866                 column.add_attribute(cell, "text", 5)
867                 self.treeview.append_column(column)
868
869                 cell = gtk.CellRendererText()
870                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
871                 column = gtk.TreeViewColumn("Date")
872                 column.set_resizable(True)
873                 column.pack_start(cell, expand=True)
874                 column.add_attribute(cell, "text", 6)
875                 self.treeview.append_column(column)
876
877                 return vbox
878
879         def about_menu_response(self, widget, string):
880                 dialog = gtk.AboutDialog()
881                 dialog.set_name("Gitview")
882                 dialog.set_version(GitView.version)
883                 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@gmail.com>"])
884                 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
885                 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
886                 dialog.set_wrap_license(True)
887                 dialog.run()
888                 dialog.destroy()
889
890
891         def construct_bottom(self):
892                 """Construct the bottom half of the window."""
893                 vbox = gtk.VBox(False, spacing=6)
894                 vbox.set_border_width(12)
895                 (width, height) = self.window.get_size()
896                 vbox.set_size_request(width, int(height / 2.5))
897                 vbox.show()
898
899                 self.table = gtk.Table(rows=4, columns=4)
900                 self.table.set_row_spacings(6)
901                 self.table.set_col_spacings(6)
902                 vbox.pack_start(self.table, expand=False, fill=True)
903                 self.table.show()
904
905                 align = gtk.Alignment(0.0, 0.5)
906                 label = gtk.Label()
907                 label.set_markup("<b>Revision:</b>")
908                 align.add(label)
909                 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
910                 label.show()
911                 align.show()
912
913                 align = gtk.Alignment(0.0, 0.5)
914                 self.revid_label = gtk.Label()
915                 self.revid_label.set_selectable(True)
916                 align.add(self.revid_label)
917                 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
918                 self.revid_label.show()
919                 align.show()
920
921                 align = gtk.Alignment(0.0, 0.5)
922                 label = gtk.Label()
923                 label.set_markup("<b>Committer:</b>")
924                 align.add(label)
925                 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
926                 label.show()
927                 align.show()
928
929                 align = gtk.Alignment(0.0, 0.5)
930                 self.committer_label = gtk.Label()
931                 self.committer_label.set_selectable(True)
932                 align.add(self.committer_label)
933                 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
934                 self.committer_label.show()
935                 align.show()
936
937                 align = gtk.Alignment(0.0, 0.5)
938                 label = gtk.Label()
939                 label.set_markup("<b>Timestamp:</b>")
940                 align.add(label)
941                 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
942                 label.show()
943                 align.show()
944
945                 align = gtk.Alignment(0.0, 0.5)
946                 self.timestamp_label = gtk.Label()
947                 self.timestamp_label.set_selectable(True)
948                 align.add(self.timestamp_label)
949                 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
950                 self.timestamp_label.show()
951                 align.show()
952
953                 align = gtk.Alignment(0.0, 0.5)
954                 label = gtk.Label()
955                 label.set_markup("<b>Parents:</b>")
956                 align.add(label)
957                 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
958                 label.show()
959                 align.show()
960                 self.parents_widgets = []
961
962                 align = gtk.Alignment(0.0, 0.5)
963                 label = gtk.Label()
964                 label.set_markup("<b>Children:</b>")
965                 align.add(label)
966                 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
967                 label.show()
968                 align.show()
969                 self.children_widgets = []
970
971                 scrollwin = gtk.ScrolledWindow()
972                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
973                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
974                 vbox.pack_start(scrollwin, expand=True, fill=True)
975                 scrollwin.show()
976
977                 (self.message_buffer, sourceview) = get_source_buffer_and_view()
978
979                 sourceview.set_editable(False)
980                 sourceview.modify_font(pango.FontDescription("Monospace"))
981                 scrollwin.add(sourceview)
982                 sourceview.show()
983
984                 return vbox
985
986         def _treeview_cursor_cb(self, *args):
987                 """Callback for when the treeview cursor changes."""
988                 (path, col) = self.treeview.get_cursor()
989                 commit = self.model[path][0]
990
991                 if commit.committer is not None:
992                         committer = commit.committer
993                         timestamp = commit.commit_date
994                         message   =  commit.get_message(self.with_diff)
995                         revid_label = commit.commit_sha1
996                 else:
997                         committer = ""
998                         timestamp = ""
999                         message = ""
1000                         revid_label = ""
1001
1002                 self.revid_label.set_text(revid_label)
1003                 self.committer_label.set_text(committer)
1004                 self.timestamp_label.set_text(timestamp)
1005                 self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
1006
1007                 for widget in self.parents_widgets:
1008                         self.table.remove(widget)
1009
1010                 self.parents_widgets = []
1011                 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
1012                 for idx, parent_id in enumerate(commit.parent_sha1):
1013                         self.table.set_row_spacing(idx + 3, 0)
1014
1015                         align = gtk.Alignment(0.0, 0.0)
1016                         self.parents_widgets.append(align)
1017                         self.table.attach(align, 1, 2, idx + 3, idx + 4,
1018                                         gtk.EXPAND | gtk.FILL, gtk.FILL)
1019                         align.show()
1020
1021                         hbox = gtk.HBox(False, 0)
1022                         align.add(hbox)
1023                         hbox.show()
1024
1025                         label = gtk.Label(parent_id)
1026                         label.set_selectable(True)
1027                         hbox.pack_start(label, expand=False, fill=True)
1028                         label.show()
1029
1030                         image = gtk.Image()
1031                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1032                         image.show()
1033
1034                         button = gtk.Button()
1035                         button.add(image)
1036                         button.set_relief(gtk.RELIEF_NONE)
1037                         button.connect("clicked", self._go_clicked_cb, parent_id)
1038                         hbox.pack_start(button, expand=False, fill=True)
1039                         button.show()
1040
1041                         image = gtk.Image()
1042                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1043                         image.show()
1044
1045                         button = gtk.Button()
1046                         button.add(image)
1047                         button.set_relief(gtk.RELIEF_NONE)
1048                         button.set_sensitive(True)
1049                         button.connect("clicked", self._show_clicked_cb,
1050                                         commit.commit_sha1, parent_id, self.encoding)
1051                         hbox.pack_start(button, expand=False, fill=True)
1052                         button.show()
1053
1054                 # Populate with child details
1055                 for widget in self.children_widgets:
1056                         self.table.remove(widget)
1057
1058                 self.children_widgets = []
1059                 try:
1060                         child_sha1 = Commit.children_sha1[commit.commit_sha1]
1061                 except KeyError:
1062                         # We don't have child
1063                         child_sha1 = [ 0 ]
1064
1065                 if ( len(child_sha1) > len(commit.parent_sha1)):
1066                         self.table.resize(4 + len(child_sha1) - 1, 4)
1067
1068                 for idx, child_id in enumerate(child_sha1):
1069                         self.table.set_row_spacing(idx + 3, 0)
1070
1071                         align = gtk.Alignment(0.0, 0.0)
1072                         self.children_widgets.append(align)
1073                         self.table.attach(align, 3, 4, idx + 3, idx + 4,
1074                                         gtk.EXPAND | gtk.FILL, gtk.FILL)
1075                         align.show()
1076
1077                         hbox = gtk.HBox(False, 0)
1078                         align.add(hbox)
1079                         hbox.show()
1080
1081                         label = gtk.Label(child_id)
1082                         label.set_selectable(True)
1083                         hbox.pack_start(label, expand=False, fill=True)
1084                         label.show()
1085
1086                         image = gtk.Image()
1087                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1088                         image.show()
1089
1090                         button = gtk.Button()
1091                         button.add(image)
1092                         button.set_relief(gtk.RELIEF_NONE)
1093                         button.connect("clicked", self._go_clicked_cb, child_id)
1094                         hbox.pack_start(button, expand=False, fill=True)
1095                         button.show()
1096
1097                         image = gtk.Image()
1098                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1099                         image.show()
1100
1101                         button = gtk.Button()
1102                         button.add(image)
1103                         button.set_relief(gtk.RELIEF_NONE)
1104                         button.set_sensitive(True)
1105                         button.connect("clicked", self._show_clicked_cb,
1106                                         child_id, commit.commit_sha1, self.encoding)
1107                         hbox.pack_start(button, expand=False, fill=True)
1108                         button.show()
1109
1110         def _destroy_cb(self, widget):
1111                 """Callback for when a window we manage is destroyed."""
1112                 self.quit()
1113
1114
1115         def quit(self):
1116                 """Stop the GTK+ main loop."""
1117                 gtk.main_quit()
1118
1119         def run(self, args):
1120                 self.set_branch(args)
1121                 self.window.connect("destroy", self._destroy_cb)
1122                 self.window.show()
1123                 gtk.main()
1124
1125         def set_branch(self, args):
1126                 """Fill in different windows with info from the reposiroty"""
1127                 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
1128                 git_rev_list_cmd = fp.read()
1129                 fp.close()
1130                 fp = os.popen("git rev-list  --header --topo-order --parents " + git_rev_list_cmd)
1131                 self.update_window(fp)
1132
1133         def update_window(self, fp):
1134                 commit_lines = []
1135
1136                 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
1137                                 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
1138
1139                 # used for cursor positioning
1140                 self.index = {}
1141
1142                 self.colours = {}
1143                 self.nodepos = {}
1144                 self.incomplete_line = {}
1145                 self.commits = []
1146
1147                 index = 0
1148                 last_colour = 0
1149                 last_nodepos = -1
1150                 out_line = []
1151                 input_line = fp.readline()
1152                 while (input_line != ""):
1153                         # The commit header ends with '\0'
1154                         # This NULL is immediately followed by the sha1 of the
1155                         # next commit
1156                         if (input_line[0] != '\0'):
1157                                 commit_lines.append(input_line)
1158                                 input_line = fp.readline()
1159                                 continue;
1160
1161                         commit = Commit(commit_lines)
1162                         if (commit != None ):
1163                                 self.commits.append(commit)
1164
1165                         # Skip the '\0
1166                         commit_lines = []
1167                         commit_lines.append(input_line[1:])
1168                         input_line = fp.readline()
1169
1170                 fp.close()
1171
1172                 for commit in self.commits:
1173                         (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
1174                                                                                 index, out_line,
1175                                                                                 last_colour,
1176                                                                                 last_nodepos)
1177                         self.index[commit.commit_sha1] = index
1178                         index += 1
1179
1180                 self.treeview.set_model(self.model)
1181                 self.treeview.show()
1182
1183         def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
1184                 in_line=[]
1185
1186                 #   |   -> outline
1187                 #   X
1188                 #   |\  <- inline
1189
1190                 # Reset nodepostion
1191                 if (last_nodepos > 5):
1192                         last_nodepos = -1
1193
1194                 # Add the incomplete lines of the last cell in this
1195                 try:
1196                         colour = self.colours[commit.commit_sha1]
1197                 except KeyError:
1198                         self.colours[commit.commit_sha1] = last_colour+1
1199                         last_colour = self.colours[commit.commit_sha1]
1200                         colour =   self.colours[commit.commit_sha1]
1201
1202                 try:
1203                         node_pos = self.nodepos[commit.commit_sha1]
1204                 except KeyError:
1205                         self.nodepos[commit.commit_sha1] = last_nodepos+1
1206                         last_nodepos = self.nodepos[commit.commit_sha1]
1207                         node_pos =  self.nodepos[commit.commit_sha1]
1208
1209                 #The first parent always continue on the same line
1210                 try:
1211                         # check we alreay have the value
1212                         tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
1213                 except KeyError:
1214                         self.colours[commit.parent_sha1[0]] = colour
1215                         self.nodepos[commit.parent_sha1[0]] = node_pos
1216
1217                 for sha1 in self.incomplete_line.keys():
1218                         if (sha1 != commit.commit_sha1):
1219                                 self.draw_incomplete_line(sha1, node_pos,
1220                                                 out_line, in_line, index)
1221                         else:
1222                                 del self.incomplete_line[sha1]
1223
1224
1225                 for parent_id in commit.parent_sha1:
1226                         try:
1227                                 tmp_node_pos = self.nodepos[parent_id]
1228                         except KeyError:
1229                                 self.colours[parent_id] = last_colour+1
1230                                 last_colour = self.colours[parent_id]
1231                                 self.nodepos[parent_id] = last_nodepos+1
1232                                 last_nodepos = self.nodepos[parent_id]
1233
1234                         in_line.append((node_pos, self.nodepos[parent_id],
1235                                                 self.colours[parent_id]))
1236                         self.add_incomplete_line(parent_id)
1237
1238                 try:
1239                         branch_tag = self.bt_sha1[commit.commit_sha1]
1240                 except KeyError:
1241                         branch_tag = [ ]
1242
1243
1244                 node = (node_pos, colour, branch_tag)
1245
1246                 self.model.append([commit, node, out_line, in_line,
1247                                 commit.message, commit.author, commit.date])
1248
1249                 return (in_line, last_colour, last_nodepos)
1250
1251         def add_incomplete_line(self, sha1):
1252                 try:
1253                         self.incomplete_line[sha1].append(self.nodepos[sha1])
1254                 except KeyError:
1255                         self.incomplete_line[sha1] = [self.nodepos[sha1]]
1256
1257         def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
1258                 for idx, pos in enumerate(self.incomplete_line[sha1]):
1259                         if(pos == node_pos):
1260                                 #remove the straight line and add a slash
1261                                 if ((pos, pos, self.colours[sha1]) in out_line):
1262                                         out_line.remove((pos, pos, self.colours[sha1]))
1263                                 out_line.append((pos, pos+0.5, self.colours[sha1]))
1264                                 self.incomplete_line[sha1][idx] = pos = pos+0.5
1265                         try:
1266                                 next_commit = self.commits[index+1]
1267                                 if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
1268                                 # join the line back to the node point
1269                                 # This need to be done only if we modified it
1270                                         in_line.append((pos, pos-0.5, self.colours[sha1]))
1271                                         continue;
1272                         except IndexError:
1273                                 pass
1274                         in_line.append((pos, pos, self.colours[sha1]))
1275
1276
1277         def _go_clicked_cb(self, widget, revid):
1278                 """Callback for when the go button for a parent is clicked."""
1279                 try:
1280                         self.treeview.set_cursor(self.index[revid])
1281                 except KeyError:
1282                         dialog = gtk.MessageDialog(parent=None, flags=0,
1283                                         type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
1284                                         message_format=None)
1285                         dialog.set_markup("Revision <b>%s</b> not present in the list" % revid)
1286                         # revid == 0 is the parent of the first commit
1287                         if (revid != 0 ):
1288                                 dialog.format_secondary_text("Try running gitview without any options")
1289                         dialog.run()
1290                         dialog.destroy()
1291
1292                 self.treeview.grab_focus()
1293
1294         def _show_clicked_cb(self, widget,  commit_sha1, parent_sha1, encoding):
1295                 """Callback for when the show button for a parent is clicked."""
1296                 window = DiffWindow()
1297                 window.set_diff(commit_sha1, parent_sha1, encoding)
1298                 self.treeview.grab_focus()
1299
1300 without_diff = 0
1301 if __name__ == "__main__":
1302
1303         if (len(sys.argv) > 1 ):
1304                 if (sys.argv[1] == "--without-diff"):
1305                         without_diff = 1
1306
1307         view = GitView( without_diff != 1)
1308         view.run(sys.argv[without_diff:])