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_gtksourceview2 = True
34 have_gtksourceview2 = False
38 have_gtksourceview = True
40 have_gtksourceview = False
42 if not have_gtksourceview2 and not have_gtksourceview:
43 print "Running without gtksourceview2 or gtksourceview module"
45 re_ident = re.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
47 def list_to_string(args, skip):
52 str_arg = str_arg + args[i]
53 str_arg = str_arg + " "
58 def show_date(epoch, tz):
60 tzsecs = float(tz[1:3]) * 3600
61 tzsecs += float(tz[3:5]) * 60
67 return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs))
69 def get_source_buffer_and_view():
70 if have_gtksourceview2:
71 buffer = gtksourceview2.Buffer()
72 slm = gtksourceview2.LanguageManager()
73 gsl = slm.get_language("diff")
74 buffer.set_highlight_syntax(True)
75 buffer.set_language(gsl)
76 view = gtksourceview2.View(buffer)
77 elif have_gtksourceview:
78 buffer = gtksourceview.SourceBuffer()
79 slm = gtksourceview.SourceLanguagesManager()
80 gsl = slm.get_language_from_mime_type("text/x-patch")
81 buffer.set_highlight(True)
82 buffer.set_language(gsl)
83 view = gtksourceview.SourceView(buffer)
85 buffer = gtk.TextBuffer()
86 view = gtk.TextView(buffer)
90 class CellRendererGraph(gtk.GenericCellRenderer):
91 """Cell renderer for directed graph.
93 This module contains the implementation of a custom GtkCellRenderer that
94 draws part of the directed graph based on the lines suggested by the code
97 Because we're shiny, we use Cairo to do this, and because we're naughty
98 we cheat and draw over the bits of the TreeViewColumn that are supposed to
99 just be for the background.
102 node (column, colour, [ names ]) tuple to draw revision node,
103 in_lines (start, end, colour) tuple list to draw inward lines,
104 out_lines (start, end, colour) tuple list to draw outward lines.
108 "node": ( gobject.TYPE_PYOBJECT, "node",
109 "revision node instruction",
110 gobject.PARAM_WRITABLE
112 "in-lines": ( gobject.TYPE_PYOBJECT, "in-lines",
113 "instructions to draw lines into the cell",
114 gobject.PARAM_WRITABLE
116 "out-lines": ( gobject.TYPE_PYOBJECT, "out-lines",
117 "instructions to draw lines out of the cell",
118 gobject.PARAM_WRITABLE
122 def do_set_property(self, property, value):
123 """Set properties from GObject properties."""
124 if property.name == "node":
126 elif property.name == "in-lines":
127 self.in_lines = value
128 elif property.name == "out-lines":
129 self.out_lines = value
131 raise AttributeError, "no such property: '%s'" % property.name
133 def box_size(self, widget):
134 """Calculate box size based on widget's font.
136 Cache this as it's probably expensive to get. It ensures that we
137 draw the graph at least as large as the text.
140 return self._box_size
141 except AttributeError:
142 pango_ctx = widget.get_pango_context()
143 font_desc = widget.get_style().font_desc
144 metrics = pango_ctx.get_metrics(font_desc)
146 ascent = pango.PIXELS(metrics.get_ascent())
147 descent = pango.PIXELS(metrics.get_descent())
149 self._box_size = ascent + descent + 6
150 return self._box_size
152 def set_colour(self, ctx, colour, bg, fg):
153 """Set the context source colour.
155 Picks a distinct colour based on an internal wheel; the bg
156 parameter provides the value that should be assigned to the 'zero'
157 colours and the fg parameter provides the multiplier that should be
158 applied to the foreground colours.
169 colour %= len(colours)
170 red = (colours[colour][0] * fg) or bg
171 green = (colours[colour][1] * fg) or bg
172 blue = (colours[colour][2] * fg) or bg
174 ctx.set_source_rgb(red, green, blue)
176 def on_get_size(self, widget, cell_area):
177 """Return the size we need for this cell.
179 Each cell is drawn individually and is only as wide as it needs
180 to be, we let the TreeViewColumn take care of making them all
183 box_size = self.box_size(widget)
186 for start, end, colour in self.in_lines + self.out_lines:
187 cols = int(max(cols, start, end))
189 (column, colour, names) = self.node
191 if (len(names) != 0):
193 names_len += len(item)
195 width = box_size * (cols + 1 ) + names_len
198 # FIXME I have no idea how to use cell_area properly
199 return (0, 0, width, height)
201 def on_render(self, window, widget, bg_area, cell_area, exp_area, flags):
202 """Render an individual cell.
204 Draws the cell contents using cairo, taking care to clip what we
205 do to within the background area so we don't draw over other cells.
206 Note that we're a bit naughty there and should really be drawing
207 in the cell_area (or even the exposed area), but we explicitly don't
210 We try and be a little clever, if the line we need to draw is going
211 to cross other columns we actually draw it as in the .---' style
212 instead of a pure diagonal ... this reduces confusion by an
215 ctx = window.cairo_create()
216 ctx.rectangle(bg_area.x, bg_area.y, bg_area.width, bg_area.height)
219 box_size = self.box_size(widget)
221 ctx.set_line_width(box_size / 8)
222 ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
224 # Draw lines into the cell
225 for start, end, colour in self.in_lines:
226 ctx.move_to(cell_area.x + box_size * start + box_size / 2,
227 bg_area.y - bg_area.height / 2)
230 ctx.line_to(cell_area.x + box_size * start, bg_area.y)
231 ctx.line_to(cell_area.x + box_size * end + box_size, bg_area.y)
232 elif start - end < -1:
233 ctx.line_to(cell_area.x + box_size * start + box_size,
235 ctx.line_to(cell_area.x + box_size * end, bg_area.y)
237 ctx.line_to(cell_area.x + box_size * end + box_size / 2,
238 bg_area.y + bg_area.height / 2)
240 self.set_colour(ctx, colour, 0.0, 0.65)
243 # Draw lines out of the cell
244 for start, end, colour in self.out_lines:
245 ctx.move_to(cell_area.x + box_size * start + box_size / 2,
246 bg_area.y + bg_area.height / 2)
249 ctx.line_to(cell_area.x + box_size * start,
250 bg_area.y + bg_area.height)
251 ctx.line_to(cell_area.x + box_size * end + box_size,
252 bg_area.y + bg_area.height)
253 elif start - end < -1:
254 ctx.line_to(cell_area.x + box_size * start + box_size,
255 bg_area.y + bg_area.height)
256 ctx.line_to(cell_area.x + box_size * end,
257 bg_area.y + bg_area.height)
259 ctx.line_to(cell_area.x + box_size * end + box_size / 2,
260 bg_area.y + bg_area.height / 2 + bg_area.height)
262 self.set_colour(ctx, colour, 0.0, 0.65)
265 # Draw the revision node in the right column
266 (column, colour, names) = self.node
267 ctx.arc(cell_area.x + box_size * column + box_size / 2,
268 cell_area.y + cell_area.height / 2,
269 box_size / 4, 0, 2 * math.pi)
272 self.set_colour(ctx, colour, 0.0, 0.5)
273 ctx.stroke_preserve()
275 self.set_colour(ctx, colour, 0.5, 1.0)
278 if (len(names) != 0):
281 name = name + item + " "
283 ctx.set_font_size(13)
285 self.set_colour(ctx, colour, 0.5, 1.0)
287 self.set_colour(ctx, colour, 0.0, 0.5)
290 class Commit(object):
291 """ This represent a commit object obtained after parsing the git-rev-list
294 __slots__ = ['children_sha1', 'message', 'author', 'date', 'committer',
295 'commit_date', 'commit_sha1', 'parent_sha1']
299 def __init__(self, commit_lines):
304 self.commit_date = ""
305 self.commit_sha1 = ""
306 self.parent_sha1 = [ ]
307 self.parse_commit(commit_lines)
310 def parse_commit(self, commit_lines):
312 # First line is the sha1 lines
313 line = string.strip(commit_lines[0])
314 sha1 = re.split(" ", line)
315 self.commit_sha1 = sha1[0]
316 self.parent_sha1 = sha1[1:]
318 #build the child list
319 for parent_id in self.parent_sha1:
321 Commit.children_sha1[parent_id].append(self.commit_sha1)
323 Commit.children_sha1[parent_id] = [self.commit_sha1]
325 # IF we don't have parent
326 if (len(self.parent_sha1) == 0):
327 self.parent_sha1 = [0]
329 for line in commit_lines[1:]:
330 m = re.match("^ ", line)
332 # First line of the commit message used for short log
333 if self.message == "":
334 self.message = string.strip(line)
337 m = re.match("tree", line)
341 m = re.match("parent", line)
345 m = re_ident.match(line)
347 date = show_date(m.group('epoch'), m.group('tz'))
348 if m.group(1) == "author":
349 self.author = m.group('ident')
351 elif m.group(1) == "committer":
352 self.committer = m.group('ident')
353 self.commit_date = date
357 def get_message(self, with_diff=0):
359 message = self.diff_tree()
361 fp = os.popen("git cat-file commit " + self.commit_sha1)
368 fp = os.popen("git diff-tree --pretty --cc -v -p --always " + self.commit_sha1)
373 class AnnotateWindow(object):
375 This object represents and manages a single window containing the
376 annotate information of the file
380 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
381 self.window.set_border_width(0)
382 self.window.set_title("Git repository browser annotation window")
385 # Use two thirds of the screen by default
386 screen = self.window.get_screen()
387 monitor = screen.get_monitor_geometry(0)
388 width = int(monitor.width * 0.66)
389 height = int(monitor.height * 0.66)
390 self.window.set_default_size(width, height)
392 def add_file_data(self, filename, commit_sha1, line_num):
393 fp = os.popen("git cat-file blob " + commit_sha1 +":"+filename)
395 for line in fp.readlines():
396 line = string.rstrip(line)
397 self.model.append(None, ["HEAD", filename, line, i])
401 # now set the cursor position
402 self.treeview.set_cursor(line_num-1)
403 self.treeview.grab_focus()
405 def _treeview_cursor_cb(self, *args):
406 """Callback for when the treeview cursor changes."""
407 (path, col) = self.treeview.get_cursor()
408 commit_sha1 = self.model[path][0]
410 fp = os.popen("git cat-file commit " + commit_sha1)
411 for line in fp.readlines():
412 commit_msg = commit_msg + line
415 self.commit_buffer.set_text(commit_msg)
417 def _treeview_row_activated(self, *args):
418 """Callback for when the treeview row gets selected."""
419 (path, col) = self.treeview.get_cursor()
420 commit_sha1 = self.model[path][0]
421 filename = self.model[path][1]
422 line_num = self.model[path][3]
424 window = AnnotateWindow();
425 fp = os.popen("git rev-parse "+ commit_sha1 + "~1")
426 commit_sha1 = string.strip(fp.readline())
428 window.annotate(filename, commit_sha1, line_num)
430 def data_ready(self, source, condition):
433 # A simple readline doesn't work
435 buffer = source.read(100)
438 # resource temporary not available
441 if (len(buffer) == 0):
442 gobject.source_remove(self.io_watch_tag)
446 if (self.prev_read != ""):
447 buffer = self.prev_read + buffer
450 if (buffer[len(buffer) -1] != '\n'):
452 newline_index = buffer.rindex("\n")
456 self.prev_read = buffer[newline_index:(len(buffer))]
457 buffer = buffer[0:newline_index]
459 for buff in buffer.split("\n"):
460 annotate_line = re.compile('^([0-9a-f]{40}) (.+) (.+) (.+)$')
461 m = annotate_line.match(buff)
463 annotate_line = re.compile('^(filename) (.+)$')
464 m = annotate_line.match(buff)
467 filename = m.group(2)
469 self.commit_sha1 = m.group(1)
470 self.source_line = int(m.group(2))
471 self.result_line = int(m.group(3))
472 self.count = int(m.group(4))
473 #set the details only when we have the file name
476 while (self.count > 0):
477 # set at result_line + count-1 the sha1 as commit_sha1
478 self.count = self.count - 1
479 iter = self.model.iter_nth_child(None, self.result_line + self.count-1)
480 self.model.set(iter, 0, self.commit_sha1, 1, filename, 3, self.source_line)
483 def annotate(self, filename, commit_sha1, line_num):
484 # verify the commit_sha1 specified has this filename
486 fp = os.popen("git ls-tree "+ commit_sha1 + " -- " + filename)
487 line = string.strip(fp.readline())
489 # pop up the message the file is not there as a part of the commit
491 dialog = gtk.MessageDialog(parent=None, flags=0,
492 type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
494 dialog.set_markup("The file %s is not present in the parent commit %s" % (filename, commit_sha1))
502 self.window.add(vpan);
505 scrollwin = gtk.ScrolledWindow()
506 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
507 scrollwin.set_shadow_type(gtk.SHADOW_IN)
508 vpan.pack1(scrollwin, True, True);
511 self.model = gtk.TreeStore(str, str, str, int)
512 self.treeview = gtk.TreeView(self.model)
513 self.treeview.set_rules_hint(True)
514 self.treeview.set_search_column(0)
515 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
516 self.treeview.connect("row-activated", self._treeview_row_activated)
517 scrollwin.add(self.treeview)
520 cell = gtk.CellRendererText()
521 cell.set_property("width-chars", 10)
522 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
523 column = gtk.TreeViewColumn("Commit")
524 column.set_resizable(True)
525 column.pack_start(cell, expand=True)
526 column.add_attribute(cell, "text", 0)
527 self.treeview.append_column(column)
529 cell = gtk.CellRendererText()
530 cell.set_property("width-chars", 20)
531 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
532 column = gtk.TreeViewColumn("File Name")
533 column.set_resizable(True)
534 column.pack_start(cell, expand=True)
535 column.add_attribute(cell, "text", 1)
536 self.treeview.append_column(column)
538 cell = gtk.CellRendererText()
539 cell.set_property("width-chars", 20)
540 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
541 column = gtk.TreeViewColumn("Data")
542 column.set_resizable(True)
543 column.pack_start(cell, expand=True)
544 column.add_attribute(cell, "text", 2)
545 self.treeview.append_column(column)
547 # The commit message window
548 scrollwin = gtk.ScrolledWindow()
549 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
550 scrollwin.set_shadow_type(gtk.SHADOW_IN)
551 vpan.pack2(scrollwin, True, True);
554 commit_text = gtk.TextView()
555 self.commit_buffer = gtk.TextBuffer()
556 commit_text.set_buffer(self.commit_buffer)
557 scrollwin.add(commit_text)
562 self.add_file_data(filename, commit_sha1, line_num)
564 fp = os.popen("git blame --incremental -C -C -- " + filename + " " + commit_sha1)
565 flags = fcntl.fcntl(fp.fileno(), fcntl.F_GETFL)
566 fcntl.fcntl(fp.fileno(), fcntl.F_SETFL, flags | os.O_NONBLOCK)
567 self.io_watch_tag = gobject.io_add_watch(fp, gobject.IO_IN, self.data_ready)
570 class DiffWindow(object):
572 This object represents and manages a single window containing the
573 differences between two revisions on a branch.
577 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
578 self.window.set_border_width(0)
579 self.window.set_title("Git repository browser diff window")
581 # Use two thirds of the screen by default
582 screen = self.window.get_screen()
583 monitor = screen.get_monitor_geometry(0)
584 width = int(monitor.width * 0.66)
585 height = int(monitor.height * 0.66)
586 self.window.set_default_size(width, height)
592 """Construct the window contents."""
594 self.window.add(vbox)
597 menu_bar = gtk.MenuBar()
598 save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
599 save_menu.connect("activate", self.save_menu_response, "save")
601 menu_bar.append(save_menu)
602 vbox.pack_start(menu_bar, expand=False, fill=True)
607 scrollwin = gtk.ScrolledWindow()
608 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
609 scrollwin.set_shadow_type(gtk.SHADOW_IN)
610 hpan.pack1(scrollwin, True, True)
613 (self.buffer, sourceview) = get_source_buffer_and_view()
615 sourceview.set_editable(False)
616 sourceview.modify_font(pango.FontDescription("Monospace"))
617 scrollwin.add(sourceview)
620 # The file hierarchy: a scrollable treeview
621 scrollwin = gtk.ScrolledWindow()
622 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
623 scrollwin.set_shadow_type(gtk.SHADOW_IN)
624 scrollwin.set_size_request(20, -1)
625 hpan.pack2(scrollwin, True, True)
628 self.model = gtk.TreeStore(str, str, str)
629 self.treeview = gtk.TreeView(self.model)
630 self.treeview.set_search_column(1)
631 self.treeview.connect("cursor-changed", self._treeview_clicked)
632 scrollwin.add(self.treeview)
635 cell = gtk.CellRendererText()
636 cell.set_property("width-chars", 20)
637 column = gtk.TreeViewColumn("Select to annotate")
638 column.pack_start(cell, expand=True)
639 column.add_attribute(cell, "text", 0)
640 self.treeview.append_column(column)
642 vbox.pack_start(hpan, expand=True, fill=True)
645 def _treeview_clicked(self, *args):
646 """Callback for when the treeview cursor changes."""
647 (path, col) = self.treeview.get_cursor()
648 specific_file = self.model[path][1]
649 commit_sha1 = self.model[path][2]
650 if specific_file == None :
652 elif specific_file == "" :
655 window = AnnotateWindow();
656 window.annotate(specific_file, commit_sha1, 1)
659 def commit_files(self, commit_sha1, parent_sha1):
661 add = self.model.append(None, [ "Added", None, None])
662 dele = self.model.append(None, [ "Deleted", None, None])
663 mod = self.model.append(None, [ "Modified", None, None])
664 diff_tree = re.compile('^(:.{6}) (.{6}) (.{40}) (.{40}) (A|D|M)\s(.+)$')
665 fp = os.popen("git diff-tree -r --no-commit-id " + parent_sha1 + " " + commit_sha1)
667 line = string.strip(fp.readline())
670 m = diff_tree.match(line)
675 filename = m.group(6)
677 self.model.append(add, [filename, filename, commit_sha1])
679 self.model.append(dele, [filename, filename, commit_sha1])
681 self.model.append(mod, [filename, filename, commit_sha1])
684 self.treeview.expand_all()
686 def set_diff(self, commit_sha1, parent_sha1, encoding):
687 """Set the differences showed by this window.
688 Compares the two trees and populates the window with the
691 # Diff with the first commit or the last commit shows nothing
692 if (commit_sha1 == 0 or parent_sha1 == 0 ):
695 fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
696 self.buffer.set_text(unicode(fp.read(), encoding).encode('utf-8'))
698 self.commit_files(commit_sha1, parent_sha1)
701 def save_menu_response(self, widget, string):
702 dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
703 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
704 gtk.STOCK_SAVE, gtk.RESPONSE_OK))
705 dialog.set_default_response(gtk.RESPONSE_OK)
706 response = dialog.run()
707 if response == gtk.RESPONSE_OK:
708 patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
709 self.buffer.get_end_iter())
710 fp = open(dialog.get_filename(), "w")
711 fp.write(patch_buffer)
715 class GitView(object):
716 """ This is the main class
720 def __init__(self, with_diff=0):
721 self.with_diff = with_diff
722 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
723 self.window.set_border_width(0)
724 self.window.set_title("Git repository browser")
729 # Use three-quarters of the screen by default
730 screen = self.window.get_screen()
731 monitor = screen.get_monitor_geometry(0)
732 width = int(monitor.width * 0.75)
733 height = int(monitor.height * 0.75)
734 self.window.set_default_size(width, height)
737 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
738 self.window.set_icon(icon)
740 self.accel_group = gtk.AccelGroup()
741 self.window.add_accel_group(self.accel_group)
742 self.accel_group.connect_group(0xffc2, 0, gtk.ACCEL_LOCKED, self.refresh);
743 self.accel_group.connect_group(0xffc1, 0, gtk.ACCEL_LOCKED, self.maximize);
744 self.accel_group.connect_group(0xffc8, 0, gtk.ACCEL_LOCKED, self.fullscreen);
745 self.accel_group.connect_group(0xffc9, 0, gtk.ACCEL_LOCKED, self.unfullscreen);
747 self.window.add(self.construct())
749 def refresh(self, widget, event=None, *arguments, **keywords):
752 Commit.children_sha1 = {}
753 self.set_branch(sys.argv[without_diff:])
757 def maximize(self, widget, event=None, *arguments, **keywords):
758 self.window.maximize()
761 def fullscreen(self, widget, event=None, *arguments, **keywords):
762 self.window.fullscreen()
765 def unfullscreen(self, widget, event=None, *arguments, **keywords):
766 self.window.unfullscreen()
769 def get_bt_sha1(self):
770 """ Update the bt_sha1 dictionary with the
771 respective sha1 details """
774 ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
775 fp = os.popen('git ls-remote "${GIT_DIR-.git}"')
777 line = string.strip(fp.readline())
780 m = ls_remote.match(line)
783 (sha1, name) = (m.group(1), m.group(2))
784 if not self.bt_sha1.has_key(sha1):
785 self.bt_sha1[sha1] = []
786 self.bt_sha1[sha1].append(name)
789 def get_encoding(self):
790 fp = os.popen("git config --get i18n.commitencoding")
791 self.encoding=string.strip(fp.readline())
793 if (self.encoding == ""):
794 self.encoding = "utf-8"
798 """Construct the window contents."""
801 paned.pack1(self.construct_top(), resize=False, shrink=True)
802 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
803 menu_bar = gtk.MenuBar()
804 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
805 help_menu = gtk.MenuItem("Help")
807 about_menu = gtk.MenuItem("About")
808 menu.append(about_menu)
809 about_menu.connect("activate", self.about_menu_response, "about")
811 help_menu.set_submenu(menu)
813 menu_bar.append(help_menu)
815 vbox.pack_start(menu_bar, expand=False, fill=True)
816 vbox.pack_start(paned, expand=True, fill=True)
822 def construct_top(self):
823 """Construct the top-half of the window."""
824 vbox = gtk.VBox(spacing=6)
825 vbox.set_border_width(12)
829 scrollwin = gtk.ScrolledWindow()
830 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
831 scrollwin.set_shadow_type(gtk.SHADOW_IN)
832 vbox.pack_start(scrollwin, expand=True, fill=True)
835 self.treeview = gtk.TreeView()
836 self.treeview.set_rules_hint(True)
837 self.treeview.set_search_column(4)
838 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
839 scrollwin.add(self.treeview)
842 cell = CellRendererGraph()
843 column = gtk.TreeViewColumn()
844 column.set_resizable(True)
845 column.pack_start(cell, expand=True)
846 column.add_attribute(cell, "node", 1)
847 column.add_attribute(cell, "in-lines", 2)
848 column.add_attribute(cell, "out-lines", 3)
849 self.treeview.append_column(column)
851 cell = gtk.CellRendererText()
852 cell.set_property("width-chars", 65)
853 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
854 column = gtk.TreeViewColumn("Message")
855 column.set_resizable(True)
856 column.pack_start(cell, expand=True)
857 column.add_attribute(cell, "text", 4)
858 self.treeview.append_column(column)
860 cell = gtk.CellRendererText()
861 cell.set_property("width-chars", 40)
862 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
863 column = gtk.TreeViewColumn("Author")
864 column.set_resizable(True)
865 column.pack_start(cell, expand=True)
866 column.add_attribute(cell, "text", 5)
867 self.treeview.append_column(column)
869 cell = gtk.CellRendererText()
870 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
871 column = gtk.TreeViewColumn("Date")
872 column.set_resizable(True)
873 column.pack_start(cell, expand=True)
874 column.add_attribute(cell, "text", 6)
875 self.treeview.append_column(column)
879 def about_menu_response(self, widget, string):
880 dialog = gtk.AboutDialog()
881 dialog.set_name("Gitview")
882 dialog.set_version(GitView.version)
883 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@gmail.com>"])
884 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
885 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
886 dialog.set_wrap_license(True)
891 def construct_bottom(self):
892 """Construct the bottom half of the window."""
893 vbox = gtk.VBox(False, spacing=6)
894 vbox.set_border_width(12)
895 (width, height) = self.window.get_size()
896 vbox.set_size_request(width, int(height / 2.5))
899 self.table = gtk.Table(rows=4, columns=4)
900 self.table.set_row_spacings(6)
901 self.table.set_col_spacings(6)
902 vbox.pack_start(self.table, expand=False, fill=True)
905 align = gtk.Alignment(0.0, 0.5)
907 label.set_markup("<b>Revision:</b>")
909 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
913 align = gtk.Alignment(0.0, 0.5)
914 self.revid_label = gtk.Label()
915 self.revid_label.set_selectable(True)
916 align.add(self.revid_label)
917 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
918 self.revid_label.show()
921 align = gtk.Alignment(0.0, 0.5)
923 label.set_markup("<b>Committer:</b>")
925 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
929 align = gtk.Alignment(0.0, 0.5)
930 self.committer_label = gtk.Label()
931 self.committer_label.set_selectable(True)
932 align.add(self.committer_label)
933 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
934 self.committer_label.show()
937 align = gtk.Alignment(0.0, 0.5)
939 label.set_markup("<b>Timestamp:</b>")
941 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
945 align = gtk.Alignment(0.0, 0.5)
946 self.timestamp_label = gtk.Label()
947 self.timestamp_label.set_selectable(True)
948 align.add(self.timestamp_label)
949 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
950 self.timestamp_label.show()
953 align = gtk.Alignment(0.0, 0.5)
955 label.set_markup("<b>Parents:</b>")
957 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
960 self.parents_widgets = []
962 align = gtk.Alignment(0.0, 0.5)
964 label.set_markup("<b>Children:</b>")
966 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
969 self.children_widgets = []
971 scrollwin = gtk.ScrolledWindow()
972 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
973 scrollwin.set_shadow_type(gtk.SHADOW_IN)
974 vbox.pack_start(scrollwin, expand=True, fill=True)
977 (self.message_buffer, sourceview) = get_source_buffer_and_view()
979 sourceview.set_editable(False)
980 sourceview.modify_font(pango.FontDescription("Monospace"))
981 scrollwin.add(sourceview)
986 def _treeview_cursor_cb(self, *args):
987 """Callback for when the treeview cursor changes."""
988 (path, col) = self.treeview.get_cursor()
989 commit = self.model[path][0]
991 if commit.committer is not None:
992 committer = commit.committer
993 timestamp = commit.commit_date
994 message = commit.get_message(self.with_diff)
995 revid_label = commit.commit_sha1
1002 self.revid_label.set_text(revid_label)
1003 self.committer_label.set_text(committer)
1004 self.timestamp_label.set_text(timestamp)
1005 self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
1007 for widget in self.parents_widgets:
1008 self.table.remove(widget)
1010 self.parents_widgets = []
1011 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
1012 for idx, parent_id in enumerate(commit.parent_sha1):
1013 self.table.set_row_spacing(idx + 3, 0)
1015 align = gtk.Alignment(0.0, 0.0)
1016 self.parents_widgets.append(align)
1017 self.table.attach(align, 1, 2, idx + 3, idx + 4,
1018 gtk.EXPAND | gtk.FILL, gtk.FILL)
1021 hbox = gtk.HBox(False, 0)
1025 label = gtk.Label(parent_id)
1026 label.set_selectable(True)
1027 hbox.pack_start(label, expand=False, fill=True)
1031 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1034 button = gtk.Button()
1036 button.set_relief(gtk.RELIEF_NONE)
1037 button.connect("clicked", self._go_clicked_cb, parent_id)
1038 hbox.pack_start(button, expand=False, fill=True)
1042 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1045 button = gtk.Button()
1047 button.set_relief(gtk.RELIEF_NONE)
1048 button.set_sensitive(True)
1049 button.connect("clicked", self._show_clicked_cb,
1050 commit.commit_sha1, parent_id, self.encoding)
1051 hbox.pack_start(button, expand=False, fill=True)
1054 # Populate with child details
1055 for widget in self.children_widgets:
1056 self.table.remove(widget)
1058 self.children_widgets = []
1060 child_sha1 = Commit.children_sha1[commit.commit_sha1]
1062 # We don't have child
1065 if ( len(child_sha1) > len(commit.parent_sha1)):
1066 self.table.resize(4 + len(child_sha1) - 1, 4)
1068 for idx, child_id in enumerate(child_sha1):
1069 self.table.set_row_spacing(idx + 3, 0)
1071 align = gtk.Alignment(0.0, 0.0)
1072 self.children_widgets.append(align)
1073 self.table.attach(align, 3, 4, idx + 3, idx + 4,
1074 gtk.EXPAND | gtk.FILL, gtk.FILL)
1077 hbox = gtk.HBox(False, 0)
1081 label = gtk.Label(child_id)
1082 label.set_selectable(True)
1083 hbox.pack_start(label, expand=False, fill=True)
1087 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1090 button = gtk.Button()
1092 button.set_relief(gtk.RELIEF_NONE)
1093 button.connect("clicked", self._go_clicked_cb, child_id)
1094 hbox.pack_start(button, expand=False, fill=True)
1098 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1101 button = gtk.Button()
1103 button.set_relief(gtk.RELIEF_NONE)
1104 button.set_sensitive(True)
1105 button.connect("clicked", self._show_clicked_cb,
1106 child_id, commit.commit_sha1, self.encoding)
1107 hbox.pack_start(button, expand=False, fill=True)
1110 def _destroy_cb(self, widget):
1111 """Callback for when a window we manage is destroyed."""
1116 """Stop the GTK+ main loop."""
1119 def run(self, args):
1120 self.set_branch(args)
1121 self.window.connect("destroy", self._destroy_cb)
1125 def set_branch(self, args):
1126 """Fill in different windows with info from the reposiroty"""
1127 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
1128 git_rev_list_cmd = fp.read()
1130 fp = os.popen("git rev-list --header --topo-order --parents " + git_rev_list_cmd)
1131 self.update_window(fp)
1133 def update_window(self, fp):
1136 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
1137 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
1139 # used for cursor positioning
1144 self.incomplete_line = {}
1151 input_line = fp.readline()
1152 while (input_line != ""):
1153 # The commit header ends with '\0'
1154 # This NULL is immediately followed by the sha1 of the
1156 if (input_line[0] != '\0'):
1157 commit_lines.append(input_line)
1158 input_line = fp.readline()
1161 commit = Commit(commit_lines)
1162 if (commit != None ):
1163 self.commits.append(commit)
1167 commit_lines.append(input_line[1:])
1168 input_line = fp.readline()
1172 for commit in self.commits:
1173 (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
1177 self.index[commit.commit_sha1] = index
1180 self.treeview.set_model(self.model)
1181 self.treeview.show()
1183 def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
1191 if (last_nodepos > 5):
1194 # Add the incomplete lines of the last cell in this
1196 colour = self.colours[commit.commit_sha1]
1198 self.colours[commit.commit_sha1] = last_colour+1
1199 last_colour = self.colours[commit.commit_sha1]
1200 colour = self.colours[commit.commit_sha1]
1203 node_pos = self.nodepos[commit.commit_sha1]
1205 self.nodepos[commit.commit_sha1] = last_nodepos+1
1206 last_nodepos = self.nodepos[commit.commit_sha1]
1207 node_pos = self.nodepos[commit.commit_sha1]
1209 #The first parent always continue on the same line
1211 # check we alreay have the value
1212 tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
1214 self.colours[commit.parent_sha1[0]] = colour
1215 self.nodepos[commit.parent_sha1[0]] = node_pos
1217 for sha1 in self.incomplete_line.keys():
1218 if (sha1 != commit.commit_sha1):
1219 self.draw_incomplete_line(sha1, node_pos,
1220 out_line, in_line, index)
1222 del self.incomplete_line[sha1]
1225 for parent_id in commit.parent_sha1:
1227 tmp_node_pos = self.nodepos[parent_id]
1229 self.colours[parent_id] = last_colour+1
1230 last_colour = self.colours[parent_id]
1231 self.nodepos[parent_id] = last_nodepos+1
1232 last_nodepos = self.nodepos[parent_id]
1234 in_line.append((node_pos, self.nodepos[parent_id],
1235 self.colours[parent_id]))
1236 self.add_incomplete_line(parent_id)
1239 branch_tag = self.bt_sha1[commit.commit_sha1]
1244 node = (node_pos, colour, branch_tag)
1246 self.model.append([commit, node, out_line, in_line,
1247 commit.message, commit.author, commit.date])
1249 return (in_line, last_colour, last_nodepos)
1251 def add_incomplete_line(self, sha1):
1253 self.incomplete_line[sha1].append(self.nodepos[sha1])
1255 self.incomplete_line[sha1] = [self.nodepos[sha1]]
1257 def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
1258 for idx, pos in enumerate(self.incomplete_line[sha1]):
1259 if(pos == node_pos):
1260 #remove the straight line and add a slash
1261 if ((pos, pos, self.colours[sha1]) in out_line):
1262 out_line.remove((pos, pos, self.colours[sha1]))
1263 out_line.append((pos, pos+0.5, self.colours[sha1]))
1264 self.incomplete_line[sha1][idx] = pos = pos+0.5
1266 next_commit = self.commits[index+1]
1267 if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
1268 # join the line back to the node point
1269 # This need to be done only if we modified it
1270 in_line.append((pos, pos-0.5, self.colours[sha1]))
1274 in_line.append((pos, pos, self.colours[sha1]))
1277 def _go_clicked_cb(self, widget, revid):
1278 """Callback for when the go button for a parent is clicked."""
1280 self.treeview.set_cursor(self.index[revid])
1282 dialog = gtk.MessageDialog(parent=None, flags=0,
1283 type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
1284 message_format=None)
1285 dialog.set_markup("Revision <b>%s</b> not present in the list" % revid)
1286 # revid == 0 is the parent of the first commit
1288 dialog.format_secondary_text("Try running gitview without any options")
1292 self.treeview.grab_focus()
1294 def _show_clicked_cb(self, widget, commit_sha1, parent_sha1, encoding):
1295 """Callback for when the show button for a parent is clicked."""
1296 window = DiffWindow()
1297 window.set_diff(commit_sha1, parent_sha1, encoding)
1298 self.treeview.grab_focus()
1301 if __name__ == "__main__":
1303 if (len(sys.argv) > 1 ):
1304 if (sys.argv[1] == "--without-diff"):
1307 view = GitView( without_diff != 1)
1308 view.run(sys.argv[without_diff:])