gitview: Add some useful keybindings.
[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                 self.accel_group.connect_group(0xffc1, 0, gtk.ACCEL_LOCKED, self.maximize);
454                 self.accel_group.connect_group(0xffc8, 0, gtk.ACCEL_LOCKED, self.fullscreen);
455                 self.accel_group.connect_group(0xffc9, 0, gtk.ACCEL_LOCKED, self.unfullscreen);
456
457                 self.window.add(self.construct())
458
459         def refresh(self, widget, event=None, *arguments, **keywords):
460                 self.get_encoding()
461                 self.get_bt_sha1()
462                 Commit.children_sha1 = {}
463                 self.set_branch(sys.argv[without_diff:])
464                 self.window.show()
465                 return True
466
467         def maximize(self, widget, event=None, *arguments, **keywords):
468                 self.window.maximize()
469                 return True
470
471         def fullscreen(self, widget, event=None, *arguments, **keywords):
472                 self.window.fullscreen()
473                 return True
474
475         def unfullscreen(self, widget, event=None, *arguments, **keywords):
476                 self.window.unfullscreen()
477                 return True
478
479         def get_bt_sha1(self):
480                 """ Update the bt_sha1 dictionary with the
481                 respective sha1 details """
482
483                 self.bt_sha1 = { }
484                 ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
485                 fp = os.popen('git ls-remote "${GIT_DIR-.git}"')
486                 while 1:
487                         line = string.strip(fp.readline())
488                         if line == '':
489                                 break
490                         m = ls_remote.match(line)
491                         if not m:
492                                 continue
493                         (sha1, name) = (m.group(1), m.group(2))
494                         if not self.bt_sha1.has_key(sha1):
495                                 self.bt_sha1[sha1] = []
496                         self.bt_sha1[sha1].append(name)
497                 fp.close()
498
499         def get_encoding(self):
500                 fp = os.popen("git repo-config --get i18n.commitencoding")
501                 self.encoding=string.strip(fp.readline())
502                 fp.close()
503                 if (self.encoding == ""):
504                         self.encoding = "utf-8"
505
506
507         def construct(self):
508                 """Construct the window contents."""
509                 vbox = gtk.VBox()
510                 paned = gtk.VPaned()
511                 paned.pack1(self.construct_top(), resize=False, shrink=True)
512                 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
513                 menu_bar = gtk.MenuBar()
514                 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
515                 help_menu = gtk.MenuItem("Help")
516                 menu = gtk.Menu()
517                 about_menu = gtk.MenuItem("About")
518                 menu.append(about_menu)
519                 about_menu.connect("activate", self.about_menu_response, "about")
520                 about_menu.show()
521                 help_menu.set_submenu(menu)
522                 help_menu.show()
523                 menu_bar.append(help_menu)
524                 menu_bar.show()
525                 vbox.pack_start(menu_bar, expand=False, fill=True)
526                 vbox.pack_start(paned, expand=True, fill=True)
527                 paned.show()
528                 vbox.show()
529                 return vbox
530
531
532         def construct_top(self):
533                 """Construct the top-half of the window."""
534                 vbox = gtk.VBox(spacing=6)
535                 vbox.set_border_width(12)
536                 vbox.show()
537
538
539                 scrollwin = gtk.ScrolledWindow()
540                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
541                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
542                 vbox.pack_start(scrollwin, expand=True, fill=True)
543                 scrollwin.show()
544
545                 self.treeview = gtk.TreeView()
546                 self.treeview.set_rules_hint(True)
547                 self.treeview.set_search_column(4)
548                 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
549                 scrollwin.add(self.treeview)
550                 self.treeview.show()
551
552                 cell = CellRendererGraph()
553                 column = gtk.TreeViewColumn()
554                 column.set_resizable(True)
555                 column.pack_start(cell, expand=True)
556                 column.add_attribute(cell, "node", 1)
557                 column.add_attribute(cell, "in-lines", 2)
558                 column.add_attribute(cell, "out-lines", 3)
559                 self.treeview.append_column(column)
560
561                 cell = gtk.CellRendererText()
562                 cell.set_property("width-chars", 65)
563                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
564                 column = gtk.TreeViewColumn("Message")
565                 column.set_resizable(True)
566                 column.pack_start(cell, expand=True)
567                 column.add_attribute(cell, "text", 4)
568                 self.treeview.append_column(column)
569
570                 cell = gtk.CellRendererText()
571                 cell.set_property("width-chars", 40)
572                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
573                 column = gtk.TreeViewColumn("Author")
574                 column.set_resizable(True)
575                 column.pack_start(cell, expand=True)
576                 column.add_attribute(cell, "text", 5)
577                 self.treeview.append_column(column)
578
579                 cell = gtk.CellRendererText()
580                 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
581                 column = gtk.TreeViewColumn("Date")
582                 column.set_resizable(True)
583                 column.pack_start(cell, expand=True)
584                 column.add_attribute(cell, "text", 6)
585                 self.treeview.append_column(column)
586
587                 return vbox
588
589         def about_menu_response(self, widget, string):
590                 dialog = gtk.AboutDialog()
591                 dialog.set_name("Gitview")
592                 dialog.set_version(GitView.version)
593                 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
594                 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
595                 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
596                 dialog.set_wrap_license(True)
597                 dialog.run()
598                 dialog.destroy()
599
600
601         def construct_bottom(self):
602                 """Construct the bottom half of the window."""
603                 vbox = gtk.VBox(False, spacing=6)
604                 vbox.set_border_width(12)
605                 (width, height) = self.window.get_size()
606                 vbox.set_size_request(width, int(height / 2.5))
607                 vbox.show()
608
609                 self.table = gtk.Table(rows=4, columns=4)
610                 self.table.set_row_spacings(6)
611                 self.table.set_col_spacings(6)
612                 vbox.pack_start(self.table, expand=False, fill=True)
613                 self.table.show()
614
615                 align = gtk.Alignment(0.0, 0.5)
616                 label = gtk.Label()
617                 label.set_markup("<b>Revision:</b>")
618                 align.add(label)
619                 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
620                 label.show()
621                 align.show()
622
623                 align = gtk.Alignment(0.0, 0.5)
624                 self.revid_label = gtk.Label()
625                 self.revid_label.set_selectable(True)
626                 align.add(self.revid_label)
627                 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
628                 self.revid_label.show()
629                 align.show()
630
631                 align = gtk.Alignment(0.0, 0.5)
632                 label = gtk.Label()
633                 label.set_markup("<b>Committer:</b>")
634                 align.add(label)
635                 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
636                 label.show()
637                 align.show()
638
639                 align = gtk.Alignment(0.0, 0.5)
640                 self.committer_label = gtk.Label()
641                 self.committer_label.set_selectable(True)
642                 align.add(self.committer_label)
643                 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
644                 self.committer_label.show()
645                 align.show()
646
647                 align = gtk.Alignment(0.0, 0.5)
648                 label = gtk.Label()
649                 label.set_markup("<b>Timestamp:</b>")
650                 align.add(label)
651                 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
652                 label.show()
653                 align.show()
654
655                 align = gtk.Alignment(0.0, 0.5)
656                 self.timestamp_label = gtk.Label()
657                 self.timestamp_label.set_selectable(True)
658                 align.add(self.timestamp_label)
659                 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
660                 self.timestamp_label.show()
661                 align.show()
662
663                 align = gtk.Alignment(0.0, 0.5)
664                 label = gtk.Label()
665                 label.set_markup("<b>Parents:</b>")
666                 align.add(label)
667                 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
668                 label.show()
669                 align.show()
670                 self.parents_widgets = []
671
672                 align = gtk.Alignment(0.0, 0.5)
673                 label = gtk.Label()
674                 label.set_markup("<b>Children:</b>")
675                 align.add(label)
676                 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
677                 label.show()
678                 align.show()
679                 self.children_widgets = []
680
681                 scrollwin = gtk.ScrolledWindow()
682                 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
683                 scrollwin.set_shadow_type(gtk.SHADOW_IN)
684                 vbox.pack_start(scrollwin, expand=True, fill=True)
685                 scrollwin.show()
686
687                 if have_gtksourceview:
688                         self.message_buffer = gtksourceview.SourceBuffer()
689                         slm = gtksourceview.SourceLanguagesManager()
690                         gsl = slm.get_language_from_mime_type("text/x-patch")
691                         self.message_buffer.set_highlight(True)
692                         self.message_buffer.set_language(gsl)
693                         sourceview = gtksourceview.SourceView(self.message_buffer)
694                 else:
695                         self.message_buffer = gtk.TextBuffer()
696                         sourceview = gtk.TextView(self.message_buffer)
697
698                 sourceview.set_editable(False)
699                 sourceview.modify_font(pango.FontDescription("Monospace"))
700                 scrollwin.add(sourceview)
701                 sourceview.show()
702
703                 return vbox
704
705         def _treeview_cursor_cb(self, *args):
706                 """Callback for when the treeview cursor changes."""
707                 (path, col) = self.treeview.get_cursor()
708                 commit = self.model[path][0]
709
710                 if commit.committer is not None:
711                         committer = commit.committer
712                         timestamp = commit.commit_date
713                         message   =  commit.get_message(self.with_diff)
714                         revid_label = commit.commit_sha1
715                 else:
716                         committer = ""
717                         timestamp = ""
718                         message = ""
719                         revid_label = ""
720
721                 self.revid_label.set_text(revid_label)
722                 self.committer_label.set_text(committer)
723                 self.timestamp_label.set_text(timestamp)
724                 self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
725
726                 for widget in self.parents_widgets:
727                         self.table.remove(widget)
728
729                 self.parents_widgets = []
730                 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
731                 for idx, parent_id in enumerate(commit.parent_sha1):
732                         self.table.set_row_spacing(idx + 3, 0)
733
734                         align = gtk.Alignment(0.0, 0.0)
735                         self.parents_widgets.append(align)
736                         self.table.attach(align, 1, 2, idx + 3, idx + 4,
737                                         gtk.EXPAND | gtk.FILL, gtk.FILL)
738                         align.show()
739
740                         hbox = gtk.HBox(False, 0)
741                         align.add(hbox)
742                         hbox.show()
743
744                         label = gtk.Label(parent_id)
745                         label.set_selectable(True)
746                         hbox.pack_start(label, expand=False, fill=True)
747                         label.show()
748
749                         image = gtk.Image()
750                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
751                         image.show()
752
753                         button = gtk.Button()
754                         button.add(image)
755                         button.set_relief(gtk.RELIEF_NONE)
756                         button.connect("clicked", self._go_clicked_cb, parent_id)
757                         hbox.pack_start(button, expand=False, fill=True)
758                         button.show()
759
760                         image = gtk.Image()
761                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
762                         image.show()
763
764                         button = gtk.Button()
765                         button.add(image)
766                         button.set_relief(gtk.RELIEF_NONE)
767                         button.set_sensitive(True)
768                         button.connect("clicked", self._show_clicked_cb,
769                                         commit.commit_sha1, parent_id, self.encoding)
770                         hbox.pack_start(button, expand=False, fill=True)
771                         button.show()
772
773                 # Populate with child details
774                 for widget in self.children_widgets:
775                         self.table.remove(widget)
776
777                 self.children_widgets = []
778                 try:
779                         child_sha1 = Commit.children_sha1[commit.commit_sha1]
780                 except KeyError:
781                         # We don't have child
782                         child_sha1 = [ 0 ]
783
784                 if ( len(child_sha1) > len(commit.parent_sha1)):
785                         self.table.resize(4 + len(child_sha1) - 1, 4)
786
787                 for idx, child_id in enumerate(child_sha1):
788                         self.table.set_row_spacing(idx + 3, 0)
789
790                         align = gtk.Alignment(0.0, 0.0)
791                         self.children_widgets.append(align)
792                         self.table.attach(align, 3, 4, idx + 3, idx + 4,
793                                         gtk.EXPAND | gtk.FILL, gtk.FILL)
794                         align.show()
795
796                         hbox = gtk.HBox(False, 0)
797                         align.add(hbox)
798                         hbox.show()
799
800                         label = gtk.Label(child_id)
801                         label.set_selectable(True)
802                         hbox.pack_start(label, expand=False, fill=True)
803                         label.show()
804
805                         image = gtk.Image()
806                         image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
807                         image.show()
808
809                         button = gtk.Button()
810                         button.add(image)
811                         button.set_relief(gtk.RELIEF_NONE)
812                         button.connect("clicked", self._go_clicked_cb, child_id)
813                         hbox.pack_start(button, expand=False, fill=True)
814                         button.show()
815
816                         image = gtk.Image()
817                         image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
818                         image.show()
819
820                         button = gtk.Button()
821                         button.add(image)
822                         button.set_relief(gtk.RELIEF_NONE)
823                         button.set_sensitive(True)
824                         button.connect("clicked", self._show_clicked_cb,
825                                         child_id, commit.commit_sha1, self.encoding)
826                         hbox.pack_start(button, expand=False, fill=True)
827                         button.show()
828
829         def _destroy_cb(self, widget):
830                 """Callback for when a window we manage is destroyed."""
831                 self.quit()
832
833
834         def quit(self):
835                 """Stop the GTK+ main loop."""
836                 gtk.main_quit()
837
838         def run(self, args):
839                 self.set_branch(args)
840                 self.window.connect("destroy", self._destroy_cb)
841                 self.window.show()
842                 gtk.main()
843
844         def set_branch(self, args):
845                 """Fill in different windows with info from the reposiroty"""
846                 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
847                 git_rev_list_cmd = fp.read()
848                 fp.close()
849                 fp = os.popen("git rev-list  --header --topo-order --parents " + git_rev_list_cmd)
850                 self.update_window(fp)
851
852         def update_window(self, fp):
853                 commit_lines = []
854
855                 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
856                                 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
857
858                 # used for cursor positioning
859                 self.index = {}
860
861                 self.colours = {}
862                 self.nodepos = {}
863                 self.incomplete_line = {}
864                 self.commits = []
865
866                 index = 0
867                 last_colour = 0
868                 last_nodepos = -1
869                 out_line = []
870                 input_line = fp.readline()
871                 while (input_line != ""):
872                         # The commit header ends with '\0'
873                         # This NULL is immediately followed by the sha1 of the
874                         # next commit
875                         if (input_line[0] != '\0'):
876                                 commit_lines.append(input_line)
877                                 input_line = fp.readline()
878                                 continue;
879
880                         commit = Commit(commit_lines)
881                         if (commit != None ):
882                                 self.commits.append(commit)
883
884                         # Skip the '\0
885                         commit_lines = []
886                         commit_lines.append(input_line[1:])
887                         input_line = fp.readline()
888
889                 fp.close()
890
891                 for commit in self.commits:
892                         (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
893                                                                                 index, out_line,
894                                                                                 last_colour,
895                                                                                 last_nodepos)
896                         self.index[commit.commit_sha1] = index
897                         index += 1
898
899                 self.treeview.set_model(self.model)
900                 self.treeview.show()
901
902         def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
903                 in_line=[]
904
905                 #   |   -> outline
906                 #   X
907                 #   |\  <- inline
908
909                 # Reset nodepostion
910                 if (last_nodepos > 5):
911                         last_nodepos = -1
912
913                 # Add the incomplete lines of the last cell in this
914                 try:
915                         colour = self.colours[commit.commit_sha1]
916                 except KeyError:
917                         self.colours[commit.commit_sha1] = last_colour+1
918                         last_colour = self.colours[commit.commit_sha1]
919                         colour =   self.colours[commit.commit_sha1]
920
921                 try:
922                         node_pos = self.nodepos[commit.commit_sha1]
923                 except KeyError:
924                         self.nodepos[commit.commit_sha1] = last_nodepos+1
925                         last_nodepos = self.nodepos[commit.commit_sha1]
926                         node_pos =  self.nodepos[commit.commit_sha1]
927
928                 #The first parent always continue on the same line
929                 try:
930                         # check we alreay have the value
931                         tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
932                 except KeyError:
933                         self.colours[commit.parent_sha1[0]] = colour
934                         self.nodepos[commit.parent_sha1[0]] = node_pos
935
936                 for sha1 in self.incomplete_line.keys():
937                         if (sha1 != commit.commit_sha1):
938                                 self.draw_incomplete_line(sha1, node_pos,
939                                                 out_line, in_line, index)
940                         else:
941                                 del self.incomplete_line[sha1]
942
943
944                 for parent_id in commit.parent_sha1:
945                         try:
946                                 tmp_node_pos = self.nodepos[parent_id]
947                         except KeyError:
948                                 self.colours[parent_id] = last_colour+1
949                                 last_colour = self.colours[parent_id]
950                                 self.nodepos[parent_id] = last_nodepos+1
951                                 last_nodepos = self.nodepos[parent_id]
952
953                         in_line.append((node_pos, self.nodepos[parent_id],
954                                                 self.colours[parent_id]))
955                         self.add_incomplete_line(parent_id)
956
957                 try:
958                         branch_tag = self.bt_sha1[commit.commit_sha1]
959                 except KeyError:
960                         branch_tag = [ ]
961
962
963                 node = (node_pos, colour, branch_tag)
964
965                 self.model.append([commit, node, out_line, in_line,
966                                 commit.message, commit.author, commit.date])
967
968                 return (in_line, last_colour, last_nodepos)
969
970         def add_incomplete_line(self, sha1):
971                 try:
972                         self.incomplete_line[sha1].append(self.nodepos[sha1])
973                 except KeyError:
974                         self.incomplete_line[sha1] = [self.nodepos[sha1]]
975
976         def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
977                 for idx, pos in enumerate(self.incomplete_line[sha1]):
978                         if(pos == node_pos):
979                                 #remove the straight line and add a slash
980                                 if ((pos, pos, self.colours[sha1]) in out_line):
981                                         out_line.remove((pos, pos, self.colours[sha1]))
982                                 out_line.append((pos, pos+0.5, self.colours[sha1]))
983                                 self.incomplete_line[sha1][idx] = pos = pos+0.5
984                         try:
985                                 next_commit = self.commits[index+1]
986                                 if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
987                                 # join the line back to the node point
988                                 # This need to be done only if we modified it
989                                         in_line.append((pos, pos-0.5, self.colours[sha1]))
990                                         continue;
991                         except IndexError:
992                                 pass
993                         in_line.append((pos, pos, self.colours[sha1]))
994
995
996         def _go_clicked_cb(self, widget, revid):
997                 """Callback for when the go button for a parent is clicked."""
998                 try:
999                         self.treeview.set_cursor(self.index[revid])
1000                 except KeyError:
1001                         dialog = gtk.MessageDialog(parent=None, flags=0,
1002                                         type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
1003                                         message_format=None)
1004                         dialog.set_markup("Revision <b>%s</b> not present in the list" % revid)
1005                         # revid == 0 is the parent of the first commit
1006                         if (revid != 0 ):
1007                                 dialog.format_secondary_text("Try running gitview without any options")
1008                         dialog.run()
1009                         dialog.destroy()
1010
1011                 self.treeview.grab_focus()
1012
1013         def _show_clicked_cb(self, widget,  commit_sha1, parent_sha1, encoding):
1014                 """Callback for when the show button for a parent is clicked."""
1015                 window = DiffWindow()
1016                 window.set_diff(commit_sha1, parent_sha1, encoding)
1017                 self.treeview.grab_focus()
1018
1019 without_diff = 0
1020 if __name__ == "__main__":
1021
1022         if (len(sys.argv) > 1 ):
1023                 if (sys.argv[1] == "--without-diff"):
1024                         without_diff = 1
1025
1026         view = GitView( without_diff != 1)
1027         view.run(sys.argv[without_diff:])
1028
1029