Code

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