gitview: Add key binding for F5.
[git.git] / contrib / gitview / gitview
1 #! /usr/bin/env python
2
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
7
8 """ gitview
9 GUI browser for git repository
10 This program is based on bzrk by Scott James Remnant <scott@ubuntu.com>
11 """
12 __copyright__ = "Copyright (C) 2006 Hewlett-Packard Development Company, L.P."
13 __author__    = "Aneesh Kumar K.V <aneesh.kumar@hp.com>"
14
15
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
27
28 try:
29     import gtksourceview
30     have_gtksourceview = True
31 except ImportError:
32     have_gtksourceview = False
33     print "Running without gtksourceview module"
34
35 re_ident = re.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
36
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
45
46         return str_arg
47
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
56
57         return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs))
58
59
60 class CellRendererGraph(gtk.GenericCellRenderer):
61         """Cell renderer for directed graph.
62
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.
66
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.
70
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         """
76
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         }
91
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
102
103         def box_size(self, widget):
104                 """Calculate box size based on widget's font.
105
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)
115
116                         ascent = pango.PIXELS(metrics.get_ascent())
117                         descent = pango.PIXELS(metrics.get_descent())
118
119                         self._box_size = ascent + descent + 6
120                         return self._box_size
121
122         def set_colour(self, ctx, colour, bg, fg):
123                 """Set the context source colour.
124
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                     ]
138
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
143
144                 ctx.set_source_rgb(red, green, blue)
145
146         def on_get_size(self, widget, cell_area):
147                 """Return the size we need for this cell.
148
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)
154
155                 cols = self.node[0]
156                 for start, end, colour in self.in_lines + self.out_lines:
157                         cols = int(max(cols, start, end))
158
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)
164
165                 width = box_size * (cols + 1 ) + names_len
166                 height = box_size
167
168                 # FIXME I have no idea how to use cell_area properly
169                 return (0, 0, width, height)
170
171         def on_render(self, window, widget, bg_area, cell_area, exp_area, flags):
172                 """Render an individual cell.
173
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.
179
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()
188
189                 box_size = self.box_size(widget)
190
191                 ctx.set_line_width(box_size / 8)
192                 ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
193
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)
198
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)
206
207                         ctx.line_to(cell_area.x + box_size * end + box_size / 2,
208                                         bg_area.y + bg_area.height / 2)
209
210                         self.set_colour(ctx, colour, 0.0, 0.65)
211                         ctx.stroke()
212
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)
217
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)
228
229                         ctx.line_to(cell_area.x + box_size * end + box_size / 2,
230                                         bg_area.y + bg_area.height / 2 + bg_area.height)
231
232                         self.set_colour(ctx, colour, 0.0, 0.65)
233                         ctx.stroke()
234
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)
240
241
242                 self.set_colour(ctx, colour, 0.0, 0.5)
243                 ctx.stroke_preserve()
244
245                 self.set_colour(ctx, colour, 0.5, 1.0)
246                 ctx.fill_preserve()
247
248                 if (len(names) != 0):
249                         name = " "
250                         for item in names:
251                                 name = name + item + " "
252
253                         ctx.set_font_size(13)
254                         if (flags & 1):
255                                 self.set_colour(ctx, colour, 0.5, 1.0)
256                         else:
257                                 self.set_colour(ctx, colour, 0.0, 0.5)
258                         ctx.show_text(name)
259
260 class Commit:
261         """ This represent a commit object obtained after parsing the git-rev-list
262         output """
263
264         children_sha1 = {}
265
266         def __init__(self, commit_lines):
267                 self.message            = ""
268                 self.author             = ""
269                 self.date               = ""
270                 self.committer          = ""
271                 self.commit_date        = ""
272                 self.commit_sha1        = ""
273                 self.parent_sha1        = [ ]
274                 self.parse_commit(commit_lines)
275
276
277         def parse_commit(self, commit_lines):
278
279                 # First line is the sha1 lines
280                 line = string.strip(commit_lines[0])
281                 sha1 = re.split(" ", line)
282                 self.commit_sha1 = sha1[0]
283                 self.parent_sha1 = sha1[1:]
284
285                 #build the child list
286                 for parent_id in self.parent_sha1:
287                         try:
288                                 Commit.children_sha1[parent_id].append(self.commit_sha1)
289                         except KeyError:
290                                 Commit.children_sha1[parent_id] = [self.commit_sha1]
291
292                 # IF we don't have parent
293                 if (len(self.parent_sha1) == 0):
294                         self.parent_sha1 = [0]
295
296                 for line in commit_lines[1:]:
297                         m = re.match("^ ", line)
298                         if (m != None):
299                                 # First line of the commit message used for short log
300                                 if self.message == "":
301                                         self.message = string.strip(line)
302                                 continue
303
304                         m = re.match("tree", line)
305                         if (m != None):
306                                 continue
307
308                         m = re.match("parent", line)
309                         if (m != None):
310                                 continue
311
312                         m = re_ident.match(line)
313                         if (m != None):
314                                 date = show_date(m.group('epoch'), m.group('tz'))
315                                 if m.group(1) == "author":
316                                         self.author = m.group('ident')
317                                         self.date = date
318                                 elif m.group(1) == "committer":
319                                         self.committer = m.group('ident')
320                                         self.commit_date = date
321
322                                 continue
323
324         def get_message(self, with_diff=0):
325                 if (with_diff == 1):
326                         message = self.diff_tree()
327                 else:
328                         fp = os.popen("git cat-file commit " + self.commit_sha1)
329                         message = fp.read()
330                         fp.close()
331
332                 return message
333
334         def diff_tree(self):
335                 fp = os.popen("git diff-tree --pretty --cc  -v -p --always " +  self.commit_sha1)
336                 diff = fp.read()
337                 fp.close()
338                 return diff
339
340 class DiffWindow:
341         """Diff window.
342         This object represents and manages a single window containing the
343         differences between two revisions on a branch.
344         """
345
346         def __init__(self):
347                 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
348                 self.window.set_border_width(0)
349                 self.window.set_title("Git repository browser diff window")
350
351                 # Use two thirds of the screen by default
352                 screen = self.window.get_screen()
353                 monitor = screen.get_monitor_geometry(0)
354                 width = int(monitor.width * 0.66)
355                 height = int(monitor.height * 0.66)
356                 self.window.set_default_size(width, height)
357
358                 self.construct()
359
360         def construct(self):
361                 """Construct the window contents."""
362                 vbox = gtk.VBox()
363                 self.window.add(vbox)
364                 vbox.show()
365
366                 menu_bar = gtk.MenuBar()
367                 save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
368                 save_menu.connect("activate", self.save_menu_response, "save")
369                 save_menu.show()
370                 menu_bar.append(save_menu)
371                 vbox.pack_start(menu_bar, expand=False, fill=True)
372                 menu_bar.show()
373
374                 scrollwin = gtk.ScrolledWindow()
375                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
376                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
377                 vbox.pack_start(scrollwin, expand=True, fill=True)
378                 scrollwin.show()
379
380                 if have_gtksourceview:
381                         self.buffer = gtksourceview.SourceBuffer()
382                         slm = gtksourceview.SourceLanguagesManager()
383                         gsl = slm.get_language_from_mime_type("text/x-patch")
384                         self.buffer.set_highlight(True)
385                         self.buffer.set_language(gsl)
386                         sourceview = gtksourceview.SourceView(self.buffer)
387                 else:
388                         self.buffer = gtk.TextBuffer()
389                         sourceview = gtk.TextView(self.buffer)
390
391                 sourceview.set_editable(False)
392                 sourceview.modify_font(pango.FontDescription("Monospace"))
393                 scrollwin.add(sourceview)
394                 sourceview.show()
395
396
397         def set_diff(self, commit_sha1, parent_sha1, encoding):
398                 """Set the differences showed by this window.
399                 Compares the two trees and populates the window with the
400                 differences.
401                 """
402                 # Diff with the first commit or the last commit shows nothing
403                 if (commit_sha1 == 0 or parent_sha1 == 0 ):
404                         return
405
406                 fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
407                 self.buffer.set_text(unicode(fp.read(), encoding).encode('utf-8'))
408                 fp.close()
409                 self.window.show()
410
411         def save_menu_response(self, widget, string):
412                 dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
413                                 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
414                                         gtk.STOCK_SAVE, gtk.RESPONSE_OK))
415                 dialog.set_default_response(gtk.RESPONSE_OK)
416                 response = dialog.run()
417                 if response == gtk.RESPONSE_OK:
418                         patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
419                                         self.buffer.get_end_iter())
420                         fp = open(dialog.get_filename(), "w")
421                         fp.write(patch_buffer)
422                         fp.close()
423                 dialog.destroy()
424
425 class GitView:
426         """ This is the main class
427         """
428         version = "0.8"
429
430         def __init__(self, with_diff=0):
431                 self.with_diff = with_diff
432                 self.window =   gtk.Window(gtk.WINDOW_TOPLEVEL)
433                 self.window.set_border_width(0)
434                 self.window.set_title("Git repository browser")
435
436                 self.get_encoding()
437                 self.get_bt_sha1()
438
439                 # Use three-quarters of the screen by default
440                 screen = self.window.get_screen()
441                 monitor = screen.get_monitor_geometry(0)
442                 width = int(monitor.width * 0.75)
443                 height = int(monitor.height * 0.75)
444                 self.window.set_default_size(width, height)
445
446                 # FIXME AndyFitz!
447                 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
448                 self.window.set_icon(icon)
449
450                 self.accel_group = gtk.AccelGroup()
451                 self.window.add_accel_group(self.accel_group)
452                 self.accel_group.connect_group(0xffc2, 0, gtk.ACCEL_LOCKED, self.refresh);
453
454                 self.window.add(self.construct())
455
456         def refresh(self, widget, event=None, *arguments, **keywords):
457                 self.get_encoding()
458                 self.get_bt_sha1()
459                 Commit.children_sha1 = {}
460                 self.set_branch(sys.argv[without_diff:])
461                 self.window.show()
462                 return True
463
464         def get_bt_sha1(self):
465                 """ Update the bt_sha1 dictionary with the
466                 respective sha1 details """
467
468                 self.bt_sha1 = { }
469                 ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
470                 fp = os.popen('git ls-remote "${GIT_DIR-.git}"')
471                 while 1:
472                         line = string.strip(fp.readline())
473                         if line == '':
474                                 break
475                         m = ls_remote.match(line)
476                         if not m:
477                                 continue
478                         (sha1, name) = (m.group(1), m.group(2))
479                         if not self.bt_sha1.has_key(sha1):
480                                 self.bt_sha1[sha1] = []
481                         self.bt_sha1[sha1].append(name)
482                 fp.close()
483
484         def get_encoding(self):
485                 fp = os.popen("git repo-config --get i18n.commitencoding")
486                 self.encoding=string.strip(fp.readline())
487                 fp.close()
488                 if (self.encoding == ""):
489                         self.encoding = "utf-8"
490
491
492         def construct(self):
493                 """Construct the window contents."""
494                 vbox = gtk.VBox()
495                 paned = gtk.VPaned()
496                 paned.pack1(self.construct_top(), resize=False, shrink=True)
497                 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
498                 menu_bar = gtk.MenuBar()
499                 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
500                 help_menu = gtk.MenuItem("Help")
501                 menu = gtk.Menu()
502                 about_menu = gtk.MenuItem("About")
503                 menu.append(about_menu)
504                 about_menu.connect("activate", self.about_menu_response, "about")
505                 about_menu.show()
506                 help_menu.set_submenu(menu)
507                 help_menu.show()
508                 menu_bar.append(help_menu)
509                 menu_bar.show()
510                 vbox.pack_start(menu_bar, expand=False, fill=True)
511                 vbox.pack_start(paned, expand=True, fill=True)
512                 paned.show()
513                 vbox.show()
514                 return vbox
515
516
517         def construct_top(self):
518                 """Construct the top-half of the window."""
519                 vbox = gtk.VBox(spacing=6)
520                 vbox.set_border_width(12)
521                 vbox.show()
522
523
524                 scrollwin = gtk.ScrolledWindow()
525                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
526                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
527                 vbox.pack_start(scrollwin, expand=True, fill=True)
528                 scrollwin.show()
529
530                 self.treeview = gtk.TreeView()
531                 self.treeview.set_rules_hint(True)
532                 self.treeview.set_search_column(4)
533                 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
534                 scrollwin.add(self.treeview)
535                 self.treeview.show()
536
537                 cell = CellRendererGraph()
538                 column = gtk.TreeViewColumn()
539                 column.set_resizable(True)
540                 column.pack_start(cell, expand=True)
541                 column.add_attribute(cell, "node", 1)
542                 column.add_attribute(cell, "in-lines", 2)
543                 column.add_attribute(cell, "out-lines", 3)
544                 self.treeview.append_column(column)
545
546                 cell = gtk.CellRendererText()
547                 cell.set_property("width-chars", 65)
548                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
549                 column = gtk.TreeViewColumn("Message")
550                 column.set_resizable(True)
551                 column.pack_start(cell, expand=True)
552                 column.add_attribute(cell, "text", 4)
553                 self.treeview.append_column(column)
554
555                 cell = gtk.CellRendererText()
556                 cell.set_property("width-chars", 40)
557                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
558                 column = gtk.TreeViewColumn("Author")
559                 column.set_resizable(True)
560                 column.pack_start(cell, expand=True)
561                 column.add_attribute(cell, "text", 5)
562                 self.treeview.append_column(column)
563
564                 cell = gtk.CellRendererText()
565                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
566                 column = gtk.TreeViewColumn("Date")
567                 column.set_resizable(True)
568                 column.pack_start(cell, expand=True)
569                 column.add_attribute(cell, "text", 6)
570                 self.treeview.append_column(column)
571
572                 return vbox
573
574         def about_menu_response(self, widget, string):
575                 dialog = gtk.AboutDialog()
576                 dialog.set_name("Gitview")
577                 dialog.set_version(GitView.version)
578                 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
579                 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
580                 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
581                 dialog.set_wrap_license(True)
582                 dialog.run()
583                 dialog.destroy()
584
585
586         def construct_bottom(self):
587                 """Construct the bottom half of the window."""
588                 vbox = gtk.VBox(False, spacing=6)
589                 vbox.set_border_width(12)
590                 (width, height) = self.window.get_size()
591                 vbox.set_size_request(width, int(height / 2.5))
592                 vbox.show()
593
594                 self.table = gtk.Table(rows=4, columns=4)
595                 self.table.set_row_spacings(6)
596                 self.table.set_col_spacings(6)
597                 vbox.pack_start(self.table, expand=False, fill=True)
598                 self.table.show()
599
600                 align = gtk.Alignment(0.0, 0.5)
601                 label = gtk.Label()
602                 label.set_markup("<b>Revision:</b>")
603                 align.add(label)
604                 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
605                 label.show()
606                 align.show()
607
608                 align = gtk.Alignment(0.0, 0.5)
609                 self.revid_label = gtk.Label()
610                 self.revid_label.set_selectable(True)
611                 align.add(self.revid_label)
612                 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
613                 self.revid_label.show()
614                 align.show()
615
616                 align = gtk.Alignment(0.0, 0.5)
617                 label = gtk.Label()
618                 label.set_markup("<b>Committer:</b>")
619                 align.add(label)
620                 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
621                 label.show()
622                 align.show()
623
624                 align = gtk.Alignment(0.0, 0.5)
625                 self.committer_label = gtk.Label()
626                 self.committer_label.set_selectable(True)
627                 align.add(self.committer_label)
628                 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
629                 self.committer_label.show()
630                 align.show()
631
632                 align = gtk.Alignment(0.0, 0.5)
633                 label = gtk.Label()
634                 label.set_markup("<b>Timestamp:</b>")
635                 align.add(label)
636                 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
637                 label.show()
638                 align.show()
639
640                 align = gtk.Alignment(0.0, 0.5)
641                 self.timestamp_label = gtk.Label()
642                 self.timestamp_label.set_selectable(True)
643                 align.add(self.timestamp_label)
644                 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
645                 self.timestamp_label.show()
646                 align.show()
647
648                 align = gtk.Alignment(0.0, 0.5)
649                 label = gtk.Label()
650                 label.set_markup("<b>Parents:</b>")
651                 align.add(label)
652                 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
653                 label.show()
654                 align.show()
655                 self.parents_widgets = []
656
657                 align = gtk.Alignment(0.0, 0.5)
658                 label = gtk.Label()
659                 label.set_markup("<b>Children:</b>")
660                 align.add(label)
661                 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
662                 label.show()
663                 align.show()
664                 self.children_widgets = []
665
666                 scrollwin = gtk.ScrolledWindow()
667                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
668                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
669                 vbox.pack_start(scrollwin, expand=True, fill=True)
670                 scrollwin.show()
671
672                 if have_gtksourceview:
673                         self.message_buffer = gtksourceview.SourceBuffer()
674                         slm = gtksourceview.SourceLanguagesManager()
675                         gsl = slm.get_language_from_mime_type("text/x-patch")
676                         self.message_buffer.set_highlight(True)
677                         self.message_buffer.set_language(gsl)
678                         sourceview = gtksourceview.SourceView(self.message_buffer)
679                 else:
680                         self.message_buffer = gtk.TextBuffer()
681                         sourceview = gtk.TextView(self.message_buffer)
682
683                 sourceview.set_editable(False)
684                 sourceview.modify_font(pango.FontDescription("Monospace"))
685                 scrollwin.add(sourceview)
686                 sourceview.show()
687
688                 return vbox
689
690         def _treeview_cursor_cb(self, *args):
691                 """Callback for when the treeview cursor changes."""
692                 (path, col) = self.treeview.get_cursor()
693                 commit = self.model[path][0]
694
695                 if commit.committer is not None:
696                         committer = commit.committer
697                         timestamp = commit.commit_date
698                         message   =  commit.get_message(self.with_diff)
699                         revid_label = commit.commit_sha1
700                 else:
701                         committer = ""
702                         timestamp = ""
703                         message = ""
704                         revid_label = ""
705
706                 self.revid_label.set_text(revid_label)
707                 self.committer_label.set_text(committer)
708                 self.timestamp_label.set_text(timestamp)
709                 self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
710
711                 for widget in self.parents_widgets:
712                         self.table.remove(widget)
713
714                 self.parents_widgets = []
715                 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
716                 for idx, parent_id in enumerate(commit.parent_sha1):
717                         self.table.set_row_spacing(idx + 3, 0)
718
719                         align = gtk.Alignment(0.0, 0.0)
720                         self.parents_widgets.append(align)
721                         self.table.attach(align, 1, 2, idx + 3, idx + 4,
722                                         gtk.EXPAND | gtk.FILL, gtk.FILL)
723                         align.show()
724
725                         hbox = gtk.HBox(False, 0)
726                         align.add(hbox)
727                         hbox.show()
728
729                         label = gtk.Label(parent_id)
730                         label.set_selectable(True)
731                         hbox.pack_start(label, expand=False, fill=True)
732                         label.show()
733
734                         image = gtk.Image()
735                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
736                         image.show()
737
738                         button = gtk.Button()
739                         button.add(image)
740                         button.set_relief(gtk.RELIEF_NONE)
741                         button.connect("clicked", self._go_clicked_cb, parent_id)
742                         hbox.pack_start(button, expand=False, fill=True)
743                         button.show()
744
745                         image = gtk.Image()
746                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
747                         image.show()
748
749                         button = gtk.Button()
750                         button.add(image)
751                         button.set_relief(gtk.RELIEF_NONE)
752                         button.set_sensitive(True)
753                         button.connect("clicked", self._show_clicked_cb,
754                                         commit.commit_sha1, parent_id, self.encoding)
755                         hbox.pack_start(button, expand=False, fill=True)
756                         button.show()
757
758                 # Populate with child details
759                 for widget in self.children_widgets:
760                         self.table.remove(widget)
761
762                 self.children_widgets = []
763                 try:
764                         child_sha1 = Commit.children_sha1[commit.commit_sha1]
765                 except KeyError:
766                         # We don't have child
767                         child_sha1 = [ 0 ]
768
769                 if ( len(child_sha1) > len(commit.parent_sha1)):
770                         self.table.resize(4 + len(child_sha1) - 1, 4)
771
772                 for idx, child_id in enumerate(child_sha1):
773                         self.table.set_row_spacing(idx + 3, 0)
774
775                         align = gtk.Alignment(0.0, 0.0)
776                         self.children_widgets.append(align)
777                         self.table.attach(align, 3, 4, idx + 3, idx + 4,
778                                         gtk.EXPAND | gtk.FILL, gtk.FILL)
779                         align.show()
780
781                         hbox = gtk.HBox(False, 0)
782                         align.add(hbox)
783                         hbox.show()
784
785                         label = gtk.Label(child_id)
786                         label.set_selectable(True)
787                         hbox.pack_start(label, expand=False, fill=True)
788                         label.show()
789
790                         image = gtk.Image()
791                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
792                         image.show()
793
794                         button = gtk.Button()
795                         button.add(image)
796                         button.set_relief(gtk.RELIEF_NONE)
797                         button.connect("clicked", self._go_clicked_cb, child_id)
798                         hbox.pack_start(button, expand=False, fill=True)
799                         button.show()
800
801                         image = gtk.Image()
802                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
803                         image.show()
804
805                         button = gtk.Button()
806                         button.add(image)
807                         button.set_relief(gtk.RELIEF_NONE)
808                         button.set_sensitive(True)
809                         button.connect("clicked", self._show_clicked_cb,
810                                         child_id, commit.commit_sha1, self.encoding)
811                         hbox.pack_start(button, expand=False, fill=True)
812                         button.show()
813
814         def _destroy_cb(self, widget):
815                 """Callback for when a window we manage is destroyed."""
816                 self.quit()
817
818
819         def quit(self):
820                 """Stop the GTK+ main loop."""
821                 gtk.main_quit()
822
823         def run(self, args):
824                 self.set_branch(args)
825                 self.window.connect("destroy", self._destroy_cb)
826                 self.window.show()
827                 gtk.main()
828
829         def set_branch(self, args):
830                 """Fill in different windows with info from the reposiroty"""
831                 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
832                 git_rev_list_cmd = fp.read()
833                 fp.close()
834                 fp = os.popen("git rev-list  --header --topo-order --parents " + git_rev_list_cmd)
835                 self.update_window(fp)
836
837         def update_window(self, fp):
838                 commit_lines = []
839
840                 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
841                                 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
842
843                 # used for cursor positioning
844                 self.index = {}
845
846                 self.colours = {}
847                 self.nodepos = {}
848                 self.incomplete_line = {}
849                 self.commits = []
850
851                 index = 0
852                 last_colour = 0
853                 last_nodepos = -1
854                 out_line = []
855                 input_line = fp.readline()
856                 while (input_line != ""):
857                         # The commit header ends with '\0'
858                         # This NULL is immediately followed by the sha1 of the
859                         # next commit
860                         if (input_line[0] != '\0'):
861                                 commit_lines.append(input_line)
862                                 input_line = fp.readline()
863                                 continue;
864
865                         commit = Commit(commit_lines)
866                         if (commit != None ):
867                                 self.commits.append(commit)
868
869                         # Skip the '\0
870                         commit_lines = []
871                         commit_lines.append(input_line[1:])
872                         input_line = fp.readline()
873
874                 fp.close()
875
876                 for commit in self.commits:
877                         (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
878                                                                                 index, out_line,
879                                                                                 last_colour,
880                                                                                 last_nodepos)
881                         self.index[commit.commit_sha1] = index
882                         index += 1
883
884                 self.treeview.set_model(self.model)
885                 self.treeview.show()
886
887         def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
888                 in_line=[]
889
890                 #   |   -> outline
891                 #   X
892                 #   |\  <- inline
893
894                 # Reset nodepostion
895                 if (last_nodepos > 5):
896                         last_nodepos = -1
897
898                 # Add the incomplete lines of the last cell in this
899                 try:
900                         colour = self.colours[commit.commit_sha1]
901                 except KeyError:
902                         self.colours[commit.commit_sha1] = last_colour+1
903                         last_colour = self.colours[commit.commit_sha1]
904                         colour =   self.colours[commit.commit_sha1]
905
906                 try:
907                         node_pos = self.nodepos[commit.commit_sha1]
908                 except KeyError:
909                         self.nodepos[commit.commit_sha1] = last_nodepos+1
910                         last_nodepos = self.nodepos[commit.commit_sha1]
911                         node_pos =  self.nodepos[commit.commit_sha1]
912
913                 #The first parent always continue on the same line
914                 try:
915                         # check we alreay have the value
916                         tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
917                 except KeyError:
918                         self.colours[commit.parent_sha1[0]] = colour
919                         self.nodepos[commit.parent_sha1[0]] = node_pos
920
921                 for sha1 in self.incomplete_line.keys():
922                         if (sha1 != commit.commit_sha1):
923                                 self.draw_incomplete_line(sha1, node_pos,
924                                                 out_line, in_line, index)
925                         else:
926                                 del self.incomplete_line[sha1]
927
928
929                 for parent_id in commit.parent_sha1:
930                         try:
931                                 tmp_node_pos = self.nodepos[parent_id]
932                         except KeyError:
933                                 self.colours[parent_id] = last_colour+1
934                                 last_colour = self.colours[parent_id]
935                                 self.nodepos[parent_id] = last_nodepos+1
936                                 last_nodepos = self.nodepos[parent_id]
937
938                         in_line.append((node_pos, self.nodepos[parent_id],
939                                                 self.colours[parent_id]))
940                         self.add_incomplete_line(parent_id)
941
942                 try:
943                         branch_tag = self.bt_sha1[commit.commit_sha1]
944                 except KeyError:
945                         branch_tag = [ ]
946
947
948                 node = (node_pos, colour, branch_tag)
949
950                 self.model.append([commit, node, out_line, in_line,
951                                 commit.message, commit.author, commit.date])
952
953                 return (in_line, last_colour, last_nodepos)
954
955         def add_incomplete_line(self, sha1):
956                 try:
957                         self.incomplete_line[sha1].append(self.nodepos[sha1])
958                 except KeyError:
959                         self.incomplete_line[sha1] = [self.nodepos[sha1]]
960
961         def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
962                 for idx, pos in enumerate(self.incomplete_line[sha1]):
963                         if(pos == node_pos):
964                                 #remove the straight line and add a slash
965                                 if ((pos, pos, self.colours[sha1]) in out_line):
966                                         out_line.remove((pos, pos, self.colours[sha1]))
967                                 out_line.append((pos, pos+0.5, self.colours[sha1]))
968                                 self.incomplete_line[sha1][idx] = pos = pos+0.5
969                         try:
970                                 next_commit = self.commits[index+1]
971                                 if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
972                                 # join the line back to the node point
973                                 # This need to be done only if we modified it
974                                         in_line.append((pos, pos-0.5, self.colours[sha1]))
975                                         continue;
976                         except IndexError:
977                                 pass
978                         in_line.append((pos, pos, self.colours[sha1]))
979
980
981         def _go_clicked_cb(self, widget, revid):
982                 """Callback for when the go button for a parent is clicked."""
983                 try:
984                         self.treeview.set_cursor(self.index[revid])
985                 except KeyError:
986                         print "Revision %s not present in the list" % revid
987                         # revid == 0 is the parent of the first commit
988                         if (revid != 0 ):
989                                 print "Try running gitview without any options"
990
991                 self.treeview.grab_focus()
992
993         def _show_clicked_cb(self, widget,  commit_sha1, parent_sha1, encoding):
994                 """Callback for when the show button for a parent is clicked."""
995                 window = DiffWindow()
996                 window.set_diff(commit_sha1, parent_sha1, encoding)
997                 self.treeview.grab_focus()
998
999 without_diff = 0
1000 if __name__ == "__main__":
1001
1002         if (len(sys.argv) > 1 ):
1003                 if (sys.argv[1] == "--without-diff"):
1004                         without_diff = 1
1005
1006         view = GitView( without_diff != 1)
1007         view.run(sys.argv[without_diff:])
1008
1009