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