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