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