Merge branch 'maint'
[git] / contrib / gitview / gitview
1 #! /usr/bin/env python
2
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
7
8 """ gitview
9 GUI browser for git repository
10 This program is based on bzrk by Scott James Remnant <scott@ubuntu.com>
11 """
12 __copyright__ = "Copyright (C) 2006 Hewlett-Packard Development Company, L.P."
13 __copyright__ = "Copyright (C) 2007 Aneesh Kumar K.V <aneesh.kumar@gmail.com"
14 __author__    = "Aneesh Kumar K.V <aneesh.kumar@gmail.com>"
15
16
17 import sys
18 import os
19 import gtk
20 import pygtk
21 import pango
22 import re
23 import time
24 import gobject
25 import cairo
26 import math
27 import string
28 import fcntl
29
30 try:
31     import 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
356                 # Use two thirds of the screen by default
357                 screen = self.window.get_screen()
358                 monitor = screen.get_monitor_geometry(0)
359                 width = int(monitor.width * 0.66)
360                 height = int(monitor.height * 0.66)
361                 self.window.set_default_size(width, height)
362
363         def add_file_data(self, filename, commit_sha1, line_num):
364                 fp = os.popen("git cat-file blob " + commit_sha1 +":"+filename)
365                 i = 1;
366                 for line in fp.readlines():
367                         line = string.rstrip(line)
368                         self.model.append(None, ["HEAD", filename, line, i])
369                         i = i+1
370                 fp.close()
371
372                 # now set the cursor position
373                 self.treeview.set_cursor(line_num-1)
374                 self.treeview.grab_focus()
375
376         def _treeview_cursor_cb(self, *args):
377                 """Callback for when the treeview cursor changes."""
378                 (path, col) = self.treeview.get_cursor()
379                 commit_sha1 = self.model[path][0]
380                 commit_msg = ""
381                 fp = os.popen("git cat-file commit " + commit_sha1)
382                 for line in fp.readlines():
383                         commit_msg =  commit_msg + line
384                 fp.close()
385
386                 self.commit_buffer.set_text(commit_msg)
387
388         def _treeview_row_activated(self, *args):
389                 """Callback for when the treeview row gets selected."""
390                 (path, col) = self.treeview.get_cursor()
391                 commit_sha1 = self.model[path][0]
392                 filename    = self.model[path][1]
393                 line_num    = self.model[path][3]
394
395                 window = AnnotateWindow();
396                 fp = os.popen("git rev-parse "+ commit_sha1 + "~1")
397                 commit_sha1 = string.strip(fp.readline())
398                 fp.close()
399                 window.annotate(filename, commit_sha1, line_num)
400
401         def data_ready(self, source, condition):
402                 while (1):
403                         try :
404                                 buffer = source.read(8192)
405                         except:
406                                 # resource temporary not available
407                                 return True
408
409                         if (len(buffer) == 0):
410                                 gobject.source_remove(self.io_watch_tag)
411                                 source.close()
412                                 return False
413
414                         for buff in buffer.split("\n"):
415                                 annotate_line = re.compile('^([0-9a-f]{40}) (.+) (.+) (.+)$')
416                                 m = annotate_line.match(buff)
417                                 if not m:
418                                         annotate_line = re.compile('^(filename) (.+)$')
419                                         m = annotate_line.match(buff)
420                                         if not m:
421                                                 continue
422                                         filename = m.group(2)
423                                 else:
424                                         self.commit_sha1 = m.group(1)
425                                         self.source_line = int(m.group(2))
426                                         self.result_line = int(m.group(3))
427                                         self.count          = int(m.group(4))
428                                         #set the details only when we have the file name
429                                         continue
430
431                                 while (self.count > 0):
432                                         # set at result_line + count-1 the sha1 as commit_sha1
433                                         self.count = self.count - 1
434                                         iter = self.model.iter_nth_child(None, self.result_line + self.count-1)
435                                         self.model.set(iter, 0, self.commit_sha1, 1, filename, 3, self.source_line)
436
437
438         def annotate(self, filename, commit_sha1, line_num):
439                 # verify the commit_sha1 specified has this filename
440
441                 fp = os.popen("git ls-tree "+ commit_sha1 + " -- " + filename)
442                 line = string.strip(fp.readline())
443                 if line == '':
444                         # pop up the message the file is not there as a part of the commit
445                         fp.close()
446                         dialog = gtk.MessageDialog(parent=None, flags=0,
447                                         type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
448                                         message_format=None)
449                         dialog.set_markup("The file %s is not present in the parent commit %s" % (filename, commit_sha1))
450                         dialog.run()
451                         dialog.destroy()
452                         return
453
454                 fp.close()
455
456                 vpan = gtk.VPaned();
457                 self.window.add(vpan);
458                 vpan.show()
459
460                 scrollwin = gtk.ScrolledWindow()
461                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
462                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
463                 vpan.pack1(scrollwin, True, True);
464                 scrollwin.show()
465
466                 self.model = gtk.TreeStore(str, str, str, int)
467                 self.treeview = gtk.TreeView(self.model)
468                 self.treeview.set_rules_hint(True)
469                 self.treeview.set_search_column(0)
470                 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
471                 self.treeview.connect("row-activated", self._treeview_row_activated)
472                 scrollwin.add(self.treeview)
473                 self.treeview.show()
474
475                 cell = gtk.CellRendererText()
476                 cell.set_property("width-chars", 10)
477                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
478                 column = gtk.TreeViewColumn("Commit")
479                 column.set_resizable(True)
480                 column.pack_start(cell, expand=True)
481                 column.add_attribute(cell, "text", 0)
482                 self.treeview.append_column(column)
483
484                 cell = gtk.CellRendererText()
485                 cell.set_property("width-chars", 20)
486                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
487                 column = gtk.TreeViewColumn("File Name")
488                 column.set_resizable(True)
489                 column.pack_start(cell, expand=True)
490                 column.add_attribute(cell, "text", 1)
491                 self.treeview.append_column(column)
492
493                 cell = gtk.CellRendererText()
494                 cell.set_property("width-chars", 20)
495                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
496                 column = gtk.TreeViewColumn("Data")
497                 column.set_resizable(True)
498                 column.pack_start(cell, expand=True)
499                 column.add_attribute(cell, "text", 2)
500                 self.treeview.append_column(column)
501
502                 # The commit message window
503                 scrollwin = gtk.ScrolledWindow()
504                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
505                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
506                 vpan.pack2(scrollwin, True, True);
507                 scrollwin.show()
508
509                 commit_text = gtk.TextView()
510                 self.commit_buffer = gtk.TextBuffer()
511                 commit_text.set_buffer(self.commit_buffer)
512                 scrollwin.add(commit_text)
513                 commit_text.show()
514
515                 self.window.show()
516
517                 self.add_file_data(filename, commit_sha1, line_num)
518
519                 fp = os.popen("git blame --incremental -- " + filename + " " + commit_sha1)
520                 flags = fcntl.fcntl(fp.fileno(), fcntl.F_GETFL)
521                 fcntl.fcntl(fp.fileno(), fcntl.F_SETFL, flags | os.O_NONBLOCK)
522                 self.io_watch_tag = gobject.io_add_watch(fp, gobject.IO_IN, self.data_ready)
523
524
525 class DiffWindow(object):
526         """Diff window.
527         This object represents and manages a single window containing the
528         differences between two revisions on a branch.
529         """
530
531         def __init__(self):
532                 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
533                 self.window.set_border_width(0)
534                 self.window.set_title("Git repository browser diff window")
535
536                 # Use two thirds of the screen by default
537                 screen = self.window.get_screen()
538                 monitor = screen.get_monitor_geometry(0)
539                 width = int(monitor.width * 0.66)
540                 height = int(monitor.height * 0.66)
541                 self.window.set_default_size(width, height)
542
543
544                 self.construct()
545
546         def construct(self):
547                 """Construct the window contents."""
548                 vbox = gtk.VBox()
549                 self.window.add(vbox)
550                 vbox.show()
551
552                 menu_bar = gtk.MenuBar()
553                 save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
554                 save_menu.connect("activate", self.save_menu_response, "save")
555                 save_menu.show()
556                 menu_bar.append(save_menu)
557                 vbox.pack_start(menu_bar, expand=False, fill=True)
558                 menu_bar.show()
559
560                 hpan = gtk.HPaned()
561
562                 scrollwin = gtk.ScrolledWindow()
563                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
564                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
565                 hpan.pack1(scrollwin, True, True)
566                 scrollwin.show()
567
568                 if have_gtksourceview:
569                         self.buffer = gtksourceview.SourceBuffer()
570                         slm = gtksourceview.SourceLanguagesManager()
571                         gsl = slm.get_language_from_mime_type("text/x-patch")
572                         self.buffer.set_highlight(True)
573                         self.buffer.set_language(gsl)
574                         sourceview = gtksourceview.SourceView(self.buffer)
575                 else:
576                         self.buffer = gtk.TextBuffer()
577                         sourceview = gtk.TextView(self.buffer)
578
579
580                 sourceview.set_editable(False)
581                 sourceview.modify_font(pango.FontDescription("Monospace"))
582                 scrollwin.add(sourceview)
583                 sourceview.show()
584
585                 # The file hierarchy: a scrollable treeview
586                 scrollwin = gtk.ScrolledWindow()
587                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
588                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
589                 scrollwin.set_size_request(20, -1)
590                 hpan.pack2(scrollwin, True, True)
591                 scrollwin.show()
592
593                 self.model = gtk.TreeStore(str, str, str)
594                 self.treeview = gtk.TreeView(self.model)
595                 self.treeview.set_search_column(1)
596                 self.treeview.connect("cursor-changed", self._treeview_clicked)
597                 scrollwin.add(self.treeview)
598                 self.treeview.show()
599
600                 cell = gtk.CellRendererText()
601                 cell.set_property("width-chars", 20)
602                 column = gtk.TreeViewColumn("Select to annotate")
603                 column.pack_start(cell, expand=True)
604                 column.add_attribute(cell, "text", 0)
605                 self.treeview.append_column(column)
606
607                 vbox.pack_start(hpan, expand=True, fill=True)
608                 hpan.show()
609
610         def _treeview_clicked(self, *args):
611                 """Callback for when the treeview cursor changes."""
612                 (path, col) = self.treeview.get_cursor()
613                 specific_file = self.model[path][1]
614                 commit_sha1 =  self.model[path][2]
615                 if specific_file ==  None :
616                         return
617                 elif specific_file ==  "" :
618                         specific_file =  None
619
620                 window = AnnotateWindow();
621                 window.annotate(specific_file, commit_sha1, 1)
622
623
624         def commit_files(self, commit_sha1, parent_sha1):
625                 self.model.clear()
626                 add  = self.model.append(None, [ "Added", None, None])
627                 dele = self.model.append(None, [ "Deleted", None, None])
628                 mod  = self.model.append(None, [ "Modified", None, None])
629                 diff_tree = re.compile('^(:.{6}) (.{6}) (.{40}) (.{40}) (A|D|M)\s(.+)$')
630                 fp = os.popen("git diff-tree -r --no-commit-id " + parent_sha1 + " " + commit_sha1)
631                 while 1:
632                         line = string.strip(fp.readline())
633                         if line == '':
634                                 break
635                         m = diff_tree.match(line)
636                         if not m:
637                                 continue
638
639                         attr = m.group(5)
640                         filename = m.group(6)
641                         if attr == "A":
642                                 self.model.append(add,  [filename, filename, commit_sha1])
643                         elif attr == "D":
644                                 self.model.append(dele, [filename, filename, commit_sha1])
645                         elif attr == "M":
646                                 self.model.append(mod,  [filename, filename, commit_sha1])
647                 fp.close()
648
649                 self.treeview.expand_all()
650
651         def set_diff(self, commit_sha1, parent_sha1, encoding):
652                 """Set the differences showed by this window.
653                 Compares the two trees and populates the window with the
654                 differences.
655                 """
656                 # Diff with the first commit or the last commit shows nothing
657                 if (commit_sha1 == 0 or parent_sha1 == 0 ):
658                         return
659
660                 fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
661                 self.buffer.set_text(unicode(fp.read(), encoding).encode('utf-8'))
662                 fp.close()
663                 self.commit_files(commit_sha1, parent_sha1)
664                 self.window.show()
665
666         def save_menu_response(self, widget, string):
667                 dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
668                                 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
669                                         gtk.STOCK_SAVE, gtk.RESPONSE_OK))
670                 dialog.set_default_response(gtk.RESPONSE_OK)
671                 response = dialog.run()
672                 if response == gtk.RESPONSE_OK:
673                         patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
674                                         self.buffer.get_end_iter())
675                         fp = open(dialog.get_filename(), "w")
676                         fp.write(patch_buffer)
677                         fp.close()
678                 dialog.destroy()
679
680 class GitView(object):
681         """ This is the main class
682         """
683         version = "0.9"
684
685         def __init__(self, with_diff=0):
686                 self.with_diff = with_diff
687                 self.window =   gtk.Window(gtk.WINDOW_TOPLEVEL)
688                 self.window.set_border_width(0)
689                 self.window.set_title("Git repository browser")
690
691                 self.get_encoding()
692                 self.get_bt_sha1()
693
694                 # Use three-quarters of the screen by default
695                 screen = self.window.get_screen()
696                 monitor = screen.get_monitor_geometry(0)
697                 width = int(monitor.width * 0.75)
698                 height = int(monitor.height * 0.75)
699                 self.window.set_default_size(width, height)
700
701                 # FIXME AndyFitz!
702                 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
703                 self.window.set_icon(icon)
704
705                 self.accel_group = gtk.AccelGroup()
706                 self.window.add_accel_group(self.accel_group)
707                 self.accel_group.connect_group(0xffc2, 0, gtk.ACCEL_LOCKED, self.refresh);
708                 self.accel_group.connect_group(0xffc1, 0, gtk.ACCEL_LOCKED, self.maximize);
709                 self.accel_group.connect_group(0xffc8, 0, gtk.ACCEL_LOCKED, self.fullscreen);
710                 self.accel_group.connect_group(0xffc9, 0, gtk.ACCEL_LOCKED, self.unfullscreen);
711
712                 self.window.add(self.construct())
713
714         def refresh(self, widget, event=None, *arguments, **keywords):
715                 self.get_encoding()
716                 self.get_bt_sha1()
717                 Commit.children_sha1 = {}
718                 self.set_branch(sys.argv[without_diff:])
719                 self.window.show()
720                 return True
721
722         def maximize(self, widget, event=None, *arguments, **keywords):
723                 self.window.maximize()
724                 return True
725
726         def fullscreen(self, widget, event=None, *arguments, **keywords):
727                 self.window.fullscreen()
728                 return True
729
730         def unfullscreen(self, widget, event=None, *arguments, **keywords):
731                 self.window.unfullscreen()
732                 return True
733
734         def get_bt_sha1(self):
735                 """ Update the bt_sha1 dictionary with the
736                 respective sha1 details """
737
738                 self.bt_sha1 = { }
739                 ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
740                 fp = os.popen('git ls-remote "${GIT_DIR-.git}"')
741                 while 1:
742                         line = string.strip(fp.readline())
743                         if line == '':
744                                 break
745                         m = ls_remote.match(line)
746                         if not m:
747                                 continue
748                         (sha1, name) = (m.group(1), m.group(2))
749                         if not self.bt_sha1.has_key(sha1):
750                                 self.bt_sha1[sha1] = []
751                         self.bt_sha1[sha1].append(name)
752                 fp.close()
753
754         def get_encoding(self):
755                 fp = os.popen("git config --get i18n.commitencoding")
756                 self.encoding=string.strip(fp.readline())
757                 fp.close()
758                 if (self.encoding == ""):
759                         self.encoding = "utf-8"
760
761
762         def construct(self):
763                 """Construct the window contents."""
764                 vbox = gtk.VBox()
765                 paned = gtk.VPaned()
766                 paned.pack1(self.construct_top(), resize=False, shrink=True)
767                 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
768                 menu_bar = gtk.MenuBar()
769                 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
770                 help_menu = gtk.MenuItem("Help")
771                 menu = gtk.Menu()
772                 about_menu = gtk.MenuItem("About")
773                 menu.append(about_menu)
774                 about_menu.connect("activate", self.about_menu_response, "about")
775                 about_menu.show()
776                 help_menu.set_submenu(menu)
777                 help_menu.show()
778                 menu_bar.append(help_menu)
779                 menu_bar.show()
780                 vbox.pack_start(menu_bar, expand=False, fill=True)
781                 vbox.pack_start(paned, expand=True, fill=True)
782                 paned.show()
783                 vbox.show()
784                 return vbox
785
786
787         def construct_top(self):
788                 """Construct the top-half of the window."""
789                 vbox = gtk.VBox(spacing=6)
790                 vbox.set_border_width(12)
791                 vbox.show()
792
793
794                 scrollwin = gtk.ScrolledWindow()
795                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
796                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
797                 vbox.pack_start(scrollwin, expand=True, fill=True)
798                 scrollwin.show()
799
800                 self.treeview = gtk.TreeView()
801                 self.treeview.set_rules_hint(True)
802                 self.treeview.set_search_column(4)
803                 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
804                 scrollwin.add(self.treeview)
805                 self.treeview.show()
806
807                 cell = CellRendererGraph()
808                 column = gtk.TreeViewColumn()
809                 column.set_resizable(True)
810                 column.pack_start(cell, expand=True)
811                 column.add_attribute(cell, "node", 1)
812                 column.add_attribute(cell, "in-lines", 2)
813                 column.add_attribute(cell, "out-lines", 3)
814                 self.treeview.append_column(column)
815
816                 cell = gtk.CellRendererText()
817                 cell.set_property("width-chars", 65)
818                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
819                 column = gtk.TreeViewColumn("Message")
820                 column.set_resizable(True)
821                 column.pack_start(cell, expand=True)
822                 column.add_attribute(cell, "text", 4)
823                 self.treeview.append_column(column)
824
825                 cell = gtk.CellRendererText()
826                 cell.set_property("width-chars", 40)
827                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
828                 column = gtk.TreeViewColumn("Author")
829                 column.set_resizable(True)
830                 column.pack_start(cell, expand=True)
831                 column.add_attribute(cell, "text", 5)
832                 self.treeview.append_column(column)
833
834                 cell = gtk.CellRendererText()
835                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
836                 column = gtk.TreeViewColumn("Date")
837                 column.set_resizable(True)
838                 column.pack_start(cell, expand=True)
839                 column.add_attribute(cell, "text", 6)
840                 self.treeview.append_column(column)
841
842                 return vbox
843
844         def about_menu_response(self, widget, string):
845                 dialog = gtk.AboutDialog()
846                 dialog.set_name("Gitview")
847                 dialog.set_version(GitView.version)
848                 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@gmail.com>"])
849                 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
850                 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
851                 dialog.set_wrap_license(True)
852                 dialog.run()
853                 dialog.destroy()
854
855
856         def construct_bottom(self):
857                 """Construct the bottom half of the window."""
858                 vbox = gtk.VBox(False, spacing=6)
859                 vbox.set_border_width(12)
860                 (width, height) = self.window.get_size()
861                 vbox.set_size_request(width, int(height / 2.5))
862                 vbox.show()
863
864                 self.table = gtk.Table(rows=4, columns=4)
865                 self.table.set_row_spacings(6)
866                 self.table.set_col_spacings(6)
867                 vbox.pack_start(self.table, expand=False, fill=True)
868                 self.table.show()
869
870                 align = gtk.Alignment(0.0, 0.5)
871                 label = gtk.Label()
872                 label.set_markup("<b>Revision:</b>")
873                 align.add(label)
874                 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
875                 label.show()
876                 align.show()
877
878                 align = gtk.Alignment(0.0, 0.5)
879                 self.revid_label = gtk.Label()
880                 self.revid_label.set_selectable(True)
881                 align.add(self.revid_label)
882                 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
883                 self.revid_label.show()
884                 align.show()
885
886                 align = gtk.Alignment(0.0, 0.5)
887                 label = gtk.Label()
888                 label.set_markup("<b>Committer:</b>")
889                 align.add(label)
890                 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
891                 label.show()
892                 align.show()
893
894                 align = gtk.Alignment(0.0, 0.5)
895                 self.committer_label = gtk.Label()
896                 self.committer_label.set_selectable(True)
897                 align.add(self.committer_label)
898                 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
899                 self.committer_label.show()
900                 align.show()
901
902                 align = gtk.Alignment(0.0, 0.5)
903                 label = gtk.Label()
904                 label.set_markup("<b>Timestamp:</b>")
905                 align.add(label)
906                 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
907                 label.show()
908                 align.show()
909
910                 align = gtk.Alignment(0.0, 0.5)
911                 self.timestamp_label = gtk.Label()
912                 self.timestamp_label.set_selectable(True)
913                 align.add(self.timestamp_label)
914                 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
915                 self.timestamp_label.show()
916                 align.show()
917
918                 align = gtk.Alignment(0.0, 0.5)
919                 label = gtk.Label()
920                 label.set_markup("<b>Parents:</b>")
921                 align.add(label)
922                 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
923                 label.show()
924                 align.show()
925                 self.parents_widgets = []
926
927                 align = gtk.Alignment(0.0, 0.5)
928                 label = gtk.Label()
929                 label.set_markup("<b>Children:</b>")
930                 align.add(label)
931                 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
932                 label.show()
933                 align.show()
934                 self.children_widgets = []
935
936                 scrollwin = gtk.ScrolledWindow()
937                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
938                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
939                 vbox.pack_start(scrollwin, expand=True, fill=True)
940                 scrollwin.show()
941
942                 if have_gtksourceview:
943                         self.message_buffer = gtksourceview.SourceBuffer()
944                         slm = gtksourceview.SourceLanguagesManager()
945                         gsl = slm.get_language_from_mime_type("text/x-patch")
946                         self.message_buffer.set_highlight(True)
947                         self.message_buffer.set_language(gsl)
948                         sourceview = gtksourceview.SourceView(self.message_buffer)
949                 else:
950                         self.message_buffer = gtk.TextBuffer()
951                         sourceview = gtk.TextView(self.message_buffer)
952
953                 sourceview.set_editable(False)
954                 sourceview.modify_font(pango.FontDescription("Monospace"))
955                 scrollwin.add(sourceview)
956                 sourceview.show()
957
958                 return vbox
959
960         def _treeview_cursor_cb(self, *args):
961                 """Callback for when the treeview cursor changes."""
962                 (path, col) = self.treeview.get_cursor()
963                 commit = self.model[path][0]
964
965                 if commit.committer is not None:
966                         committer = commit.committer
967                         timestamp = commit.commit_date
968                         message   =  commit.get_message(self.with_diff)
969                         revid_label = commit.commit_sha1
970                 else:
971                         committer = ""
972                         timestamp = ""
973                         message = ""
974                         revid_label = ""
975
976                 self.revid_label.set_text(revid_label)
977                 self.committer_label.set_text(committer)
978                 self.timestamp_label.set_text(timestamp)
979                 self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
980
981                 for widget in self.parents_widgets:
982                         self.table.remove(widget)
983
984                 self.parents_widgets = []
985                 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
986                 for idx, parent_id in enumerate(commit.parent_sha1):
987                         self.table.set_row_spacing(idx + 3, 0)
988
989                         align = gtk.Alignment(0.0, 0.0)
990                         self.parents_widgets.append(align)
991                         self.table.attach(align, 1, 2, idx + 3, idx + 4,
992                                         gtk.EXPAND | gtk.FILL, gtk.FILL)
993                         align.show()
994
995                         hbox = gtk.HBox(False, 0)
996                         align.add(hbox)
997                         hbox.show()
998
999                         label = gtk.Label(parent_id)
1000                         label.set_selectable(True)
1001                         hbox.pack_start(label, expand=False, fill=True)
1002                         label.show()
1003
1004                         image = gtk.Image()
1005                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1006                         image.show()
1007
1008                         button = gtk.Button()
1009                         button.add(image)
1010                         button.set_relief(gtk.RELIEF_NONE)
1011                         button.connect("clicked", self._go_clicked_cb, parent_id)
1012                         hbox.pack_start(button, expand=False, fill=True)
1013                         button.show()
1014
1015                         image = gtk.Image()
1016                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1017                         image.show()
1018
1019                         button = gtk.Button()
1020                         button.add(image)
1021                         button.set_relief(gtk.RELIEF_NONE)
1022                         button.set_sensitive(True)
1023                         button.connect("clicked", self._show_clicked_cb,
1024                                         commit.commit_sha1, parent_id, self.encoding)
1025                         hbox.pack_start(button, expand=False, fill=True)
1026                         button.show()
1027
1028                 # Populate with child details
1029                 for widget in self.children_widgets:
1030                         self.table.remove(widget)
1031
1032                 self.children_widgets = []
1033                 try:
1034                         child_sha1 = Commit.children_sha1[commit.commit_sha1]
1035                 except KeyError:
1036                         # We don't have child
1037                         child_sha1 = [ 0 ]
1038
1039                 if ( len(child_sha1) > len(commit.parent_sha1)):
1040                         self.table.resize(4 + len(child_sha1) - 1, 4)
1041
1042                 for idx, child_id in enumerate(child_sha1):
1043                         self.table.set_row_spacing(idx + 3, 0)
1044
1045                         align = gtk.Alignment(0.0, 0.0)
1046                         self.children_widgets.append(align)
1047                         self.table.attach(align, 3, 4, idx + 3, idx + 4,
1048                                         gtk.EXPAND | gtk.FILL, gtk.FILL)
1049                         align.show()
1050
1051                         hbox = gtk.HBox(False, 0)
1052                         align.add(hbox)
1053                         hbox.show()
1054
1055                         label = gtk.Label(child_id)
1056                         label.set_selectable(True)
1057                         hbox.pack_start(label, expand=False, fill=True)
1058                         label.show()
1059
1060                         image = gtk.Image()
1061                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1062                         image.show()
1063
1064                         button = gtk.Button()
1065                         button.add(image)
1066                         button.set_relief(gtk.RELIEF_NONE)
1067                         button.connect("clicked", self._go_clicked_cb, child_id)
1068                         hbox.pack_start(button, expand=False, fill=True)
1069                         button.show()
1070
1071                         image = gtk.Image()
1072                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1073                         image.show()
1074
1075                         button = gtk.Button()
1076                         button.add(image)
1077                         button.set_relief(gtk.RELIEF_NONE)
1078                         button.set_sensitive(True)
1079                         button.connect("clicked", self._show_clicked_cb,
1080                                         child_id, commit.commit_sha1, self.encoding)
1081                         hbox.pack_start(button, expand=False, fill=True)
1082                         button.show()
1083
1084         def _destroy_cb(self, widget):
1085                 """Callback for when a window we manage is destroyed."""
1086                 self.quit()
1087
1088
1089         def quit(self):
1090                 """Stop the GTK+ main loop."""
1091                 gtk.main_quit()
1092
1093         def run(self, args):
1094                 self.set_branch(args)
1095                 self.window.connect("destroy", self._destroy_cb)
1096                 self.window.show()
1097                 gtk.main()
1098
1099         def set_branch(self, args):
1100                 """Fill in different windows with info from the reposiroty"""
1101                 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
1102                 git_rev_list_cmd = fp.read()
1103                 fp.close()
1104                 fp = os.popen("git rev-list  --header --topo-order --parents " + git_rev_list_cmd)
1105                 self.update_window(fp)
1106
1107         def update_window(self, fp):
1108                 commit_lines = []
1109
1110                 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
1111                                 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
1112
1113                 # used for cursor positioning
1114                 self.index = {}
1115
1116                 self.colours = {}
1117                 self.nodepos = {}
1118                 self.incomplete_line = {}
1119                 self.commits = []
1120
1121                 index = 0
1122                 last_colour = 0
1123                 last_nodepos = -1
1124                 out_line = []
1125                 input_line = fp.readline()
1126                 while (input_line != ""):
1127                         # The commit header ends with '\0'
1128                         # This NULL is immediately followed by the sha1 of the
1129                         # next commit
1130                         if (input_line[0] != '\0'):
1131                                 commit_lines.append(input_line)
1132                                 input_line = fp.readline()
1133                                 continue;
1134
1135                         commit = Commit(commit_lines)
1136                         if (commit != None ):
1137                                 self.commits.append(commit)
1138
1139                         # Skip the '\0
1140                         commit_lines = []
1141                         commit_lines.append(input_line[1:])
1142                         input_line = fp.readline()
1143
1144                 fp.close()
1145
1146                 for commit in self.commits:
1147                         (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
1148                                                                                 index, out_line,
1149                                                                                 last_colour,
1150                                                                                 last_nodepos)
1151                         self.index[commit.commit_sha1] = index
1152                         index += 1
1153
1154                 self.treeview.set_model(self.model)
1155                 self.treeview.show()
1156
1157         def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
1158                 in_line=[]
1159
1160                 #   |   -> outline
1161                 #   X
1162                 #   |\  <- inline
1163
1164                 # Reset nodepostion
1165                 if (last_nodepos > 5):
1166                         last_nodepos = -1
1167
1168                 # Add the incomplete lines of the last cell in this
1169                 try:
1170                         colour = self.colours[commit.commit_sha1]
1171                 except KeyError:
1172                         self.colours[commit.commit_sha1] = last_colour+1
1173                         last_colour = self.colours[commit.commit_sha1]
1174                         colour =   self.colours[commit.commit_sha1]
1175
1176                 try:
1177                         node_pos = self.nodepos[commit.commit_sha1]
1178                 except KeyError:
1179                         self.nodepos[commit.commit_sha1] = last_nodepos+1
1180                         last_nodepos = self.nodepos[commit.commit_sha1]
1181                         node_pos =  self.nodepos[commit.commit_sha1]
1182
1183                 #The first parent always continue on the same line
1184                 try:
1185                         # check we alreay have the value
1186                         tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
1187                 except KeyError:
1188                         self.colours[commit.parent_sha1[0]] = colour
1189                         self.nodepos[commit.parent_sha1[0]] = node_pos
1190
1191                 for sha1 in self.incomplete_line.keys():
1192                         if (sha1 != commit.commit_sha1):
1193                                 self.draw_incomplete_line(sha1, node_pos,
1194                                                 out_line, in_line, index)
1195                         else:
1196                                 del self.incomplete_line[sha1]
1197
1198
1199                 for parent_id in commit.parent_sha1:
1200                         try:
1201                                 tmp_node_pos = self.nodepos[parent_id]
1202                         except KeyError:
1203                                 self.colours[parent_id] = last_colour+1
1204                                 last_colour = self.colours[parent_id]
1205                                 self.nodepos[parent_id] = last_nodepos+1
1206                                 last_nodepos = self.nodepos[parent_id]
1207
1208                         in_line.append((node_pos, self.nodepos[parent_id],
1209                                                 self.colours[parent_id]))
1210                         self.add_incomplete_line(parent_id)
1211
1212                 try:
1213                         branch_tag = self.bt_sha1[commit.commit_sha1]
1214                 except KeyError:
1215                         branch_tag = [ ]
1216
1217
1218                 node = (node_pos, colour, branch_tag)
1219
1220                 self.model.append([commit, node, out_line, in_line,
1221                                 commit.message, commit.author, commit.date])
1222
1223                 return (in_line, last_colour, last_nodepos)
1224
1225         def add_incomplete_line(self, sha1):
1226                 try:
1227                         self.incomplete_line[sha1].append(self.nodepos[sha1])
1228                 except KeyError:
1229                         self.incomplete_line[sha1] = [self.nodepos[sha1]]
1230
1231         def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
1232                 for idx, pos in enumerate(self.incomplete_line[sha1]):
1233                         if(pos == node_pos):
1234                                 #remove the straight line and add a slash
1235                                 if ((pos, pos, self.colours[sha1]) in out_line):
1236                                         out_line.remove((pos, pos, self.colours[sha1]))
1237                                 out_line.append((pos, pos+0.5, self.colours[sha1]))
1238                                 self.incomplete_line[sha1][idx] = pos = pos+0.5
1239                         try:
1240                                 next_commit = self.commits[index+1]
1241                                 if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
1242                                 # join the line back to the node point
1243                                 # This need to be done only if we modified it
1244                                         in_line.append((pos, pos-0.5, self.colours[sha1]))
1245                                         continue;
1246                         except IndexError:
1247                                 pass
1248                         in_line.append((pos, pos, self.colours[sha1]))
1249
1250
1251         def _go_clicked_cb(self, widget, revid):
1252                 """Callback for when the go button for a parent is clicked."""
1253                 try:
1254                         self.treeview.set_cursor(self.index[revid])
1255                 except KeyError:
1256                         dialog = gtk.MessageDialog(parent=None, flags=0,
1257                                         type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
1258                                         message_format=None)
1259                         dialog.set_markup("Revision <b>%s</b> not present in the list" % revid)
1260                         # revid == 0 is the parent of the first commit
1261                         if (revid != 0 ):
1262                                 dialog.format_secondary_text("Try running gitview without any options")
1263                         dialog.run()
1264                         dialog.destroy()
1265
1266                 self.treeview.grab_focus()
1267
1268         def _show_clicked_cb(self, widget,  commit_sha1, parent_sha1, encoding):
1269                 """Callback for when the show button for a parent is clicked."""
1270                 window = DiffWindow()
1271                 window.set_diff(commit_sha1, parent_sha1, encoding)
1272                 self.treeview.grab_focus()
1273
1274 without_diff = 0
1275 if __name__ == "__main__":
1276
1277         if (len(sys.argv) > 1 ):
1278                 if (sys.argv[1] == "--without-diff"):
1279                         without_diff = 1
1280
1281         view = GitView( without_diff != 1)
1282         view.run(sys.argv[without_diff:])