Code

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