gitview: Read tag and branch information using git ls-remote
[git/debian.git] / contrib / gitview / gitview
blobb75b39e5fc94bcf96e368ff20ac14e4afb90726f
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
82 "in-lines": ( gobject.TYPE_PYOBJECT, "in-lines",
83 "instructions to draw lines into the cell",
84 gobject.PARAM_WRITABLE
86 "out-lines": ( gobject.TYPE_PYOBJECT, "out-lines",
87 "instructions to draw lines out of the cell",
88 gobject.PARAM_WRITABLE
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.
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.
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 ),
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.
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 = 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)/3
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.
185 ctx = window.cairo_create()
186 ctx.rectangle(bg_area.x, bg_area.y, bg_area.width, bg_area.height)
187 ctx.clip()
189 box_size = self.box_size(widget)
191 ctx.set_line_width(box_size / 8)
192 ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
194 # Draw lines into the cell
195 for start, end, colour in self.in_lines:
196 ctx.move_to(cell_area.x + box_size * start + box_size / 2,
197 bg_area.y - bg_area.height / 2)
199 if start - end > 1:
200 ctx.line_to(cell_area.x + box_size * start, bg_area.y)
201 ctx.line_to(cell_area.x + box_size * end + box_size, bg_area.y)
202 elif start - end < -1:
203 ctx.line_to(cell_area.x + box_size * start + box_size,
204 bg_area.y)
205 ctx.line_to(cell_area.x + box_size * end, bg_area.y)
207 ctx.line_to(cell_area.x + box_size * end + box_size / 2,
208 bg_area.y + bg_area.height / 2)
210 self.set_colour(ctx, colour, 0.0, 0.65)
211 ctx.stroke()
213 # Draw lines out of the cell
214 for start, end, colour in self.out_lines:
215 ctx.move_to(cell_area.x + box_size * start + box_size / 2,
216 bg_area.y + bg_area.height / 2)
218 if start - end > 1:
219 ctx.line_to(cell_area.x + box_size * start,
220 bg_area.y + bg_area.height)
221 ctx.line_to(cell_area.x + box_size * end + box_size,
222 bg_area.y + bg_area.height)
223 elif start - end < -1:
224 ctx.line_to(cell_area.x + box_size * start + box_size,
225 bg_area.y + bg_area.height)
226 ctx.line_to(cell_area.x + box_size * end,
227 bg_area.y + bg_area.height)
229 ctx.line_to(cell_area.x + box_size * end + box_size / 2,
230 bg_area.y + bg_area.height / 2 + bg_area.height)
232 self.set_colour(ctx, colour, 0.0, 0.65)
233 ctx.stroke()
235 # Draw the revision node in the right column
236 (column, colour, names) = self.node
237 ctx.arc(cell_area.x + box_size * column + box_size / 2,
238 cell_area.y + cell_area.height / 2,
239 box_size / 4, 0, 2 * math.pi)
242 if (len(names) != 0):
243 name = " "
244 for item in names:
245 name = name + item + " "
247 ctx.text_path(name)
249 self.set_colour(ctx, colour, 0.0, 0.5)
250 ctx.stroke_preserve()
252 self.set_colour(ctx, colour, 0.5, 1.0)
253 ctx.fill()
255 class Commit:
256 """ This represent a commit object obtained after parsing the git-rev-list
257 output """
259 children_sha1 = {}
261 def __init__(self, commit_lines):
262 self.message = ""
263 self.author = ""
264 self.date = ""
265 self.committer = ""
266 self.commit_date = ""
267 self.commit_sha1 = ""
268 self.parent_sha1 = [ ]
269 self.parse_commit(commit_lines)
272 def parse_commit(self, commit_lines):
274 # First line is the sha1 lines
275 line = string.strip(commit_lines[0])
276 sha1 = re.split(" ", line)
277 self.commit_sha1 = sha1[0]
278 self.parent_sha1 = sha1[1:]
280 #build the child list
281 for parent_id in self.parent_sha1:
282 try:
283 Commit.children_sha1[parent_id].append(self.commit_sha1)
284 except KeyError:
285 Commit.children_sha1[parent_id] = [self.commit_sha1]
287 # IF we don't have parent
288 if (len(self.parent_sha1) == 0):
289 self.parent_sha1 = [0]
291 for line in commit_lines[1:]:
292 m = re.match("^ ", line)
293 if (m != None):
294 # First line of the commit message used for short log
295 if self.message == "":
296 self.message = string.strip(line)
297 continue
299 m = re.match("tree", line)
300 if (m != None):
301 continue
303 m = re.match("parent", line)
304 if (m != None):
305 continue
307 m = re_ident.match(line)
308 if (m != None):
309 date = show_date(m.group('epoch'), m.group('tz'))
310 if m.group(1) == "author":
311 self.author = m.group('ident')
312 self.date = date
313 elif m.group(1) == "committer":
314 self.committer = m.group('ident')
315 self.commit_date = date
317 continue
319 def get_message(self, with_diff=0):
320 if (with_diff == 1):
321 message = self.diff_tree()
322 else:
323 fp = os.popen("git cat-file commit " + self.commit_sha1)
324 message = fp.read()
325 fp.close()
327 return message
329 def diff_tree(self):
330 fp = os.popen("git diff-tree --pretty --cc -v -p --always " + self.commit_sha1)
331 diff = fp.read()
332 fp.close()
333 return diff
335 class DiffWindow:
336 """Diff window.
337 This object represents and manages a single window containing the
338 differences between two revisions on a branch.
341 def __init__(self):
342 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
343 self.window.set_border_width(0)
344 self.window.set_title("Git repository browser diff window")
346 # Use two thirds of the screen by default
347 screen = self.window.get_screen()
348 monitor = screen.get_monitor_geometry(0)
349 width = int(monitor.width * 0.66)
350 height = int(monitor.height * 0.66)
351 self.window.set_default_size(width, height)
353 self.construct()
355 def construct(self):
356 """Construct the window contents."""
357 vbox = gtk.VBox()
358 self.window.add(vbox)
359 vbox.show()
361 menu_bar = gtk.MenuBar()
362 save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
363 save_menu.connect("activate", self.save_menu_response, "save")
364 save_menu.show()
365 menu_bar.append(save_menu)
366 vbox.pack_start(menu_bar, False, False, 2)
367 menu_bar.show()
369 scrollwin = gtk.ScrolledWindow()
370 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
371 scrollwin.set_shadow_type(gtk.SHADOW_IN)
372 vbox.pack_start(scrollwin, expand=True, fill=True)
373 scrollwin.show()
375 if have_gtksourceview:
376 self.buffer = gtksourceview.SourceBuffer()
377 slm = gtksourceview.SourceLanguagesManager()
378 gsl = slm.get_language_from_mime_type("text/x-patch")
379 self.buffer.set_highlight(True)
380 self.buffer.set_language(gsl)
381 sourceview = gtksourceview.SourceView(self.buffer)
382 else:
383 self.buffer = gtk.TextBuffer()
384 sourceview = gtk.TextView(self.buffer)
386 sourceview.set_editable(False)
387 sourceview.modify_font(pango.FontDescription("Monospace"))
388 scrollwin.add(sourceview)
389 sourceview.show()
392 def set_diff(self, commit_sha1, parent_sha1):
393 """Set the differences showed by this window.
394 Compares the two trees and populates the window with the
395 differences.
397 # Diff with the first commit or the last commit shows nothing
398 if (commit_sha1 == 0 or parent_sha1 == 0 ):
399 return
401 fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
402 self.buffer.set_text(fp.read())
403 fp.close()
404 self.window.show()
406 def save_menu_response(self, widget, string):
407 dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
408 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
409 gtk.STOCK_SAVE, gtk.RESPONSE_OK))
410 dialog.set_default_response(gtk.RESPONSE_OK)
411 response = dialog.run()
412 if response == gtk.RESPONSE_OK:
413 patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
414 self.buffer.get_end_iter())
415 fp = open(dialog.get_filename(), "w")
416 fp.write(patch_buffer)
417 fp.close()
418 dialog.destroy()
420 class GitView:
421 """ This is the main class
423 version = "0.6"
425 def __init__(self, with_diff=0):
426 self.with_diff = with_diff
427 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
428 self.window.set_border_width(0)
429 self.window.set_title("Git repository browser")
431 self.get_bt_sha1()
433 # Use three-quarters of the screen by default
434 screen = self.window.get_screen()
435 monitor = screen.get_monitor_geometry(0)
436 width = int(monitor.width * 0.75)
437 height = int(monitor.height * 0.75)
438 self.window.set_default_size(width, height)
440 # FIXME AndyFitz!
441 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
442 self.window.set_icon(icon)
444 self.accel_group = gtk.AccelGroup()
445 self.window.add_accel_group(self.accel_group)
447 self.construct()
449 def get_bt_sha1(self):
450 """ Update the bt_sha1 dictionary with the
451 respective sha1 details """
453 self.bt_sha1 = { }
454 ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
455 git_dir = os.getenv("GIT_DIR")
456 if (git_dir == None):
457 git_dir = ".git"
459 fp = os.popen('git ls-remote ' + git_dir)
460 while 1:
461 line = string.strip(fp.readline())
462 if line == '':
463 break
464 m = ls_remote.match(line)
465 if not m:
466 continue
467 (sha1, name) = (m.group(1), m.group(2))
468 if not self.bt_sha1.has_key(sha1):
469 self.bt_sha1[sha1] = []
470 self.bt_sha1[sha1].append(name)
471 fp.close()
474 def construct(self):
475 """Construct the window contents."""
476 paned = gtk.VPaned()
477 paned.pack1(self.construct_top(), resize=False, shrink=True)
478 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
479 self.window.add(paned)
480 paned.show()
483 def construct_top(self):
484 """Construct the top-half of the window."""
485 vbox = gtk.VBox(spacing=6)
486 vbox.set_border_width(12)
487 vbox.show()
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 vbox.pack_start(menu_bar, False, False, 2)
501 menu_bar.show()
503 scrollwin = gtk.ScrolledWindow()
504 scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
505 scrollwin.set_shadow_type(gtk.SHADOW_IN)
506 vbox.pack_start(scrollwin, expand=True, fill=True)
507 scrollwin.show()
509 self.treeview = gtk.TreeView()
510 self.treeview.set_rules_hint(True)
511 self.treeview.set_search_column(4)
512 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
513 scrollwin.add(self.treeview)
514 self.treeview.show()
516 cell = CellRendererGraph()
517 column = gtk.TreeViewColumn()
518 column.set_resizable(False)
519 column.pack_start(cell, expand=False)
520 column.add_attribute(cell, "node", 1)
521 column.add_attribute(cell, "in-lines", 2)
522 column.add_attribute(cell, "out-lines", 3)
523 self.treeview.append_column(column)
525 cell = gtk.CellRendererText()
526 cell.set_property("width-chars", 65)
527 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
528 column = gtk.TreeViewColumn("Message")
529 column.set_resizable(True)
530 column.pack_start(cell, expand=True)
531 column.add_attribute(cell, "text", 4)
532 self.treeview.append_column(column)
534 cell = gtk.CellRendererText()
535 cell.set_property("width-chars", 40)
536 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
537 column = gtk.TreeViewColumn("Author")
538 column.set_resizable(True)
539 column.pack_start(cell, expand=True)
540 column.add_attribute(cell, "text", 5)
541 self.treeview.append_column(column)
543 cell = gtk.CellRendererText()
544 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
545 column = gtk.TreeViewColumn("Date")
546 column.set_resizable(True)
547 column.pack_start(cell, expand=True)
548 column.add_attribute(cell, "text", 6)
549 self.treeview.append_column(column)
551 return vbox
553 def about_menu_response(self, widget, string):
554 dialog = gtk.AboutDialog()
555 dialog.set_name("Gitview")
556 dialog.set_version(GitView.version)
557 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
558 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
559 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
560 dialog.set_wrap_license(True)
561 dialog.run()
562 dialog.destroy()
565 def construct_bottom(self):
566 """Construct the bottom half of the window."""
567 vbox = gtk.VBox(False, spacing=6)
568 vbox.set_border_width(12)
569 (width, height) = self.window.get_size()
570 vbox.set_size_request(width, int(height / 2.5))
571 vbox.show()
573 self.table = gtk.Table(rows=4, columns=4)
574 self.table.set_row_spacings(6)
575 self.table.set_col_spacings(6)
576 vbox.pack_start(self.table, expand=False, fill=True)
577 self.table.show()
579 align = gtk.Alignment(0.0, 0.5)
580 label = gtk.Label()
581 label.set_markup("<b>Revision:</b>")
582 align.add(label)
583 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
584 label.show()
585 align.show()
587 align = gtk.Alignment(0.0, 0.5)
588 self.revid_label = gtk.Label()
589 self.revid_label.set_selectable(True)
590 align.add(self.revid_label)
591 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
592 self.revid_label.show()
593 align.show()
595 align = gtk.Alignment(0.0, 0.5)
596 label = gtk.Label()
597 label.set_markup("<b>Committer:</b>")
598 align.add(label)
599 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
600 label.show()
601 align.show()
603 align = gtk.Alignment(0.0, 0.5)
604 self.committer_label = gtk.Label()
605 self.committer_label.set_selectable(True)
606 align.add(self.committer_label)
607 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
608 self.committer_label.show()
609 align.show()
611 align = gtk.Alignment(0.0, 0.5)
612 label = gtk.Label()
613 label.set_markup("<b>Timestamp:</b>")
614 align.add(label)
615 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
616 label.show()
617 align.show()
619 align = gtk.Alignment(0.0, 0.5)
620 self.timestamp_label = gtk.Label()
621 self.timestamp_label.set_selectable(True)
622 align.add(self.timestamp_label)
623 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
624 self.timestamp_label.show()
625 align.show()
627 align = gtk.Alignment(0.0, 0.5)
628 label = gtk.Label()
629 label.set_markup("<b>Parents:</b>")
630 align.add(label)
631 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
632 label.show()
633 align.show()
634 self.parents_widgets = []
636 align = gtk.Alignment(0.0, 0.5)
637 label = gtk.Label()
638 label.set_markup("<b>Children:</b>")
639 align.add(label)
640 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
641 label.show()
642 align.show()
643 self.children_widgets = []
645 scrollwin = gtk.ScrolledWindow()
646 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
647 scrollwin.set_shadow_type(gtk.SHADOW_IN)
648 vbox.pack_start(scrollwin, expand=True, fill=True)
649 scrollwin.show()
651 if have_gtksourceview:
652 self.message_buffer = gtksourceview.SourceBuffer()
653 slm = gtksourceview.SourceLanguagesManager()
654 gsl = slm.get_language_from_mime_type("text/x-patch")
655 self.message_buffer.set_highlight(True)
656 self.message_buffer.set_language(gsl)
657 sourceview = gtksourceview.SourceView(self.message_buffer)
658 else:
659 self.message_buffer = gtk.TextBuffer()
660 sourceview = gtk.TextView(self.message_buffer)
662 sourceview.set_editable(False)
663 sourceview.modify_font(pango.FontDescription("Monospace"))
664 scrollwin.add(sourceview)
665 sourceview.show()
667 return vbox
669 def _treeview_cursor_cb(self, *args):
670 """Callback for when the treeview cursor changes."""
671 (path, col) = self.treeview.get_cursor()
672 commit = self.model[path][0]
674 if commit.committer is not None:
675 committer = commit.committer
676 timestamp = commit.commit_date
677 message = commit.get_message(self.with_diff)
678 revid_label = commit.commit_sha1
679 else:
680 committer = ""
681 timestamp = ""
682 message = ""
683 revid_label = ""
685 self.revid_label.set_text(revid_label)
686 self.committer_label.set_text(committer)
687 self.timestamp_label.set_text(timestamp)
688 self.message_buffer.set_text(message)
690 for widget in self.parents_widgets:
691 self.table.remove(widget)
693 self.parents_widgets = []
694 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
695 for idx, parent_id in enumerate(commit.parent_sha1):
696 self.table.set_row_spacing(idx + 3, 0)
698 align = gtk.Alignment(0.0, 0.0)
699 self.parents_widgets.append(align)
700 self.table.attach(align, 1, 2, idx + 3, idx + 4,
701 gtk.EXPAND | gtk.FILL, gtk.FILL)
702 align.show()
704 hbox = gtk.HBox(False, 0)
705 align.add(hbox)
706 hbox.show()
708 label = gtk.Label(parent_id)
709 label.set_selectable(True)
710 hbox.pack_start(label, expand=False, fill=True)
711 label.show()
713 image = gtk.Image()
714 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
715 image.show()
717 button = gtk.Button()
718 button.add(image)
719 button.set_relief(gtk.RELIEF_NONE)
720 button.connect("clicked", self._go_clicked_cb, parent_id)
721 hbox.pack_start(button, expand=False, fill=True)
722 button.show()
724 image = gtk.Image()
725 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
726 image.show()
728 button = gtk.Button()
729 button.add(image)
730 button.set_relief(gtk.RELIEF_NONE)
731 button.set_sensitive(True)
732 button.connect("clicked", self._show_clicked_cb,
733 commit.commit_sha1, parent_id)
734 hbox.pack_start(button, expand=False, fill=True)
735 button.show()
737 # Populate with child details
738 for widget in self.children_widgets:
739 self.table.remove(widget)
741 self.children_widgets = []
742 try:
743 child_sha1 = Commit.children_sha1[commit.commit_sha1]
744 except KeyError:
745 # We don't have child
746 child_sha1 = [ 0 ]
748 if ( len(child_sha1) > len(commit.parent_sha1)):
749 self.table.resize(4 + len(child_sha1) - 1, 4)
751 for idx, child_id in enumerate(child_sha1):
752 self.table.set_row_spacing(idx + 3, 0)
754 align = gtk.Alignment(0.0, 0.0)
755 self.children_widgets.append(align)
756 self.table.attach(align, 3, 4, idx + 3, idx + 4,
757 gtk.EXPAND | gtk.FILL, gtk.FILL)
758 align.show()
760 hbox = gtk.HBox(False, 0)
761 align.add(hbox)
762 hbox.show()
764 label = gtk.Label(child_id)
765 label.set_selectable(True)
766 hbox.pack_start(label, expand=False, fill=True)
767 label.show()
769 image = gtk.Image()
770 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
771 image.show()
773 button = gtk.Button()
774 button.add(image)
775 button.set_relief(gtk.RELIEF_NONE)
776 button.connect("clicked", self._go_clicked_cb, child_id)
777 hbox.pack_start(button, expand=False, fill=True)
778 button.show()
780 image = gtk.Image()
781 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
782 image.show()
784 button = gtk.Button()
785 button.add(image)
786 button.set_relief(gtk.RELIEF_NONE)
787 button.set_sensitive(True)
788 button.connect("clicked", self._show_clicked_cb,
789 child_id, commit.commit_sha1)
790 hbox.pack_start(button, expand=False, fill=True)
791 button.show()
793 def _destroy_cb(self, widget):
794 """Callback for when a window we manage is destroyed."""
795 self.quit()
798 def quit(self):
799 """Stop the GTK+ main loop."""
800 gtk.main_quit()
802 def run(self, args):
803 self.set_branch(args)
804 self.window.connect("destroy", self._destroy_cb)
805 self.window.show()
806 gtk.main()
808 def set_branch(self, args):
809 """Fill in different windows with info from the reposiroty"""
810 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
811 git_rev_list_cmd = fp.read()
812 fp.close()
813 fp = os.popen("git rev-list --header --topo-order --parents " + git_rev_list_cmd)
814 self.update_window(fp)
816 def update_window(self, fp):
817 commit_lines = []
819 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
820 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
822 # used for cursor positioning
823 self.index = {}
825 self.colours = {}
826 self.nodepos = {}
827 self.incomplete_line = {}
829 index = 0
830 last_colour = 0
831 last_nodepos = -1
832 out_line = []
833 input_line = fp.readline()
834 while (input_line != ""):
835 # The commit header ends with '\0'
836 # This NULL is immediately followed by the sha1 of the
837 # next commit
838 if (input_line[0] != '\0'):
839 commit_lines.append(input_line)
840 input_line = fp.readline()
841 continue;
843 commit = Commit(commit_lines)
844 if (commit != None ):
845 (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
846 index, out_line,
847 last_colour,
848 last_nodepos)
849 self.index[commit.commit_sha1] = index
850 index += 1
852 # Skip the '\0
853 commit_lines = []
854 commit_lines.append(input_line[1:])
855 input_line = fp.readline()
857 fp.close()
859 self.treeview.set_model(self.model)
860 self.treeview.show()
862 def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
863 in_line=[]
865 # | -> outline
867 # |\ <- inline
869 # Reset nodepostion
870 if (last_nodepos > 5):
871 last_nodepos = 0
873 # Add the incomplete lines of the last cell in this
874 for sha1 in self.incomplete_line.keys():
875 if ( sha1 != commit.commit_sha1):
876 for pos in self.incomplete_line[sha1]:
877 in_line.append((pos, pos, self.colours[sha1]))
878 else:
879 del self.incomplete_line[sha1]
881 try:
882 colour = self.colours[commit.commit_sha1]
883 except KeyError:
884 last_colour +=1
885 self.colours[commit.commit_sha1] = last_colour
886 colour = last_colour
887 try:
888 node_pos = self.nodepos[commit.commit_sha1]
889 except KeyError:
890 last_nodepos +=1
891 self.nodepos[commit.commit_sha1] = last_nodepos
892 node_pos = last_nodepos
894 #The first parent always continue on the same line
895 try:
896 # check we alreay have the value
897 tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
898 except KeyError:
899 self.colours[commit.parent_sha1[0]] = colour
900 self.nodepos[commit.parent_sha1[0]] = node_pos
902 in_line.append((node_pos, self.nodepos[commit.parent_sha1[0]],
903 self.colours[commit.parent_sha1[0]]))
905 self.add_incomplete_line(commit.parent_sha1[0], index+1)
907 if (len(commit.parent_sha1) > 1):
908 for parent_id in commit.parent_sha1[1:]:
909 try:
910 tmp_node_pos = self.nodepos[parent_id]
911 except KeyError:
912 last_colour += 1;
913 self.colours[parent_id] = last_colour
914 last_nodepos +=1
915 self.nodepos[parent_id] = last_nodepos
917 in_line.append((node_pos, self.nodepos[parent_id],
918 self.colours[parent_id]))
919 self.add_incomplete_line(parent_id, index+1)
922 try:
923 branch_tag = self.bt_sha1[commit.commit_sha1]
924 except KeyError:
925 branch_tag = [ ]
928 node = (node_pos, colour, branch_tag)
930 self.model.append([commit, node, out_line, in_line,
931 commit.message, commit.author, commit.date])
933 return (in_line, last_colour, last_nodepos)
935 def add_incomplete_line(self, sha1, index):
936 try:
937 self.incomplete_line[sha1].append(self.nodepos[sha1])
938 except KeyError:
939 self.incomplete_line[sha1] = [self.nodepos[sha1]]
942 def _go_clicked_cb(self, widget, revid):
943 """Callback for when the go button for a parent is clicked."""
944 try:
945 self.treeview.set_cursor(self.index[revid])
946 except KeyError:
947 print "Revision %s not present in the list" % revid
948 # revid == 0 is the parent of the first commit
949 if (revid != 0 ):
950 print "Try running gitview without any options"
952 self.treeview.grab_focus()
954 def _show_clicked_cb(self, widget, commit_sha1, parent_sha1):
955 """Callback for when the show button for a parent is clicked."""
956 window = DiffWindow()
957 window.set_diff(commit_sha1, parent_sha1)
958 self.treeview.grab_focus()
960 if __name__ == "__main__":
961 without_diff = 0
963 if (len(sys.argv) > 1 ):
964 if (sys.argv[1] == "--without-diff"):
965 without_diff = 1
967 view = GitView( without_diff != 1)
968 view.run(sys.argv[without_diff:])