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