aded7ede7028fc8dd3298fce81e4ad0da9d54a02
1 #! /usr/bin/env python
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
8 """ gitview
9 GUI browser for git repository
10 This program is based on bzrk by Scott James Remnant <scott@ubuntu.com>
11 """
12 __copyright__ = "Copyright (C) 2006 Hewlett-Packard Development Company, L.P."
13 __author__ = "Aneesh Kumar K.V <aneesh.kumar@hp.com>"
16 import sys
17 import os
18 import gtk
19 import pygtk
20 import pango
21 import re
22 import time
23 import gobject
24 import cairo
25 import math
26 import string
28 try:
29 import gtksourceview
30 have_gtksourceview = True
31 except ImportError:
32 have_gtksourceview = False
33 print "Running without gtksourceview module"
35 re_ident = re.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
37 def list_to_string(args, skip):
38 count = len(args)
39 i = skip
40 str_arg=" "
41 while (i < count ):
42 str_arg = str_arg + args[i]
43 str_arg = str_arg + " "
44 i = i+1
46 return str_arg
48 def show_date(epoch, tz):
49 secs = float(epoch)
50 tzsecs = float(tz[1:3]) * 3600
51 tzsecs += float(tz[3:5]) * 60
52 if (tz[0] == "+"):
53 secs += tzsecs
54 else:
55 secs -= tzsecs
57 return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs))
60 class CellRendererGraph(gtk.GenericCellRenderer):
61 """Cell renderer for directed graph.
63 This module contains the implementation of a custom GtkCellRenderer that
64 draws part of the directed graph based on the lines suggested by the code
65 in graph.py.
67 Because we're shiny, we use Cairo to do this, and because we're naughty
68 we cheat and draw over the bits of the TreeViewColumn that are supposed to
69 just be for the background.
71 Properties:
72 node (column, colour, [ names ]) tuple to draw revision node,
73 in_lines (start, end, colour) tuple list to draw inward lines,
74 out_lines (start, end, colour) tuple list to draw outward lines.
75 """
77 __gproperties__ = {
78 "node": ( gobject.TYPE_PYOBJECT, "node",
79 "revision node instruction",
80 gobject.PARAM_WRITABLE
81 ),
82 "in-lines": ( gobject.TYPE_PYOBJECT, "in-lines",
83 "instructions to draw lines into the cell",
84 gobject.PARAM_WRITABLE
85 ),
86 "out-lines": ( gobject.TYPE_PYOBJECT, "out-lines",
87 "instructions to draw lines out of the cell",
88 gobject.PARAM_WRITABLE
89 ),
90 }
92 def do_set_property(self, property, value):
93 """Set properties from GObject properties."""
94 if property.name == "node":
95 self.node = value
96 elif property.name == "in-lines":
97 self.in_lines = value
98 elif property.name == "out-lines":
99 self.out_lines = value
100 else:
101 raise AttributeError, "no such property: '%s'" % property.name
103 def box_size(self, widget):
104 """Calculate box size based on widget's font.
106 Cache this as it's probably expensive to get. It ensures that we
107 draw the graph at least as large as the text.
108 """
109 try:
110 return self._box_size
111 except AttributeError:
112 pango_ctx = widget.get_pango_context()
113 font_desc = widget.get_style().font_desc
114 metrics = pango_ctx.get_metrics(font_desc)
116 ascent = pango.PIXELS(metrics.get_ascent())
117 descent = pango.PIXELS(metrics.get_descent())
119 self._box_size = ascent + descent + 6
120 return self._box_size
122 def set_colour(self, ctx, colour, bg, fg):
123 """Set the context source colour.
125 Picks a distinct colour based on an internal wheel; the bg
126 parameter provides the value that should be assigned to the 'zero'
127 colours and the fg parameter provides the multiplier that should be
128 applied to the foreground colours.
129 """
130 colours = [
131 ( 1.0, 0.0, 0.0 ),
132 ( 1.0, 1.0, 0.0 ),
133 ( 0.0, 1.0, 0.0 ),
134 ( 0.0, 1.0, 1.0 ),
135 ( 0.0, 0.0, 1.0 ),
136 ( 1.0, 0.0, 1.0 ),
137 ]
139 colour %= len(colours)
140 red = (colours[colour][0] * fg) or bg
141 green = (colours[colour][1] * fg) or bg
142 blue = (colours[colour][2] * fg) or bg
144 ctx.set_source_rgb(red, green, blue)
146 def on_get_size(self, widget, cell_area):
147 """Return the size we need for this cell.
149 Each cell is drawn individually and is only as wide as it needs
150 to be, we let the TreeViewColumn take care of making them all
151 line up.
152 """
153 box_size = self.box_size(widget)
155 cols = self.node[0]
156 for start, end, colour in self.in_lines + self.out_lines:
157 cols = int(max(cols, start, end))
159 (column, colour, names) = self.node
160 names_len = 0
161 if (len(names) != 0):
162 for item in names:
163 names_len += len(item)
165 width = box_size * (cols + 1 ) + names_len
166 height = box_size
168 # FIXME I have no idea how to use cell_area properly
169 return (0, 0, width, height)
171 def on_render(self, window, widget, bg_area, cell_area, exp_area, flags):
172 """Render an individual cell.
174 Draws the cell contents using cairo, taking care to clip what we
175 do to within the background area so we don't draw over other cells.
176 Note that we're a bit naughty there and should really be drawing
177 in the cell_area (or even the exposed area), but we explicitly don't
178 want any gutter.
180 We try and be a little clever, if the line we need to draw is going
181 to cross other columns we actually draw it as in the .---' style
182 instead of a pure diagonal ... this reduces confusion by an
183 incredible amount.
184 """
185 ctx = window.cairo_create()
186 ctx.rectangle(bg_area.x, bg_area.y, bg_area.width, bg_area.height)
187 ctx.clip()
189 box_size = self.box_size(widget)
191 ctx.set_line_width(box_size / 8)
192 ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
194 # Draw lines into the cell
195 for start, end, colour in self.in_lines:
196 ctx.move_to(cell_area.x + box_size * start + box_size / 2,
197 bg_area.y - bg_area.height / 2)
199 if start - end > 1:
200 ctx.line_to(cell_area.x + box_size * start, bg_area.y)
201 ctx.line_to(cell_area.x + box_size * end + box_size, bg_area.y)
202 elif start - end < -1:
203 ctx.line_to(cell_area.x + box_size * start + box_size,
204 bg_area.y)
205 ctx.line_to(cell_area.x + box_size * end, bg_area.y)
207 ctx.line_to(cell_area.x + box_size * end + box_size / 2,
208 bg_area.y + bg_area.height / 2)
210 self.set_colour(ctx, colour, 0.0, 0.65)
211 ctx.stroke()
213 # Draw lines out of the cell
214 for start, end, colour in self.out_lines:
215 ctx.move_to(cell_area.x + box_size * start + box_size / 2,
216 bg_area.y + bg_area.height / 2)
218 if start - end > 1:
219 ctx.line_to(cell_area.x + box_size * start,
220 bg_area.y + bg_area.height)
221 ctx.line_to(cell_area.x + box_size * end + box_size,
222 bg_area.y + bg_area.height)
223 elif start - end < -1:
224 ctx.line_to(cell_area.x + box_size * start + box_size,
225 bg_area.y + bg_area.height)
226 ctx.line_to(cell_area.x + box_size * end,
227 bg_area.y + bg_area.height)
229 ctx.line_to(cell_area.x + box_size * end + box_size / 2,
230 bg_area.y + bg_area.height / 2 + bg_area.height)
232 self.set_colour(ctx, colour, 0.0, 0.65)
233 ctx.stroke()
235 # Draw the revision node in the right column
236 (column, colour, names) = self.node
237 ctx.arc(cell_area.x + box_size * column + box_size / 2,
238 cell_area.y + cell_area.height / 2,
239 box_size / 4, 0, 2 * math.pi)
242 self.set_colour(ctx, colour, 0.0, 0.5)
243 ctx.stroke_preserve()
245 self.set_colour(ctx, colour, 0.5, 1.0)
246 ctx.fill_preserve()
248 if (len(names) != 0):
249 name = " "
250 for item in names:
251 name = name + item + " "
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)
260 class Commit:
261 """ This represent a commit object obtained after parsing the git-rev-list
262 output """
264 children_sha1 = {}
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)
277 def parse_commit(self, commit_lines):
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:]
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]
292 # IF we don't have parent
293 if (len(self.parent_sha1) == 0):
294 self.parent_sha1 = [0]
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
304 m = re.match("tree", line)
305 if (m != None):
306 continue
308 m = re.match("parent", line)
309 if (m != None):
310 continue
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
322 continue
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()
332 return message
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
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 """
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")
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)
358 self.construct()
360 def construct(self):
361 """Construct the window contents."""
362 vbox = gtk.VBox()
363 self.window.add(vbox)
364 vbox.show()
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, False, False, 2)
372 menu_bar.show()
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()
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)
391 sourceview.set_editable(False)
392 sourceview.modify_font(pango.FontDescription("Monospace"))
393 scrollwin.add(sourceview)
394 sourceview.show()
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
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()
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()
425 class GitView:
426 """ This is the main class
427 """
428 version = "0.7"
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")
436 self.get_encoding()
437 self.get_bt_sha1()
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)
446 # FIXME AndyFitz!
447 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
448 self.window.set_icon(icon)
450 self.accel_group = gtk.AccelGroup()
451 self.window.add_accel_group(self.accel_group)
453 self.construct()
455 def get_bt_sha1(self):
456 """ Update the bt_sha1 dictionary with the
457 respective sha1 details """
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()
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"
483 def construct(self):
484 """Construct the window contents."""
485 paned = gtk.VPaned()
486 paned.pack1(self.construct_top(), resize=False, shrink=True)
487 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
488 self.window.add(paned)
489 paned.show()
492 def construct_top(self):
493 """Construct the top-half of the window."""
494 vbox = gtk.VBox(spacing=6)
495 vbox.set_border_width(12)
496 vbox.show()
498 menu_bar = gtk.MenuBar()
499 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
500 help_menu = gtk.MenuItem("Help")
501 menu = gtk.Menu()
502 about_menu = gtk.MenuItem("About")
503 menu.append(about_menu)
504 about_menu.connect("activate", self.about_menu_response, "about")
505 about_menu.show()
506 help_menu.set_submenu(menu)
507 help_menu.show()
508 menu_bar.append(help_menu)
509 vbox.pack_start(menu_bar, False, False, 2)
510 menu_bar.show()
512 scrollwin = gtk.ScrolledWindow()
513 scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
514 scrollwin.set_shadow_type(gtk.SHADOW_IN)
515 vbox.pack_start(scrollwin, expand=True, fill=True)
516 scrollwin.show()
518 self.treeview = gtk.TreeView()
519 self.treeview.set_rules_hint(True)
520 self.treeview.set_search_column(4)
521 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
522 scrollwin.add(self.treeview)
523 self.treeview.show()
525 cell = CellRendererGraph()
526 column = gtk.TreeViewColumn()
527 column.set_resizable(True)
528 column.pack_start(cell, expand=True)
529 column.add_attribute(cell, "node", 1)
530 column.add_attribute(cell, "in-lines", 2)
531 column.add_attribute(cell, "out-lines", 3)
532 self.treeview.append_column(column)
534 cell = gtk.CellRendererText()
535 cell.set_property("width-chars", 65)
536 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
537 column = gtk.TreeViewColumn("Message")
538 column.set_resizable(True)
539 column.pack_start(cell, expand=True)
540 column.add_attribute(cell, "text", 4)
541 self.treeview.append_column(column)
543 cell = gtk.CellRendererText()
544 cell.set_property("width-chars", 40)
545 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
546 column = gtk.TreeViewColumn("Author")
547 column.set_resizable(True)
548 column.pack_start(cell, expand=True)
549 column.add_attribute(cell, "text", 5)
550 self.treeview.append_column(column)
552 cell = gtk.CellRendererText()
553 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
554 column = gtk.TreeViewColumn("Date")
555 column.set_resizable(True)
556 column.pack_start(cell, expand=True)
557 column.add_attribute(cell, "text", 6)
558 self.treeview.append_column(column)
560 return vbox
562 def about_menu_response(self, widget, string):
563 dialog = gtk.AboutDialog()
564 dialog.set_name("Gitview")
565 dialog.set_version(GitView.version)
566 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
567 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
568 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
569 dialog.set_wrap_license(True)
570 dialog.run()
571 dialog.destroy()
574 def construct_bottom(self):
575 """Construct the bottom half of the window."""
576 vbox = gtk.VBox(False, spacing=6)
577 vbox.set_border_width(12)
578 (width, height) = self.window.get_size()
579 vbox.set_size_request(width, int(height / 2.5))
580 vbox.show()
582 self.table = gtk.Table(rows=4, columns=4)
583 self.table.set_row_spacings(6)
584 self.table.set_col_spacings(6)
585 vbox.pack_start(self.table, expand=False, fill=True)
586 self.table.show()
588 align = gtk.Alignment(0.0, 0.5)
589 label = gtk.Label()
590 label.set_markup("<b>Revision:</b>")
591 align.add(label)
592 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
593 label.show()
594 align.show()
596 align = gtk.Alignment(0.0, 0.5)
597 self.revid_label = gtk.Label()
598 self.revid_label.set_selectable(True)
599 align.add(self.revid_label)
600 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
601 self.revid_label.show()
602 align.show()
604 align = gtk.Alignment(0.0, 0.5)
605 label = gtk.Label()
606 label.set_markup("<b>Committer:</b>")
607 align.add(label)
608 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
609 label.show()
610 align.show()
612 align = gtk.Alignment(0.0, 0.5)
613 self.committer_label = gtk.Label()
614 self.committer_label.set_selectable(True)
615 align.add(self.committer_label)
616 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
617 self.committer_label.show()
618 align.show()
620 align = gtk.Alignment(0.0, 0.5)
621 label = gtk.Label()
622 label.set_markup("<b>Timestamp:</b>")
623 align.add(label)
624 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
625 label.show()
626 align.show()
628 align = gtk.Alignment(0.0, 0.5)
629 self.timestamp_label = gtk.Label()
630 self.timestamp_label.set_selectable(True)
631 align.add(self.timestamp_label)
632 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
633 self.timestamp_label.show()
634 align.show()
636 align = gtk.Alignment(0.0, 0.5)
637 label = gtk.Label()
638 label.set_markup("<b>Parents:</b>")
639 align.add(label)
640 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
641 label.show()
642 align.show()
643 self.parents_widgets = []
645 align = gtk.Alignment(0.0, 0.5)
646 label = gtk.Label()
647 label.set_markup("<b>Children:</b>")
648 align.add(label)
649 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
650 label.show()
651 align.show()
652 self.children_widgets = []
654 scrollwin = gtk.ScrolledWindow()
655 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
656 scrollwin.set_shadow_type(gtk.SHADOW_IN)
657 vbox.pack_start(scrollwin, expand=True, fill=True)
658 scrollwin.show()
660 if have_gtksourceview:
661 self.message_buffer = gtksourceview.SourceBuffer()
662 slm = gtksourceview.SourceLanguagesManager()
663 gsl = slm.get_language_from_mime_type("text/x-patch")
664 self.message_buffer.set_highlight(True)
665 self.message_buffer.set_language(gsl)
666 sourceview = gtksourceview.SourceView(self.message_buffer)
667 else:
668 self.message_buffer = gtk.TextBuffer()
669 sourceview = gtk.TextView(self.message_buffer)
671 sourceview.set_editable(False)
672 sourceview.modify_font(pango.FontDescription("Monospace"))
673 scrollwin.add(sourceview)
674 sourceview.show()
676 return vbox
678 def _treeview_cursor_cb(self, *args):
679 """Callback for when the treeview cursor changes."""
680 (path, col) = self.treeview.get_cursor()
681 commit = self.model[path][0]
683 if commit.committer is not None:
684 committer = commit.committer
685 timestamp = commit.commit_date
686 message = commit.get_message(self.with_diff)
687 revid_label = commit.commit_sha1
688 else:
689 committer = ""
690 timestamp = ""
691 message = ""
692 revid_label = ""
694 self.revid_label.set_text(revid_label)
695 self.committer_label.set_text(committer)
696 self.timestamp_label.set_text(timestamp)
697 self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
699 for widget in self.parents_widgets:
700 self.table.remove(widget)
702 self.parents_widgets = []
703 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
704 for idx, parent_id in enumerate(commit.parent_sha1):
705 self.table.set_row_spacing(idx + 3, 0)
707 align = gtk.Alignment(0.0, 0.0)
708 self.parents_widgets.append(align)
709 self.table.attach(align, 1, 2, idx + 3, idx + 4,
710 gtk.EXPAND | gtk.FILL, gtk.FILL)
711 align.show()
713 hbox = gtk.HBox(False, 0)
714 align.add(hbox)
715 hbox.show()
717 label = gtk.Label(parent_id)
718 label.set_selectable(True)
719 hbox.pack_start(label, expand=False, fill=True)
720 label.show()
722 image = gtk.Image()
723 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
724 image.show()
726 button = gtk.Button()
727 button.add(image)
728 button.set_relief(gtk.RELIEF_NONE)
729 button.connect("clicked", self._go_clicked_cb, parent_id)
730 hbox.pack_start(button, expand=False, fill=True)
731 button.show()
733 image = gtk.Image()
734 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
735 image.show()
737 button = gtk.Button()
738 button.add(image)
739 button.set_relief(gtk.RELIEF_NONE)
740 button.set_sensitive(True)
741 button.connect("clicked", self._show_clicked_cb,
742 commit.commit_sha1, parent_id, self.encoding)
743 hbox.pack_start(button, expand=False, fill=True)
744 button.show()
746 # Populate with child details
747 for widget in self.children_widgets:
748 self.table.remove(widget)
750 self.children_widgets = []
751 try:
752 child_sha1 = Commit.children_sha1[commit.commit_sha1]
753 except KeyError:
754 # We don't have child
755 child_sha1 = [ 0 ]
757 if ( len(child_sha1) > len(commit.parent_sha1)):
758 self.table.resize(4 + len(child_sha1) - 1, 4)
760 for idx, child_id in enumerate(child_sha1):
761 self.table.set_row_spacing(idx + 3, 0)
763 align = gtk.Alignment(0.0, 0.0)
764 self.children_widgets.append(align)
765 self.table.attach(align, 3, 4, idx + 3, idx + 4,
766 gtk.EXPAND | gtk.FILL, gtk.FILL)
767 align.show()
769 hbox = gtk.HBox(False, 0)
770 align.add(hbox)
771 hbox.show()
773 label = gtk.Label(child_id)
774 label.set_selectable(True)
775 hbox.pack_start(label, expand=False, fill=True)
776 label.show()
778 image = gtk.Image()
779 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
780 image.show()
782 button = gtk.Button()
783 button.add(image)
784 button.set_relief(gtk.RELIEF_NONE)
785 button.connect("clicked", self._go_clicked_cb, child_id)
786 hbox.pack_start(button, expand=False, fill=True)
787 button.show()
789 image = gtk.Image()
790 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
791 image.show()
793 button = gtk.Button()
794 button.add(image)
795 button.set_relief(gtk.RELIEF_NONE)
796 button.set_sensitive(True)
797 button.connect("clicked", self._show_clicked_cb,
798 child_id, commit.commit_sha1)
799 hbox.pack_start(button, expand=False, fill=True)
800 button.show()
802 def _destroy_cb(self, widget):
803 """Callback for when a window we manage is destroyed."""
804 self.quit()
807 def quit(self):
808 """Stop the GTK+ main loop."""
809 gtk.main_quit()
811 def run(self, args):
812 self.set_branch(args)
813 self.window.connect("destroy", self._destroy_cb)
814 self.window.show()
815 gtk.main()
817 def set_branch(self, args):
818 """Fill in different windows with info from the reposiroty"""
819 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
820 git_rev_list_cmd = fp.read()
821 fp.close()
822 fp = os.popen("git rev-list --header --topo-order --parents " + git_rev_list_cmd)
823 self.update_window(fp)
825 def update_window(self, fp):
826 commit_lines = []
828 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
829 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
831 # used for cursor positioning
832 self.index = {}
834 self.colours = {}
835 self.nodepos = {}
836 self.incomplete_line = {}
837 self.commits = []
839 index = 0
840 last_colour = 0
841 last_nodepos = -1
842 out_line = []
843 input_line = fp.readline()
844 while (input_line != ""):
845 # The commit header ends with '\0'
846 # This NULL is immediately followed by the sha1 of the
847 # next commit
848 if (input_line[0] != '\0'):
849 commit_lines.append(input_line)
850 input_line = fp.readline()
851 continue;
853 commit = Commit(commit_lines)
854 if (commit != None ):
855 self.commits.append(commit)
857 # Skip the '\0
858 commit_lines = []
859 commit_lines.append(input_line[1:])
860 input_line = fp.readline()
862 fp.close()
864 for commit in self.commits:
865 (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
866 index, out_line,
867 last_colour,
868 last_nodepos)
869 self.index[commit.commit_sha1] = index
870 index += 1
872 self.treeview.set_model(self.model)
873 self.treeview.show()
875 def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
876 in_line=[]
878 # | -> outline
879 # X
880 # |\ <- inline
882 # Reset nodepostion
883 if (last_nodepos > 5):
884 last_nodepos = -1
886 # Add the incomplete lines of the last cell in this
887 try:
888 colour = self.colours[commit.commit_sha1]
889 except KeyError:
890 self.colours[commit.commit_sha1] = last_colour+1
891 last_colour = self.colours[commit.commit_sha1]
892 colour = self.colours[commit.commit_sha1]
894 try:
895 node_pos = self.nodepos[commit.commit_sha1]
896 except KeyError:
897 self.nodepos[commit.commit_sha1] = last_nodepos+1
898 last_nodepos = self.nodepos[commit.commit_sha1]
899 node_pos = self.nodepos[commit.commit_sha1]
901 #The first parent always continue on the same line
902 try:
903 # check we alreay have the value
904 tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
905 except KeyError:
906 self.colours[commit.parent_sha1[0]] = colour
907 self.nodepos[commit.parent_sha1[0]] = node_pos
909 for sha1 in self.incomplete_line.keys():
910 if (sha1 != commit.commit_sha1):
911 self.draw_incomplete_line(sha1, node_pos,
912 out_line, in_line, index)
913 else:
914 del self.incomplete_line[sha1]
917 for parent_id in commit.parent_sha1:
918 try:
919 tmp_node_pos = self.nodepos[parent_id]
920 except KeyError:
921 self.colours[parent_id] = last_colour+1
922 last_colour = self.colours[parent_id]
923 self.nodepos[parent_id] = last_nodepos+1
924 last_nodepos = self.nodepos[parent_id]
926 in_line.append((node_pos, self.nodepos[parent_id],
927 self.colours[parent_id]))
928 self.add_incomplete_line(parent_id)
930 try:
931 branch_tag = self.bt_sha1[commit.commit_sha1]
932 except KeyError:
933 branch_tag = [ ]
936 node = (node_pos, colour, branch_tag)
938 self.model.append([commit, node, out_line, in_line,
939 commit.message, commit.author, commit.date])
941 return (in_line, last_colour, last_nodepos)
943 def add_incomplete_line(self, sha1):
944 try:
945 self.incomplete_line[sha1].append(self.nodepos[sha1])
946 except KeyError:
947 self.incomplete_line[sha1] = [self.nodepos[sha1]]
949 def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
950 for idx, pos in enumerate(self.incomplete_line[sha1]):
951 if(pos == node_pos):
952 #remove the straight line and add a slash
953 if ((pos, pos, self.colours[sha1]) in out_line):
954 out_line.remove((pos, pos, self.colours[sha1]))
955 out_line.append((pos, pos+0.5, self.colours[sha1]))
956 self.incomplete_line[sha1][idx] = pos = pos+0.5
957 try:
958 next_commit = self.commits[index+1]
959 if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
960 # join the line back to the node point
961 # This need to be done only if we modified it
962 in_line.append((pos, pos-0.5, self.colours[sha1]))
963 continue;
964 except IndexError:
965 pass
966 in_line.append((pos, pos, self.colours[sha1]))
969 def _go_clicked_cb(self, widget, revid):
970 """Callback for when the go button for a parent is clicked."""
971 try:
972 self.treeview.set_cursor(self.index[revid])
973 except KeyError:
974 print "Revision %s not present in the list" % revid
975 # revid == 0 is the parent of the first commit
976 if (revid != 0 ):
977 print "Try running gitview without any options"
979 self.treeview.grab_focus()
981 def _show_clicked_cb(self, widget, commit_sha1, parent_sha1, encoding):
982 """Callback for when the show button for a parent is clicked."""
983 window = DiffWindow()
984 window.set_diff(commit_sha1, parent_sha1, encoding)
985 self.treeview.grab_focus()
987 if __name__ == "__main__":
988 without_diff = 0
990 if (len(sys.argv) > 1 ):
991 if (sys.argv[1] == "--without-diff"):
992 without_diff = 1
994 view = GitView( without_diff != 1)
995 view.run(sys.argv[without_diff:])