Code

Merge part of kh/svnimport branch into master
[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 __author__    = "Aneesh Kumar K.V <aneesh.kumar@hp.com>"
16 import sys
17 import os
18 import gtk
19 import pygtk
20 import pango
21 import re
22 import time
23 import gobject
24 import cairo
25 import math
26 import string
28 try:
29     import gtksourceview
30     have_gtksourceview = True
31 except ImportError:
32     have_gtksourceview = False
33     print "Running without gtksourceview module"
35 re_ident = re.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
37 def list_to_string(args, skip):
38         count = len(args)
39         i = skip
40         str_arg=" "
41         while (i < count ):
42                 str_arg = str_arg + args[i]
43                 str_arg = str_arg + " "
44                 i = i+1
46         return str_arg
48 def show_date(epoch, tz):
49         secs = float(epoch)
50         tzsecs = float(tz[1:3]) * 3600
51         tzsecs += float(tz[3:5]) * 60
52         if (tz[0] == "+"):
53                 secs += tzsecs
54         else:
55                 secs -= tzsecs
57         return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs))
60 class CellRendererGraph(gtk.GenericCellRenderer):
61         """Cell renderer for directed graph.
63         This module contains the implementation of a custom GtkCellRenderer that
64         draws part of the directed graph based on the lines suggested by the code
65         in graph.py.
67         Because we're shiny, we use Cairo to do this, and because we're naughty
68         we cheat and draw over the bits of the TreeViewColumn that are supposed to
69         just be for the background.
71         Properties:
72         node              (column, colour, [ names ]) tuple to draw revision node,
73         in_lines          (start, end, colour) tuple list to draw inward lines,
74         out_lines         (start, end, colour) tuple list to draw outward lines.
75         """
77         __gproperties__ = {
78         "node":         ( gobject.TYPE_PYOBJECT, "node",
79                           "revision node instruction",
80                           gobject.PARAM_WRITABLE
81                         ),
82         "in-lines":     ( gobject.TYPE_PYOBJECT, "in-lines",
83                           "instructions to draw lines into the cell",
84                           gobject.PARAM_WRITABLE
85                         ),
86         "out-lines":    ( gobject.TYPE_PYOBJECT, "out-lines",
87                           "instructions to draw lines out of the cell",
88                           gobject.PARAM_WRITABLE
89                         ),
90         }
92         def do_set_property(self, property, value):
93                 """Set properties from GObject properties."""
94                 if property.name == "node":
95                         self.node = value
96                 elif property.name == "in-lines":
97                         self.in_lines = value
98                 elif property.name == "out-lines":
99                         self.out_lines = value
100                 else:
101                         raise AttributeError, "no such property: '%s'" % property.name
103         def box_size(self, widget):
104                 """Calculate box size based on widget's font.
106                 Cache this as it's probably expensive to get.  It ensures that we
107                 draw the graph at least as large as the text.
108                 """
109                 try:
110                         return self._box_size
111                 except AttributeError:
112                         pango_ctx = widget.get_pango_context()
113                         font_desc = widget.get_style().font_desc
114                         metrics = pango_ctx.get_metrics(font_desc)
116                         ascent = pango.PIXELS(metrics.get_ascent())
117                         descent = pango.PIXELS(metrics.get_descent())
119                         self._box_size = ascent + descent + 6
120                         return self._box_size
122         def set_colour(self, ctx, colour, bg, fg):
123                 """Set the context source colour.
125                 Picks a distinct colour based on an internal wheel; the bg
126                 parameter provides the value that should be assigned to the 'zero'
127                 colours and the fg parameter provides the multiplier that should be
128                 applied to the foreground colours.
129                 """
130                 colours = [
131                     ( 1.0, 0.0, 0.0 ),
132                     ( 1.0, 1.0, 0.0 ),
133                     ( 0.0, 1.0, 0.0 ),
134                     ( 0.0, 1.0, 1.0 ),
135                     ( 0.0, 0.0, 1.0 ),
136                     ( 1.0, 0.0, 1.0 ),
137                     ]
139                 colour %= len(colours)
140                 red   = (colours[colour][0] * fg) or bg
141                 green = (colours[colour][1] * fg) or bg
142                 blue  = (colours[colour][2] * fg) or bg
144                 ctx.set_source_rgb(red, green, blue)
146         def on_get_size(self, widget, cell_area):
147                 """Return the size we need for this cell.
149                 Each cell is drawn individually and is only as wide as it needs
150                 to be, we let the TreeViewColumn take care of making them all
151                 line up.
152                 """
153                 box_size = self.box_size(widget)
155                 cols = self.node[0]
156                 for start, end, colour in self.in_lines + self.out_lines:
157                         cols = int(max(cols, start, end))
159                 (column, colour, names) = self.node
160                 names_len = 0
161                 if (len(names) != 0):
162                         for item in names:
163                                 names_len += len(item)
165                 width = box_size * (cols + 1 ) + names_len
166                 height = box_size
168                 # FIXME I have no idea how to use cell_area properly
169                 return (0, 0, width, height)
171         def on_render(self, window, widget, bg_area, cell_area, exp_area, flags):
172                 """Render an individual cell.
174                 Draws the cell contents using cairo, taking care to clip what we
175                 do to within the background area so we don't draw over other cells.
176                 Note that we're a bit naughty there and should really be drawing
177                 in the cell_area (or even the exposed area), but we explicitly don't
178                 want any gutter.
180                 We try and be a little clever, if the line we need to draw is going
181                 to cross other columns we actually draw it as in the .---' style
182                 instead of a pure diagonal ... this reduces confusion by an
183                 incredible amount.
184                 """
185                 ctx = window.cairo_create()
186                 ctx.rectangle(bg_area.x, bg_area.y, bg_area.width, bg_area.height)
187                 ctx.clip()
189                 box_size = self.box_size(widget)
191                 ctx.set_line_width(box_size / 8)
192                 ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
194                 # Draw lines into the cell
195                 for start, end, colour in self.in_lines:
196                         ctx.move_to(cell_area.x + box_size * start + box_size / 2,
197                                         bg_area.y - bg_area.height / 2)
199                         if start - end > 1:
200                                 ctx.line_to(cell_area.x + box_size * start, bg_area.y)
201                                 ctx.line_to(cell_area.x + box_size * end + box_size, bg_area.y)
202                         elif start - end < -1:
203                                 ctx.line_to(cell_area.x + box_size * start + box_size,
204                                                 bg_area.y)
205                                 ctx.line_to(cell_area.x + box_size * end, bg_area.y)
207                         ctx.line_to(cell_area.x + box_size * end + box_size / 2,
208                                         bg_area.y + bg_area.height / 2)
210                         self.set_colour(ctx, colour, 0.0, 0.65)
211                         ctx.stroke()
213                 # Draw lines out of the cell
214                 for start, end, colour in self.out_lines:
215                         ctx.move_to(cell_area.x + box_size * start + box_size / 2,
216                                         bg_area.y + bg_area.height / 2)
218                         if start - end > 1:
219                                 ctx.line_to(cell_area.x + box_size * start,
220                                                 bg_area.y + bg_area.height)
221                                 ctx.line_to(cell_area.x + box_size * end + box_size,
222                                                 bg_area.y + bg_area.height)
223                         elif start - end < -1:
224                                 ctx.line_to(cell_area.x + box_size * start + box_size,
225                                                 bg_area.y + bg_area.height)
226                                 ctx.line_to(cell_area.x + box_size * end,
227                                                 bg_area.y + bg_area.height)
229                         ctx.line_to(cell_area.x + box_size * end + box_size / 2,
230                                         bg_area.y + bg_area.height / 2 + bg_area.height)
232                         self.set_colour(ctx, colour, 0.0, 0.65)
233                         ctx.stroke()
235                 # Draw the revision node in the right column
236                 (column, colour, names) = self.node
237                 ctx.arc(cell_area.x + box_size * column + box_size / 2,
238                                 cell_area.y + cell_area.height / 2,
239                                 box_size / 4, 0, 2 * math.pi)
242                 if (len(names) != 0):
243                         name = " "
244                         for item in names:
245                                 name = name + item + " "
247                         ctx.select_font_face("Monospace")
248                         ctx.set_font_size(13)
249                         ctx.text_path(name)
251                 self.set_colour(ctx, colour, 0.0, 0.5)
252                 ctx.stroke_preserve()
254                 self.set_colour(ctx, colour, 0.5, 1.0)
255                 ctx.fill()
257 class Commit:
258         """ This represent a commit object obtained after parsing the git-rev-list
259         output """
261         children_sha1 = {}
263         def __init__(self, commit_lines):
264                 self.message            = ""
265                 self.author             = ""
266                 self.date               = ""
267                 self.committer          = ""
268                 self.commit_date        = ""
269                 self.commit_sha1        = ""
270                 self.parent_sha1        = [ ]
271                 self.parse_commit(commit_lines)
274         def parse_commit(self, commit_lines):
276                 # First line is the sha1 lines
277                 line = string.strip(commit_lines[0])
278                 sha1 = re.split(" ", line)
279                 self.commit_sha1 = sha1[0]
280                 self.parent_sha1 = sha1[1:]
282                 #build the child list
283                 for parent_id in self.parent_sha1:
284                         try:
285                                 Commit.children_sha1[parent_id].append(self.commit_sha1)
286                         except KeyError:
287                                 Commit.children_sha1[parent_id] = [self.commit_sha1]
289                 # IF we don't have parent
290                 if (len(self.parent_sha1) == 0):
291                         self.parent_sha1 = [0]
293                 for line in commit_lines[1:]:
294                         m = re.match("^ ", line)
295                         if (m != None):
296                                 # First line of the commit message used for short log
297                                 if self.message == "":
298                                         self.message = string.strip(line)
299                                 continue
301                         m = re.match("tree", line)
302                         if (m != None):
303                                 continue
305                         m = re.match("parent", line)
306                         if (m != None):
307                                 continue
309                         m = re_ident.match(line)
310                         if (m != None):
311                                 date = show_date(m.group('epoch'), m.group('tz'))
312                                 if m.group(1) == "author":
313                                         self.author = m.group('ident')
314                                         self.date = date
315                                 elif m.group(1) == "committer":
316                                         self.committer = m.group('ident')
317                                         self.commit_date = date
319                                 continue
321         def get_message(self, with_diff=0):
322                 if (with_diff == 1):
323                         message = self.diff_tree()
324                 else:
325                         fp = os.popen("git cat-file commit " + self.commit_sha1)
326                         message = fp.read()
327                         fp.close()
329                 return message
331         def diff_tree(self):
332                 fp = os.popen("git diff-tree --pretty --cc  -v -p --always " +  self.commit_sha1)
333                 diff = fp.read()
334                 fp.close()
335                 return diff
337 class DiffWindow:
338         """Diff window.
339         This object represents and manages a single window containing the
340         differences between two revisions on a branch.
341         """
343         def __init__(self):
344                 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
345                 self.window.set_border_width(0)
346                 self.window.set_title("Git repository browser diff window")
348                 # Use two thirds of the screen by default
349                 screen = self.window.get_screen()
350                 monitor = screen.get_monitor_geometry(0)
351                 width = int(monitor.width * 0.66)
352                 height = int(monitor.height * 0.66)
353                 self.window.set_default_size(width, height)
355                 self.construct()
357         def construct(self):
358                 """Construct the window contents."""
359                 vbox = gtk.VBox()
360                 self.window.add(vbox)
361                 vbox.show()
363                 menu_bar = gtk.MenuBar()
364                 save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
365                 save_menu.connect("activate", self.save_menu_response, "save")
366                 save_menu.show()
367                 menu_bar.append(save_menu)
368                 vbox.pack_start(menu_bar, False, False, 2)
369                 menu_bar.show()
371                 scrollwin = gtk.ScrolledWindow()
372                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
373                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
374                 vbox.pack_start(scrollwin, expand=True, fill=True)
375                 scrollwin.show()
377                 if have_gtksourceview:
378                         self.buffer = gtksourceview.SourceBuffer()
379                         slm = gtksourceview.SourceLanguagesManager()
380                         gsl = slm.get_language_from_mime_type("text/x-patch")
381                         self.buffer.set_highlight(True)
382                         self.buffer.set_language(gsl)
383                         sourceview = gtksourceview.SourceView(self.buffer)
384                 else:
385                         self.buffer = gtk.TextBuffer()
386                         sourceview = gtk.TextView(self.buffer)
388                 sourceview.set_editable(False)
389                 sourceview.modify_font(pango.FontDescription("Monospace"))
390                 scrollwin.add(sourceview)
391                 sourceview.show()
394         def set_diff(self, commit_sha1, parent_sha1, encoding):
395                 """Set the differences showed by this window.
396                 Compares the two trees and populates the window with the
397                 differences.
398                 """
399                 # Diff with the first commit or the last commit shows nothing
400                 if (commit_sha1 == 0 or parent_sha1 == 0 ):
401                         return
403                 fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
404                 self.buffer.set_text(unicode(fp.read(), encoding).encode('utf-8'))
405                 fp.close()
406                 self.window.show()
408         def save_menu_response(self, widget, string):
409                 dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
410                                 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
411                                         gtk.STOCK_SAVE, gtk.RESPONSE_OK))
412                 dialog.set_default_response(gtk.RESPONSE_OK)
413                 response = dialog.run()
414                 if response == gtk.RESPONSE_OK:
415                         patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
416                                         self.buffer.get_end_iter())
417                         fp = open(dialog.get_filename(), "w")
418                         fp.write(patch_buffer)
419                         fp.close()
420                 dialog.destroy()
422 class GitView:
423         """ This is the main class
424         """
425         version = "0.7"
427         def __init__(self, with_diff=0):
428                 self.with_diff = with_diff
429                 self.window =   gtk.Window(gtk.WINDOW_TOPLEVEL)
430                 self.window.set_border_width(0)
431                 self.window.set_title("Git repository browser")
433                 self.get_encoding()
434                 self.get_bt_sha1()
436                 # Use three-quarters of the screen by default
437                 screen = self.window.get_screen()
438                 monitor = screen.get_monitor_geometry(0)
439                 width = int(monitor.width * 0.75)
440                 height = int(monitor.height * 0.75)
441                 self.window.set_default_size(width, height)
443                 # FIXME AndyFitz!
444                 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
445                 self.window.set_icon(icon)
447                 self.accel_group = gtk.AccelGroup()
448                 self.window.add_accel_group(self.accel_group)
450                 self.construct()
452         def get_bt_sha1(self):
453                 """ Update the bt_sha1 dictionary with the
454                 respective sha1 details """
456                 self.bt_sha1 = { }
457                 ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
458                 fp = os.popen('git ls-remote "${GIT_DIR-.git}"')
459                 while 1:
460                         line = string.strip(fp.readline())
461                         if line == '':
462                                 break
463                         m = ls_remote.match(line)
464                         if not m:
465                                 continue
466                         (sha1, name) = (m.group(1), m.group(2))
467                         if not self.bt_sha1.has_key(sha1):
468                                 self.bt_sha1[sha1] = []
469                         self.bt_sha1[sha1].append(name)
470                 fp.close()
472         def get_encoding(self):
473                 fp = os.popen("git repo-config --get i18n.commitencoding")
474                 self.encoding=string.strip(fp.readline())
475                 fp.close()
476                 if (self.encoding == ""):
477                         self.encoding = "utf-8"
480         def construct(self):
481                 """Construct the window contents."""
482                 paned = gtk.VPaned()
483                 paned.pack1(self.construct_top(), resize=False, shrink=True)
484                 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
485                 self.window.add(paned)
486                 paned.show()
489         def construct_top(self):
490                 """Construct the top-half of the window."""
491                 vbox = gtk.VBox(spacing=6)
492                 vbox.set_border_width(12)
493                 vbox.show()
495                 menu_bar = gtk.MenuBar()
496                 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
497                 help_menu = gtk.MenuItem("Help")
498                 menu = gtk.Menu()
499                 about_menu = gtk.MenuItem("About")
500                 menu.append(about_menu)
501                 about_menu.connect("activate", self.about_menu_response, "about")
502                 about_menu.show()
503                 help_menu.set_submenu(menu)
504                 help_menu.show()
505                 menu_bar.append(help_menu)
506                 vbox.pack_start(menu_bar, False, False, 2)
507                 menu_bar.show()
509                 scrollwin = gtk.ScrolledWindow()
510                 scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
511                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
512                 vbox.pack_start(scrollwin, expand=True, fill=True)
513                 scrollwin.show()
515                 self.treeview = gtk.TreeView()
516                 self.treeview.set_rules_hint(True)
517                 self.treeview.set_search_column(4)
518                 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
519                 scrollwin.add(self.treeview)
520                 self.treeview.show()
522                 cell = CellRendererGraph()
523                 column = gtk.TreeViewColumn()
524                 column.set_resizable(True)
525                 column.pack_start(cell, expand=True)
526                 column.add_attribute(cell, "node", 1)
527                 column.add_attribute(cell, "in-lines", 2)
528                 column.add_attribute(cell, "out-lines", 3)
529                 self.treeview.append_column(column)
531                 cell = gtk.CellRendererText()
532                 cell.set_property("width-chars", 65)
533                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
534                 column = gtk.TreeViewColumn("Message")
535                 column.set_resizable(True)
536                 column.pack_start(cell, expand=True)
537                 column.add_attribute(cell, "text", 4)
538                 self.treeview.append_column(column)
540                 cell = gtk.CellRendererText()
541                 cell.set_property("width-chars", 40)
542                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
543                 column = gtk.TreeViewColumn("Author")
544                 column.set_resizable(True)
545                 column.pack_start(cell, expand=True)
546                 column.add_attribute(cell, "text", 5)
547                 self.treeview.append_column(column)
549                 cell = gtk.CellRendererText()
550                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
551                 column = gtk.TreeViewColumn("Date")
552                 column.set_resizable(True)
553                 column.pack_start(cell, expand=True)
554                 column.add_attribute(cell, "text", 6)
555                 self.treeview.append_column(column)
557                 return vbox
559         def about_menu_response(self, widget, string):
560                 dialog = gtk.AboutDialog()
561                 dialog.set_name("Gitview")
562                 dialog.set_version(GitView.version)
563                 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
564                 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
565                 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
566                 dialog.set_wrap_license(True)
567                 dialog.run()
568                 dialog.destroy()
571         def construct_bottom(self):
572                 """Construct the bottom half of the window."""
573                 vbox = gtk.VBox(False, spacing=6)
574                 vbox.set_border_width(12)
575                 (width, height) = self.window.get_size()
576                 vbox.set_size_request(width, int(height / 2.5))
577                 vbox.show()
579                 self.table = gtk.Table(rows=4, columns=4)
580                 self.table.set_row_spacings(6)
581                 self.table.set_col_spacings(6)
582                 vbox.pack_start(self.table, expand=False, fill=True)
583                 self.table.show()
585                 align = gtk.Alignment(0.0, 0.5)
586                 label = gtk.Label()
587                 label.set_markup("<b>Revision:</b>")
588                 align.add(label)
589                 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
590                 label.show()
591                 align.show()
593                 align = gtk.Alignment(0.0, 0.5)
594                 self.revid_label = gtk.Label()
595                 self.revid_label.set_selectable(True)
596                 align.add(self.revid_label)
597                 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
598                 self.revid_label.show()
599                 align.show()
601                 align = gtk.Alignment(0.0, 0.5)
602                 label = gtk.Label()
603                 label.set_markup("<b>Committer:</b>")
604                 align.add(label)
605                 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
606                 label.show()
607                 align.show()
609                 align = gtk.Alignment(0.0, 0.5)
610                 self.committer_label = gtk.Label()
611                 self.committer_label.set_selectable(True)
612                 align.add(self.committer_label)
613                 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
614                 self.committer_label.show()
615                 align.show()
617                 align = gtk.Alignment(0.0, 0.5)
618                 label = gtk.Label()
619                 label.set_markup("<b>Timestamp:</b>")
620                 align.add(label)
621                 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
622                 label.show()
623                 align.show()
625                 align = gtk.Alignment(0.0, 0.5)
626                 self.timestamp_label = gtk.Label()
627                 self.timestamp_label.set_selectable(True)
628                 align.add(self.timestamp_label)
629                 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
630                 self.timestamp_label.show()
631                 align.show()
633                 align = gtk.Alignment(0.0, 0.5)
634                 label = gtk.Label()
635                 label.set_markup("<b>Parents:</b>")
636                 align.add(label)
637                 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
638                 label.show()
639                 align.show()
640                 self.parents_widgets = []
642                 align = gtk.Alignment(0.0, 0.5)
643                 label = gtk.Label()
644                 label.set_markup("<b>Children:</b>")
645                 align.add(label)
646                 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
647                 label.show()
648                 align.show()
649                 self.children_widgets = []
651                 scrollwin = gtk.ScrolledWindow()
652                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
653                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
654                 vbox.pack_start(scrollwin, expand=True, fill=True)
655                 scrollwin.show()
657                 if have_gtksourceview:
658                         self.message_buffer = gtksourceview.SourceBuffer()
659                         slm = gtksourceview.SourceLanguagesManager()
660                         gsl = slm.get_language_from_mime_type("text/x-patch")
661                         self.message_buffer.set_highlight(True)
662                         self.message_buffer.set_language(gsl)
663                         sourceview = gtksourceview.SourceView(self.message_buffer)
664                 else:
665                         self.message_buffer = gtk.TextBuffer()
666                         sourceview = gtk.TextView(self.message_buffer)
668                 sourceview.set_editable(False)
669                 sourceview.modify_font(pango.FontDescription("Monospace"))
670                 scrollwin.add(sourceview)
671                 sourceview.show()
673                 return vbox
675         def _treeview_cursor_cb(self, *args):
676                 """Callback for when the treeview cursor changes."""
677                 (path, col) = self.treeview.get_cursor()
678                 commit = self.model[path][0]
680                 if commit.committer is not None:
681                         committer = commit.committer
682                         timestamp = commit.commit_date
683                         message   =  commit.get_message(self.with_diff)
684                         revid_label = commit.commit_sha1
685                 else:
686                         committer = ""
687                         timestamp = ""
688                         message = ""
689                         revid_label = ""
691                 self.revid_label.set_text(revid_label)
692                 self.committer_label.set_text(committer)
693                 self.timestamp_label.set_text(timestamp)
694                 self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
696                 for widget in self.parents_widgets:
697                         self.table.remove(widget)
699                 self.parents_widgets = []
700                 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
701                 for idx, parent_id in enumerate(commit.parent_sha1):
702                         self.table.set_row_spacing(idx + 3, 0)
704                         align = gtk.Alignment(0.0, 0.0)
705                         self.parents_widgets.append(align)
706                         self.table.attach(align, 1, 2, idx + 3, idx + 4,
707                                         gtk.EXPAND | gtk.FILL, gtk.FILL)
708                         align.show()
710                         hbox = gtk.HBox(False, 0)
711                         align.add(hbox)
712                         hbox.show()
714                         label = gtk.Label(parent_id)
715                         label.set_selectable(True)
716                         hbox.pack_start(label, expand=False, fill=True)
717                         label.show()
719                         image = gtk.Image()
720                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
721                         image.show()
723                         button = gtk.Button()
724                         button.add(image)
725                         button.set_relief(gtk.RELIEF_NONE)
726                         button.connect("clicked", self._go_clicked_cb, parent_id)
727                         hbox.pack_start(button, expand=False, fill=True)
728                         button.show()
730                         image = gtk.Image()
731                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
732                         image.show()
734                         button = gtk.Button()
735                         button.add(image)
736                         button.set_relief(gtk.RELIEF_NONE)
737                         button.set_sensitive(True)
738                         button.connect("clicked", self._show_clicked_cb,
739                                         commit.commit_sha1, parent_id, self.encoding)
740                         hbox.pack_start(button, expand=False, fill=True)
741                         button.show()
743                 # Populate with child details
744                 for widget in self.children_widgets:
745                         self.table.remove(widget)
747                 self.children_widgets = []
748                 try:
749                         child_sha1 = Commit.children_sha1[commit.commit_sha1]
750                 except KeyError:
751                         # We don't have child
752                         child_sha1 = [ 0 ]
754                 if ( len(child_sha1) > len(commit.parent_sha1)):
755                         self.table.resize(4 + len(child_sha1) - 1, 4)
757                 for idx, child_id in enumerate(child_sha1):
758                         self.table.set_row_spacing(idx + 3, 0)
760                         align = gtk.Alignment(0.0, 0.0)
761                         self.children_widgets.append(align)
762                         self.table.attach(align, 3, 4, idx + 3, idx + 4,
763                                         gtk.EXPAND | gtk.FILL, gtk.FILL)
764                         align.show()
766                         hbox = gtk.HBox(False, 0)
767                         align.add(hbox)
768                         hbox.show()
770                         label = gtk.Label(child_id)
771                         label.set_selectable(True)
772                         hbox.pack_start(label, expand=False, fill=True)
773                         label.show()
775                         image = gtk.Image()
776                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
777                         image.show()
779                         button = gtk.Button()
780                         button.add(image)
781                         button.set_relief(gtk.RELIEF_NONE)
782                         button.connect("clicked", self._go_clicked_cb, child_id)
783                         hbox.pack_start(button, expand=False, fill=True)
784                         button.show()
786                         image = gtk.Image()
787                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
788                         image.show()
790                         button = gtk.Button()
791                         button.add(image)
792                         button.set_relief(gtk.RELIEF_NONE)
793                         button.set_sensitive(True)
794                         button.connect("clicked", self._show_clicked_cb,
795                                         child_id, commit.commit_sha1)
796                         hbox.pack_start(button, expand=False, fill=True)
797                         button.show()
799         def _destroy_cb(self, widget):
800                 """Callback for when a window we manage is destroyed."""
801                 self.quit()
804         def quit(self):
805                 """Stop the GTK+ main loop."""
806                 gtk.main_quit()
808         def run(self, args):
809                 self.set_branch(args)
810                 self.window.connect("destroy", self._destroy_cb)
811                 self.window.show()
812                 gtk.main()
814         def set_branch(self, args):
815                 """Fill in different windows with info from the reposiroty"""
816                 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
817                 git_rev_list_cmd = fp.read()
818                 fp.close()
819                 fp = os.popen("git rev-list  --header --topo-order --parents " + git_rev_list_cmd)
820                 self.update_window(fp)
822         def update_window(self, fp):
823                 commit_lines = []
825                 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
826                                 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
828                 # used for cursor positioning
829                 self.index = {}
831                 self.colours = {}
832                 self.nodepos = {}
833                 self.incomplete_line = {}
834                 self.commits = []
836                 index = 0
837                 last_colour = 0
838                 last_nodepos = -1
839                 out_line = []
840                 input_line = fp.readline()
841                 while (input_line != ""):
842                         # The commit header ends with '\0'
843                         # This NULL is immediately followed by the sha1 of the
844                         # next commit
845                         if (input_line[0] != '\0'):
846                                 commit_lines.append(input_line)
847                                 input_line = fp.readline()
848                                 continue;
850                         commit = Commit(commit_lines)
851                         if (commit != None ):
852                                 self.commits.append(commit)
854                         # Skip the '\0
855                         commit_lines = []
856                         commit_lines.append(input_line[1:])
857                         input_line = fp.readline()
859                 fp.close()
861                 for commit in self.commits:
862                         (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
863                                                                                 index, out_line,
864                                                                                 last_colour,
865                                                                                 last_nodepos)
866                         self.index[commit.commit_sha1] = index
867                         index += 1
869                 self.treeview.set_model(self.model)
870                 self.treeview.show()
872         def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
873                 in_line=[]
875                 #   |   -> outline
876                 #   X
877                 #   |\  <- inline
879                 # Reset nodepostion
880                 if (last_nodepos > 5):
881                         last_nodepos = -1
883                 # Add the incomplete lines of the last cell in this
884                 try:
885                         colour = self.colours[commit.commit_sha1]
886                 except KeyError:
887                         self.colours[commit.commit_sha1] = last_colour+1
888                         last_colour = self.colours[commit.commit_sha1]
889                         colour =   self.colours[commit.commit_sha1]
891                 try:
892                         node_pos = self.nodepos[commit.commit_sha1]
893                 except KeyError:
894                         self.nodepos[commit.commit_sha1] = last_nodepos+1
895                         last_nodepos = self.nodepos[commit.commit_sha1]
896                         node_pos =  self.nodepos[commit.commit_sha1]
898                 #The first parent always continue on the same line
899                 try:
900                         # check we alreay have the value
901                         tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
902                 except KeyError:
903                         self.colours[commit.parent_sha1[0]] = colour
904                         self.nodepos[commit.parent_sha1[0]] = node_pos
906                 for sha1 in self.incomplete_line.keys():
907                         if (sha1 != commit.commit_sha1):
908                                 self.draw_incomplete_line(sha1, node_pos,
909                                                 out_line, in_line, index)
910                         else:
911                                 del self.incomplete_line[sha1]
914                 for parent_id in commit.parent_sha1:
915                         try:
916                                 tmp_node_pos = self.nodepos[parent_id]
917                         except KeyError:
918                                 self.colours[parent_id] = last_colour+1
919                                 last_colour = self.colours[parent_id]
920                                 self.nodepos[parent_id] = last_nodepos+1
921                                 last_nodepos = self.nodepos[parent_id]
923                         in_line.append((node_pos, self.nodepos[parent_id],
924                                                 self.colours[parent_id]))
925                         self.add_incomplete_line(parent_id)
927                 try:
928                         branch_tag = self.bt_sha1[commit.commit_sha1]
929                 except KeyError:
930                         branch_tag = [ ]
933                 node = (node_pos, colour, branch_tag)
935                 self.model.append([commit, node, out_line, in_line,
936                                 commit.message, commit.author, commit.date])
938                 return (in_line, last_colour, last_nodepos)
940         def add_incomplete_line(self, sha1):
941                 try:
942                         self.incomplete_line[sha1].append(self.nodepos[sha1])
943                 except KeyError:
944                         self.incomplete_line[sha1] = [self.nodepos[sha1]]
946         def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
947                 for idx, pos in enumerate(self.incomplete_line[sha1]):
948                         if(pos == node_pos):
949                                 #remove the straight line and add a slash
950                                 if ((pos, pos, self.colours[sha1]) in out_line):
951                                         out_line.remove((pos, pos, self.colours[sha1]))
952                                 out_line.append((pos, pos+0.5, self.colours[sha1]))
953                                 self.incomplete_line[sha1][idx] = pos = pos+0.5
954                         try:
955                                 next_commit = self.commits[index+1]
956                                 if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
957                                 # join the line back to the node point
958                                 # This need to be done only if we modified it
959                                         in_line.append((pos, pos-0.5, self.colours[sha1]))
960                                         continue;
961                         except IndexError:
962                                 pass
963                         in_line.append((pos, pos, self.colours[sha1]))
966         def _go_clicked_cb(self, widget, revid):
967                 """Callback for when the go button for a parent is clicked."""
968                 try:
969                         self.treeview.set_cursor(self.index[revid])
970                 except KeyError:
971                         print "Revision %s not present in the list" % revid
972                         # revid == 0 is the parent of the first commit
973                         if (revid != 0 ):
974                                 print "Try running gitview without any options"
976                 self.treeview.grab_focus()
978         def _show_clicked_cb(self, widget,  commit_sha1, parent_sha1, encoding):
979                 """Callback for when the show button for a parent is clicked."""
980                 window = DiffWindow()
981                 window.set_diff(commit_sha1, parent_sha1, encoding)
982                 self.treeview.grab_focus()
984 if __name__ == "__main__":
985         without_diff = 0
987         if (len(sys.argv) > 1 ):
988                 if (sys.argv[1] == "--without-diff"):
989                         without_diff = 1
991         view = GitView( without_diff != 1)
992         view.run(sys.argv[without_diff:])