gitview: Define __slots__ for Commit
[git/jrn.git] / contrib / gitview / gitview
blob098cb01353d4adbfd32bdd4f70183b4b474f2158
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(object):
263 """ This represent a commit object obtained after parsing the git-rev-list
264 output """
266 __slots__ = ['children_sha1', 'message', 'author', 'date', 'committer',
267 'commit_date', 'commit_sha1', 'parent_sha1']
269 children_sha1 = {}
271 def __init__(self, commit_lines):
272 self.message = ""
273 self.author = ""
274 self.date = ""
275 self.committer = ""
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:
292 try:
293 Commit.children_sha1[parent_id].append(self.commit_sha1)
294 except KeyError:
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)
303 if (m != None):
304 # First line of the commit message used for short log
305 if self.message == "":
306 self.message = string.strip(line)
307 continue
309 m = re.match("tree", line)
310 if (m != None):
311 continue
313 m = re.match("parent", line)
314 if (m != None):
315 continue
317 m = re_ident.match(line)
318 if (m != None):
319 date = show_date(m.group('epoch'), m.group('tz'))
320 if m.group(1) == "author":
321 self.author = m.group('ident')
322 self.date = date
323 elif m.group(1) == "committer":
324 self.committer = m.group('ident')
325 self.commit_date = date
327 continue
329 def get_message(self, with_diff=0):
330 if (with_diff == 1):
331 message = self.diff_tree()
332 else:
333 fp = os.popen("git cat-file commit " + self.commit_sha1)
334 message = fp.read()
335 fp.close()
337 return message
339 def diff_tree(self):
340 fp = os.popen("git diff-tree --pretty --cc -v -p --always " + self.commit_sha1)
341 diff = fp.read()
342 fp.close()
343 return diff
345 class AnnotateWindow(object):
346 """Annotate window.
347 This object represents and manages a single window containing the
348 annotate information of the file
351 def __init__(self):
352 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
353 self.window.set_border_width(0)
354 self.window.set_title("Git repository browser annotation window")
356 # Use two thirds of the screen by default
357 screen = self.window.get_screen()
358 monitor = screen.get_monitor_geometry(0)
359 width = int(monitor.width * 0.66)
360 height = int(monitor.height * 0.66)
361 self.window.set_default_size(width, height)
363 def add_file_data(self, filename, commit_sha1, line_num):
364 fp = os.popen("git cat-file blob " + commit_sha1 +":"+filename)
365 i = 1;
366 for line in fp.readlines():
367 line = string.rstrip(line)
368 self.model.append(None, ["HEAD", filename, line, i])
369 i = i+1
370 fp.close()
372 # now set the cursor position
373 self.treeview.set_cursor(line_num-1)
374 self.treeview.grab_focus()
376 def _treeview_cursor_cb(self, *args):
377 """Callback for when the treeview cursor changes."""
378 (path, col) = self.treeview.get_cursor()
379 commit_sha1 = self.model[path][0]
380 commit_msg = ""
381 fp = os.popen("git cat-file commit " + commit_sha1)
382 for line in fp.readlines():
383 commit_msg = commit_msg + line
384 fp.close()
386 self.commit_buffer.set_text(commit_msg)
388 def _treeview_row_activated(self, *args):
389 """Callback for when the treeview row gets selected."""
390 (path, col) = self.treeview.get_cursor()
391 commit_sha1 = self.model[path][0]
392 filename = self.model[path][1]
393 line_num = self.model[path][3]
395 window = AnnotateWindow();
396 fp = os.popen("git rev-parse "+ commit_sha1 + "~1")
397 commit_sha1 = string.strip(fp.readline())
398 fp.close()
399 window.annotate(filename, commit_sha1, line_num)
401 def data_ready(self, source, condition):
402 while (1):
403 try :
404 buffer = source.read(8192)
405 except:
406 # resource temporary not available
407 return True
409 if (len(buffer) == 0):
410 gobject.source_remove(self.io_watch_tag)
411 source.close()
412 return False
414 for buff in buffer.split("\n"):
415 annotate_line = re.compile('^([0-9a-f]{40}) (.+) (.+) (.+)$')
416 m = annotate_line.match(buff)
417 if not m:
418 annotate_line = re.compile('^(filename) (.+)$')
419 m = annotate_line.match(buff)
420 if not m:
421 continue
422 filename = m.group(2)
423 else:
424 self.commit_sha1 = m.group(1)
425 self.source_line = int(m.group(2))
426 self.result_line = int(m.group(3))
427 self.count = int(m.group(4))
428 #set the details only when we have the file name
429 continue
431 while (self.count > 0):
432 # set at result_line + count-1 the sha1 as commit_sha1
433 self.count = self.count - 1
434 iter = self.model.iter_nth_child(None, self.result_line + self.count-1)
435 self.model.set(iter, 0, self.commit_sha1, 1, filename, 3, self.source_line)
438 def annotate(self, filename, commit_sha1, line_num):
439 # verify the commit_sha1 specified has this filename
441 fp = os.popen("git ls-tree "+ commit_sha1 + " -- " + filename)
442 line = string.strip(fp.readline())
443 if line == '':
444 # pop up the message the file is not there as a part of the commit
445 fp.close()
446 dialog = gtk.MessageDialog(parent=None, flags=0,
447 type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
448 message_format=None)
449 dialog.set_markup("The file %s is not present in the parent commit %s" % (filename, commit_sha1))
450 dialog.run()
451 dialog.destroy()
452 return
454 fp.close()
456 vpan = gtk.VPaned();
457 self.window.add(vpan);
458 vpan.show()
460 scrollwin = gtk.ScrolledWindow()
461 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
462 scrollwin.set_shadow_type(gtk.SHADOW_IN)
463 vpan.pack1(scrollwin, True, True);
464 scrollwin.show()
466 self.model = gtk.TreeStore(str, str, str, int)
467 self.treeview = gtk.TreeView(self.model)
468 self.treeview.set_rules_hint(True)
469 self.treeview.set_search_column(0)
470 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
471 self.treeview.connect("row-activated", self._treeview_row_activated)
472 scrollwin.add(self.treeview)
473 self.treeview.show()
475 cell = gtk.CellRendererText()
476 cell.set_property("width-chars", 10)
477 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
478 column = gtk.TreeViewColumn("Commit")
479 column.set_resizable(True)
480 column.pack_start(cell, expand=True)
481 column.add_attribute(cell, "text", 0)
482 self.treeview.append_column(column)
484 cell = gtk.CellRendererText()
485 cell.set_property("width-chars", 20)
486 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
487 column = gtk.TreeViewColumn("File Name")
488 column.set_resizable(True)
489 column.pack_start(cell, expand=True)
490 column.add_attribute(cell, "text", 1)
491 self.treeview.append_column(column)
493 cell = gtk.CellRendererText()
494 cell.set_property("width-chars", 20)
495 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
496 column = gtk.TreeViewColumn("Data")
497 column.set_resizable(True)
498 column.pack_start(cell, expand=True)
499 column.add_attribute(cell, "text", 2)
500 self.treeview.append_column(column)
502 # The commit message window
503 scrollwin = gtk.ScrolledWindow()
504 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
505 scrollwin.set_shadow_type(gtk.SHADOW_IN)
506 vpan.pack2(scrollwin, True, True);
507 scrollwin.show()
509 commit_text = gtk.TextView()
510 self.commit_buffer = gtk.TextBuffer()
511 commit_text.set_buffer(self.commit_buffer)
512 scrollwin.add(commit_text)
513 commit_text.show()
515 self.window.show()
517 self.add_file_data(filename, commit_sha1, line_num)
519 fp = os.popen("git blame --incremental -- " + filename + " " + commit_sha1)
520 flags = fcntl.fcntl(fp.fileno(), fcntl.F_GETFL)
521 fcntl.fcntl(fp.fileno(), fcntl.F_SETFL, flags | os.O_NONBLOCK)
522 self.io_watch_tag = gobject.io_add_watch(fp, gobject.IO_IN, self.data_ready)
525 class DiffWindow(object):
526 """Diff window.
527 This object represents and manages a single window containing the
528 differences between two revisions on a branch.
531 def __init__(self):
532 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
533 self.window.set_border_width(0)
534 self.window.set_title("Git repository browser diff window")
536 # Use two thirds of the screen by default
537 screen = self.window.get_screen()
538 monitor = screen.get_monitor_geometry(0)
539 width = int(monitor.width * 0.66)
540 height = int(monitor.height * 0.66)
541 self.window.set_default_size(width, height)
544 self.construct()
546 def construct(self):
547 """Construct the window contents."""
548 vbox = gtk.VBox()
549 self.window.add(vbox)
550 vbox.show()
552 menu_bar = gtk.MenuBar()
553 save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
554 save_menu.connect("activate", self.save_menu_response, "save")
555 save_menu.show()
556 menu_bar.append(save_menu)
557 vbox.pack_start(menu_bar, expand=False, fill=True)
558 menu_bar.show()
560 hpan = gtk.HPaned()
562 scrollwin = gtk.ScrolledWindow()
563 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
564 scrollwin.set_shadow_type(gtk.SHADOW_IN)
565 hpan.pack1(scrollwin, True, True)
566 scrollwin.show()
568 if have_gtksourceview:
569 self.buffer = gtksourceview.SourceBuffer()
570 slm = gtksourceview.SourceLanguagesManager()
571 gsl = slm.get_language_from_mime_type("text/x-patch")
572 self.buffer.set_highlight(True)
573 self.buffer.set_language(gsl)
574 sourceview = gtksourceview.SourceView(self.buffer)
575 else:
576 self.buffer = gtk.TextBuffer()
577 sourceview = gtk.TextView(self.buffer)
580 sourceview.set_editable(False)
581 sourceview.modify_font(pango.FontDescription("Monospace"))
582 scrollwin.add(sourceview)
583 sourceview.show()
585 # The file hierarchy: a scrollable treeview
586 scrollwin = gtk.ScrolledWindow()
587 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
588 scrollwin.set_shadow_type(gtk.SHADOW_IN)
589 scrollwin.set_size_request(20, -1)
590 hpan.pack2(scrollwin, True, True)
591 scrollwin.show()
593 self.model = gtk.TreeStore(str, str, str)
594 self.treeview = gtk.TreeView(self.model)
595 self.treeview.set_search_column(1)
596 self.treeview.connect("cursor-changed", self._treeview_clicked)
597 scrollwin.add(self.treeview)
598 self.treeview.show()
600 cell = gtk.CellRendererText()
601 cell.set_property("width-chars", 20)
602 column = gtk.TreeViewColumn("Select to annotate")
603 column.pack_start(cell, expand=True)
604 column.add_attribute(cell, "text", 0)
605 self.treeview.append_column(column)
607 vbox.pack_start(hpan, expand=True, fill=True)
608 hpan.show()
610 def _treeview_clicked(self, *args):
611 """Callback for when the treeview cursor changes."""
612 (path, col) = self.treeview.get_cursor()
613 specific_file = self.model[path][1]
614 commit_sha1 = self.model[path][2]
615 if specific_file == None :
616 return
617 elif specific_file == "" :
618 specific_file = None
620 window = AnnotateWindow();
621 window.annotate(specific_file, commit_sha1, 1)
624 def commit_files(self, commit_sha1, parent_sha1):
625 self.model.clear()
626 add = self.model.append(None, [ "Added", None, None])
627 dele = self.model.append(None, [ "Deleted", None, None])
628 mod = self.model.append(None, [ "Modified", None, None])
629 diff_tree = re.compile('^(:.{6}) (.{6}) (.{40}) (.{40}) (A|D|M)\s(.+)$')
630 fp = os.popen("git diff-tree -r --no-commit-id " + parent_sha1 + " " + commit_sha1)
631 while 1:
632 line = string.strip(fp.readline())
633 if line == '':
634 break
635 m = diff_tree.match(line)
636 if not m:
637 continue
639 attr = m.group(5)
640 filename = m.group(6)
641 if attr == "A":
642 self.model.append(add, [filename, filename, commit_sha1])
643 elif attr == "D":
644 self.model.append(dele, [filename, filename, commit_sha1])
645 elif attr == "M":
646 self.model.append(mod, [filename, filename, commit_sha1])
647 fp.close()
649 self.treeview.expand_all()
651 def set_diff(self, commit_sha1, parent_sha1, encoding):
652 """Set the differences showed by this window.
653 Compares the two trees and populates the window with the
654 differences.
656 # Diff with the first commit or the last commit shows nothing
657 if (commit_sha1 == 0 or parent_sha1 == 0 ):
658 return
660 fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
661 self.buffer.set_text(unicode(fp.read(), encoding).encode('utf-8'))
662 fp.close()
663 self.commit_files(commit_sha1, parent_sha1)
664 self.window.show()
666 def save_menu_response(self, widget, string):
667 dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
668 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
669 gtk.STOCK_SAVE, gtk.RESPONSE_OK))
670 dialog.set_default_response(gtk.RESPONSE_OK)
671 response = dialog.run()
672 if response == gtk.RESPONSE_OK:
673 patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
674 self.buffer.get_end_iter())
675 fp = open(dialog.get_filename(), "w")
676 fp.write(patch_buffer)
677 fp.close()
678 dialog.destroy()
680 class GitView(object):
681 """ This is the main class
683 version = "0.9"
685 def __init__(self, with_diff=0):
686 self.with_diff = with_diff
687 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
688 self.window.set_border_width(0)
689 self.window.set_title("Git repository browser")
691 self.get_encoding()
692 self.get_bt_sha1()
694 # Use three-quarters of the screen by default
695 screen = self.window.get_screen()
696 monitor = screen.get_monitor_geometry(0)
697 width = int(monitor.width * 0.75)
698 height = int(monitor.height * 0.75)
699 self.window.set_default_size(width, height)
701 # FIXME AndyFitz!
702 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
703 self.window.set_icon(icon)
705 self.accel_group = gtk.AccelGroup()
706 self.window.add_accel_group(self.accel_group)
707 self.accel_group.connect_group(0xffc2, 0, gtk.ACCEL_LOCKED, self.refresh);
708 self.accel_group.connect_group(0xffc1, 0, gtk.ACCEL_LOCKED, self.maximize);
709 self.accel_group.connect_group(0xffc8, 0, gtk.ACCEL_LOCKED, self.fullscreen);
710 self.accel_group.connect_group(0xffc9, 0, gtk.ACCEL_LOCKED, self.unfullscreen);
712 self.window.add(self.construct())
714 def refresh(self, widget, event=None, *arguments, **keywords):
715 self.get_encoding()
716 self.get_bt_sha1()
717 Commit.children_sha1 = {}
718 self.set_branch(sys.argv[without_diff:])
719 self.window.show()
720 return True
722 def maximize(self, widget, event=None, *arguments, **keywords):
723 self.window.maximize()
724 return True
726 def fullscreen(self, widget, event=None, *arguments, **keywords):
727 self.window.fullscreen()
728 return True
730 def unfullscreen(self, widget, event=None, *arguments, **keywords):
731 self.window.unfullscreen()
732 return True
734 def get_bt_sha1(self):
735 """ Update the bt_sha1 dictionary with the
736 respective sha1 details """
738 self.bt_sha1 = { }
739 ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
740 fp = os.popen('git ls-remote "${GIT_DIR-.git}"')
741 while 1:
742 line = string.strip(fp.readline())
743 if line == '':
744 break
745 m = ls_remote.match(line)
746 if not m:
747 continue
748 (sha1, name) = (m.group(1), m.group(2))
749 if not self.bt_sha1.has_key(sha1):
750 self.bt_sha1[sha1] = []
751 self.bt_sha1[sha1].append(name)
752 fp.close()
754 def get_encoding(self):
755 fp = os.popen("git config --get i18n.commitencoding")
756 self.encoding=string.strip(fp.readline())
757 fp.close()
758 if (self.encoding == ""):
759 self.encoding = "utf-8"
762 def construct(self):
763 """Construct the window contents."""
764 vbox = gtk.VBox()
765 paned = gtk.VPaned()
766 paned.pack1(self.construct_top(), resize=False, shrink=True)
767 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
768 menu_bar = gtk.MenuBar()
769 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
770 help_menu = gtk.MenuItem("Help")
771 menu = gtk.Menu()
772 about_menu = gtk.MenuItem("About")
773 menu.append(about_menu)
774 about_menu.connect("activate", self.about_menu_response, "about")
775 about_menu.show()
776 help_menu.set_submenu(menu)
777 help_menu.show()
778 menu_bar.append(help_menu)
779 menu_bar.show()
780 vbox.pack_start(menu_bar, expand=False, fill=True)
781 vbox.pack_start(paned, expand=True, fill=True)
782 paned.show()
783 vbox.show()
784 return vbox
787 def construct_top(self):
788 """Construct the top-half of the window."""
789 vbox = gtk.VBox(spacing=6)
790 vbox.set_border_width(12)
791 vbox.show()
794 scrollwin = gtk.ScrolledWindow()
795 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
796 scrollwin.set_shadow_type(gtk.SHADOW_IN)
797 vbox.pack_start(scrollwin, expand=True, fill=True)
798 scrollwin.show()
800 self.treeview = gtk.TreeView()
801 self.treeview.set_rules_hint(True)
802 self.treeview.set_search_column(4)
803 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
804 scrollwin.add(self.treeview)
805 self.treeview.show()
807 cell = CellRendererGraph()
808 column = gtk.TreeViewColumn()
809 column.set_resizable(True)
810 column.pack_start(cell, expand=True)
811 column.add_attribute(cell, "node", 1)
812 column.add_attribute(cell, "in-lines", 2)
813 column.add_attribute(cell, "out-lines", 3)
814 self.treeview.append_column(column)
816 cell = gtk.CellRendererText()
817 cell.set_property("width-chars", 65)
818 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
819 column = gtk.TreeViewColumn("Message")
820 column.set_resizable(True)
821 column.pack_start(cell, expand=True)
822 column.add_attribute(cell, "text", 4)
823 self.treeview.append_column(column)
825 cell = gtk.CellRendererText()
826 cell.set_property("width-chars", 40)
827 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
828 column = gtk.TreeViewColumn("Author")
829 column.set_resizable(True)
830 column.pack_start(cell, expand=True)
831 column.add_attribute(cell, "text", 5)
832 self.treeview.append_column(column)
834 cell = gtk.CellRendererText()
835 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
836 column = gtk.TreeViewColumn("Date")
837 column.set_resizable(True)
838 column.pack_start(cell, expand=True)
839 column.add_attribute(cell, "text", 6)
840 self.treeview.append_column(column)
842 return vbox
844 def about_menu_response(self, widget, string):
845 dialog = gtk.AboutDialog()
846 dialog.set_name("Gitview")
847 dialog.set_version(GitView.version)
848 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@gmail.com>"])
849 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
850 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
851 dialog.set_wrap_license(True)
852 dialog.run()
853 dialog.destroy()
856 def construct_bottom(self):
857 """Construct the bottom half of the window."""
858 vbox = gtk.VBox(False, spacing=6)
859 vbox.set_border_width(12)
860 (width, height) = self.window.get_size()
861 vbox.set_size_request(width, int(height / 2.5))
862 vbox.show()
864 self.table = gtk.Table(rows=4, columns=4)
865 self.table.set_row_spacings(6)
866 self.table.set_col_spacings(6)
867 vbox.pack_start(self.table, expand=False, fill=True)
868 self.table.show()
870 align = gtk.Alignment(0.0, 0.5)
871 label = gtk.Label()
872 label.set_markup("<b>Revision:</b>")
873 align.add(label)
874 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
875 label.show()
876 align.show()
878 align = gtk.Alignment(0.0, 0.5)
879 self.revid_label = gtk.Label()
880 self.revid_label.set_selectable(True)
881 align.add(self.revid_label)
882 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
883 self.revid_label.show()
884 align.show()
886 align = gtk.Alignment(0.0, 0.5)
887 label = gtk.Label()
888 label.set_markup("<b>Committer:</b>")
889 align.add(label)
890 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
891 label.show()
892 align.show()
894 align = gtk.Alignment(0.0, 0.5)
895 self.committer_label = gtk.Label()
896 self.committer_label.set_selectable(True)
897 align.add(self.committer_label)
898 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
899 self.committer_label.show()
900 align.show()
902 align = gtk.Alignment(0.0, 0.5)
903 label = gtk.Label()
904 label.set_markup("<b>Timestamp:</b>")
905 align.add(label)
906 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
907 label.show()
908 align.show()
910 align = gtk.Alignment(0.0, 0.5)
911 self.timestamp_label = gtk.Label()
912 self.timestamp_label.set_selectable(True)
913 align.add(self.timestamp_label)
914 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
915 self.timestamp_label.show()
916 align.show()
918 align = gtk.Alignment(0.0, 0.5)
919 label = gtk.Label()
920 label.set_markup("<b>Parents:</b>")
921 align.add(label)
922 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
923 label.show()
924 align.show()
925 self.parents_widgets = []
927 align = gtk.Alignment(0.0, 0.5)
928 label = gtk.Label()
929 label.set_markup("<b>Children:</b>")
930 align.add(label)
931 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
932 label.show()
933 align.show()
934 self.children_widgets = []
936 scrollwin = gtk.ScrolledWindow()
937 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
938 scrollwin.set_shadow_type(gtk.SHADOW_IN)
939 vbox.pack_start(scrollwin, expand=True, fill=True)
940 scrollwin.show()
942 if have_gtksourceview:
943 self.message_buffer = gtksourceview.SourceBuffer()
944 slm = gtksourceview.SourceLanguagesManager()
945 gsl = slm.get_language_from_mime_type("text/x-patch")
946 self.message_buffer.set_highlight(True)
947 self.message_buffer.set_language(gsl)
948 sourceview = gtksourceview.SourceView(self.message_buffer)
949 else:
950 self.message_buffer = gtk.TextBuffer()
951 sourceview = gtk.TextView(self.message_buffer)
953 sourceview.set_editable(False)
954 sourceview.modify_font(pango.FontDescription("Monospace"))
955 scrollwin.add(sourceview)
956 sourceview.show()
958 return vbox
960 def _treeview_cursor_cb(self, *args):
961 """Callback for when the treeview cursor changes."""
962 (path, col) = self.treeview.get_cursor()
963 commit = self.model[path][0]
965 if commit.committer is not None:
966 committer = commit.committer
967 timestamp = commit.commit_date
968 message = commit.get_message(self.with_diff)
969 revid_label = commit.commit_sha1
970 else:
971 committer = ""
972 timestamp = ""
973 message = ""
974 revid_label = ""
976 self.revid_label.set_text(revid_label)
977 self.committer_label.set_text(committer)
978 self.timestamp_label.set_text(timestamp)
979 self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
981 for widget in self.parents_widgets:
982 self.table.remove(widget)
984 self.parents_widgets = []
985 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
986 for idx, parent_id in enumerate(commit.parent_sha1):
987 self.table.set_row_spacing(idx + 3, 0)
989 align = gtk.Alignment(0.0, 0.0)
990 self.parents_widgets.append(align)
991 self.table.attach(align, 1, 2, idx + 3, idx + 4,
992 gtk.EXPAND | gtk.FILL, gtk.FILL)
993 align.show()
995 hbox = gtk.HBox(False, 0)
996 align.add(hbox)
997 hbox.show()
999 label = gtk.Label(parent_id)
1000 label.set_selectable(True)
1001 hbox.pack_start(label, expand=False, fill=True)
1002 label.show()
1004 image = gtk.Image()
1005 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1006 image.show()
1008 button = gtk.Button()
1009 button.add(image)
1010 button.set_relief(gtk.RELIEF_NONE)
1011 button.connect("clicked", self._go_clicked_cb, parent_id)
1012 hbox.pack_start(button, expand=False, fill=True)
1013 button.show()
1015 image = gtk.Image()
1016 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1017 image.show()
1019 button = gtk.Button()
1020 button.add(image)
1021 button.set_relief(gtk.RELIEF_NONE)
1022 button.set_sensitive(True)
1023 button.connect("clicked", self._show_clicked_cb,
1024 commit.commit_sha1, parent_id, self.encoding)
1025 hbox.pack_start(button, expand=False, fill=True)
1026 button.show()
1028 # Populate with child details
1029 for widget in self.children_widgets:
1030 self.table.remove(widget)
1032 self.children_widgets = []
1033 try:
1034 child_sha1 = Commit.children_sha1[commit.commit_sha1]
1035 except KeyError:
1036 # We don't have child
1037 child_sha1 = [ 0 ]
1039 if ( len(child_sha1) > len(commit.parent_sha1)):
1040 self.table.resize(4 + len(child_sha1) - 1, 4)
1042 for idx, child_id in enumerate(child_sha1):
1043 self.table.set_row_spacing(idx + 3, 0)
1045 align = gtk.Alignment(0.0, 0.0)
1046 self.children_widgets.append(align)
1047 self.table.attach(align, 3, 4, idx + 3, idx + 4,
1048 gtk.EXPAND | gtk.FILL, gtk.FILL)
1049 align.show()
1051 hbox = gtk.HBox(False, 0)
1052 align.add(hbox)
1053 hbox.show()
1055 label = gtk.Label(child_id)
1056 label.set_selectable(True)
1057 hbox.pack_start(label, expand=False, fill=True)
1058 label.show()
1060 image = gtk.Image()
1061 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1062 image.show()
1064 button = gtk.Button()
1065 button.add(image)
1066 button.set_relief(gtk.RELIEF_NONE)
1067 button.connect("clicked", self._go_clicked_cb, child_id)
1068 hbox.pack_start(button, expand=False, fill=True)
1069 button.show()
1071 image = gtk.Image()
1072 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1073 image.show()
1075 button = gtk.Button()
1076 button.add(image)
1077 button.set_relief(gtk.RELIEF_NONE)
1078 button.set_sensitive(True)
1079 button.connect("clicked", self._show_clicked_cb,
1080 child_id, commit.commit_sha1, self.encoding)
1081 hbox.pack_start(button, expand=False, fill=True)
1082 button.show()
1084 def _destroy_cb(self, widget):
1085 """Callback for when a window we manage is destroyed."""
1086 self.quit()
1089 def quit(self):
1090 """Stop the GTK+ main loop."""
1091 gtk.main_quit()
1093 def run(self, args):
1094 self.set_branch(args)
1095 self.window.connect("destroy", self._destroy_cb)
1096 self.window.show()
1097 gtk.main()
1099 def set_branch(self, args):
1100 """Fill in different windows with info from the reposiroty"""
1101 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
1102 git_rev_list_cmd = fp.read()
1103 fp.close()
1104 fp = os.popen("git rev-list --header --topo-order --parents " + git_rev_list_cmd)
1105 self.update_window(fp)
1107 def update_window(self, fp):
1108 commit_lines = []
1110 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
1111 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
1113 # used for cursor positioning
1114 self.index = {}
1116 self.colours = {}
1117 self.nodepos = {}
1118 self.incomplete_line = {}
1119 self.commits = []
1121 index = 0
1122 last_colour = 0
1123 last_nodepos = -1
1124 out_line = []
1125 input_line = fp.readline()
1126 while (input_line != ""):
1127 # The commit header ends with '\0'
1128 # This NULL is immediately followed by the sha1 of the
1129 # next commit
1130 if (input_line[0] != '\0'):
1131 commit_lines.append(input_line)
1132 input_line = fp.readline()
1133 continue;
1135 commit = Commit(commit_lines)
1136 if (commit != None ):
1137 self.commits.append(commit)
1139 # Skip the '\0
1140 commit_lines = []
1141 commit_lines.append(input_line[1:])
1142 input_line = fp.readline()
1144 fp.close()
1146 for commit in self.commits:
1147 (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
1148 index, out_line,
1149 last_colour,
1150 last_nodepos)
1151 self.index[commit.commit_sha1] = index
1152 index += 1
1154 self.treeview.set_model(self.model)
1155 self.treeview.show()
1157 def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
1158 in_line=[]
1160 # | -> outline
1162 # |\ <- inline
1164 # Reset nodepostion
1165 if (last_nodepos > 5):
1166 last_nodepos = -1
1168 # Add the incomplete lines of the last cell in this
1169 try:
1170 colour = self.colours[commit.commit_sha1]
1171 except KeyError:
1172 self.colours[commit.commit_sha1] = last_colour+1
1173 last_colour = self.colours[commit.commit_sha1]
1174 colour = self.colours[commit.commit_sha1]
1176 try:
1177 node_pos = self.nodepos[commit.commit_sha1]
1178 except KeyError:
1179 self.nodepos[commit.commit_sha1] = last_nodepos+1
1180 last_nodepos = self.nodepos[commit.commit_sha1]
1181 node_pos = self.nodepos[commit.commit_sha1]
1183 #The first parent always continue on the same line
1184 try:
1185 # check we alreay have the value
1186 tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
1187 except KeyError:
1188 self.colours[commit.parent_sha1[0]] = colour
1189 self.nodepos[commit.parent_sha1[0]] = node_pos
1191 for sha1 in self.incomplete_line.keys():
1192 if (sha1 != commit.commit_sha1):
1193 self.draw_incomplete_line(sha1, node_pos,
1194 out_line, in_line, index)
1195 else:
1196 del self.incomplete_line[sha1]
1199 for parent_id in commit.parent_sha1:
1200 try:
1201 tmp_node_pos = self.nodepos[parent_id]
1202 except KeyError:
1203 self.colours[parent_id] = last_colour+1
1204 last_colour = self.colours[parent_id]
1205 self.nodepos[parent_id] = last_nodepos+1
1206 last_nodepos = self.nodepos[parent_id]
1208 in_line.append((node_pos, self.nodepos[parent_id],
1209 self.colours[parent_id]))
1210 self.add_incomplete_line(parent_id)
1212 try:
1213 branch_tag = self.bt_sha1[commit.commit_sha1]
1214 except KeyError:
1215 branch_tag = [ ]
1218 node = (node_pos, colour, branch_tag)
1220 self.model.append([commit, node, out_line, in_line,
1221 commit.message, commit.author, commit.date])
1223 return (in_line, last_colour, last_nodepos)
1225 def add_incomplete_line(self, sha1):
1226 try:
1227 self.incomplete_line[sha1].append(self.nodepos[sha1])
1228 except KeyError:
1229 self.incomplete_line[sha1] = [self.nodepos[sha1]]
1231 def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
1232 for idx, pos in enumerate(self.incomplete_line[sha1]):
1233 if(pos == node_pos):
1234 #remove the straight line and add a slash
1235 if ((pos, pos, self.colours[sha1]) in out_line):
1236 out_line.remove((pos, pos, self.colours[sha1]))
1237 out_line.append((pos, pos+0.5, self.colours[sha1]))
1238 self.incomplete_line[sha1][idx] = pos = pos+0.5
1239 try:
1240 next_commit = self.commits[index+1]
1241 if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
1242 # join the line back to the node point
1243 # This need to be done only if we modified it
1244 in_line.append((pos, pos-0.5, self.colours[sha1]))
1245 continue;
1246 except IndexError:
1247 pass
1248 in_line.append((pos, pos, self.colours[sha1]))
1251 def _go_clicked_cb(self, widget, revid):
1252 """Callback for when the go button for a parent is clicked."""
1253 try:
1254 self.treeview.set_cursor(self.index[revid])
1255 except KeyError:
1256 dialog = gtk.MessageDialog(parent=None, flags=0,
1257 type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
1258 message_format=None)
1259 dialog.set_markup("Revision <b>%s</b> not present in the list" % revid)
1260 # revid == 0 is the parent of the first commit
1261 if (revid != 0 ):
1262 dialog.format_secondary_text("Try running gitview without any options")
1263 dialog.run()
1264 dialog.destroy()
1266 self.treeview.grab_focus()
1268 def _show_clicked_cb(self, widget, commit_sha1, parent_sha1, encoding):
1269 """Callback for when the show button for a parent is clicked."""
1270 window = DiffWindow()
1271 window.set_diff(commit_sha1, parent_sha1, encoding)
1272 self.treeview.grab_focus()
1274 without_diff = 0
1275 if __name__ == "__main__":
1277 if (len(sys.argv) > 1 ):
1278 if (sys.argv[1] == "--without-diff"):
1279 without_diff = 1
1281 view = GitView( without_diff != 1)
1282 view.run(sys.argv[without_diff:])