gitview: annotation support
[git/dscho.git] / contrib / gitview / gitview
blob2d80e2bad2e6f322d7ff7e9f03a6897a11f74231
1 #! /usr/bin/env python
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
8 """ gitview
9 GUI browser for git repository
10 This program is based on bzrk by Scott James Remnant <scott@ubuntu.com>
11 """
12 __copyright__ = "Copyright (C) 2006 Hewlett-Packard Development Company, L.P."
13 __copyright__ = "Copyright (C) 2007 Aneesh Kumar K.V <aneesh.kumar@gmail.com"
14 __author__ = "Aneesh Kumar K.V <aneesh.kumar@gmail.com>"
17 import sys
18 import os
19 import gtk
20 import pygtk
21 import pango
22 import re
23 import time
24 import gobject
25 import cairo
26 import math
27 import string
28 import fcntl
30 try:
31 import gtksourceview
32 have_gtksourceview = True
33 except ImportError:
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):
40 count = len(args)
41 i = skip
42 str_arg=" "
43 while (i < count ):
44 str_arg = str_arg + args[i]
45 str_arg = str_arg + " "
46 i = i+1
48 return str_arg
50 def show_date(epoch, tz):
51 secs = float(epoch)
52 tzsecs = float(tz[1:3]) * 3600
53 tzsecs += float(tz[3:5]) * 60
54 if (tz[0] == "+"):
55 secs += tzsecs
56 else:
57 secs -= tzsecs
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
67 in graph.py.
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.
73 Properties:
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.
77 """
79 __gproperties__ = {
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":
97 self.node = value
98 elif property.name == "in-lines":
99 self.in_lines = value
100 elif property.name == "out-lines":
101 self.out_lines = value
102 else:
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.
111 try:
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.
132 colours = [
133 ( 1.0, 0.0, 0.0 ),
134 ( 1.0, 1.0, 0.0 ),
135 ( 0.0, 1.0, 0.0 ),
136 ( 0.0, 1.0, 1.0 ),
137 ( 0.0, 0.0, 1.0 ),
138 ( 1.0, 0.0, 1.0 ),
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
153 line up.
155 box_size = self.box_size(widget)
157 cols = self.node[0]
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
162 names_len = 0
163 if (len(names) != 0):
164 for item in names:
165 names_len += len(item)
167 width = box_size * (cols + 1 ) + names_len
168 height = box_size
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
180 want any gutter.
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
185 incredible amount.
187 ctx = window.cairo_create()
188 ctx.rectangle(bg_area.x, bg_area.y, bg_area.width, bg_area.height)
189 ctx.clip()
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)
201 if start - end > 1:
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,
206 bg_area.y)
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)
213 ctx.stroke()
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)
220 if start - end > 1:
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)
235 ctx.stroke()
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)
248 ctx.fill_preserve()
250 if (len(names) != 0):
251 name = " "
252 for item in names:
253 name = name + item + " "
255 ctx.set_font_size(13)
256 if (flags & 1):
257 self.set_colour(ctx, colour, 0.5, 1.0)
258 else:
259 self.set_colour(ctx, colour, 0.0, 0.5)
260 ctx.show_text(name)
262 class Commit:
263 """ This represent a commit object obtained after parsing the git-rev-list
264 output """
266 children_sha1 = {}
268 def __init__(self, commit_lines):
269 self.message = ""
270 self.author = ""
271 self.date = ""
272 self.committer = ""
273 self.commit_date = ""
274 self.commit_sha1 = ""
275 self.parent_sha1 = [ ]
276 self.parse_commit(commit_lines)
279 def parse_commit(self, commit_lines):
281 # First line is the sha1 lines
282 line = string.strip(commit_lines[0])
283 sha1 = re.split(" ", line)
284 self.commit_sha1 = sha1[0]
285 self.parent_sha1 = sha1[1:]
287 #build the child list
288 for parent_id in self.parent_sha1:
289 try:
290 Commit.children_sha1[parent_id].append(self.commit_sha1)
291 except KeyError:
292 Commit.children_sha1[parent_id] = [self.commit_sha1]
294 # IF we don't have parent
295 if (len(self.parent_sha1) == 0):
296 self.parent_sha1 = [0]
298 for line in commit_lines[1:]:
299 m = re.match("^ ", line)
300 if (m != None):
301 # First line of the commit message used for short log
302 if self.message == "":
303 self.message = string.strip(line)
304 continue
306 m = re.match("tree", line)
307 if (m != None):
308 continue
310 m = re.match("parent", line)
311 if (m != None):
312 continue
314 m = re_ident.match(line)
315 if (m != None):
316 date = show_date(m.group('epoch'), m.group('tz'))
317 if m.group(1) == "author":
318 self.author = m.group('ident')
319 self.date = date
320 elif m.group(1) == "committer":
321 self.committer = m.group('ident')
322 self.commit_date = date
324 continue
326 def get_message(self, with_diff=0):
327 if (with_diff == 1):
328 message = self.diff_tree()
329 else:
330 fp = os.popen("git cat-file commit " + self.commit_sha1)
331 message = fp.read()
332 fp.close()
334 return message
336 def diff_tree(self):
337 fp = os.popen("git diff-tree --pretty --cc -v -p --always " + self.commit_sha1)
338 diff = fp.read()
339 fp.close()
340 return diff
342 class AnnotateWindow:
343 """Annotate window.
344 This object represents and manages a single window containing the
345 annotate information of the file
348 def __init__(self):
349 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
350 self.window.set_border_width(0)
351 self.window.set_title("Git repository browser annotation window")
353 # Use two thirds of the screen by default
354 screen = self.window.get_screen()
355 monitor = screen.get_monitor_geometry(0)
356 width = int(monitor.width * 0.66)
357 height = int(monitor.height * 0.66)
358 self.window.set_default_size(width, height)
360 def add_file_data(self, filename, commit_sha1, line_num):
361 fp = os.popen("git cat-file blob " + commit_sha1 +":"+filename)
362 i = 1;
363 for line in fp.readlines():
364 line = string.rstrip(line)
365 self.model.append(None, ["HEAD", filename, line, i])
366 i = i+1
367 fp.close()
369 # now set the cursor position
370 self.treeview.set_cursor(line_num-1)
371 self.treeview.grab_focus()
373 def _treeview_cursor_cb(self, *args):
374 """Callback for when the treeview cursor changes."""
375 (path, col) = self.treeview.get_cursor()
376 commit_sha1 = self.model[path][0]
377 commit_msg = ""
378 fp = os.popen("git cat-file commit " + commit_sha1)
379 for line in fp.readlines():
380 commit_msg = commit_msg + line
381 fp.close()
383 self.commit_buffer.set_text(commit_msg)
385 def _treeview_row_activated(self, *args):
386 """Callback for when the treeview row gets selected."""
387 (path, col) = self.treeview.get_cursor()
388 commit_sha1 = self.model[path][0]
389 filename = self.model[path][1]
390 line_num = self.model[path][3]
392 window = AnnotateWindow();
393 fp = os.popen("git rev-parse "+ commit_sha1 + "~1")
394 commit_sha1 = string.strip(fp.readline())
395 fp.close()
396 window.annotate(filename, commit_sha1, line_num)
398 def data_ready(self, source, condition):
399 while (1):
400 try :
401 buffer = source.read(8192)
402 except:
403 # resource temporary not available
404 return True
406 if (len(buffer) == 0):
407 gobject.source_remove(self.io_watch_tag)
408 source.close()
409 return False
411 for buff in buffer.split("\n"):
412 annotate_line = re.compile('^([0-9a-f]{40}) (.+) (.+) (.+)$')
413 m = annotate_line.match(buff)
414 if not m:
415 annotate_line = re.compile('^(filename) (.+)$')
416 m = annotate_line.match(buff)
417 if not m:
418 continue
419 filename = m.group(2)
420 else:
421 self.commit_sha1 = m.group(1)
422 self.source_line = int(m.group(2))
423 self.result_line = int(m.group(3))
424 self.count = int(m.group(4))
425 #set the details only when we have the file name
426 continue
428 while (self.count > 0):
429 # set at result_line + count-1 the sha1 as commit_sha1
430 self.count = self.count - 1
431 iter = self.model.iter_nth_child(None, self.result_line + self.count-1)
432 self.model.set(iter, 0, self.commit_sha1, 1, filename, 3, self.source_line)
435 def annotate(self, filename, commit_sha1, line_num):
436 # verify the commit_sha1 specified has this filename
438 fp = os.popen("git ls-tree "+ commit_sha1 + " -- " + filename)
439 line = string.strip(fp.readline())
440 if line == '':
441 # pop up the message the file is not there as a part of the commit
442 fp.close()
443 dialog = gtk.MessageDialog(parent=None, flags=0,
444 type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
445 message_format=None)
446 dialog.set_markup("The file %s is not present in the parent commit %s" % (filename, commit_sha1))
447 dialog.run()
448 dialog.destroy()
449 return
451 fp.close()
453 vpan = gtk.VPaned();
454 self.window.add(vpan);
455 vpan.show()
457 scrollwin = gtk.ScrolledWindow()
458 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
459 scrollwin.set_shadow_type(gtk.SHADOW_IN)
460 vpan.pack1(scrollwin, True, True);
461 scrollwin.show()
463 self.model = gtk.TreeStore(str, str, str, int)
464 self.treeview = gtk.TreeView(self.model)
465 self.treeview.set_rules_hint(True)
466 self.treeview.set_search_column(0)
467 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
468 self.treeview.connect("row-activated", self._treeview_row_activated)
469 scrollwin.add(self.treeview)
470 self.treeview.show()
472 cell = gtk.CellRendererText()
473 cell.set_property("width-chars", 10)
474 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
475 column = gtk.TreeViewColumn("Commit")
476 column.set_resizable(True)
477 column.pack_start(cell, expand=True)
478 column.add_attribute(cell, "text", 0)
479 self.treeview.append_column(column)
481 cell = gtk.CellRendererText()
482 cell.set_property("width-chars", 20)
483 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
484 column = gtk.TreeViewColumn("File Name")
485 column.set_resizable(True)
486 column.pack_start(cell, expand=True)
487 column.add_attribute(cell, "text", 1)
488 self.treeview.append_column(column)
490 cell = gtk.CellRendererText()
491 cell.set_property("width-chars", 20)
492 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
493 column = gtk.TreeViewColumn("Data")
494 column.set_resizable(True)
495 column.pack_start(cell, expand=True)
496 column.add_attribute(cell, "text", 2)
497 self.treeview.append_column(column)
499 # The commit message window
500 scrollwin = gtk.ScrolledWindow()
501 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
502 scrollwin.set_shadow_type(gtk.SHADOW_IN)
503 vpan.pack2(scrollwin, True, True);
504 scrollwin.show()
506 commit_text = gtk.TextView()
507 self.commit_buffer = gtk.TextBuffer()
508 commit_text.set_buffer(self.commit_buffer)
509 scrollwin.add(commit_text)
510 commit_text.show()
512 self.window.show()
514 self.add_file_data(filename, commit_sha1, line_num)
516 fp = os.popen("git blame --incremental -- " + filename + " " + commit_sha1)
517 flags = fcntl.fcntl(fp.fileno(), fcntl.F_GETFL)
518 fcntl.fcntl(fp.fileno(), fcntl.F_SETFL, flags | os.O_NONBLOCK)
519 self.io_watch_tag = gobject.io_add_watch(fp, gobject.IO_IN, self.data_ready)
522 class DiffWindow:
523 """Diff window.
524 This object represents and manages a single window containing the
525 differences between two revisions on a branch.
528 def __init__(self):
529 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
530 self.window.set_border_width(0)
531 self.window.set_title("Git repository browser diff window")
533 # Use two thirds of the screen by default
534 screen = self.window.get_screen()
535 monitor = screen.get_monitor_geometry(0)
536 width = int(monitor.width * 0.66)
537 height = int(monitor.height * 0.66)
538 self.window.set_default_size(width, height)
541 self.construct()
543 def construct(self):
544 """Construct the window contents."""
545 vbox = gtk.VBox()
546 self.window.add(vbox)
547 vbox.show()
549 menu_bar = gtk.MenuBar()
550 save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
551 save_menu.connect("activate", self.save_menu_response, "save")
552 save_menu.show()
553 menu_bar.append(save_menu)
554 vbox.pack_start(menu_bar, expand=False, fill=True)
555 menu_bar.show()
557 hpan = gtk.HPaned()
559 scrollwin = gtk.ScrolledWindow()
560 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
561 scrollwin.set_shadow_type(gtk.SHADOW_IN)
562 hpan.pack1(scrollwin, True, True)
563 scrollwin.show()
565 if have_gtksourceview:
566 self.buffer = gtksourceview.SourceBuffer()
567 slm = gtksourceview.SourceLanguagesManager()
568 gsl = slm.get_language_from_mime_type("text/x-patch")
569 self.buffer.set_highlight(True)
570 self.buffer.set_language(gsl)
571 sourceview = gtksourceview.SourceView(self.buffer)
572 else:
573 self.buffer = gtk.TextBuffer()
574 sourceview = gtk.TextView(self.buffer)
577 sourceview.set_editable(False)
578 sourceview.modify_font(pango.FontDescription("Monospace"))
579 scrollwin.add(sourceview)
580 sourceview.show()
582 # The file hierarchy: a scrollable treeview
583 scrollwin = gtk.ScrolledWindow()
584 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
585 scrollwin.set_shadow_type(gtk.SHADOW_IN)
586 scrollwin.set_size_request(20, -1)
587 hpan.pack2(scrollwin, True, True)
588 scrollwin.show()
590 self.model = gtk.TreeStore(str, str, str)
591 self.treeview = gtk.TreeView(self.model)
592 self.treeview.set_search_column(1)
593 self.treeview.connect("cursor-changed", self._treeview_clicked)
594 scrollwin.add(self.treeview)
595 self.treeview.show()
597 cell = gtk.CellRendererText()
598 cell.set_property("width-chars", 20)
599 column = gtk.TreeViewColumn("Select to annotate")
600 column.pack_start(cell, expand=True)
601 column.add_attribute(cell, "text", 0)
602 self.treeview.append_column(column)
604 vbox.pack_start(hpan, expand=True, fill=True)
605 hpan.show()
607 def _treeview_clicked(self, *args):
608 """Callback for when the treeview cursor changes."""
609 (path, col) = self.treeview.get_cursor()
610 specific_file = self.model[path][1]
611 commit_sha1 = self.model[path][2]
612 if specific_file == None :
613 return
614 elif specific_file == "" :
615 specific_file = None
617 window = AnnotateWindow();
618 window.annotate(specific_file, commit_sha1, 1)
621 def commit_files(self, commit_sha1, parent_sha1):
622 self.model.clear()
623 add = self.model.append(None, [ "Added", None, None])
624 dele = self.model.append(None, [ "Deleted", None, None])
625 mod = self.model.append(None, [ "Modified", None, None])
626 diff_tree = re.compile('^(:.{6}) (.{6}) (.{40}) (.{40}) (A|D|M)\s(.+)$')
627 fp = os.popen("git diff-tree -r --no-commit-id " + parent_sha1 + " " + commit_sha1)
628 while 1:
629 line = string.strip(fp.readline())
630 if line == '':
631 break
632 m = diff_tree.match(line)
633 if not m:
634 continue
636 attr = m.group(5)
637 filename = m.group(6)
638 if attr == "A":
639 self.model.append(add, [filename, filename, commit_sha1])
640 elif attr == "D":
641 self.model.append(dele, [filename, filename, commit_sha1])
642 elif attr == "M":
643 self.model.append(mod, [filename, filename, commit_sha1])
644 fp.close()
646 self.treeview.expand_all()
648 def set_diff(self, commit_sha1, parent_sha1, encoding):
649 """Set the differences showed by this window.
650 Compares the two trees and populates the window with the
651 differences.
653 # Diff with the first commit or the last commit shows nothing
654 if (commit_sha1 == 0 or parent_sha1 == 0 ):
655 return
657 fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
658 self.buffer.set_text(unicode(fp.read(), encoding).encode('utf-8'))
659 fp.close()
660 self.commit_files(commit_sha1, parent_sha1)
661 self.window.show()
663 def save_menu_response(self, widget, string):
664 dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
665 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
666 gtk.STOCK_SAVE, gtk.RESPONSE_OK))
667 dialog.set_default_response(gtk.RESPONSE_OK)
668 response = dialog.run()
669 if response == gtk.RESPONSE_OK:
670 patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
671 self.buffer.get_end_iter())
672 fp = open(dialog.get_filename(), "w")
673 fp.write(patch_buffer)
674 fp.close()
675 dialog.destroy()
677 class GitView:
678 """ This is the main class
680 version = "0.9"
682 def __init__(self, with_diff=0):
683 self.with_diff = with_diff
684 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
685 self.window.set_border_width(0)
686 self.window.set_title("Git repository browser")
688 self.get_encoding()
689 self.get_bt_sha1()
691 # Use three-quarters of the screen by default
692 screen = self.window.get_screen()
693 monitor = screen.get_monitor_geometry(0)
694 width = int(monitor.width * 0.75)
695 height = int(monitor.height * 0.75)
696 self.window.set_default_size(width, height)
698 # FIXME AndyFitz!
699 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
700 self.window.set_icon(icon)
702 self.accel_group = gtk.AccelGroup()
703 self.window.add_accel_group(self.accel_group)
704 self.accel_group.connect_group(0xffc2, 0, gtk.ACCEL_LOCKED, self.refresh);
705 self.accel_group.connect_group(0xffc1, 0, gtk.ACCEL_LOCKED, self.maximize);
706 self.accel_group.connect_group(0xffc8, 0, gtk.ACCEL_LOCKED, self.fullscreen);
707 self.accel_group.connect_group(0xffc9, 0, gtk.ACCEL_LOCKED, self.unfullscreen);
709 self.window.add(self.construct())
711 def refresh(self, widget, event=None, *arguments, **keywords):
712 self.get_encoding()
713 self.get_bt_sha1()
714 Commit.children_sha1 = {}
715 self.set_branch(sys.argv[without_diff:])
716 self.window.show()
717 return True
719 def maximize(self, widget, event=None, *arguments, **keywords):
720 self.window.maximize()
721 return True
723 def fullscreen(self, widget, event=None, *arguments, **keywords):
724 self.window.fullscreen()
725 return True
727 def unfullscreen(self, widget, event=None, *arguments, **keywords):
728 self.window.unfullscreen()
729 return True
731 def get_bt_sha1(self):
732 """ Update the bt_sha1 dictionary with the
733 respective sha1 details """
735 self.bt_sha1 = { }
736 ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
737 fp = os.popen('git ls-remote "${GIT_DIR-.git}"')
738 while 1:
739 line = string.strip(fp.readline())
740 if line == '':
741 break
742 m = ls_remote.match(line)
743 if not m:
744 continue
745 (sha1, name) = (m.group(1), m.group(2))
746 if not self.bt_sha1.has_key(sha1):
747 self.bt_sha1[sha1] = []
748 self.bt_sha1[sha1].append(name)
749 fp.close()
751 def get_encoding(self):
752 fp = os.popen("git config --get i18n.commitencoding")
753 self.encoding=string.strip(fp.readline())
754 fp.close()
755 if (self.encoding == ""):
756 self.encoding = "utf-8"
759 def construct(self):
760 """Construct the window contents."""
761 vbox = gtk.VBox()
762 paned = gtk.VPaned()
763 paned.pack1(self.construct_top(), resize=False, shrink=True)
764 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
765 menu_bar = gtk.MenuBar()
766 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
767 help_menu = gtk.MenuItem("Help")
768 menu = gtk.Menu()
769 about_menu = gtk.MenuItem("About")
770 menu.append(about_menu)
771 about_menu.connect("activate", self.about_menu_response, "about")
772 about_menu.show()
773 help_menu.set_submenu(menu)
774 help_menu.show()
775 menu_bar.append(help_menu)
776 menu_bar.show()
777 vbox.pack_start(menu_bar, expand=False, fill=True)
778 vbox.pack_start(paned, expand=True, fill=True)
779 paned.show()
780 vbox.show()
781 return vbox
784 def construct_top(self):
785 """Construct the top-half of the window."""
786 vbox = gtk.VBox(spacing=6)
787 vbox.set_border_width(12)
788 vbox.show()
791 scrollwin = gtk.ScrolledWindow()
792 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
793 scrollwin.set_shadow_type(gtk.SHADOW_IN)
794 vbox.pack_start(scrollwin, expand=True, fill=True)
795 scrollwin.show()
797 self.treeview = gtk.TreeView()
798 self.treeview.set_rules_hint(True)
799 self.treeview.set_search_column(4)
800 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
801 scrollwin.add(self.treeview)
802 self.treeview.show()
804 cell = CellRendererGraph()
805 column = gtk.TreeViewColumn()
806 column.set_resizable(True)
807 column.pack_start(cell, expand=True)
808 column.add_attribute(cell, "node", 1)
809 column.add_attribute(cell, "in-lines", 2)
810 column.add_attribute(cell, "out-lines", 3)
811 self.treeview.append_column(column)
813 cell = gtk.CellRendererText()
814 cell.set_property("width-chars", 65)
815 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
816 column = gtk.TreeViewColumn("Message")
817 column.set_resizable(True)
818 column.pack_start(cell, expand=True)
819 column.add_attribute(cell, "text", 4)
820 self.treeview.append_column(column)
822 cell = gtk.CellRendererText()
823 cell.set_property("width-chars", 40)
824 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
825 column = gtk.TreeViewColumn("Author")
826 column.set_resizable(True)
827 column.pack_start(cell, expand=True)
828 column.add_attribute(cell, "text", 5)
829 self.treeview.append_column(column)
831 cell = gtk.CellRendererText()
832 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
833 column = gtk.TreeViewColumn("Date")
834 column.set_resizable(True)
835 column.pack_start(cell, expand=True)
836 column.add_attribute(cell, "text", 6)
837 self.treeview.append_column(column)
839 return vbox
841 def about_menu_response(self, widget, string):
842 dialog = gtk.AboutDialog()
843 dialog.set_name("Gitview")
844 dialog.set_version(GitView.version)
845 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@gmail.com>"])
846 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
847 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
848 dialog.set_wrap_license(True)
849 dialog.run()
850 dialog.destroy()
853 def construct_bottom(self):
854 """Construct the bottom half of the window."""
855 vbox = gtk.VBox(False, spacing=6)
856 vbox.set_border_width(12)
857 (width, height) = self.window.get_size()
858 vbox.set_size_request(width, int(height / 2.5))
859 vbox.show()
861 self.table = gtk.Table(rows=4, columns=4)
862 self.table.set_row_spacings(6)
863 self.table.set_col_spacings(6)
864 vbox.pack_start(self.table, expand=False, fill=True)
865 self.table.show()
867 align = gtk.Alignment(0.0, 0.5)
868 label = gtk.Label()
869 label.set_markup("<b>Revision:</b>")
870 align.add(label)
871 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
872 label.show()
873 align.show()
875 align = gtk.Alignment(0.0, 0.5)
876 self.revid_label = gtk.Label()
877 self.revid_label.set_selectable(True)
878 align.add(self.revid_label)
879 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
880 self.revid_label.show()
881 align.show()
883 align = gtk.Alignment(0.0, 0.5)
884 label = gtk.Label()
885 label.set_markup("<b>Committer:</b>")
886 align.add(label)
887 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
888 label.show()
889 align.show()
891 align = gtk.Alignment(0.0, 0.5)
892 self.committer_label = gtk.Label()
893 self.committer_label.set_selectable(True)
894 align.add(self.committer_label)
895 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
896 self.committer_label.show()
897 align.show()
899 align = gtk.Alignment(0.0, 0.5)
900 label = gtk.Label()
901 label.set_markup("<b>Timestamp:</b>")
902 align.add(label)
903 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
904 label.show()
905 align.show()
907 align = gtk.Alignment(0.0, 0.5)
908 self.timestamp_label = gtk.Label()
909 self.timestamp_label.set_selectable(True)
910 align.add(self.timestamp_label)
911 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
912 self.timestamp_label.show()
913 align.show()
915 align = gtk.Alignment(0.0, 0.5)
916 label = gtk.Label()
917 label.set_markup("<b>Parents:</b>")
918 align.add(label)
919 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
920 label.show()
921 align.show()
922 self.parents_widgets = []
924 align = gtk.Alignment(0.0, 0.5)
925 label = gtk.Label()
926 label.set_markup("<b>Children:</b>")
927 align.add(label)
928 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
929 label.show()
930 align.show()
931 self.children_widgets = []
933 scrollwin = gtk.ScrolledWindow()
934 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
935 scrollwin.set_shadow_type(gtk.SHADOW_IN)
936 vbox.pack_start(scrollwin, expand=True, fill=True)
937 scrollwin.show()
939 if have_gtksourceview:
940 self.message_buffer = gtksourceview.SourceBuffer()
941 slm = gtksourceview.SourceLanguagesManager()
942 gsl = slm.get_language_from_mime_type("text/x-patch")
943 self.message_buffer.set_highlight(True)
944 self.message_buffer.set_language(gsl)
945 sourceview = gtksourceview.SourceView(self.message_buffer)
946 else:
947 self.message_buffer = gtk.TextBuffer()
948 sourceview = gtk.TextView(self.message_buffer)
950 sourceview.set_editable(False)
951 sourceview.modify_font(pango.FontDescription("Monospace"))
952 scrollwin.add(sourceview)
953 sourceview.show()
955 return vbox
957 def _treeview_cursor_cb(self, *args):
958 """Callback for when the treeview cursor changes."""
959 (path, col) = self.treeview.get_cursor()
960 commit = self.model[path][0]
962 if commit.committer is not None:
963 committer = commit.committer
964 timestamp = commit.commit_date
965 message = commit.get_message(self.with_diff)
966 revid_label = commit.commit_sha1
967 else:
968 committer = ""
969 timestamp = ""
970 message = ""
971 revid_label = ""
973 self.revid_label.set_text(revid_label)
974 self.committer_label.set_text(committer)
975 self.timestamp_label.set_text(timestamp)
976 self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
978 for widget in self.parents_widgets:
979 self.table.remove(widget)
981 self.parents_widgets = []
982 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
983 for idx, parent_id in enumerate(commit.parent_sha1):
984 self.table.set_row_spacing(idx + 3, 0)
986 align = gtk.Alignment(0.0, 0.0)
987 self.parents_widgets.append(align)
988 self.table.attach(align, 1, 2, idx + 3, idx + 4,
989 gtk.EXPAND | gtk.FILL, gtk.FILL)
990 align.show()
992 hbox = gtk.HBox(False, 0)
993 align.add(hbox)
994 hbox.show()
996 label = gtk.Label(parent_id)
997 label.set_selectable(True)
998 hbox.pack_start(label, expand=False, fill=True)
999 label.show()
1001 image = gtk.Image()
1002 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1003 image.show()
1005 button = gtk.Button()
1006 button.add(image)
1007 button.set_relief(gtk.RELIEF_NONE)
1008 button.connect("clicked", self._go_clicked_cb, parent_id)
1009 hbox.pack_start(button, expand=False, fill=True)
1010 button.show()
1012 image = gtk.Image()
1013 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1014 image.show()
1016 button = gtk.Button()
1017 button.add(image)
1018 button.set_relief(gtk.RELIEF_NONE)
1019 button.set_sensitive(True)
1020 button.connect("clicked", self._show_clicked_cb,
1021 commit.commit_sha1, parent_id, self.encoding)
1022 hbox.pack_start(button, expand=False, fill=True)
1023 button.show()
1025 # Populate with child details
1026 for widget in self.children_widgets:
1027 self.table.remove(widget)
1029 self.children_widgets = []
1030 try:
1031 child_sha1 = Commit.children_sha1[commit.commit_sha1]
1032 except KeyError:
1033 # We don't have child
1034 child_sha1 = [ 0 ]
1036 if ( len(child_sha1) > len(commit.parent_sha1)):
1037 self.table.resize(4 + len(child_sha1) - 1, 4)
1039 for idx, child_id in enumerate(child_sha1):
1040 self.table.set_row_spacing(idx + 3, 0)
1042 align = gtk.Alignment(0.0, 0.0)
1043 self.children_widgets.append(align)
1044 self.table.attach(align, 3, 4, idx + 3, idx + 4,
1045 gtk.EXPAND | gtk.FILL, gtk.FILL)
1046 align.show()
1048 hbox = gtk.HBox(False, 0)
1049 align.add(hbox)
1050 hbox.show()
1052 label = gtk.Label(child_id)
1053 label.set_selectable(True)
1054 hbox.pack_start(label, expand=False, fill=True)
1055 label.show()
1057 image = gtk.Image()
1058 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1059 image.show()
1061 button = gtk.Button()
1062 button.add(image)
1063 button.set_relief(gtk.RELIEF_NONE)
1064 button.connect("clicked", self._go_clicked_cb, child_id)
1065 hbox.pack_start(button, expand=False, fill=True)
1066 button.show()
1068 image = gtk.Image()
1069 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1070 image.show()
1072 button = gtk.Button()
1073 button.add(image)
1074 button.set_relief(gtk.RELIEF_NONE)
1075 button.set_sensitive(True)
1076 button.connect("clicked", self._show_clicked_cb,
1077 child_id, commit.commit_sha1, self.encoding)
1078 hbox.pack_start(button, expand=False, fill=True)
1079 button.show()
1081 def _destroy_cb(self, widget):
1082 """Callback for when a window we manage is destroyed."""
1083 self.quit()
1086 def quit(self):
1087 """Stop the GTK+ main loop."""
1088 gtk.main_quit()
1090 def run(self, args):
1091 self.set_branch(args)
1092 self.window.connect("destroy", self._destroy_cb)
1093 self.window.show()
1094 gtk.main()
1096 def set_branch(self, args):
1097 """Fill in different windows with info from the reposiroty"""
1098 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
1099 git_rev_list_cmd = fp.read()
1100 fp.close()
1101 fp = os.popen("git rev-list --header --topo-order --parents " + git_rev_list_cmd)
1102 self.update_window(fp)
1104 def update_window(self, fp):
1105 commit_lines = []
1107 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
1108 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
1110 # used for cursor positioning
1111 self.index = {}
1113 self.colours = {}
1114 self.nodepos = {}
1115 self.incomplete_line = {}
1116 self.commits = []
1118 index = 0
1119 last_colour = 0
1120 last_nodepos = -1
1121 out_line = []
1122 input_line = fp.readline()
1123 while (input_line != ""):
1124 # The commit header ends with '\0'
1125 # This NULL is immediately followed by the sha1 of the
1126 # next commit
1127 if (input_line[0] != '\0'):
1128 commit_lines.append(input_line)
1129 input_line = fp.readline()
1130 continue;
1132 commit = Commit(commit_lines)
1133 if (commit != None ):
1134 self.commits.append(commit)
1136 # Skip the '\0
1137 commit_lines = []
1138 commit_lines.append(input_line[1:])
1139 input_line = fp.readline()
1141 fp.close()
1143 for commit in self.commits:
1144 (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
1145 index, out_line,
1146 last_colour,
1147 last_nodepos)
1148 self.index[commit.commit_sha1] = index
1149 index += 1
1151 self.treeview.set_model(self.model)
1152 self.treeview.show()
1154 def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
1155 in_line=[]
1157 # | -> outline
1159 # |\ <- inline
1161 # Reset nodepostion
1162 if (last_nodepos > 5):
1163 last_nodepos = -1
1165 # Add the incomplete lines of the last cell in this
1166 try:
1167 colour = self.colours[commit.commit_sha1]
1168 except KeyError:
1169 self.colours[commit.commit_sha1] = last_colour+1
1170 last_colour = self.colours[commit.commit_sha1]
1171 colour = self.colours[commit.commit_sha1]
1173 try:
1174 node_pos = self.nodepos[commit.commit_sha1]
1175 except KeyError:
1176 self.nodepos[commit.commit_sha1] = last_nodepos+1
1177 last_nodepos = self.nodepos[commit.commit_sha1]
1178 node_pos = self.nodepos[commit.commit_sha1]
1180 #The first parent always continue on the same line
1181 try:
1182 # check we alreay have the value
1183 tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
1184 except KeyError:
1185 self.colours[commit.parent_sha1[0]] = colour
1186 self.nodepos[commit.parent_sha1[0]] = node_pos
1188 for sha1 in self.incomplete_line.keys():
1189 if (sha1 != commit.commit_sha1):
1190 self.draw_incomplete_line(sha1, node_pos,
1191 out_line, in_line, index)
1192 else:
1193 del self.incomplete_line[sha1]
1196 for parent_id in commit.parent_sha1:
1197 try:
1198 tmp_node_pos = self.nodepos[parent_id]
1199 except KeyError:
1200 self.colours[parent_id] = last_colour+1
1201 last_colour = self.colours[parent_id]
1202 self.nodepos[parent_id] = last_nodepos+1
1203 last_nodepos = self.nodepos[parent_id]
1205 in_line.append((node_pos, self.nodepos[parent_id],
1206 self.colours[parent_id]))
1207 self.add_incomplete_line(parent_id)
1209 try:
1210 branch_tag = self.bt_sha1[commit.commit_sha1]
1211 except KeyError:
1212 branch_tag = [ ]
1215 node = (node_pos, colour, branch_tag)
1217 self.model.append([commit, node, out_line, in_line,
1218 commit.message, commit.author, commit.date])
1220 return (in_line, last_colour, last_nodepos)
1222 def add_incomplete_line(self, sha1):
1223 try:
1224 self.incomplete_line[sha1].append(self.nodepos[sha1])
1225 except KeyError:
1226 self.incomplete_line[sha1] = [self.nodepos[sha1]]
1228 def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
1229 for idx, pos in enumerate(self.incomplete_line[sha1]):
1230 if(pos == node_pos):
1231 #remove the straight line and add a slash
1232 if ((pos, pos, self.colours[sha1]) in out_line):
1233 out_line.remove((pos, pos, self.colours[sha1]))
1234 out_line.append((pos, pos+0.5, self.colours[sha1]))
1235 self.incomplete_line[sha1][idx] = pos = pos+0.5
1236 try:
1237 next_commit = self.commits[index+1]
1238 if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
1239 # join the line back to the node point
1240 # This need to be done only if we modified it
1241 in_line.append((pos, pos-0.5, self.colours[sha1]))
1242 continue;
1243 except IndexError:
1244 pass
1245 in_line.append((pos, pos, self.colours[sha1]))
1248 def _go_clicked_cb(self, widget, revid):
1249 """Callback for when the go button for a parent is clicked."""
1250 try:
1251 self.treeview.set_cursor(self.index[revid])
1252 except KeyError:
1253 dialog = gtk.MessageDialog(parent=None, flags=0,
1254 type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
1255 message_format=None)
1256 dialog.set_markup("Revision <b>%s</b> not present in the list" % revid)
1257 # revid == 0 is the parent of the first commit
1258 if (revid != 0 ):
1259 dialog.format_secondary_text("Try running gitview without any options")
1260 dialog.run()
1261 dialog.destroy()
1263 self.treeview.grab_focus()
1265 def _show_clicked_cb(self, widget, commit_sha1, parent_sha1, encoding):
1266 """Callback for when the show button for a parent is clicked."""
1267 window = DiffWindow()
1268 window.set_diff(commit_sha1, parent_sha1, encoding)
1269 self.treeview.grab_focus()
1271 without_diff = 0
1272 if __name__ == "__main__":
1274 if (len(sys.argv) > 1 ):
1275 if (sys.argv[1] == "--without-diff"):
1276 without_diff = 1
1278 view = GitView( without_diff != 1)
1279 view.run(sys.argv[without_diff:])