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