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.
9 GUI browser for git repository
10 This program is based on bzrk by Scott James Remnant <scott@ubuntu.com>
12 __copyright__ = "Copyright (C) 2006 Hewlett-Packard Development Company, L.P."
13 __author__ = "Aneesh Kumar K.V <aneesh.kumar@hp.com>"
30 have_gtksourceview = True
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):
42 str_arg = str_arg + args[i]
43 str_arg = str_arg + " "
48 def show_date(epoch, tz):
50 tzsecs = float(tz[1:3]) * 3600
51 tzsecs += float(tz[3:5]) * 60
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
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.
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.
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":
96 elif property.name == "in-lines":
98 elif property.name == "out-lines":
99 self.out_lines = value
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.
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.
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
153 box_size = self.box_size(widget)
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
161 if (len(names) != 0):
163 names_len += len(item)
165 width = box_size * (cols + 1 ) + names_len
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
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
185 ctx = window.cairo_create()
186 ctx.rectangle(bg_area.x, bg_area.y, bg_area.width, bg_area.height)
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)
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,
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)
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)
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)
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):
245 name = name + item + " "
247 ctx.select_font_face("Monospace")
248 ctx.set_font_size(13)
251 self.set_colour(ctx, colour, 0.0, 0.5)
252 ctx.stroke_preserve()
254 self.set_colour(ctx, colour, 0.5, 1.0)
258 """ This represent a commit object obtained after parsing the git-rev-list
263 def __init__(self, commit_lines):
268 self.commit_date = ""
269 self.commit_sha1 = ""
270 self.parent_sha1 = [ ]
271 self.parse_commit(commit_lines)
274 def parse_commit(self, commit_lines):
276 # First line is the sha1 lines
277 line = string.strip(commit_lines[0])
278 sha1 = re.split(" ", line)
279 self.commit_sha1 = sha1[0]
280 self.parent_sha1 = sha1[1:]
282 #build the child list
283 for parent_id in self.parent_sha1:
285 Commit.children_sha1[parent_id].append(self.commit_sha1)
287 Commit.children_sha1[parent_id] = [self.commit_sha1]
289 # IF we don't have parent
290 if (len(self.parent_sha1) == 0):
291 self.parent_sha1 = [0]
293 for line in commit_lines[1:]:
294 m = re.match("^ ", line)
296 # First line of the commit message used for short log
297 if self.message == "":
298 self.message = string.strip(line)
301 m = re.match("tree", line)
305 m = re.match("parent", line)
309 m = re_ident.match(line)
311 date = show_date(m.group('epoch'), m.group('tz'))
312 if m.group(1) == "author":
313 self.author = m.group('ident')
315 elif m.group(1) == "committer":
316 self.committer = m.group('ident')
317 self.commit_date = date
321 def get_message(self, with_diff=0):
323 message = self.diff_tree()
325 fp = os.popen("git cat-file commit " + self.commit_sha1)
332 fp = os.popen("git diff-tree --pretty --cc -v -p --always " + self.commit_sha1)
339 This object represents and manages a single window containing the
340 differences between two revisions on a branch.
344 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
345 self.window.set_border_width(0)
346 self.window.set_title("Git repository browser diff window")
348 # Use two thirds of the screen by default
349 screen = self.window.get_screen()
350 monitor = screen.get_monitor_geometry(0)
351 width = int(monitor.width * 0.66)
352 height = int(monitor.height * 0.66)
353 self.window.set_default_size(width, height)
358 """Construct the window contents."""
360 self.window.add(vbox)
363 menu_bar = gtk.MenuBar()
364 save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
365 save_menu.connect("activate", self.save_menu_response, "save")
367 menu_bar.append(save_menu)
368 vbox.pack_start(menu_bar, False, False, 2)
371 scrollwin = gtk.ScrolledWindow()
372 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
373 scrollwin.set_shadow_type(gtk.SHADOW_IN)
374 vbox.pack_start(scrollwin, expand=True, fill=True)
377 if have_gtksourceview:
378 self.buffer = gtksourceview.SourceBuffer()
379 slm = gtksourceview.SourceLanguagesManager()
380 gsl = slm.get_language_from_mime_type("text/x-patch")
381 self.buffer.set_highlight(True)
382 self.buffer.set_language(gsl)
383 sourceview = gtksourceview.SourceView(self.buffer)
385 self.buffer = gtk.TextBuffer()
386 sourceview = gtk.TextView(self.buffer)
388 sourceview.set_editable(False)
389 sourceview.modify_font(pango.FontDescription("Monospace"))
390 scrollwin.add(sourceview)
394 def set_diff(self, commit_sha1, parent_sha1, encoding):
395 """Set the differences showed by this window.
396 Compares the two trees and populates the window with the
399 # Diff with the first commit or the last commit shows nothing
400 if (commit_sha1 == 0 or parent_sha1 == 0 ):
403 fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
404 self.buffer.set_text(unicode(fp.read(), encoding).encode('utf-8'))
408 def save_menu_response(self, widget, string):
409 dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
410 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
411 gtk.STOCK_SAVE, gtk.RESPONSE_OK))
412 dialog.set_default_response(gtk.RESPONSE_OK)
413 response = dialog.run()
414 if response == gtk.RESPONSE_OK:
415 patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
416 self.buffer.get_end_iter())
417 fp = open(dialog.get_filename(), "w")
418 fp.write(patch_buffer)
423 """ This is the main class
427 def __init__(self, with_diff=0):
428 self.with_diff = with_diff
429 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
430 self.window.set_border_width(0)
431 self.window.set_title("Git repository browser")
436 # Use three-quarters of the screen by default
437 screen = self.window.get_screen()
438 monitor = screen.get_monitor_geometry(0)
439 width = int(monitor.width * 0.75)
440 height = int(monitor.height * 0.75)
441 self.window.set_default_size(width, height)
444 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
445 self.window.set_icon(icon)
447 self.accel_group = gtk.AccelGroup()
448 self.window.add_accel_group(self.accel_group)
452 def get_bt_sha1(self):
453 """ Update the bt_sha1 dictionary with the
454 respective sha1 details """
457 ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
458 fp = os.popen('git ls-remote "${GIT_DIR-.git}"')
460 line = string.strip(fp.readline())
463 m = ls_remote.match(line)
466 (sha1, name) = (m.group(1), m.group(2))
467 if not self.bt_sha1.has_key(sha1):
468 self.bt_sha1[sha1] = []
469 self.bt_sha1[sha1].append(name)
472 def get_encoding(self):
473 fp = os.popen("git repo-config --get i18n.commitencoding")
474 self.encoding=string.strip(fp.readline())
476 if (self.encoding == ""):
477 self.encoding = "utf-8"
481 """Construct the window contents."""
483 paned.pack1(self.construct_top(), resize=False, shrink=True)
484 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
485 self.window.add(paned)
489 def construct_top(self):
490 """Construct the top-half of the window."""
491 vbox = gtk.VBox(spacing=6)
492 vbox.set_border_width(12)
495 menu_bar = gtk.MenuBar()
496 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
497 help_menu = gtk.MenuItem("Help")
499 about_menu = gtk.MenuItem("About")
500 menu.append(about_menu)
501 about_menu.connect("activate", self.about_menu_response, "about")
503 help_menu.set_submenu(menu)
505 menu_bar.append(help_menu)
506 vbox.pack_start(menu_bar, False, False, 2)
509 scrollwin = gtk.ScrolledWindow()
510 scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
511 scrollwin.set_shadow_type(gtk.SHADOW_IN)
512 vbox.pack_start(scrollwin, expand=True, fill=True)
515 self.treeview = gtk.TreeView()
516 self.treeview.set_rules_hint(True)
517 self.treeview.set_search_column(4)
518 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
519 scrollwin.add(self.treeview)
522 cell = CellRendererGraph()
523 column = gtk.TreeViewColumn()
524 column.set_resizable(True)
525 column.pack_start(cell, expand=True)
526 column.add_attribute(cell, "node", 1)
527 column.add_attribute(cell, "in-lines", 2)
528 column.add_attribute(cell, "out-lines", 3)
529 self.treeview.append_column(column)
531 cell = gtk.CellRendererText()
532 cell.set_property("width-chars", 65)
533 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
534 column = gtk.TreeViewColumn("Message")
535 column.set_resizable(True)
536 column.pack_start(cell, expand=True)
537 column.add_attribute(cell, "text", 4)
538 self.treeview.append_column(column)
540 cell = gtk.CellRendererText()
541 cell.set_property("width-chars", 40)
542 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
543 column = gtk.TreeViewColumn("Author")
544 column.set_resizable(True)
545 column.pack_start(cell, expand=True)
546 column.add_attribute(cell, "text", 5)
547 self.treeview.append_column(column)
549 cell = gtk.CellRendererText()
550 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
551 column = gtk.TreeViewColumn("Date")
552 column.set_resizable(True)
553 column.pack_start(cell, expand=True)
554 column.add_attribute(cell, "text", 6)
555 self.treeview.append_column(column)
559 def about_menu_response(self, widget, string):
560 dialog = gtk.AboutDialog()
561 dialog.set_name("Gitview")
562 dialog.set_version(GitView.version)
563 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
564 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
565 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
566 dialog.set_wrap_license(True)
571 def construct_bottom(self):
572 """Construct the bottom half of the window."""
573 vbox = gtk.VBox(False, spacing=6)
574 vbox.set_border_width(12)
575 (width, height) = self.window.get_size()
576 vbox.set_size_request(width, int(height / 2.5))
579 self.table = gtk.Table(rows=4, columns=4)
580 self.table.set_row_spacings(6)
581 self.table.set_col_spacings(6)
582 vbox.pack_start(self.table, expand=False, fill=True)
585 align = gtk.Alignment(0.0, 0.5)
587 label.set_markup("<b>Revision:</b>")
589 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
593 align = gtk.Alignment(0.0, 0.5)
594 self.revid_label = gtk.Label()
595 self.revid_label.set_selectable(True)
596 align.add(self.revid_label)
597 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
598 self.revid_label.show()
601 align = gtk.Alignment(0.0, 0.5)
603 label.set_markup("<b>Committer:</b>")
605 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
609 align = gtk.Alignment(0.0, 0.5)
610 self.committer_label = gtk.Label()
611 self.committer_label.set_selectable(True)
612 align.add(self.committer_label)
613 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
614 self.committer_label.show()
617 align = gtk.Alignment(0.0, 0.5)
619 label.set_markup("<b>Timestamp:</b>")
621 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
625 align = gtk.Alignment(0.0, 0.5)
626 self.timestamp_label = gtk.Label()
627 self.timestamp_label.set_selectable(True)
628 align.add(self.timestamp_label)
629 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
630 self.timestamp_label.show()
633 align = gtk.Alignment(0.0, 0.5)
635 label.set_markup("<b>Parents:</b>")
637 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
640 self.parents_widgets = []
642 align = gtk.Alignment(0.0, 0.5)
644 label.set_markup("<b>Children:</b>")
646 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
649 self.children_widgets = []
651 scrollwin = gtk.ScrolledWindow()
652 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
653 scrollwin.set_shadow_type(gtk.SHADOW_IN)
654 vbox.pack_start(scrollwin, expand=True, fill=True)
657 if have_gtksourceview:
658 self.message_buffer = gtksourceview.SourceBuffer()
659 slm = gtksourceview.SourceLanguagesManager()
660 gsl = slm.get_language_from_mime_type("text/x-patch")
661 self.message_buffer.set_highlight(True)
662 self.message_buffer.set_language(gsl)
663 sourceview = gtksourceview.SourceView(self.message_buffer)
665 self.message_buffer = gtk.TextBuffer()
666 sourceview = gtk.TextView(self.message_buffer)
668 sourceview.set_editable(False)
669 sourceview.modify_font(pango.FontDescription("Monospace"))
670 scrollwin.add(sourceview)
675 def _treeview_cursor_cb(self, *args):
676 """Callback for when the treeview cursor changes."""
677 (path, col) = self.treeview.get_cursor()
678 commit = self.model[path][0]
680 if commit.committer is not None:
681 committer = commit.committer
682 timestamp = commit.commit_date
683 message = commit.get_message(self.with_diff)
684 revid_label = commit.commit_sha1
691 self.revid_label.set_text(revid_label)
692 self.committer_label.set_text(committer)
693 self.timestamp_label.set_text(timestamp)
694 self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
696 for widget in self.parents_widgets:
697 self.table.remove(widget)
699 self.parents_widgets = []
700 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
701 for idx, parent_id in enumerate(commit.parent_sha1):
702 self.table.set_row_spacing(idx + 3, 0)
704 align = gtk.Alignment(0.0, 0.0)
705 self.parents_widgets.append(align)
706 self.table.attach(align, 1, 2, idx + 3, idx + 4,
707 gtk.EXPAND | gtk.FILL, gtk.FILL)
710 hbox = gtk.HBox(False, 0)
714 label = gtk.Label(parent_id)
715 label.set_selectable(True)
716 hbox.pack_start(label, expand=False, fill=True)
720 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
723 button = gtk.Button()
725 button.set_relief(gtk.RELIEF_NONE)
726 button.connect("clicked", self._go_clicked_cb, parent_id)
727 hbox.pack_start(button, expand=False, fill=True)
731 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
734 button = gtk.Button()
736 button.set_relief(gtk.RELIEF_NONE)
737 button.set_sensitive(True)
738 button.connect("clicked", self._show_clicked_cb,
739 commit.commit_sha1, parent_id, self.encoding)
740 hbox.pack_start(button, expand=False, fill=True)
743 # Populate with child details
744 for widget in self.children_widgets:
745 self.table.remove(widget)
747 self.children_widgets = []
749 child_sha1 = Commit.children_sha1[commit.commit_sha1]
751 # We don't have child
754 if ( len(child_sha1) > len(commit.parent_sha1)):
755 self.table.resize(4 + len(child_sha1) - 1, 4)
757 for idx, child_id in enumerate(child_sha1):
758 self.table.set_row_spacing(idx + 3, 0)
760 align = gtk.Alignment(0.0, 0.0)
761 self.children_widgets.append(align)
762 self.table.attach(align, 3, 4, idx + 3, idx + 4,
763 gtk.EXPAND | gtk.FILL, gtk.FILL)
766 hbox = gtk.HBox(False, 0)
770 label = gtk.Label(child_id)
771 label.set_selectable(True)
772 hbox.pack_start(label, expand=False, fill=True)
776 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
779 button = gtk.Button()
781 button.set_relief(gtk.RELIEF_NONE)
782 button.connect("clicked", self._go_clicked_cb, child_id)
783 hbox.pack_start(button, expand=False, fill=True)
787 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
790 button = gtk.Button()
792 button.set_relief(gtk.RELIEF_NONE)
793 button.set_sensitive(True)
794 button.connect("clicked", self._show_clicked_cb,
795 child_id, commit.commit_sha1)
796 hbox.pack_start(button, expand=False, fill=True)
799 def _destroy_cb(self, widget):
800 """Callback for when a window we manage is destroyed."""
805 """Stop the GTK+ main loop."""
809 self.set_branch(args)
810 self.window.connect("destroy", self._destroy_cb)
814 def set_branch(self, args):
815 """Fill in different windows with info from the reposiroty"""
816 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
817 git_rev_list_cmd = fp.read()
819 fp = os.popen("git rev-list --header --topo-order --parents " + git_rev_list_cmd)
820 self.update_window(fp)
822 def update_window(self, fp):
825 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
826 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
828 # used for cursor positioning
833 self.incomplete_line = {}
840 input_line = fp.readline()
841 while (input_line != ""):
842 # The commit header ends with '\0'
843 # This NULL is immediately followed by the sha1 of the
845 if (input_line[0] != '\0'):
846 commit_lines.append(input_line)
847 input_line = fp.readline()
850 commit = Commit(commit_lines)
851 if (commit != None ):
852 self.commits.append(commit)
856 commit_lines.append(input_line[1:])
857 input_line = fp.readline()
861 for commit in self.commits:
862 (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
866 self.index[commit.commit_sha1] = index
869 self.treeview.set_model(self.model)
872 def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
880 if (last_nodepos > 5):
883 # Add the incomplete lines of the last cell in this
885 colour = self.colours[commit.commit_sha1]
887 self.colours[commit.commit_sha1] = last_colour+1
888 last_colour = self.colours[commit.commit_sha1]
889 colour = self.colours[commit.commit_sha1]
892 node_pos = self.nodepos[commit.commit_sha1]
894 self.nodepos[commit.commit_sha1] = last_nodepos+1
895 last_nodepos = self.nodepos[commit.commit_sha1]
896 node_pos = self.nodepos[commit.commit_sha1]
898 #The first parent always continue on the same line
900 # check we alreay have the value
901 tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
903 self.colours[commit.parent_sha1[0]] = colour
904 self.nodepos[commit.parent_sha1[0]] = node_pos
906 for sha1 in self.incomplete_line.keys():
907 if (sha1 != commit.commit_sha1):
908 self.draw_incomplete_line(sha1, node_pos,
909 out_line, in_line, index)
911 del self.incomplete_line[sha1]
914 for parent_id in commit.parent_sha1:
916 tmp_node_pos = self.nodepos[parent_id]
918 self.colours[parent_id] = last_colour+1
919 last_colour = self.colours[parent_id]
920 self.nodepos[parent_id] = last_nodepos+1
921 last_nodepos = self.nodepos[parent_id]
923 in_line.append((node_pos, self.nodepos[parent_id],
924 self.colours[parent_id]))
925 self.add_incomplete_line(parent_id)
928 branch_tag = self.bt_sha1[commit.commit_sha1]
933 node = (node_pos, colour, branch_tag)
935 self.model.append([commit, node, out_line, in_line,
936 commit.message, commit.author, commit.date])
938 return (in_line, last_colour, last_nodepos)
940 def add_incomplete_line(self, sha1):
942 self.incomplete_line[sha1].append(self.nodepos[sha1])
944 self.incomplete_line[sha1] = [self.nodepos[sha1]]
946 def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
947 for idx, pos in enumerate(self.incomplete_line[sha1]):
949 #remove the straight line and add a slash
950 if ((pos, pos, self.colours[sha1]) in out_line):
951 out_line.remove((pos, pos, self.colours[sha1]))
952 out_line.append((pos, pos+0.5, self.colours[sha1]))
953 self.incomplete_line[sha1][idx] = pos = pos+0.5
955 next_commit = self.commits[index+1]
956 if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
957 # join the line back to the node point
958 # This need to be done only if we modified it
959 in_line.append((pos, pos-0.5, self.colours[sha1]))
963 in_line.append((pos, pos, self.colours[sha1]))
966 def _go_clicked_cb(self, widget, revid):
967 """Callback for when the go button for a parent is clicked."""
969 self.treeview.set_cursor(self.index[revid])
971 print "Revision %s not present in the list" % revid
972 # revid == 0 is the parent of the first commit
974 print "Try running gitview without any options"
976 self.treeview.grab_focus()
978 def _show_clicked_cb(self, widget, commit_sha1, parent_sha1, encoding):
979 """Callback for when the show button for a parent is clicked."""
980 window = DiffWindow()
981 window.set_diff(commit_sha1, parent_sha1, encoding)
982 self.treeview.grab_focus()
984 if __name__ == "__main__":
987 if (len(sys.argv) > 1 ):
988 if (sys.argv[1] == "--without-diff"):
991 view = GitView( without_diff != 1)
992 view.run(sys.argv[without_diff:])