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 __copyright__ = "Copyright (C) 2007 Aneesh Kumar K.V <aneesh.kumar@gmail.com"
14 __author__ = "Aneesh Kumar K.V <aneesh.kumar@gmail.com>"
32 have_gtksourceview = True
34 have_gtksourceview = False
35 print "Running without gtksourceview module"
37 re_ident = re.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
39 def list_to_string(args, skip):
44 str_arg = str_arg + args[i]
45 str_arg = str_arg + " "
50 def show_date(epoch, tz):
52 tzsecs = float(tz[1:3]) * 3600
53 tzsecs += float(tz[3:5]) * 60
59 return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs))
62 class CellRendererGraph(gtk.GenericCellRenderer):
63 """Cell renderer for directed graph.
65 This module contains the implementation of a custom GtkCellRenderer that
66 draws part of the directed graph based on the lines suggested by the code
69 Because we're shiny, we use Cairo to do this, and because we're naughty
70 we cheat and draw over the bits of the TreeViewColumn that are supposed to
71 just be for the background.
74 node (column, colour, [ names ]) tuple to draw revision node,
75 in_lines (start, end, colour) tuple list to draw inward lines,
76 out_lines (start, end, colour) tuple list to draw outward lines.
80 "node": ( gobject.TYPE_PYOBJECT, "node",
81 "revision node instruction",
82 gobject.PARAM_WRITABLE
84 "in-lines": ( gobject.TYPE_PYOBJECT, "in-lines",
85 "instructions to draw lines into the cell",
86 gobject.PARAM_WRITABLE
88 "out-lines": ( gobject.TYPE_PYOBJECT, "out-lines",
89 "instructions to draw lines out of the cell",
90 gobject.PARAM_WRITABLE
94 def do_set_property(self, property, value):
95 """Set properties from GObject properties."""
96 if property.name == "node":
98 elif property.name == "in-lines":
100 elif property.name == "out-lines":
101 self.out_lines = value
103 raise AttributeError, "no such property: '%s'" % property.name
105 def box_size(self, widget):
106 """Calculate box size based on widget's font.
108 Cache this as it's probably expensive to get. It ensures that we
109 draw the graph at least as large as the text.
112 return self._box_size
113 except AttributeError:
114 pango_ctx = widget.get_pango_context()
115 font_desc = widget.get_style().font_desc
116 metrics = pango_ctx.get_metrics(font_desc)
118 ascent = pango.PIXELS(metrics.get_ascent())
119 descent = pango.PIXELS(metrics.get_descent())
121 self._box_size = ascent + descent + 6
122 return self._box_size
124 def set_colour(self, ctx, colour, bg, fg):
125 """Set the context source colour.
127 Picks a distinct colour based on an internal wheel; the bg
128 parameter provides the value that should be assigned to the 'zero'
129 colours and the fg parameter provides the multiplier that should be
130 applied to the foreground colours.
141 colour %= len(colours)
142 red = (colours[colour][0] * fg) or bg
143 green = (colours[colour][1] * fg) or bg
144 blue = (colours[colour][2] * fg) or bg
146 ctx.set_source_rgb(red, green, blue)
148 def on_get_size(self, widget, cell_area):
149 """Return the size we need for this cell.
151 Each cell is drawn individually and is only as wide as it needs
152 to be, we let the TreeViewColumn take care of making them all
155 box_size = self.box_size(widget)
158 for start, end, colour in self.in_lines + self.out_lines:
159 cols = int(max(cols, start, end))
161 (column, colour, names) = self.node
163 if (len(names) != 0):
165 names_len += len(item)
167 width = box_size * (cols + 1 ) + names_len
170 # FIXME I have no idea how to use cell_area properly
171 return (0, 0, width, height)
173 def on_render(self, window, widget, bg_area, cell_area, exp_area, flags):
174 """Render an individual cell.
176 Draws the cell contents using cairo, taking care to clip what we
177 do to within the background area so we don't draw over other cells.
178 Note that we're a bit naughty there and should really be drawing
179 in the cell_area (or even the exposed area), but we explicitly don't
182 We try and be a little clever, if the line we need to draw is going
183 to cross other columns we actually draw it as in the .---' style
184 instead of a pure diagonal ... this reduces confusion by an
187 ctx = window.cairo_create()
188 ctx.rectangle(bg_area.x, bg_area.y, bg_area.width, bg_area.height)
191 box_size = self.box_size(widget)
193 ctx.set_line_width(box_size / 8)
194 ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
196 # Draw lines into the cell
197 for start, end, colour in self.in_lines:
198 ctx.move_to(cell_area.x + box_size * start + box_size / 2,
199 bg_area.y - bg_area.height / 2)
202 ctx.line_to(cell_area.x + box_size * start, bg_area.y)
203 ctx.line_to(cell_area.x + box_size * end + box_size, bg_area.y)
204 elif start - end < -1:
205 ctx.line_to(cell_area.x + box_size * start + box_size,
207 ctx.line_to(cell_area.x + box_size * end, bg_area.y)
209 ctx.line_to(cell_area.x + box_size * end + box_size / 2,
210 bg_area.y + bg_area.height / 2)
212 self.set_colour(ctx, colour, 0.0, 0.65)
215 # Draw lines out of the cell
216 for start, end, colour in self.out_lines:
217 ctx.move_to(cell_area.x + box_size * start + box_size / 2,
218 bg_area.y + bg_area.height / 2)
221 ctx.line_to(cell_area.x + box_size * start,
222 bg_area.y + bg_area.height)
223 ctx.line_to(cell_area.x + box_size * end + box_size,
224 bg_area.y + bg_area.height)
225 elif start - end < -1:
226 ctx.line_to(cell_area.x + box_size * start + box_size,
227 bg_area.y + bg_area.height)
228 ctx.line_to(cell_area.x + box_size * end,
229 bg_area.y + bg_area.height)
231 ctx.line_to(cell_area.x + box_size * end + box_size / 2,
232 bg_area.y + bg_area.height / 2 + bg_area.height)
234 self.set_colour(ctx, colour, 0.0, 0.65)
237 # Draw the revision node in the right column
238 (column, colour, names) = self.node
239 ctx.arc(cell_area.x + box_size * column + box_size / 2,
240 cell_area.y + cell_area.height / 2,
241 box_size / 4, 0, 2 * math.pi)
244 self.set_colour(ctx, colour, 0.0, 0.5)
245 ctx.stroke_preserve()
247 self.set_colour(ctx, colour, 0.5, 1.0)
250 if (len(names) != 0):
253 name = name + item + " "
255 ctx.set_font_size(13)
257 self.set_colour(ctx, colour, 0.5, 1.0)
259 self.set_colour(ctx, colour, 0.0, 0.5)
262 class Commit(object):
263 """ This represent a commit object obtained after parsing the git-rev-list
266 __slots__ = ['children_sha1', 'message', 'author', 'date', 'committer',
267 'commit_date', 'commit_sha1', 'parent_sha1']
271 def __init__(self, commit_lines):
276 self.commit_date = ""
277 self.commit_sha1 = ""
278 self.parent_sha1 = [ ]
279 self.parse_commit(commit_lines)
282 def parse_commit(self, commit_lines):
284 # First line is the sha1 lines
285 line = string.strip(commit_lines[0])
286 sha1 = re.split(" ", line)
287 self.commit_sha1 = sha1[0]
288 self.parent_sha1 = sha1[1:]
290 #build the child list
291 for parent_id in self.parent_sha1:
293 Commit.children_sha1[parent_id].append(self.commit_sha1)
295 Commit.children_sha1[parent_id] = [self.commit_sha1]
297 # IF we don't have parent
298 if (len(self.parent_sha1) == 0):
299 self.parent_sha1 = [0]
301 for line in commit_lines[1:]:
302 m = re.match("^ ", line)
304 # First line of the commit message used for short log
305 if self.message == "":
306 self.message = string.strip(line)
309 m = re.match("tree", line)
313 m = re.match("parent", line)
317 m = re_ident.match(line)
319 date = show_date(m.group('epoch'), m.group('tz'))
320 if m.group(1) == "author":
321 self.author = m.group('ident')
323 elif m.group(1) == "committer":
324 self.committer = m.group('ident')
325 self.commit_date = date
329 def get_message(self, with_diff=0):
331 message = self.diff_tree()
333 fp = os.popen("git cat-file commit " + self.commit_sha1)
340 fp = os.popen("git diff-tree --pretty --cc -v -p --always " + self.commit_sha1)
345 class AnnotateWindow(object):
347 This object represents and manages a single window containing the
348 annotate information of the file
352 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
353 self.window.set_border_width(0)
354 self.window.set_title("Git repository browser annotation window")
356 # Use two thirds of the screen by default
357 screen = self.window.get_screen()
358 monitor = screen.get_monitor_geometry(0)
359 width = int(monitor.width * 0.66)
360 height = int(monitor.height * 0.66)
361 self.window.set_default_size(width, height)
363 def add_file_data(self, filename, commit_sha1, line_num):
364 fp = os.popen("git cat-file blob " + commit_sha1 +":"+filename)
366 for line in fp.readlines():
367 line = string.rstrip(line)
368 self.model.append(None, ["HEAD", filename, line, i])
372 # now set the cursor position
373 self.treeview.set_cursor(line_num-1)
374 self.treeview.grab_focus()
376 def _treeview_cursor_cb(self, *args):
377 """Callback for when the treeview cursor changes."""
378 (path, col) = self.treeview.get_cursor()
379 commit_sha1 = self.model[path][0]
381 fp = os.popen("git cat-file commit " + commit_sha1)
382 for line in fp.readlines():
383 commit_msg = commit_msg + line
386 self.commit_buffer.set_text(commit_msg)
388 def _treeview_row_activated(self, *args):
389 """Callback for when the treeview row gets selected."""
390 (path, col) = self.treeview.get_cursor()
391 commit_sha1 = self.model[path][0]
392 filename = self.model[path][1]
393 line_num = self.model[path][3]
395 window = AnnotateWindow();
396 fp = os.popen("git rev-parse "+ commit_sha1 + "~1")
397 commit_sha1 = string.strip(fp.readline())
399 window.annotate(filename, commit_sha1, line_num)
401 def data_ready(self, source, condition):
404 buffer = source.read(8192)
406 # resource temporary not available
409 if (len(buffer) == 0):
410 gobject.source_remove(self.io_watch_tag)
414 for buff in buffer.split("\n"):
415 annotate_line = re.compile('^([0-9a-f]{40}) (.+) (.+) (.+)$')
416 m = annotate_line.match(buff)
418 annotate_line = re.compile('^(filename) (.+)$')
419 m = annotate_line.match(buff)
422 filename = m.group(2)
424 self.commit_sha1 = m.group(1)
425 self.source_line = int(m.group(2))
426 self.result_line = int(m.group(3))
427 self.count = int(m.group(4))
428 #set the details only when we have the file name
431 while (self.count > 0):
432 # set at result_line + count-1 the sha1 as commit_sha1
433 self.count = self.count - 1
434 iter = self.model.iter_nth_child(None, self.result_line + self.count-1)
435 self.model.set(iter, 0, self.commit_sha1, 1, filename, 3, self.source_line)
438 def annotate(self, filename, commit_sha1, line_num):
439 # verify the commit_sha1 specified has this filename
441 fp = os.popen("git ls-tree "+ commit_sha1 + " -- " + filename)
442 line = string.strip(fp.readline())
444 # pop up the message the file is not there as a part of the commit
446 dialog = gtk.MessageDialog(parent=None, flags=0,
447 type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
449 dialog.set_markup("The file %s is not present in the parent commit %s" % (filename, commit_sha1))
457 self.window.add(vpan);
460 scrollwin = gtk.ScrolledWindow()
461 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
462 scrollwin.set_shadow_type(gtk.SHADOW_IN)
463 vpan.pack1(scrollwin, True, True);
466 self.model = gtk.TreeStore(str, str, str, int)
467 self.treeview = gtk.TreeView(self.model)
468 self.treeview.set_rules_hint(True)
469 self.treeview.set_search_column(0)
470 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
471 self.treeview.connect("row-activated", self._treeview_row_activated)
472 scrollwin.add(self.treeview)
475 cell = gtk.CellRendererText()
476 cell.set_property("width-chars", 10)
477 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
478 column = gtk.TreeViewColumn("Commit")
479 column.set_resizable(True)
480 column.pack_start(cell, expand=True)
481 column.add_attribute(cell, "text", 0)
482 self.treeview.append_column(column)
484 cell = gtk.CellRendererText()
485 cell.set_property("width-chars", 20)
486 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
487 column = gtk.TreeViewColumn("File Name")
488 column.set_resizable(True)
489 column.pack_start(cell, expand=True)
490 column.add_attribute(cell, "text", 1)
491 self.treeview.append_column(column)
493 cell = gtk.CellRendererText()
494 cell.set_property("width-chars", 20)
495 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
496 column = gtk.TreeViewColumn("Data")
497 column.set_resizable(True)
498 column.pack_start(cell, expand=True)
499 column.add_attribute(cell, "text", 2)
500 self.treeview.append_column(column)
502 # The commit message window
503 scrollwin = gtk.ScrolledWindow()
504 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
505 scrollwin.set_shadow_type(gtk.SHADOW_IN)
506 vpan.pack2(scrollwin, True, True);
509 commit_text = gtk.TextView()
510 self.commit_buffer = gtk.TextBuffer()
511 commit_text.set_buffer(self.commit_buffer)
512 scrollwin.add(commit_text)
517 self.add_file_data(filename, commit_sha1, line_num)
519 fp = os.popen("git blame --incremental -- " + filename + " " + commit_sha1)
520 flags = fcntl.fcntl(fp.fileno(), fcntl.F_GETFL)
521 fcntl.fcntl(fp.fileno(), fcntl.F_SETFL, flags | os.O_NONBLOCK)
522 self.io_watch_tag = gobject.io_add_watch(fp, gobject.IO_IN, self.data_ready)
525 class DiffWindow(object):
527 This object represents and manages a single window containing the
528 differences between two revisions on a branch.
532 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
533 self.window.set_border_width(0)
534 self.window.set_title("Git repository browser diff window")
536 # Use two thirds of the screen by default
537 screen = self.window.get_screen()
538 monitor = screen.get_monitor_geometry(0)
539 width = int(monitor.width * 0.66)
540 height = int(monitor.height * 0.66)
541 self.window.set_default_size(width, height)
547 """Construct the window contents."""
549 self.window.add(vbox)
552 menu_bar = gtk.MenuBar()
553 save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
554 save_menu.connect("activate", self.save_menu_response, "save")
556 menu_bar.append(save_menu)
557 vbox.pack_start(menu_bar, expand=False, fill=True)
562 scrollwin = gtk.ScrolledWindow()
563 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
564 scrollwin.set_shadow_type(gtk.SHADOW_IN)
565 hpan.pack1(scrollwin, True, True)
568 if have_gtksourceview:
569 self.buffer = gtksourceview.SourceBuffer()
570 slm = gtksourceview.SourceLanguagesManager()
571 gsl = slm.get_language_from_mime_type("text/x-patch")
572 self.buffer.set_highlight(True)
573 self.buffer.set_language(gsl)
574 sourceview = gtksourceview.SourceView(self.buffer)
576 self.buffer = gtk.TextBuffer()
577 sourceview = gtk.TextView(self.buffer)
580 sourceview.set_editable(False)
581 sourceview.modify_font(pango.FontDescription("Monospace"))
582 scrollwin.add(sourceview)
585 # The file hierarchy: a scrollable treeview
586 scrollwin = gtk.ScrolledWindow()
587 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
588 scrollwin.set_shadow_type(gtk.SHADOW_IN)
589 scrollwin.set_size_request(20, -1)
590 hpan.pack2(scrollwin, True, True)
593 self.model = gtk.TreeStore(str, str, str)
594 self.treeview = gtk.TreeView(self.model)
595 self.treeview.set_search_column(1)
596 self.treeview.connect("cursor-changed", self._treeview_clicked)
597 scrollwin.add(self.treeview)
600 cell = gtk.CellRendererText()
601 cell.set_property("width-chars", 20)
602 column = gtk.TreeViewColumn("Select to annotate")
603 column.pack_start(cell, expand=True)
604 column.add_attribute(cell, "text", 0)
605 self.treeview.append_column(column)
607 vbox.pack_start(hpan, expand=True, fill=True)
610 def _treeview_clicked(self, *args):
611 """Callback for when the treeview cursor changes."""
612 (path, col) = self.treeview.get_cursor()
613 specific_file = self.model[path][1]
614 commit_sha1 = self.model[path][2]
615 if specific_file == None :
617 elif specific_file == "" :
620 window = AnnotateWindow();
621 window.annotate(specific_file, commit_sha1, 1)
624 def commit_files(self, commit_sha1, parent_sha1):
626 add = self.model.append(None, [ "Added", None, None])
627 dele = self.model.append(None, [ "Deleted", None, None])
628 mod = self.model.append(None, [ "Modified", None, None])
629 diff_tree = re.compile('^(:.{6}) (.{6}) (.{40}) (.{40}) (A|D|M)\s(.+)$')
630 fp = os.popen("git diff-tree -r --no-commit-id " + parent_sha1 + " " + commit_sha1)
632 line = string.strip(fp.readline())
635 m = diff_tree.match(line)
640 filename = m.group(6)
642 self.model.append(add, [filename, filename, commit_sha1])
644 self.model.append(dele, [filename, filename, commit_sha1])
646 self.model.append(mod, [filename, filename, commit_sha1])
649 self.treeview.expand_all()
651 def set_diff(self, commit_sha1, parent_sha1, encoding):
652 """Set the differences showed by this window.
653 Compares the two trees and populates the window with the
656 # Diff with the first commit or the last commit shows nothing
657 if (commit_sha1 == 0 or parent_sha1 == 0 ):
660 fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
661 self.buffer.set_text(unicode(fp.read(), encoding).encode('utf-8'))
663 self.commit_files(commit_sha1, parent_sha1)
666 def save_menu_response(self, widget, string):
667 dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
668 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
669 gtk.STOCK_SAVE, gtk.RESPONSE_OK))
670 dialog.set_default_response(gtk.RESPONSE_OK)
671 response = dialog.run()
672 if response == gtk.RESPONSE_OK:
673 patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
674 self.buffer.get_end_iter())
675 fp = open(dialog.get_filename(), "w")
676 fp.write(patch_buffer)
680 class GitView(object):
681 """ This is the main class
685 def __init__(self, with_diff=0):
686 self.with_diff = with_diff
687 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
688 self.window.set_border_width(0)
689 self.window.set_title("Git repository browser")
694 # Use three-quarters of the screen by default
695 screen = self.window.get_screen()
696 monitor = screen.get_monitor_geometry(0)
697 width = int(monitor.width * 0.75)
698 height = int(monitor.height * 0.75)
699 self.window.set_default_size(width, height)
702 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
703 self.window.set_icon(icon)
705 self.accel_group = gtk.AccelGroup()
706 self.window.add_accel_group(self.accel_group)
707 self.accel_group.connect_group(0xffc2, 0, gtk.ACCEL_LOCKED, self.refresh);
708 self.accel_group.connect_group(0xffc1, 0, gtk.ACCEL_LOCKED, self.maximize);
709 self.accel_group.connect_group(0xffc8, 0, gtk.ACCEL_LOCKED, self.fullscreen);
710 self.accel_group.connect_group(0xffc9, 0, gtk.ACCEL_LOCKED, self.unfullscreen);
712 self.window.add(self.construct())
714 def refresh(self, widget, event=None, *arguments, **keywords):
717 Commit.children_sha1 = {}
718 self.set_branch(sys.argv[without_diff:])
722 def maximize(self, widget, event=None, *arguments, **keywords):
723 self.window.maximize()
726 def fullscreen(self, widget, event=None, *arguments, **keywords):
727 self.window.fullscreen()
730 def unfullscreen(self, widget, event=None, *arguments, **keywords):
731 self.window.unfullscreen()
734 def get_bt_sha1(self):
735 """ Update the bt_sha1 dictionary with the
736 respective sha1 details """
739 ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
740 fp = os.popen('git ls-remote "${GIT_DIR-.git}"')
742 line = string.strip(fp.readline())
745 m = ls_remote.match(line)
748 (sha1, name) = (m.group(1), m.group(2))
749 if not self.bt_sha1.has_key(sha1):
750 self.bt_sha1[sha1] = []
751 self.bt_sha1[sha1].append(name)
754 def get_encoding(self):
755 fp = os.popen("git config --get i18n.commitencoding")
756 self.encoding=string.strip(fp.readline())
758 if (self.encoding == ""):
759 self.encoding = "utf-8"
763 """Construct the window contents."""
766 paned.pack1(self.construct_top(), resize=False, shrink=True)
767 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
768 menu_bar = gtk.MenuBar()
769 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
770 help_menu = gtk.MenuItem("Help")
772 about_menu = gtk.MenuItem("About")
773 menu.append(about_menu)
774 about_menu.connect("activate", self.about_menu_response, "about")
776 help_menu.set_submenu(menu)
778 menu_bar.append(help_menu)
780 vbox.pack_start(menu_bar, expand=False, fill=True)
781 vbox.pack_start(paned, expand=True, fill=True)
787 def construct_top(self):
788 """Construct the top-half of the window."""
789 vbox = gtk.VBox(spacing=6)
790 vbox.set_border_width(12)
794 scrollwin = gtk.ScrolledWindow()
795 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
796 scrollwin.set_shadow_type(gtk.SHADOW_IN)
797 vbox.pack_start(scrollwin, expand=True, fill=True)
800 self.treeview = gtk.TreeView()
801 self.treeview.set_rules_hint(True)
802 self.treeview.set_search_column(4)
803 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
804 scrollwin.add(self.treeview)
807 cell = CellRendererGraph()
808 column = gtk.TreeViewColumn()
809 column.set_resizable(True)
810 column.pack_start(cell, expand=True)
811 column.add_attribute(cell, "node", 1)
812 column.add_attribute(cell, "in-lines", 2)
813 column.add_attribute(cell, "out-lines", 3)
814 self.treeview.append_column(column)
816 cell = gtk.CellRendererText()
817 cell.set_property("width-chars", 65)
818 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
819 column = gtk.TreeViewColumn("Message")
820 column.set_resizable(True)
821 column.pack_start(cell, expand=True)
822 column.add_attribute(cell, "text", 4)
823 self.treeview.append_column(column)
825 cell = gtk.CellRendererText()
826 cell.set_property("width-chars", 40)
827 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
828 column = gtk.TreeViewColumn("Author")
829 column.set_resizable(True)
830 column.pack_start(cell, expand=True)
831 column.add_attribute(cell, "text", 5)
832 self.treeview.append_column(column)
834 cell = gtk.CellRendererText()
835 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
836 column = gtk.TreeViewColumn("Date")
837 column.set_resizable(True)
838 column.pack_start(cell, expand=True)
839 column.add_attribute(cell, "text", 6)
840 self.treeview.append_column(column)
844 def about_menu_response(self, widget, string):
845 dialog = gtk.AboutDialog()
846 dialog.set_name("Gitview")
847 dialog.set_version(GitView.version)
848 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@gmail.com>"])
849 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
850 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
851 dialog.set_wrap_license(True)
856 def construct_bottom(self):
857 """Construct the bottom half of the window."""
858 vbox = gtk.VBox(False, spacing=6)
859 vbox.set_border_width(12)
860 (width, height) = self.window.get_size()
861 vbox.set_size_request(width, int(height / 2.5))
864 self.table = gtk.Table(rows=4, columns=4)
865 self.table.set_row_spacings(6)
866 self.table.set_col_spacings(6)
867 vbox.pack_start(self.table, expand=False, fill=True)
870 align = gtk.Alignment(0.0, 0.5)
872 label.set_markup("<b>Revision:</b>")
874 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
878 align = gtk.Alignment(0.0, 0.5)
879 self.revid_label = gtk.Label()
880 self.revid_label.set_selectable(True)
881 align.add(self.revid_label)
882 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
883 self.revid_label.show()
886 align = gtk.Alignment(0.0, 0.5)
888 label.set_markup("<b>Committer:</b>")
890 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
894 align = gtk.Alignment(0.0, 0.5)
895 self.committer_label = gtk.Label()
896 self.committer_label.set_selectable(True)
897 align.add(self.committer_label)
898 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
899 self.committer_label.show()
902 align = gtk.Alignment(0.0, 0.5)
904 label.set_markup("<b>Timestamp:</b>")
906 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
910 align = gtk.Alignment(0.0, 0.5)
911 self.timestamp_label = gtk.Label()
912 self.timestamp_label.set_selectable(True)
913 align.add(self.timestamp_label)
914 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
915 self.timestamp_label.show()
918 align = gtk.Alignment(0.0, 0.5)
920 label.set_markup("<b>Parents:</b>")
922 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
925 self.parents_widgets = []
927 align = gtk.Alignment(0.0, 0.5)
929 label.set_markup("<b>Children:</b>")
931 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
934 self.children_widgets = []
936 scrollwin = gtk.ScrolledWindow()
937 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
938 scrollwin.set_shadow_type(gtk.SHADOW_IN)
939 vbox.pack_start(scrollwin, expand=True, fill=True)
942 if have_gtksourceview:
943 self.message_buffer = gtksourceview.SourceBuffer()
944 slm = gtksourceview.SourceLanguagesManager()
945 gsl = slm.get_language_from_mime_type("text/x-patch")
946 self.message_buffer.set_highlight(True)
947 self.message_buffer.set_language(gsl)
948 sourceview = gtksourceview.SourceView(self.message_buffer)
950 self.message_buffer = gtk.TextBuffer()
951 sourceview = gtk.TextView(self.message_buffer)
953 sourceview.set_editable(False)
954 sourceview.modify_font(pango.FontDescription("Monospace"))
955 scrollwin.add(sourceview)
960 def _treeview_cursor_cb(self, *args):
961 """Callback for when the treeview cursor changes."""
962 (path, col) = self.treeview.get_cursor()
963 commit = self.model[path][0]
965 if commit.committer is not None:
966 committer = commit.committer
967 timestamp = commit.commit_date
968 message = commit.get_message(self.with_diff)
969 revid_label = commit.commit_sha1
976 self.revid_label.set_text(revid_label)
977 self.committer_label.set_text(committer)
978 self.timestamp_label.set_text(timestamp)
979 self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
981 for widget in self.parents_widgets:
982 self.table.remove(widget)
984 self.parents_widgets = []
985 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
986 for idx, parent_id in enumerate(commit.parent_sha1):
987 self.table.set_row_spacing(idx + 3, 0)
989 align = gtk.Alignment(0.0, 0.0)
990 self.parents_widgets.append(align)
991 self.table.attach(align, 1, 2, idx + 3, idx + 4,
992 gtk.EXPAND | gtk.FILL, gtk.FILL)
995 hbox = gtk.HBox(False, 0)
999 label = gtk.Label(parent_id)
1000 label.set_selectable(True)
1001 hbox.pack_start(label, expand=False, fill=True)
1005 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1008 button = gtk.Button()
1010 button.set_relief(gtk.RELIEF_NONE)
1011 button.connect("clicked", self._go_clicked_cb, parent_id)
1012 hbox.pack_start(button, expand=False, fill=True)
1016 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1019 button = gtk.Button()
1021 button.set_relief(gtk.RELIEF_NONE)
1022 button.set_sensitive(True)
1023 button.connect("clicked", self._show_clicked_cb,
1024 commit.commit_sha1, parent_id, self.encoding)
1025 hbox.pack_start(button, expand=False, fill=True)
1028 # Populate with child details
1029 for widget in self.children_widgets:
1030 self.table.remove(widget)
1032 self.children_widgets = []
1034 child_sha1 = Commit.children_sha1[commit.commit_sha1]
1036 # We don't have child
1039 if ( len(child_sha1) > len(commit.parent_sha1)):
1040 self.table.resize(4 + len(child_sha1) - 1, 4)
1042 for idx, child_id in enumerate(child_sha1):
1043 self.table.set_row_spacing(idx + 3, 0)
1045 align = gtk.Alignment(0.0, 0.0)
1046 self.children_widgets.append(align)
1047 self.table.attach(align, 3, 4, idx + 3, idx + 4,
1048 gtk.EXPAND | gtk.FILL, gtk.FILL)
1051 hbox = gtk.HBox(False, 0)
1055 label = gtk.Label(child_id)
1056 label.set_selectable(True)
1057 hbox.pack_start(label, expand=False, fill=True)
1061 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1064 button = gtk.Button()
1066 button.set_relief(gtk.RELIEF_NONE)
1067 button.connect("clicked", self._go_clicked_cb, child_id)
1068 hbox.pack_start(button, expand=False, fill=True)
1072 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1075 button = gtk.Button()
1077 button.set_relief(gtk.RELIEF_NONE)
1078 button.set_sensitive(True)
1079 button.connect("clicked", self._show_clicked_cb,
1080 child_id, commit.commit_sha1, self.encoding)
1081 hbox.pack_start(button, expand=False, fill=True)
1084 def _destroy_cb(self, widget):
1085 """Callback for when a window we manage is destroyed."""
1090 """Stop the GTK+ main loop."""
1093 def run(self, args):
1094 self.set_branch(args)
1095 self.window.connect("destroy", self._destroy_cb)
1099 def set_branch(self, args):
1100 """Fill in different windows with info from the reposiroty"""
1101 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
1102 git_rev_list_cmd = fp.read()
1104 fp = os.popen("git rev-list --header --topo-order --parents " + git_rev_list_cmd)
1105 self.update_window(fp)
1107 def update_window(self, fp):
1110 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
1111 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
1113 # used for cursor positioning
1118 self.incomplete_line = {}
1125 input_line = fp.readline()
1126 while (input_line != ""):
1127 # The commit header ends with '\0'
1128 # This NULL is immediately followed by the sha1 of the
1130 if (input_line[0] != '\0'):
1131 commit_lines.append(input_line)
1132 input_line = fp.readline()
1135 commit = Commit(commit_lines)
1136 if (commit != None ):
1137 self.commits.append(commit)
1141 commit_lines.append(input_line[1:])
1142 input_line = fp.readline()
1146 for commit in self.commits:
1147 (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
1151 self.index[commit.commit_sha1] = index
1154 self.treeview.set_model(self.model)
1155 self.treeview.show()
1157 def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
1165 if (last_nodepos > 5):
1168 # Add the incomplete lines of the last cell in this
1170 colour = self.colours[commit.commit_sha1]
1172 self.colours[commit.commit_sha1] = last_colour+1
1173 last_colour = self.colours[commit.commit_sha1]
1174 colour = self.colours[commit.commit_sha1]
1177 node_pos = self.nodepos[commit.commit_sha1]
1179 self.nodepos[commit.commit_sha1] = last_nodepos+1
1180 last_nodepos = self.nodepos[commit.commit_sha1]
1181 node_pos = self.nodepos[commit.commit_sha1]
1183 #The first parent always continue on the same line
1185 # check we alreay have the value
1186 tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
1188 self.colours[commit.parent_sha1[0]] = colour
1189 self.nodepos[commit.parent_sha1[0]] = node_pos
1191 for sha1 in self.incomplete_line.keys():
1192 if (sha1 != commit.commit_sha1):
1193 self.draw_incomplete_line(sha1, node_pos,
1194 out_line, in_line, index)
1196 del self.incomplete_line[sha1]
1199 for parent_id in commit.parent_sha1:
1201 tmp_node_pos = self.nodepos[parent_id]
1203 self.colours[parent_id] = last_colour+1
1204 last_colour = self.colours[parent_id]
1205 self.nodepos[parent_id] = last_nodepos+1
1206 last_nodepos = self.nodepos[parent_id]
1208 in_line.append((node_pos, self.nodepos[parent_id],
1209 self.colours[parent_id]))
1210 self.add_incomplete_line(parent_id)
1213 branch_tag = self.bt_sha1[commit.commit_sha1]
1218 node = (node_pos, colour, branch_tag)
1220 self.model.append([commit, node, out_line, in_line,
1221 commit.message, commit.author, commit.date])
1223 return (in_line, last_colour, last_nodepos)
1225 def add_incomplete_line(self, sha1):
1227 self.incomplete_line[sha1].append(self.nodepos[sha1])
1229 self.incomplete_line[sha1] = [self.nodepos[sha1]]
1231 def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
1232 for idx, pos in enumerate(self.incomplete_line[sha1]):
1233 if(pos == node_pos):
1234 #remove the straight line and add a slash
1235 if ((pos, pos, self.colours[sha1]) in out_line):
1236 out_line.remove((pos, pos, self.colours[sha1]))
1237 out_line.append((pos, pos+0.5, self.colours[sha1]))
1238 self.incomplete_line[sha1][idx] = pos = pos+0.5
1240 next_commit = self.commits[index+1]
1241 if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
1242 # join the line back to the node point
1243 # This need to be done only if we modified it
1244 in_line.append((pos, pos-0.5, self.colours[sha1]))
1248 in_line.append((pos, pos, self.colours[sha1]))
1251 def _go_clicked_cb(self, widget, revid):
1252 """Callback for when the go button for a parent is clicked."""
1254 self.treeview.set_cursor(self.index[revid])
1256 dialog = gtk.MessageDialog(parent=None, flags=0,
1257 type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
1258 message_format=None)
1259 dialog.set_markup("Revision <b>%s</b> not present in the list" % revid)
1260 # revid == 0 is the parent of the first commit
1262 dialog.format_secondary_text("Try running gitview without any options")
1266 self.treeview.grab_focus()
1268 def _show_clicked_cb(self, widget, commit_sha1, parent_sha1, encoding):
1269 """Callback for when the show button for a parent is clicked."""
1270 window = DiffWindow()
1271 window.set_diff(commit_sha1, parent_sha1, encoding)
1272 self.treeview.grab_focus()
1275 if __name__ == "__main__":
1277 if (len(sys.argv) > 1 ):
1278 if (sys.argv[1] == "--without-diff"):
1281 view = GitView( without_diff != 1)
1282 view.run(sys.argv[without_diff:])