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