Code

War on whitespace
[git.git] / contrib / gitview / gitview
1 #! /usr/bin/env python
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.
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>"
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
30 try:
31     import gtksourceview
32     have_gtksourceview = True
33 except ImportError:
34     have_gtksourceview = False
35     print "Running without gtksourceview module"
37 re_ident = re.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
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
48         return str_arg
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
59         return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs))
62 class CellRendererGraph(gtk.GenericCellRenderer):
63         """Cell renderer for directed graph.
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.
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.
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         """
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         }
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
105         def box_size(self, widget):
106                 """Calculate box size based on widget's font.
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)
118                         ascent = pango.PIXELS(metrics.get_ascent())
119                         descent = pango.PIXELS(metrics.get_descent())
121                         self._box_size = ascent + descent + 6
122                         return self._box_size
124         def set_colour(self, ctx, colour, bg, fg):
125                 """Set the context source colour.
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                     ]
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
146                 ctx.set_source_rgb(red, green, blue)
148         def on_get_size(self, widget, cell_area):
149                 """Return the size we need for this cell.
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)
157                 cols = self.node[0]
158                 for start, end, colour in self.in_lines + self.out_lines:
159                         cols = int(max(cols, start, end))
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)
167                 width = box_size * (cols + 1 ) + names_len
168                 height = box_size
170                 # FIXME I have no idea how to use cell_area properly
171                 return (0, 0, width, height)
173         def on_render(self, window, widget, bg_area, cell_area, exp_area, flags):
174                 """Render an individual cell.
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.
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()
191                 box_size = self.box_size(widget)
193                 ctx.set_line_width(box_size / 8)
194                 ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
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)
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)
209                         ctx.line_to(cell_area.x + box_size * end + box_size / 2,
210                                         bg_area.y + bg_area.height / 2)
212                         self.set_colour(ctx, colour, 0.0, 0.65)
213                         ctx.stroke()
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)
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)
231                         ctx.line_to(cell_area.x + box_size * end + box_size / 2,
232                                         bg_area.y + bg_area.height / 2 + bg_area.height)
234                         self.set_colour(ctx, colour, 0.0, 0.65)
235                         ctx.stroke()
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)
244                 self.set_colour(ctx, colour, 0.0, 0.5)
245                 ctx.stroke_preserve()
247                 self.set_colour(ctx, colour, 0.5, 1.0)
248                 ctx.fill_preserve()
250                 if (len(names) != 0):
251                         name = " "
252                         for item in names:
253                                 name = name + item + " "
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)
262 class Commit:
263         """ This represent a commit object obtained after parsing the git-rev-list
264         output """
266         children_sha1 = {}
268         def __init__(self, commit_lines):
269                 self.message            = ""
270                 self.author             = ""
271                 self.date               = ""
272                 self.committer          = ""
273                 self.commit_date        = ""
274                 self.commit_sha1        = ""
275                 self.parent_sha1        = [ ]
276                 self.parse_commit(commit_lines)
279         def parse_commit(self, commit_lines):
281                 # First line is the sha1 lines
282                 line = string.strip(commit_lines[0])
283                 sha1 = re.split(" ", line)
284                 self.commit_sha1 = sha1[0]
285                 self.parent_sha1 = sha1[1:]
287                 #build the child list
288                 for parent_id in self.parent_sha1:
289                         try:
290                                 Commit.children_sha1[parent_id].append(self.commit_sha1)
291                         except KeyError:
292                                 Commit.children_sha1[parent_id] = [self.commit_sha1]
294                 # IF we don't have parent
295                 if (len(self.parent_sha1) == 0):
296                         self.parent_sha1 = [0]
298                 for line in commit_lines[1:]:
299                         m = re.match("^ ", line)
300                         if (m != None):
301                                 # First line of the commit message used for short log
302                                 if self.message == "":
303                                         self.message = string.strip(line)
304                                 continue
306                         m = re.match("tree", line)
307                         if (m != None):
308                                 continue
310                         m = re.match("parent", line)
311                         if (m != None):
312                                 continue
314                         m = re_ident.match(line)
315                         if (m != None):
316                                 date = show_date(m.group('epoch'), m.group('tz'))
317                                 if m.group(1) == "author":
318                                         self.author = m.group('ident')
319                                         self.date = date
320                                 elif m.group(1) == "committer":
321                                         self.committer = m.group('ident')
322                                         self.commit_date = date
324                                 continue
326         def get_message(self, with_diff=0):
327                 if (with_diff == 1):
328                         message = self.diff_tree()
329                 else:
330                         fp = os.popen("git cat-file commit " + self.commit_sha1)
331                         message = fp.read()
332                         fp.close()
334                 return message
336         def diff_tree(self):
337                 fp = os.popen("git diff-tree --pretty --cc  -v -p --always " +  self.commit_sha1)
338                 diff = fp.read()
339                 fp.close()
340                 return diff
342 class AnnotateWindow:
343         """Annotate window.
344         This object represents and manages a single window containing the
345         annotate information of the file
346         """
348         def __init__(self):
349                 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
350                 self.window.set_border_width(0)
351                 self.window.set_title("Git repository browser annotation window")
353                 # Use two thirds of the screen by default
354                 screen = self.window.get_screen()
355                 monitor = screen.get_monitor_geometry(0)
356                 width = int(monitor.width * 0.66)
357                 height = int(monitor.height * 0.66)
358                 self.window.set_default_size(width, height)
360         def add_file_data(self, filename, commit_sha1, line_num):
361                 fp = os.popen("git cat-file blob " + commit_sha1 +":"+filename)
362                 i = 1;
363                 for line in fp.readlines():
364                         line = string.rstrip(line)
365                         self.model.append(None, ["HEAD", filename, line, i])
366                         i = i+1
367                 fp.close()
369                 # now set the cursor position
370                 self.treeview.set_cursor(line_num-1)
371                 self.treeview.grab_focus()
373         def _treeview_cursor_cb(self, *args):
374                 """Callback for when the treeview cursor changes."""
375                 (path, col) = self.treeview.get_cursor()
376                 commit_sha1 = self.model[path][0]
377                 commit_msg = ""
378                 fp = os.popen("git cat-file commit " + commit_sha1)
379                 for line in fp.readlines():
380                         commit_msg =  commit_msg + line
381                 fp.close()
383                 self.commit_buffer.set_text(commit_msg)
385         def _treeview_row_activated(self, *args):
386                 """Callback for when the treeview row gets selected."""
387                 (path, col) = self.treeview.get_cursor()
388                 commit_sha1 = self.model[path][0]
389                 filename    = self.model[path][1]
390                 line_num    = self.model[path][3]
392                 window = AnnotateWindow();
393                 fp = os.popen("git rev-parse "+ commit_sha1 + "~1")
394                 commit_sha1 = string.strip(fp.readline())
395                 fp.close()
396                 window.annotate(filename, commit_sha1, line_num)
398         def data_ready(self, source, condition):
399                 while (1):
400                         try :
401                                 buffer = source.read(8192)
402                         except:
403                                 # resource temporary not available
404                                 return True
406                         if (len(buffer) == 0):
407                                 gobject.source_remove(self.io_watch_tag)
408                                 source.close()
409                                 return False
411                         for buff in buffer.split("\n"):
412                                 annotate_line = re.compile('^([0-9a-f]{40}) (.+) (.+) (.+)$')
413                                 m = annotate_line.match(buff)
414                                 if not m:
415                                         annotate_line = re.compile('^(filename) (.+)$')
416                                         m = annotate_line.match(buff)
417                                         if not m:
418                                                 continue
419                                         filename = m.group(2)
420                                 else:
421                                         self.commit_sha1 = m.group(1)
422                                         self.source_line = int(m.group(2))
423                                         self.result_line = int(m.group(3))
424                                         self.count          = int(m.group(4))
425                                         #set the details only when we have the file name
426                                         continue
428                                 while (self.count > 0):
429                                         # set at result_line + count-1 the sha1 as commit_sha1
430                                         self.count = self.count - 1
431                                         iter = self.model.iter_nth_child(None, self.result_line + self.count-1)
432                                         self.model.set(iter, 0, self.commit_sha1, 1, filename, 3, self.source_line)
435         def annotate(self, filename, commit_sha1, line_num):
436                 # verify the commit_sha1 specified has this filename
438                 fp = os.popen("git ls-tree "+ commit_sha1 + " -- " + filename)
439                 line = string.strip(fp.readline())
440                 if line == '':
441                         # pop up the message the file is not there as a part of the commit
442                         fp.close()
443                         dialog = gtk.MessageDialog(parent=None, flags=0,
444                                         type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
445                                         message_format=None)
446                         dialog.set_markup("The file %s is not present in the parent commit %s" % (filename, commit_sha1))
447                         dialog.run()
448                         dialog.destroy()
449                         return
451                 fp.close()
453                 vpan = gtk.VPaned();
454                 self.window.add(vpan);
455                 vpan.show()
457                 scrollwin = gtk.ScrolledWindow()
458                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
459                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
460                 vpan.pack1(scrollwin, True, True);
461                 scrollwin.show()
463                 self.model = gtk.TreeStore(str, str, str, int)
464                 self.treeview = gtk.TreeView(self.model)
465                 self.treeview.set_rules_hint(True)
466                 self.treeview.set_search_column(0)
467                 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
468                 self.treeview.connect("row-activated", self._treeview_row_activated)
469                 scrollwin.add(self.treeview)
470                 self.treeview.show()
472                 cell = gtk.CellRendererText()
473                 cell.set_property("width-chars", 10)
474                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
475                 column = gtk.TreeViewColumn("Commit")
476                 column.set_resizable(True)
477                 column.pack_start(cell, expand=True)
478                 column.add_attribute(cell, "text", 0)
479                 self.treeview.append_column(column)
481                 cell = gtk.CellRendererText()
482                 cell.set_property("width-chars", 20)
483                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
484                 column = gtk.TreeViewColumn("File Name")
485                 column.set_resizable(True)
486                 column.pack_start(cell, expand=True)
487                 column.add_attribute(cell, "text", 1)
488                 self.treeview.append_column(column)
490                 cell = gtk.CellRendererText()
491                 cell.set_property("width-chars", 20)
492                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
493                 column = gtk.TreeViewColumn("Data")
494                 column.set_resizable(True)
495                 column.pack_start(cell, expand=True)
496                 column.add_attribute(cell, "text", 2)
497                 self.treeview.append_column(column)
499                 # The commit message window
500                 scrollwin = gtk.ScrolledWindow()
501                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
502                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
503                 vpan.pack2(scrollwin, True, True);
504                 scrollwin.show()
506                 commit_text = gtk.TextView()
507                 self.commit_buffer = gtk.TextBuffer()
508                 commit_text.set_buffer(self.commit_buffer)
509                 scrollwin.add(commit_text)
510                 commit_text.show()
512                 self.window.show()
514                 self.add_file_data(filename, commit_sha1, line_num)
516                 fp = os.popen("git blame --incremental -- " + filename + " " + commit_sha1)
517                 flags = fcntl.fcntl(fp.fileno(), fcntl.F_GETFL)
518                 fcntl.fcntl(fp.fileno(), fcntl.F_SETFL, flags | os.O_NONBLOCK)
519                 self.io_watch_tag = gobject.io_add_watch(fp, gobject.IO_IN, self.data_ready)
522 class DiffWindow:
523         """Diff window.
524         This object represents and manages a single window containing the
525         differences between two revisions on a branch.
526         """
528         def __init__(self):
529                 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
530                 self.window.set_border_width(0)
531                 self.window.set_title("Git repository browser diff window")
533                 # Use two thirds of the screen by default
534                 screen = self.window.get_screen()
535                 monitor = screen.get_monitor_geometry(0)
536                 width = int(monitor.width * 0.66)
537                 height = int(monitor.height * 0.66)
538                 self.window.set_default_size(width, height)
541                 self.construct()
543         def construct(self):
544                 """Construct the window contents."""
545                 vbox = gtk.VBox()
546                 self.window.add(vbox)
547                 vbox.show()
549                 menu_bar = gtk.MenuBar()
550                 save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
551                 save_menu.connect("activate", self.save_menu_response, "save")
552                 save_menu.show()
553                 menu_bar.append(save_menu)
554                 vbox.pack_start(menu_bar, expand=False, fill=True)
555                 menu_bar.show()
557                 hpan = gtk.HPaned()
559                 scrollwin = gtk.ScrolledWindow()
560                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
561                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
562                 hpan.pack1(scrollwin, True, True)
563                 scrollwin.show()
565                 if have_gtksourceview:
566                         self.buffer = gtksourceview.SourceBuffer()
567                         slm = gtksourceview.SourceLanguagesManager()
568                         gsl = slm.get_language_from_mime_type("text/x-patch")
569                         self.buffer.set_highlight(True)
570                         self.buffer.set_language(gsl)
571                         sourceview = gtksourceview.SourceView(self.buffer)
572                 else:
573                         self.buffer = gtk.TextBuffer()
574                         sourceview = gtk.TextView(self.buffer)
577                 sourceview.set_editable(False)
578                 sourceview.modify_font(pango.FontDescription("Monospace"))
579                 scrollwin.add(sourceview)
580                 sourceview.show()
582                 # The file hierarchy: a scrollable treeview
583                 scrollwin = gtk.ScrolledWindow()
584                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
585                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
586                 scrollwin.set_size_request(20, -1)
587                 hpan.pack2(scrollwin, True, True)
588                 scrollwin.show()
590                 self.model = gtk.TreeStore(str, str, str)
591                 self.treeview = gtk.TreeView(self.model)
592                 self.treeview.set_search_column(1)
593                 self.treeview.connect("cursor-changed", self._treeview_clicked)
594                 scrollwin.add(self.treeview)
595                 self.treeview.show()
597                 cell = gtk.CellRendererText()
598                 cell.set_property("width-chars", 20)
599                 column = gtk.TreeViewColumn("Select to annotate")
600                 column.pack_start(cell, expand=True)
601                 column.add_attribute(cell, "text", 0)
602                 self.treeview.append_column(column)
604                 vbox.pack_start(hpan, expand=True, fill=True)
605                 hpan.show()
607         def _treeview_clicked(self, *args):
608                 """Callback for when the treeview cursor changes."""
609                 (path, col) = self.treeview.get_cursor()
610                 specific_file = self.model[path][1]
611                 commit_sha1 =  self.model[path][2]
612                 if specific_file ==  None :
613                         return
614                 elif specific_file ==  "" :
615                         specific_file =  None
617                 window = AnnotateWindow();
618                 window.annotate(specific_file, commit_sha1, 1)
621         def commit_files(self, commit_sha1, parent_sha1):
622                 self.model.clear()
623                 add  = self.model.append(None, [ "Added", None, None])
624                 dele = self.model.append(None, [ "Deleted", None, None])
625                 mod  = self.model.append(None, [ "Modified", None, None])
626                 diff_tree = re.compile('^(:.{6}) (.{6}) (.{40}) (.{40}) (A|D|M)\s(.+)$')
627                 fp = os.popen("git diff-tree -r --no-commit-id " + parent_sha1 + " " + commit_sha1)
628                 while 1:
629                         line = string.strip(fp.readline())
630                         if line == '':
631                                 break
632                         m = diff_tree.match(line)
633                         if not m:
634                                 continue
636                         attr = m.group(5)
637                         filename = m.group(6)
638                         if attr == "A":
639                                 self.model.append(add,  [filename, filename, commit_sha1])
640                         elif attr == "D":
641                                 self.model.append(dele, [filename, filename, commit_sha1])
642                         elif attr == "M":
643                                 self.model.append(mod,  [filename, filename, commit_sha1])
644                 fp.close()
646                 self.treeview.expand_all()
648         def set_diff(self, commit_sha1, parent_sha1, encoding):
649                 """Set the differences showed by this window.
650                 Compares the two trees and populates the window with the
651                 differences.
652                 """
653                 # Diff with the first commit or the last commit shows nothing
654                 if (commit_sha1 == 0 or parent_sha1 == 0 ):
655                         return
657                 fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
658                 self.buffer.set_text(unicode(fp.read(), encoding).encode('utf-8'))
659                 fp.close()
660                 self.commit_files(commit_sha1, parent_sha1)
661                 self.window.show()
663         def save_menu_response(self, widget, string):
664                 dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
665                                 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
666                                         gtk.STOCK_SAVE, gtk.RESPONSE_OK))
667                 dialog.set_default_response(gtk.RESPONSE_OK)
668                 response = dialog.run()
669                 if response == gtk.RESPONSE_OK:
670                         patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
671                                         self.buffer.get_end_iter())
672                         fp = open(dialog.get_filename(), "w")
673                         fp.write(patch_buffer)
674                         fp.close()
675                 dialog.destroy()
677 class GitView:
678         """ This is the main class
679         """
680         version = "0.9"
682         def __init__(self, with_diff=0):
683                 self.with_diff = with_diff
684                 self.window =   gtk.Window(gtk.WINDOW_TOPLEVEL)
685                 self.window.set_border_width(0)
686                 self.window.set_title("Git repository browser")
688                 self.get_encoding()
689                 self.get_bt_sha1()
691                 # Use three-quarters of the screen by default
692                 screen = self.window.get_screen()
693                 monitor = screen.get_monitor_geometry(0)
694                 width = int(monitor.width * 0.75)
695                 height = int(monitor.height * 0.75)
696                 self.window.set_default_size(width, height)
698                 # FIXME AndyFitz!
699                 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
700                 self.window.set_icon(icon)
702                 self.accel_group = gtk.AccelGroup()
703                 self.window.add_accel_group(self.accel_group)
704                 self.accel_group.connect_group(0xffc2, 0, gtk.ACCEL_LOCKED, self.refresh);
705                 self.accel_group.connect_group(0xffc1, 0, gtk.ACCEL_LOCKED, self.maximize);
706                 self.accel_group.connect_group(0xffc8, 0, gtk.ACCEL_LOCKED, self.fullscreen);
707                 self.accel_group.connect_group(0xffc9, 0, gtk.ACCEL_LOCKED, self.unfullscreen);
709                 self.window.add(self.construct())
711         def refresh(self, widget, event=None, *arguments, **keywords):
712                 self.get_encoding()
713                 self.get_bt_sha1()
714                 Commit.children_sha1 = {}
715                 self.set_branch(sys.argv[without_diff:])
716                 self.window.show()
717                 return True
719         def maximize(self, widget, event=None, *arguments, **keywords):
720                 self.window.maximize()
721                 return True
723         def fullscreen(self, widget, event=None, *arguments, **keywords):
724                 self.window.fullscreen()
725                 return True
727         def unfullscreen(self, widget, event=None, *arguments, **keywords):
728                 self.window.unfullscreen()
729                 return True
731         def get_bt_sha1(self):
732                 """ Update the bt_sha1 dictionary with the
733                 respective sha1 details """
735                 self.bt_sha1 = { }
736                 ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
737                 fp = os.popen('git ls-remote "${GIT_DIR-.git}"')
738                 while 1:
739                         line = string.strip(fp.readline())
740                         if line == '':
741                                 break
742                         m = ls_remote.match(line)
743                         if not m:
744                                 continue
745                         (sha1, name) = (m.group(1), m.group(2))
746                         if not self.bt_sha1.has_key(sha1):
747                                 self.bt_sha1[sha1] = []
748                         self.bt_sha1[sha1].append(name)
749                 fp.close()
751         def get_encoding(self):
752                 fp = os.popen("git config --get i18n.commitencoding")
753                 self.encoding=string.strip(fp.readline())
754                 fp.close()
755                 if (self.encoding == ""):
756                         self.encoding = "utf-8"
759         def construct(self):
760                 """Construct the window contents."""
761                 vbox = gtk.VBox()
762                 paned = gtk.VPaned()
763                 paned.pack1(self.construct_top(), resize=False, shrink=True)
764                 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
765                 menu_bar = gtk.MenuBar()
766                 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
767                 help_menu = gtk.MenuItem("Help")
768                 menu = gtk.Menu()
769                 about_menu = gtk.MenuItem("About")
770                 menu.append(about_menu)
771                 about_menu.connect("activate", self.about_menu_response, "about")
772                 about_menu.show()
773                 help_menu.set_submenu(menu)
774                 help_menu.show()
775                 menu_bar.append(help_menu)
776                 menu_bar.show()
777                 vbox.pack_start(menu_bar, expand=False, fill=True)
778                 vbox.pack_start(paned, expand=True, fill=True)
779                 paned.show()
780                 vbox.show()
781                 return vbox
784         def construct_top(self):
785                 """Construct the top-half of the window."""
786                 vbox = gtk.VBox(spacing=6)
787                 vbox.set_border_width(12)
788                 vbox.show()
791                 scrollwin = gtk.ScrolledWindow()
792                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
793                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
794                 vbox.pack_start(scrollwin, expand=True, fill=True)
795                 scrollwin.show()
797                 self.treeview = gtk.TreeView()
798                 self.treeview.set_rules_hint(True)
799                 self.treeview.set_search_column(4)
800                 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
801                 scrollwin.add(self.treeview)
802                 self.treeview.show()
804                 cell = CellRendererGraph()
805                 column = gtk.TreeViewColumn()
806                 column.set_resizable(True)
807                 column.pack_start(cell, expand=True)
808                 column.add_attribute(cell, "node", 1)
809                 column.add_attribute(cell, "in-lines", 2)
810                 column.add_attribute(cell, "out-lines", 3)
811                 self.treeview.append_column(column)
813                 cell = gtk.CellRendererText()
814                 cell.set_property("width-chars", 65)
815                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
816                 column = gtk.TreeViewColumn("Message")
817                 column.set_resizable(True)
818                 column.pack_start(cell, expand=True)
819                 column.add_attribute(cell, "text", 4)
820                 self.treeview.append_column(column)
822                 cell = gtk.CellRendererText()
823                 cell.set_property("width-chars", 40)
824                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
825                 column = gtk.TreeViewColumn("Author")
826                 column.set_resizable(True)
827                 column.pack_start(cell, expand=True)
828                 column.add_attribute(cell, "text", 5)
829                 self.treeview.append_column(column)
831                 cell = gtk.CellRendererText()
832                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
833                 column = gtk.TreeViewColumn("Date")
834                 column.set_resizable(True)
835                 column.pack_start(cell, expand=True)
836                 column.add_attribute(cell, "text", 6)
837                 self.treeview.append_column(column)
839                 return vbox
841         def about_menu_response(self, widget, string):
842                 dialog = gtk.AboutDialog()
843                 dialog.set_name("Gitview")
844                 dialog.set_version(GitView.version)
845                 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@gmail.com>"])
846                 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
847                 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
848                 dialog.set_wrap_license(True)
849                 dialog.run()
850                 dialog.destroy()
853         def construct_bottom(self):
854                 """Construct the bottom half of the window."""
855                 vbox = gtk.VBox(False, spacing=6)
856                 vbox.set_border_width(12)
857                 (width, height) = self.window.get_size()
858                 vbox.set_size_request(width, int(height / 2.5))
859                 vbox.show()
861                 self.table = gtk.Table(rows=4, columns=4)
862                 self.table.set_row_spacings(6)
863                 self.table.set_col_spacings(6)
864                 vbox.pack_start(self.table, expand=False, fill=True)
865                 self.table.show()
867                 align = gtk.Alignment(0.0, 0.5)
868                 label = gtk.Label()
869                 label.set_markup("<b>Revision:</b>")
870                 align.add(label)
871                 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
872                 label.show()
873                 align.show()
875                 align = gtk.Alignment(0.0, 0.5)
876                 self.revid_label = gtk.Label()
877                 self.revid_label.set_selectable(True)
878                 align.add(self.revid_label)
879                 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
880                 self.revid_label.show()
881                 align.show()
883                 align = gtk.Alignment(0.0, 0.5)
884                 label = gtk.Label()
885                 label.set_markup("<b>Committer:</b>")
886                 align.add(label)
887                 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
888                 label.show()
889                 align.show()
891                 align = gtk.Alignment(0.0, 0.5)
892                 self.committer_label = gtk.Label()
893                 self.committer_label.set_selectable(True)
894                 align.add(self.committer_label)
895                 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
896                 self.committer_label.show()
897                 align.show()
899                 align = gtk.Alignment(0.0, 0.5)
900                 label = gtk.Label()
901                 label.set_markup("<b>Timestamp:</b>")
902                 align.add(label)
903                 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
904                 label.show()
905                 align.show()
907                 align = gtk.Alignment(0.0, 0.5)
908                 self.timestamp_label = gtk.Label()
909                 self.timestamp_label.set_selectable(True)
910                 align.add(self.timestamp_label)
911                 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
912                 self.timestamp_label.show()
913                 align.show()
915                 align = gtk.Alignment(0.0, 0.5)
916                 label = gtk.Label()
917                 label.set_markup("<b>Parents:</b>")
918                 align.add(label)
919                 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
920                 label.show()
921                 align.show()
922                 self.parents_widgets = []
924                 align = gtk.Alignment(0.0, 0.5)
925                 label = gtk.Label()
926                 label.set_markup("<b>Children:</b>")
927                 align.add(label)
928                 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
929                 label.show()
930                 align.show()
931                 self.children_widgets = []
933                 scrollwin = gtk.ScrolledWindow()
934                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
935                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
936                 vbox.pack_start(scrollwin, expand=True, fill=True)
937                 scrollwin.show()
939                 if have_gtksourceview:
940                         self.message_buffer = gtksourceview.SourceBuffer()
941                         slm = gtksourceview.SourceLanguagesManager()
942                         gsl = slm.get_language_from_mime_type("text/x-patch")
943                         self.message_buffer.set_highlight(True)
944                         self.message_buffer.set_language(gsl)
945                         sourceview = gtksourceview.SourceView(self.message_buffer)
946                 else:
947                         self.message_buffer = gtk.TextBuffer()
948                         sourceview = gtk.TextView(self.message_buffer)
950                 sourceview.set_editable(False)
951                 sourceview.modify_font(pango.FontDescription("Monospace"))
952                 scrollwin.add(sourceview)
953                 sourceview.show()
955                 return vbox
957         def _treeview_cursor_cb(self, *args):
958                 """Callback for when the treeview cursor changes."""
959                 (path, col) = self.treeview.get_cursor()
960                 commit = self.model[path][0]
962                 if commit.committer is not None:
963                         committer = commit.committer
964                         timestamp = commit.commit_date
965                         message   =  commit.get_message(self.with_diff)
966                         revid_label = commit.commit_sha1
967                 else:
968                         committer = ""
969                         timestamp = ""
970                         message = ""
971                         revid_label = ""
973                 self.revid_label.set_text(revid_label)
974                 self.committer_label.set_text(committer)
975                 self.timestamp_label.set_text(timestamp)
976                 self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
978                 for widget in self.parents_widgets:
979                         self.table.remove(widget)
981                 self.parents_widgets = []
982                 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
983                 for idx, parent_id in enumerate(commit.parent_sha1):
984                         self.table.set_row_spacing(idx + 3, 0)
986                         align = gtk.Alignment(0.0, 0.0)
987                         self.parents_widgets.append(align)
988                         self.table.attach(align, 1, 2, idx + 3, idx + 4,
989                                         gtk.EXPAND | gtk.FILL, gtk.FILL)
990                         align.show()
992                         hbox = gtk.HBox(False, 0)
993                         align.add(hbox)
994                         hbox.show()
996                         label = gtk.Label(parent_id)
997                         label.set_selectable(True)
998                         hbox.pack_start(label, expand=False, fill=True)
999                         label.show()
1001                         image = gtk.Image()
1002                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1003                         image.show()
1005                         button = gtk.Button()
1006                         button.add(image)
1007                         button.set_relief(gtk.RELIEF_NONE)
1008                         button.connect("clicked", self._go_clicked_cb, parent_id)
1009                         hbox.pack_start(button, expand=False, fill=True)
1010                         button.show()
1012                         image = gtk.Image()
1013                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1014                         image.show()
1016                         button = gtk.Button()
1017                         button.add(image)
1018                         button.set_relief(gtk.RELIEF_NONE)
1019                         button.set_sensitive(True)
1020                         button.connect("clicked", self._show_clicked_cb,
1021                                         commit.commit_sha1, parent_id, self.encoding)
1022                         hbox.pack_start(button, expand=False, fill=True)
1023                         button.show()
1025                 # Populate with child details
1026                 for widget in self.children_widgets:
1027                         self.table.remove(widget)
1029                 self.children_widgets = []
1030                 try:
1031                         child_sha1 = Commit.children_sha1[commit.commit_sha1]
1032                 except KeyError:
1033                         # We don't have child
1034                         child_sha1 = [ 0 ]
1036                 if ( len(child_sha1) > len(commit.parent_sha1)):
1037                         self.table.resize(4 + len(child_sha1) - 1, 4)
1039                 for idx, child_id in enumerate(child_sha1):
1040                         self.table.set_row_spacing(idx + 3, 0)
1042                         align = gtk.Alignment(0.0, 0.0)
1043                         self.children_widgets.append(align)
1044                         self.table.attach(align, 3, 4, idx + 3, idx + 4,
1045                                         gtk.EXPAND | gtk.FILL, gtk.FILL)
1046                         align.show()
1048                         hbox = gtk.HBox(False, 0)
1049                         align.add(hbox)
1050                         hbox.show()
1052                         label = gtk.Label(child_id)
1053                         label.set_selectable(True)
1054                         hbox.pack_start(label, expand=False, fill=True)
1055                         label.show()
1057                         image = gtk.Image()
1058                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1059                         image.show()
1061                         button = gtk.Button()
1062                         button.add(image)
1063                         button.set_relief(gtk.RELIEF_NONE)
1064                         button.connect("clicked", self._go_clicked_cb, child_id)
1065                         hbox.pack_start(button, expand=False, fill=True)
1066                         button.show()
1068                         image = gtk.Image()
1069                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1070                         image.show()
1072                         button = gtk.Button()
1073                         button.add(image)
1074                         button.set_relief(gtk.RELIEF_NONE)
1075                         button.set_sensitive(True)
1076                         button.connect("clicked", self._show_clicked_cb,
1077                                         child_id, commit.commit_sha1, self.encoding)
1078                         hbox.pack_start(button, expand=False, fill=True)
1079                         button.show()
1081         def _destroy_cb(self, widget):
1082                 """Callback for when a window we manage is destroyed."""
1083                 self.quit()
1086         def quit(self):
1087                 """Stop the GTK+ main loop."""
1088                 gtk.main_quit()
1090         def run(self, args):
1091                 self.set_branch(args)
1092                 self.window.connect("destroy", self._destroy_cb)
1093                 self.window.show()
1094                 gtk.main()
1096         def set_branch(self, args):
1097                 """Fill in different windows with info from the reposiroty"""
1098                 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
1099                 git_rev_list_cmd = fp.read()
1100                 fp.close()
1101                 fp = os.popen("git rev-list  --header --topo-order --parents " + git_rev_list_cmd)
1102                 self.update_window(fp)
1104         def update_window(self, fp):
1105                 commit_lines = []
1107                 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
1108                                 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
1110                 # used for cursor positioning
1111                 self.index = {}
1113                 self.colours = {}
1114                 self.nodepos = {}
1115                 self.incomplete_line = {}
1116                 self.commits = []
1118                 index = 0
1119                 last_colour = 0
1120                 last_nodepos = -1
1121                 out_line = []
1122                 input_line = fp.readline()
1123                 while (input_line != ""):
1124                         # The commit header ends with '\0'
1125                         # This NULL is immediately followed by the sha1 of the
1126                         # next commit
1127                         if (input_line[0] != '\0'):
1128                                 commit_lines.append(input_line)
1129                                 input_line = fp.readline()
1130                                 continue;
1132                         commit = Commit(commit_lines)
1133                         if (commit != None ):
1134                                 self.commits.append(commit)
1136                         # Skip the '\0
1137                         commit_lines = []
1138                         commit_lines.append(input_line[1:])
1139                         input_line = fp.readline()
1141                 fp.close()
1143                 for commit in self.commits:
1144                         (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
1145                                                                                 index, out_line,
1146                                                                                 last_colour,
1147                                                                                 last_nodepos)
1148                         self.index[commit.commit_sha1] = index
1149                         index += 1
1151                 self.treeview.set_model(self.model)
1152                 self.treeview.show()
1154         def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
1155                 in_line=[]
1157                 #   |   -> outline
1158                 #   X
1159                 #   |\  <- inline
1161                 # Reset nodepostion
1162                 if (last_nodepos > 5):
1163                         last_nodepos = -1
1165                 # Add the incomplete lines of the last cell in this
1166                 try:
1167                         colour = self.colours[commit.commit_sha1]
1168                 except KeyError:
1169                         self.colours[commit.commit_sha1] = last_colour+1
1170                         last_colour = self.colours[commit.commit_sha1]
1171                         colour =   self.colours[commit.commit_sha1]
1173                 try:
1174                         node_pos = self.nodepos[commit.commit_sha1]
1175                 except KeyError:
1176                         self.nodepos[commit.commit_sha1] = last_nodepos+1
1177                         last_nodepos = self.nodepos[commit.commit_sha1]
1178                         node_pos =  self.nodepos[commit.commit_sha1]
1180                 #The first parent always continue on the same line
1181                 try:
1182                         # check we alreay have the value
1183                         tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
1184                 except KeyError:
1185                         self.colours[commit.parent_sha1[0]] = colour
1186                         self.nodepos[commit.parent_sha1[0]] = node_pos
1188                 for sha1 in self.incomplete_line.keys():
1189                         if (sha1 != commit.commit_sha1):
1190                                 self.draw_incomplete_line(sha1, node_pos,
1191                                                 out_line, in_line, index)
1192                         else:
1193                                 del self.incomplete_line[sha1]
1196                 for parent_id in commit.parent_sha1:
1197                         try:
1198                                 tmp_node_pos = self.nodepos[parent_id]
1199                         except KeyError:
1200                                 self.colours[parent_id] = last_colour+1
1201                                 last_colour = self.colours[parent_id]
1202                                 self.nodepos[parent_id] = last_nodepos+1
1203                                 last_nodepos = self.nodepos[parent_id]
1205                         in_line.append((node_pos, self.nodepos[parent_id],
1206                                                 self.colours[parent_id]))
1207                         self.add_incomplete_line(parent_id)
1209                 try:
1210                         branch_tag = self.bt_sha1[commit.commit_sha1]
1211                 except KeyError:
1212                         branch_tag = [ ]
1215                 node = (node_pos, colour, branch_tag)
1217                 self.model.append([commit, node, out_line, in_line,
1218                                 commit.message, commit.author, commit.date])
1220                 return (in_line, last_colour, last_nodepos)
1222         def add_incomplete_line(self, sha1):
1223                 try:
1224                         self.incomplete_line[sha1].append(self.nodepos[sha1])
1225                 except KeyError:
1226                         self.incomplete_line[sha1] = [self.nodepos[sha1]]
1228         def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
1229                 for idx, pos in enumerate(self.incomplete_line[sha1]):
1230                         if(pos == node_pos):
1231                                 #remove the straight line and add a slash
1232                                 if ((pos, pos, self.colours[sha1]) in out_line):
1233                                         out_line.remove((pos, pos, self.colours[sha1]))
1234                                 out_line.append((pos, pos+0.5, self.colours[sha1]))
1235                                 self.incomplete_line[sha1][idx] = pos = pos+0.5
1236                         try:
1237                                 next_commit = self.commits[index+1]
1238                                 if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
1239                                 # join the line back to the node point
1240                                 # This need to be done only if we modified it
1241                                         in_line.append((pos, pos-0.5, self.colours[sha1]))
1242                                         continue;
1243                         except IndexError:
1244                                 pass
1245                         in_line.append((pos, pos, self.colours[sha1]))
1248         def _go_clicked_cb(self, widget, revid):
1249                 """Callback for when the go button for a parent is clicked."""
1250                 try:
1251                         self.treeview.set_cursor(self.index[revid])
1252                 except KeyError:
1253                         dialog = gtk.MessageDialog(parent=None, flags=0,
1254                                         type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
1255                                         message_format=None)
1256                         dialog.set_markup("Revision <b>%s</b> not present in the list" % revid)
1257                         # revid == 0 is the parent of the first commit
1258                         if (revid != 0 ):
1259                                 dialog.format_secondary_text("Try running gitview without any options")
1260                         dialog.run()
1261                         dialog.destroy()
1263                 self.treeview.grab_focus()
1265         def _show_clicked_cb(self, widget,  commit_sha1, parent_sha1, encoding):
1266                 """Callback for when the show button for a parent is clicked."""
1267                 window = DiffWindow()
1268                 window.set_diff(commit_sha1, parent_sha1, encoding)
1269                 self.treeview.grab_focus()
1271 without_diff = 0
1272 if __name__ == "__main__":
1274         if (len(sys.argv) > 1 ):
1275                 if (sys.argv[1] == "--without-diff"):
1276                         without_diff = 1
1278         view = GitView( without_diff != 1)
1279         view.run(sys.argv[without_diff:])