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")
357 # Use two thirds of the screen by default
358 screen = self.window.get_screen()
359 monitor = screen.get_monitor_geometry(0)
360 width = int(monitor.width * 0.66)
361 height = int(monitor.height * 0.66)
362 self.window.set_default_size(width, height)
364 def add_file_data(self, filename, commit_sha1, line_num):
365 fp = os.popen("git cat-file blob " + commit_sha1 +":"+filename)
367 for line in fp.readlines():
368 line = string.rstrip(line)
369 self.model.append(None, ["HEAD", filename, line, i])
373 # now set the cursor position
374 self.treeview.set_cursor(line_num-1)
375 self.treeview.grab_focus()
377 def _treeview_cursor_cb(self, *args):
378 """Callback for when the treeview cursor changes."""
379 (path, col) = self.treeview.get_cursor()
380 commit_sha1 = self.model[path][0]
382 fp = os.popen("git cat-file commit " + commit_sha1)
383 for line in fp.readlines():
384 commit_msg = commit_msg + line
387 self.commit_buffer.set_text(commit_msg)
389 def _treeview_row_activated(self, *args):
390 """Callback for when the treeview row gets selected."""
391 (path, col) = self.treeview.get_cursor()
392 commit_sha1 = self.model[path][0]
393 filename = self.model[path][1]
394 line_num = self.model[path][3]
396 window = AnnotateWindow();
397 fp = os.popen("git rev-parse "+ commit_sha1 + "~1")
398 commit_sha1 = string.strip(fp.readline())
400 window.annotate(filename, commit_sha1, line_num)
402 def data_ready(self, source, condition):
405 # A simple readline doesn't work
407 buffer = source.read(100)
410 # resource temporary not available
413 if (len(buffer) == 0):
414 gobject.source_remove(self.io_watch_tag)
418 if (self.prev_read != ""):
419 buffer = self.prev_read + buffer
422 if (buffer[len(buffer) -1] != '\n'):
424 newline_index = buffer.rindex("\n")
428 self.prev_read = buffer[newline_index:(len(buffer))]
429 buffer = buffer[0:newline_index]
431 for buff in buffer.split("\n"):
432 annotate_line = re.compile('^([0-9a-f]{40}) (.+) (.+) (.+)$')
433 m = annotate_line.match(buff)
435 annotate_line = re.compile('^(filename) (.+)$')
436 m = annotate_line.match(buff)
439 filename = m.group(2)
441 self.commit_sha1 = m.group(1)
442 self.source_line = int(m.group(2))
443 self.result_line = int(m.group(3))
444 self.count = int(m.group(4))
445 #set the details only when we have the file name
448 while (self.count > 0):
449 # set at result_line + count-1 the sha1 as commit_sha1
450 self.count = self.count - 1
451 iter = self.model.iter_nth_child(None, self.result_line + self.count-1)
452 self.model.set(iter, 0, self.commit_sha1, 1, filename, 3, self.source_line)
455 def annotate(self, filename, commit_sha1, line_num):
456 # verify the commit_sha1 specified has this filename
458 fp = os.popen("git ls-tree "+ commit_sha1 + " -- " + filename)
459 line = string.strip(fp.readline())
461 # pop up the message the file is not there as a part of the commit
463 dialog = gtk.MessageDialog(parent=None, flags=0,
464 type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
466 dialog.set_markup("The file %s is not present in the parent commit %s" % (filename, commit_sha1))
474 self.window.add(vpan);
477 scrollwin = gtk.ScrolledWindow()
478 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
479 scrollwin.set_shadow_type(gtk.SHADOW_IN)
480 vpan.pack1(scrollwin, True, True);
483 self.model = gtk.TreeStore(str, str, str, int)
484 self.treeview = gtk.TreeView(self.model)
485 self.treeview.set_rules_hint(True)
486 self.treeview.set_search_column(0)
487 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
488 self.treeview.connect("row-activated", self._treeview_row_activated)
489 scrollwin.add(self.treeview)
492 cell = gtk.CellRendererText()
493 cell.set_property("width-chars", 10)
494 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
495 column = gtk.TreeViewColumn("Commit")
496 column.set_resizable(True)
497 column.pack_start(cell, expand=True)
498 column.add_attribute(cell, "text", 0)
499 self.treeview.append_column(column)
501 cell = gtk.CellRendererText()
502 cell.set_property("width-chars", 20)
503 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
504 column = gtk.TreeViewColumn("File Name")
505 column.set_resizable(True)
506 column.pack_start(cell, expand=True)
507 column.add_attribute(cell, "text", 1)
508 self.treeview.append_column(column)
510 cell = gtk.CellRendererText()
511 cell.set_property("width-chars", 20)
512 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
513 column = gtk.TreeViewColumn("Data")
514 column.set_resizable(True)
515 column.pack_start(cell, expand=True)
516 column.add_attribute(cell, "text", 2)
517 self.treeview.append_column(column)
519 # The commit message window
520 scrollwin = gtk.ScrolledWindow()
521 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
522 scrollwin.set_shadow_type(gtk.SHADOW_IN)
523 vpan.pack2(scrollwin, True, True);
526 commit_text = gtk.TextView()
527 self.commit_buffer = gtk.TextBuffer()
528 commit_text.set_buffer(self.commit_buffer)
529 scrollwin.add(commit_text)
534 self.add_file_data(filename, commit_sha1, line_num)
536 fp = os.popen("git blame --incremental -C -C -- " + filename + " " + commit_sha1)
537 flags = fcntl.fcntl(fp.fileno(), fcntl.F_GETFL)
538 fcntl.fcntl(fp.fileno(), fcntl.F_SETFL, flags | os.O_NONBLOCK)
539 self.io_watch_tag = gobject.io_add_watch(fp, gobject.IO_IN, self.data_ready)
542 class DiffWindow(object):
544 This object represents and manages a single window containing the
545 differences between two revisions on a branch.
549 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
550 self.window.set_border_width(0)
551 self.window.set_title("Git repository browser diff window")
553 # Use two thirds of the screen by default
554 screen = self.window.get_screen()
555 monitor = screen.get_monitor_geometry(0)
556 width = int(monitor.width * 0.66)
557 height = int(monitor.height * 0.66)
558 self.window.set_default_size(width, height)
564 """Construct the window contents."""
566 self.window.add(vbox)
569 menu_bar = gtk.MenuBar()
570 save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
571 save_menu.connect("activate", self.save_menu_response, "save")
573 menu_bar.append(save_menu)
574 vbox.pack_start(menu_bar, expand=False, fill=True)
579 scrollwin = gtk.ScrolledWindow()
580 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
581 scrollwin.set_shadow_type(gtk.SHADOW_IN)
582 hpan.pack1(scrollwin, True, True)
585 if have_gtksourceview:
586 self.buffer = gtksourceview.SourceBuffer()
587 slm = gtksourceview.SourceLanguagesManager()
588 gsl = slm.get_language_from_mime_type("text/x-patch")
589 self.buffer.set_highlight(True)
590 self.buffer.set_language(gsl)
591 sourceview = gtksourceview.SourceView(self.buffer)
593 self.buffer = gtk.TextBuffer()
594 sourceview = gtk.TextView(self.buffer)
597 sourceview.set_editable(False)
598 sourceview.modify_font(pango.FontDescription("Monospace"))
599 scrollwin.add(sourceview)
602 # The file hierarchy: a scrollable treeview
603 scrollwin = gtk.ScrolledWindow()
604 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
605 scrollwin.set_shadow_type(gtk.SHADOW_IN)
606 scrollwin.set_size_request(20, -1)
607 hpan.pack2(scrollwin, True, True)
610 self.model = gtk.TreeStore(str, str, str)
611 self.treeview = gtk.TreeView(self.model)
612 self.treeview.set_search_column(1)
613 self.treeview.connect("cursor-changed", self._treeview_clicked)
614 scrollwin.add(self.treeview)
617 cell = gtk.CellRendererText()
618 cell.set_property("width-chars", 20)
619 column = gtk.TreeViewColumn("Select to annotate")
620 column.pack_start(cell, expand=True)
621 column.add_attribute(cell, "text", 0)
622 self.treeview.append_column(column)
624 vbox.pack_start(hpan, expand=True, fill=True)
627 def _treeview_clicked(self, *args):
628 """Callback for when the treeview cursor changes."""
629 (path, col) = self.treeview.get_cursor()
630 specific_file = self.model[path][1]
631 commit_sha1 = self.model[path][2]
632 if specific_file == None :
634 elif specific_file == "" :
637 window = AnnotateWindow();
638 window.annotate(specific_file, commit_sha1, 1)
641 def commit_files(self, commit_sha1, parent_sha1):
643 add = self.model.append(None, [ "Added", None, None])
644 dele = self.model.append(None, [ "Deleted", None, None])
645 mod = self.model.append(None, [ "Modified", None, None])
646 diff_tree = re.compile('^(:.{6}) (.{6}) (.{40}) (.{40}) (A|D|M)\s(.+)$')
647 fp = os.popen("git diff-tree -r --no-commit-id " + parent_sha1 + " " + commit_sha1)
649 line = string.strip(fp.readline())
652 m = diff_tree.match(line)
657 filename = m.group(6)
659 self.model.append(add, [filename, filename, commit_sha1])
661 self.model.append(dele, [filename, filename, commit_sha1])
663 self.model.append(mod, [filename, filename, commit_sha1])
666 self.treeview.expand_all()
668 def set_diff(self, commit_sha1, parent_sha1, encoding):
669 """Set the differences showed by this window.
670 Compares the two trees and populates the window with the
673 # Diff with the first commit or the last commit shows nothing
674 if (commit_sha1 == 0 or parent_sha1 == 0 ):
677 fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
678 self.buffer.set_text(unicode(fp.read(), encoding).encode('utf-8'))
680 self.commit_files(commit_sha1, parent_sha1)
683 def save_menu_response(self, widget, string):
684 dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
685 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
686 gtk.STOCK_SAVE, gtk.RESPONSE_OK))
687 dialog.set_default_response(gtk.RESPONSE_OK)
688 response = dialog.run()
689 if response == gtk.RESPONSE_OK:
690 patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
691 self.buffer.get_end_iter())
692 fp = open(dialog.get_filename(), "w")
693 fp.write(patch_buffer)
697 class GitView(object):
698 """ This is the main class
702 def __init__(self, with_diff=0):
703 self.with_diff = with_diff
704 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
705 self.window.set_border_width(0)
706 self.window.set_title("Git repository browser")
711 # Use three-quarters of the screen by default
712 screen = self.window.get_screen()
713 monitor = screen.get_monitor_geometry(0)
714 width = int(monitor.width * 0.75)
715 height = int(monitor.height * 0.75)
716 self.window.set_default_size(width, height)
719 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
720 self.window.set_icon(icon)
722 self.accel_group = gtk.AccelGroup()
723 self.window.add_accel_group(self.accel_group)
724 self.accel_group.connect_group(0xffc2, 0, gtk.ACCEL_LOCKED, self.refresh);
725 self.accel_group.connect_group(0xffc1, 0, gtk.ACCEL_LOCKED, self.maximize);
726 self.accel_group.connect_group(0xffc8, 0, gtk.ACCEL_LOCKED, self.fullscreen);
727 self.accel_group.connect_group(0xffc9, 0, gtk.ACCEL_LOCKED, self.unfullscreen);
729 self.window.add(self.construct())
731 def refresh(self, widget, event=None, *arguments, **keywords):
734 Commit.children_sha1 = {}
735 self.set_branch(sys.argv[without_diff:])
739 def maximize(self, widget, event=None, *arguments, **keywords):
740 self.window.maximize()
743 def fullscreen(self, widget, event=None, *arguments, **keywords):
744 self.window.fullscreen()
747 def unfullscreen(self, widget, event=None, *arguments, **keywords):
748 self.window.unfullscreen()
751 def get_bt_sha1(self):
752 """ Update the bt_sha1 dictionary with the
753 respective sha1 details """
756 ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
757 fp = os.popen('git ls-remote "${GIT_DIR-.git}"')
759 line = string.strip(fp.readline())
762 m = ls_remote.match(line)
765 (sha1, name) = (m.group(1), m.group(2))
766 if not self.bt_sha1.has_key(sha1):
767 self.bt_sha1[sha1] = []
768 self.bt_sha1[sha1].append(name)
771 def get_encoding(self):
772 fp = os.popen("git config --get i18n.commitencoding")
773 self.encoding=string.strip(fp.readline())
775 if (self.encoding == ""):
776 self.encoding = "utf-8"
780 """Construct the window contents."""
783 paned.pack1(self.construct_top(), resize=False, shrink=True)
784 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
785 menu_bar = gtk.MenuBar()
786 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
787 help_menu = gtk.MenuItem("Help")
789 about_menu = gtk.MenuItem("About")
790 menu.append(about_menu)
791 about_menu.connect("activate", self.about_menu_response, "about")
793 help_menu.set_submenu(menu)
795 menu_bar.append(help_menu)
797 vbox.pack_start(menu_bar, expand=False, fill=True)
798 vbox.pack_start(paned, expand=True, fill=True)
804 def construct_top(self):
805 """Construct the top-half of the window."""
806 vbox = gtk.VBox(spacing=6)
807 vbox.set_border_width(12)
811 scrollwin = gtk.ScrolledWindow()
812 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
813 scrollwin.set_shadow_type(gtk.SHADOW_IN)
814 vbox.pack_start(scrollwin, expand=True, fill=True)
817 self.treeview = gtk.TreeView()
818 self.treeview.set_rules_hint(True)
819 self.treeview.set_search_column(4)
820 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
821 scrollwin.add(self.treeview)
824 cell = CellRendererGraph()
825 column = gtk.TreeViewColumn()
826 column.set_resizable(True)
827 column.pack_start(cell, expand=True)
828 column.add_attribute(cell, "node", 1)
829 column.add_attribute(cell, "in-lines", 2)
830 column.add_attribute(cell, "out-lines", 3)
831 self.treeview.append_column(column)
833 cell = gtk.CellRendererText()
834 cell.set_property("width-chars", 65)
835 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
836 column = gtk.TreeViewColumn("Message")
837 column.set_resizable(True)
838 column.pack_start(cell, expand=True)
839 column.add_attribute(cell, "text", 4)
840 self.treeview.append_column(column)
842 cell = gtk.CellRendererText()
843 cell.set_property("width-chars", 40)
844 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
845 column = gtk.TreeViewColumn("Author")
846 column.set_resizable(True)
847 column.pack_start(cell, expand=True)
848 column.add_attribute(cell, "text", 5)
849 self.treeview.append_column(column)
851 cell = gtk.CellRendererText()
852 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
853 column = gtk.TreeViewColumn("Date")
854 column.set_resizable(True)
855 column.pack_start(cell, expand=True)
856 column.add_attribute(cell, "text", 6)
857 self.treeview.append_column(column)
861 def about_menu_response(self, widget, string):
862 dialog = gtk.AboutDialog()
863 dialog.set_name("Gitview")
864 dialog.set_version(GitView.version)
865 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@gmail.com>"])
866 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
867 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
868 dialog.set_wrap_license(True)
873 def construct_bottom(self):
874 """Construct the bottom half of the window."""
875 vbox = gtk.VBox(False, spacing=6)
876 vbox.set_border_width(12)
877 (width, height) = self.window.get_size()
878 vbox.set_size_request(width, int(height / 2.5))
881 self.table = gtk.Table(rows=4, columns=4)
882 self.table.set_row_spacings(6)
883 self.table.set_col_spacings(6)
884 vbox.pack_start(self.table, expand=False, fill=True)
887 align = gtk.Alignment(0.0, 0.5)
889 label.set_markup("<b>Revision:</b>")
891 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
895 align = gtk.Alignment(0.0, 0.5)
896 self.revid_label = gtk.Label()
897 self.revid_label.set_selectable(True)
898 align.add(self.revid_label)
899 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
900 self.revid_label.show()
903 align = gtk.Alignment(0.0, 0.5)
905 label.set_markup("<b>Committer:</b>")
907 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
911 align = gtk.Alignment(0.0, 0.5)
912 self.committer_label = gtk.Label()
913 self.committer_label.set_selectable(True)
914 align.add(self.committer_label)
915 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
916 self.committer_label.show()
919 align = gtk.Alignment(0.0, 0.5)
921 label.set_markup("<b>Timestamp:</b>")
923 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
927 align = gtk.Alignment(0.0, 0.5)
928 self.timestamp_label = gtk.Label()
929 self.timestamp_label.set_selectable(True)
930 align.add(self.timestamp_label)
931 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
932 self.timestamp_label.show()
935 align = gtk.Alignment(0.0, 0.5)
937 label.set_markup("<b>Parents:</b>")
939 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
942 self.parents_widgets = []
944 align = gtk.Alignment(0.0, 0.5)
946 label.set_markup("<b>Children:</b>")
948 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
951 self.children_widgets = []
953 scrollwin = gtk.ScrolledWindow()
954 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
955 scrollwin.set_shadow_type(gtk.SHADOW_IN)
956 vbox.pack_start(scrollwin, expand=True, fill=True)
959 if have_gtksourceview:
960 self.message_buffer = gtksourceview.SourceBuffer()
961 slm = gtksourceview.SourceLanguagesManager()
962 gsl = slm.get_language_from_mime_type("text/x-patch")
963 self.message_buffer.set_highlight(True)
964 self.message_buffer.set_language(gsl)
965 sourceview = gtksourceview.SourceView(self.message_buffer)
967 self.message_buffer = gtk.TextBuffer()
968 sourceview = gtk.TextView(self.message_buffer)
970 sourceview.set_editable(False)
971 sourceview.modify_font(pango.FontDescription("Monospace"))
972 scrollwin.add(sourceview)
977 def _treeview_cursor_cb(self, *args):
978 """Callback for when the treeview cursor changes."""
979 (path, col) = self.treeview.get_cursor()
980 commit = self.model[path][0]
982 if commit.committer is not None:
983 committer = commit.committer
984 timestamp = commit.commit_date
985 message = commit.get_message(self.with_diff)
986 revid_label = commit.commit_sha1
993 self.revid_label.set_text(revid_label)
994 self.committer_label.set_text(committer)
995 self.timestamp_label.set_text(timestamp)
996 self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
998 for widget in self.parents_widgets:
999 self.table.remove(widget)
1001 self.parents_widgets = []
1002 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
1003 for idx, parent_id in enumerate(commit.parent_sha1):
1004 self.table.set_row_spacing(idx + 3, 0)
1006 align = gtk.Alignment(0.0, 0.0)
1007 self.parents_widgets.append(align)
1008 self.table.attach(align, 1, 2, idx + 3, idx + 4,
1009 gtk.EXPAND | gtk.FILL, gtk.FILL)
1012 hbox = gtk.HBox(False, 0)
1016 label = gtk.Label(parent_id)
1017 label.set_selectable(True)
1018 hbox.pack_start(label, expand=False, fill=True)
1022 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1025 button = gtk.Button()
1027 button.set_relief(gtk.RELIEF_NONE)
1028 button.connect("clicked", self._go_clicked_cb, parent_id)
1029 hbox.pack_start(button, expand=False, fill=True)
1033 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1036 button = gtk.Button()
1038 button.set_relief(gtk.RELIEF_NONE)
1039 button.set_sensitive(True)
1040 button.connect("clicked", self._show_clicked_cb,
1041 commit.commit_sha1, parent_id, self.encoding)
1042 hbox.pack_start(button, expand=False, fill=True)
1045 # Populate with child details
1046 for widget in self.children_widgets:
1047 self.table.remove(widget)
1049 self.children_widgets = []
1051 child_sha1 = Commit.children_sha1[commit.commit_sha1]
1053 # We don't have child
1056 if ( len(child_sha1) > len(commit.parent_sha1)):
1057 self.table.resize(4 + len(child_sha1) - 1, 4)
1059 for idx, child_id in enumerate(child_sha1):
1060 self.table.set_row_spacing(idx + 3, 0)
1062 align = gtk.Alignment(0.0, 0.0)
1063 self.children_widgets.append(align)
1064 self.table.attach(align, 3, 4, idx + 3, idx + 4,
1065 gtk.EXPAND | gtk.FILL, gtk.FILL)
1068 hbox = gtk.HBox(False, 0)
1072 label = gtk.Label(child_id)
1073 label.set_selectable(True)
1074 hbox.pack_start(label, expand=False, fill=True)
1078 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1081 button = gtk.Button()
1083 button.set_relief(gtk.RELIEF_NONE)
1084 button.connect("clicked", self._go_clicked_cb, child_id)
1085 hbox.pack_start(button, expand=False, fill=True)
1089 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1092 button = gtk.Button()
1094 button.set_relief(gtk.RELIEF_NONE)
1095 button.set_sensitive(True)
1096 button.connect("clicked", self._show_clicked_cb,
1097 child_id, commit.commit_sha1, self.encoding)
1098 hbox.pack_start(button, expand=False, fill=True)
1101 def _destroy_cb(self, widget):
1102 """Callback for when a window we manage is destroyed."""
1107 """Stop the GTK+ main loop."""
1110 def run(self, args):
1111 self.set_branch(args)
1112 self.window.connect("destroy", self._destroy_cb)
1116 def set_branch(self, args):
1117 """Fill in different windows with info from the reposiroty"""
1118 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
1119 git_rev_list_cmd = fp.read()
1121 fp = os.popen("git rev-list --header --topo-order --parents " + git_rev_list_cmd)
1122 self.update_window(fp)
1124 def update_window(self, fp):
1127 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
1128 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
1130 # used for cursor positioning
1135 self.incomplete_line = {}
1142 input_line = fp.readline()
1143 while (input_line != ""):
1144 # The commit header ends with '\0'
1145 # This NULL is immediately followed by the sha1 of the
1147 if (input_line[0] != '\0'):
1148 commit_lines.append(input_line)
1149 input_line = fp.readline()
1152 commit = Commit(commit_lines)
1153 if (commit != None ):
1154 self.commits.append(commit)
1158 commit_lines.append(input_line[1:])
1159 input_line = fp.readline()
1163 for commit in self.commits:
1164 (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
1168 self.index[commit.commit_sha1] = index
1171 self.treeview.set_model(self.model)
1172 self.treeview.show()
1174 def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
1182 if (last_nodepos > 5):
1185 # Add the incomplete lines of the last cell in this
1187 colour = self.colours[commit.commit_sha1]
1189 self.colours[commit.commit_sha1] = last_colour+1
1190 last_colour = self.colours[commit.commit_sha1]
1191 colour = self.colours[commit.commit_sha1]
1194 node_pos = self.nodepos[commit.commit_sha1]
1196 self.nodepos[commit.commit_sha1] = last_nodepos+1
1197 last_nodepos = self.nodepos[commit.commit_sha1]
1198 node_pos = self.nodepos[commit.commit_sha1]
1200 #The first parent always continue on the same line
1202 # check we alreay have the value
1203 tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
1205 self.colours[commit.parent_sha1[0]] = colour
1206 self.nodepos[commit.parent_sha1[0]] = node_pos
1208 for sha1 in self.incomplete_line.keys():
1209 if (sha1 != commit.commit_sha1):
1210 self.draw_incomplete_line(sha1, node_pos,
1211 out_line, in_line, index)
1213 del self.incomplete_line[sha1]
1216 for parent_id in commit.parent_sha1:
1218 tmp_node_pos = self.nodepos[parent_id]
1220 self.colours[parent_id] = last_colour+1
1221 last_colour = self.colours[parent_id]
1222 self.nodepos[parent_id] = last_nodepos+1
1223 last_nodepos = self.nodepos[parent_id]
1225 in_line.append((node_pos, self.nodepos[parent_id],
1226 self.colours[parent_id]))
1227 self.add_incomplete_line(parent_id)
1230 branch_tag = self.bt_sha1[commit.commit_sha1]
1235 node = (node_pos, colour, branch_tag)
1237 self.model.append([commit, node, out_line, in_line,
1238 commit.message, commit.author, commit.date])
1240 return (in_line, last_colour, last_nodepos)
1242 def add_incomplete_line(self, sha1):
1244 self.incomplete_line[sha1].append(self.nodepos[sha1])
1246 self.incomplete_line[sha1] = [self.nodepos[sha1]]
1248 def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
1249 for idx, pos in enumerate(self.incomplete_line[sha1]):
1250 if(pos == node_pos):
1251 #remove the straight line and add a slash
1252 if ((pos, pos, self.colours[sha1]) in out_line):
1253 out_line.remove((pos, pos, self.colours[sha1]))
1254 out_line.append((pos, pos+0.5, self.colours[sha1]))
1255 self.incomplete_line[sha1][idx] = pos = pos+0.5
1257 next_commit = self.commits[index+1]
1258 if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
1259 # join the line back to the node point
1260 # This need to be done only if we modified it
1261 in_line.append((pos, pos-0.5, self.colours[sha1]))
1265 in_line.append((pos, pos, self.colours[sha1]))
1268 def _go_clicked_cb(self, widget, revid):
1269 """Callback for when the go button for a parent is clicked."""
1271 self.treeview.set_cursor(self.index[revid])
1273 dialog = gtk.MessageDialog(parent=None, flags=0,
1274 type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
1275 message_format=None)
1276 dialog.set_markup("Revision <b>%s</b> not present in the list" % revid)
1277 # revid == 0 is the parent of the first commit
1279 dialog.format_secondary_text("Try running gitview without any options")
1283 self.treeview.grab_focus()
1285 def _show_clicked_cb(self, widget, commit_sha1, parent_sha1, encoding):
1286 """Callback for when the show button for a parent is clicked."""
1287 window = DiffWindow()
1288 window.set_diff(commit_sha1, parent_sha1, encoding)
1289 self.treeview.grab_focus()
1292 if __name__ == "__main__":
1294 if (len(sys.argv) > 1 ):
1295 if (sys.argv[1] == "--without-diff"):
1298 view = GitView( without_diff != 1)
1299 view.run(sys.argv[without_diff:])