1#! /usr/bin/env python
2
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.
7
8""" gitview
9GUI browser for git repository
10This 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>"
15
16
17import sys
18import os
19import gtk
20import pygtk
21import pango
22import re
23import time
24import gobject
25import cairo
26import math
27import string
28import fcntl
29
30try:
31 import gtksourceview
32 have_gtksourceview = True
33except ImportError:
34 have_gtksourceview = False
35 print "Running without gtksourceview module"
36
37re_ident = re.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
38
39def 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
47
48 return str_arg
49
50def 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
58
59 return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs))
60
61
62class CellRendererGraph(gtk.GenericCellRenderer):
63 """Cell renderer for directed graph.
64
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.
68
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.
72
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 """
78
79 __gproperties__ = {
80 "node": ( gobject.TYPE_PYOBJECT, "node",
81 "revision node instruction",
82 gobject.PARAM_WRITABLE
83 ),
84 "in-lines": ( gobject.TYPE_PYOBJECT, "in-lines",
85 "instructions to draw lines into the cell",
86 gobject.PARAM_WRITABLE
87 ),
88 "out-lines": ( gobject.TYPE_PYOBJECT, "out-lines",
89 "instructions to draw lines out of the cell",
90 gobject.PARAM_WRITABLE
91 ),
92 }
93
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
104
105 def box_size(self, widget):
106 """Calculate box size based on widget's font.
107
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.
110 """
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)
117
118 ascent = pango.PIXELS(metrics.get_ascent())
119 descent = pango.PIXELS(metrics.get_descent())
120
121 self._box_size = ascent + descent + 6
122 return self._box_size
123
124 def set_colour(self, ctx, colour, bg, fg):
125 """Set the context source colour.
126
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.
131 """
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 ),
139 ]
140
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
145
146 ctx.set_source_rgb(red, green, blue)
147
148 def on_get_size(self, widget, cell_area):
149 """Return the size we need for this cell.
150
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.
154 """
155 box_size = self.box_size(widget)
156
157 cols = self.node[0]
158 for start, end, colour in self.in_lines + self.out_lines:
159 cols = int(max(cols, start, end))
160
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)
166
167 width = box_size * (cols + 1 ) + names_len
168 height = box_size
169
170 # FIXME I have no idea how to use cell_area properly
171 return (0, 0, width, height)
172
173 def on_render(self, window, widget, bg_area, cell_area, exp_area, flags):
174 """Render an individual cell.
175
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.
181
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.
186 """
187 ctx = window.cairo_create()
188 ctx.rectangle(bg_area.x, bg_area.y, bg_area.width, bg_area.height)
189 ctx.clip()
190
191 box_size = self.box_size(widget)
192
193 ctx.set_line_width(box_size / 8)
194 ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
195
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)
200
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)
208
209 ctx.line_to(cell_area.x + box_size * end + box_size / 2,
210 bg_area.y + bg_area.height / 2)
211
212 self.set_colour(ctx, colour, 0.0, 0.65)
213 ctx.stroke()
214
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)
219
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)
230
231 ctx.line_to(cell_area.x + box_size * end + box_size / 2,
232 bg_area.y + bg_area.height / 2 + bg_area.height)
233
234 self.set_colour(ctx, colour, 0.0, 0.65)
235 ctx.stroke()
236
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)
242
243
244 self.set_colour(ctx, colour, 0.0, 0.5)
245 ctx.stroke_preserve()
246
247 self.set_colour(ctx, colour, 0.5, 1.0)
248 ctx.fill_preserve()
249
250 if (len(names) != 0):
251 name = " "
252 for item in names:
253 name = name + item + " "
254
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)
261
262class Commit(object):
263 """ This represent a commit object obtained after parsing the git-rev-list
264 output """
265
266 __slots__ = ['children_sha1', 'message', 'author', 'date', 'committer',
267 'commit_date', 'commit_sha1', 'parent_sha1']
268
269 children_sha1 = {}
270
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)
280
281
282 def parse_commit(self, commit_lines):
283
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:]
289
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]
296
297 # IF we don't have parent
298 if (len(self.parent_sha1) == 0):
299 self.parent_sha1 = [0]
300
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
308
309 m = re.match("tree", line)
310 if (m != None):
311 continue
312
313 m = re.match("parent", line)
314 if (m != None):
315 continue
316
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
326
327 continue
328
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()
336
337 return message
338
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
344
345class AnnotateWindow(object):
346 """Annotate window.
347 This object represents and manages a single window containing the
348 annotate information of the file
349 """
350
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
356 # Use two thirds of the screen by default
357 screen = self.window.get_screen()
358 monitor = screen.get_monitor_geometry(0)
359 width = int(monitor.width * 0.66)
360 height = int(monitor.height * 0.66)
361 self.window.set_default_size(width, height)
362
363 def add_file_data(self, filename, commit_sha1, line_num):
364 fp = os.popen("git cat-file blob " + commit_sha1 +":"+filename)
365 i = 1;
366 for line in fp.readlines():
367 line = string.rstrip(line)
368 self.model.append(None, ["HEAD", filename, line, i])
369 i = i+1
370 fp.close()
371
372 # now set the cursor position
373 self.treeview.set_cursor(line_num-1)
374 self.treeview.grab_focus()
375
376 def _treeview_cursor_cb(self, *args):
377 """Callback for when the treeview cursor changes."""
378 (path, col) = self.treeview.get_cursor()
379 commit_sha1 = self.model[path][0]
380 commit_msg = ""
381 fp = os.popen("git cat-file commit " + commit_sha1)
382 for line in fp.readlines():
383 commit_msg = commit_msg + line
384 fp.close()
385
386 self.commit_buffer.set_text(commit_msg)
387
388 def _treeview_row_activated(self, *args):
389 """Callback for when the treeview row gets selected."""
390 (path, col) = self.treeview.get_cursor()
391 commit_sha1 = self.model[path][0]
392 filename = self.model[path][1]
393 line_num = self.model[path][3]
394
395 window = AnnotateWindow();
396 fp = os.popen("git rev-parse "+ commit_sha1 + "~1")
397 commit_sha1 = string.strip(fp.readline())
398 fp.close()
399 window.annotate(filename, commit_sha1, line_num)
400
401 def data_ready(self, source, condition):
402 while (1):
403 try :
404 buffer = source.read(8192)
405 except:
406 # resource temporary not available
407 return True
408
409 if (len(buffer) == 0):
410 gobject.source_remove(self.io_watch_tag)
411 source.close()
412 return False
413
414 for buff in buffer.split("\n"):
415 annotate_line = re.compile('^([0-9a-f]{40}) (.+) (.+) (.+)$')
416 m = annotate_line.match(buff)
417 if not m:
418 annotate_line = re.compile('^(filename) (.+)$')
419 m = annotate_line.match(buff)
420 if not m:
421 continue
422 filename = m.group(2)
423 else:
424 self.commit_sha1 = m.group(1)
425 self.source_line = int(m.group(2))
426 self.result_line = int(m.group(3))
427 self.count = int(m.group(4))
428 #set the details only when we have the file name
429 continue
430
431 while (self.count > 0):
432 # set at result_line + count-1 the sha1 as commit_sha1
433 self.count = self.count - 1
434 iter = self.model.iter_nth_child(None, self.result_line + self.count-1)
435 self.model.set(iter, 0, self.commit_sha1, 1, filename, 3, self.source_line)
436
437
438 def annotate(self, filename, commit_sha1, line_num):
439 # verify the commit_sha1 specified has this filename
440
441 fp = os.popen("git ls-tree "+ commit_sha1 + " -- " + filename)
442 line = string.strip(fp.readline())
443 if line == '':
444 # pop up the message the file is not there as a part of the commit
445 fp.close()
446 dialog = gtk.MessageDialog(parent=None, flags=0,
447 type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
448 message_format=None)
449 dialog.set_markup("The file %s is not present in the parent commit %s" % (filename, commit_sha1))
450 dialog.run()
451 dialog.destroy()
452 return
453
454 fp.close()
455
456 vpan = gtk.VPaned();
457 self.window.add(vpan);
458 vpan.show()
459
460 scrollwin = gtk.ScrolledWindow()
461 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
462 scrollwin.set_shadow_type(gtk.SHADOW_IN)
463 vpan.pack1(scrollwin, True, True);
464 scrollwin.show()
465
466 self.model = gtk.TreeStore(str, str, str, int)
467 self.treeview = gtk.TreeView(self.model)
468 self.treeview.set_rules_hint(True)
469 self.treeview.set_search_column(0)
470 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
471 self.treeview.connect("row-activated", self._treeview_row_activated)
472 scrollwin.add(self.treeview)
473 self.treeview.show()
474
475 cell = gtk.CellRendererText()
476 cell.set_property("width-chars", 10)
477 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
478 column = gtk.TreeViewColumn("Commit")
479 column.set_resizable(True)
480 column.pack_start(cell, expand=True)
481 column.add_attribute(cell, "text", 0)
482 self.treeview.append_column(column)
483
484 cell = gtk.CellRendererText()
485 cell.set_property("width-chars", 20)
486 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
487 column = gtk.TreeViewColumn("File Name")
488 column.set_resizable(True)
489 column.pack_start(cell, expand=True)
490 column.add_attribute(cell, "text", 1)
491 self.treeview.append_column(column)
492
493 cell = gtk.CellRendererText()
494 cell.set_property("width-chars", 20)
495 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
496 column = gtk.TreeViewColumn("Data")
497 column.set_resizable(True)
498 column.pack_start(cell, expand=True)
499 column.add_attribute(cell, "text", 2)
500 self.treeview.append_column(column)
501
502 # The commit message window
503 scrollwin = gtk.ScrolledWindow()
504 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
505 scrollwin.set_shadow_type(gtk.SHADOW_IN)
506 vpan.pack2(scrollwin, True, True);
507 scrollwin.show()
508
509 commit_text = gtk.TextView()
510 self.commit_buffer = gtk.TextBuffer()
511 commit_text.set_buffer(self.commit_buffer)
512 scrollwin.add(commit_text)
513 commit_text.show()
514
515 self.window.show()
516
517 self.add_file_data(filename, commit_sha1, line_num)
518
519 fp = os.popen("git blame --incremental -- " + filename + " " + commit_sha1)
520 flags = fcntl.fcntl(fp.fileno(), fcntl.F_GETFL)
521 fcntl.fcntl(fp.fileno(), fcntl.F_SETFL, flags | os.O_NONBLOCK)
522 self.io_watch_tag = gobject.io_add_watch(fp, gobject.IO_IN, self.data_ready)
523
524
525class DiffWindow(object):
526 """Diff window.
527 This object represents and manages a single window containing the
528 differences between two revisions on a branch.
529 """
530
531 def __init__(self):
532 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
533 self.window.set_border_width(0)
534 self.window.set_title("Git repository browser diff window")
535
536 # Use two thirds of the screen by default
537 screen = self.window.get_screen()
538 monitor = screen.get_monitor_geometry(0)
539 width = int(monitor.width * 0.66)
540 height = int(monitor.height * 0.66)
541 self.window.set_default_size(width, height)
542
543
544 self.construct()
545
546 def construct(self):
547 """Construct the window contents."""
548 vbox = gtk.VBox()
549 self.window.add(vbox)
550 vbox.show()
551
552 menu_bar = gtk.MenuBar()
553 save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
554 save_menu.connect("activate", self.save_menu_response, "save")
555 save_menu.show()
556 menu_bar.append(save_menu)
557 vbox.pack_start(menu_bar, expand=False, fill=True)
558 menu_bar.show()
559
560 hpan = gtk.HPaned()
561
562 scrollwin = gtk.ScrolledWindow()
563 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
564 scrollwin.set_shadow_type(gtk.SHADOW_IN)
565 hpan.pack1(scrollwin, True, True)
566 scrollwin.show()
567
568 if have_gtksourceview:
569 self.buffer = gtksourceview.SourceBuffer()
570 slm = gtksourceview.SourceLanguagesManager()
571 gsl = slm.get_language_from_mime_type("text/x-patch")
572 self.buffer.set_highlight(True)
573 self.buffer.set_language(gsl)
574 sourceview = gtksourceview.SourceView(self.buffer)
575 else:
576 self.buffer = gtk.TextBuffer()
577 sourceview = gtk.TextView(self.buffer)
578
579
580 sourceview.set_editable(False)
581 sourceview.modify_font(pango.FontDescription("Monospace"))
582 scrollwin.add(sourceview)
583 sourceview.show()
584
585 # The file hierarchy: a scrollable treeview
586 scrollwin = gtk.ScrolledWindow()
587 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
588 scrollwin.set_shadow_type(gtk.SHADOW_IN)
589 scrollwin.set_size_request(20, -1)
590 hpan.pack2(scrollwin, True, True)
591 scrollwin.show()
592
593 self.model = gtk.TreeStore(str, str, str)
594 self.treeview = gtk.TreeView(self.model)
595 self.treeview.set_search_column(1)
596 self.treeview.connect("cursor-changed", self._treeview_clicked)
597 scrollwin.add(self.treeview)
598 self.treeview.show()
599
600 cell = gtk.CellRendererText()
601 cell.set_property("width-chars", 20)
602 column = gtk.TreeViewColumn("Select to annotate")
603 column.pack_start(cell, expand=True)
604 column.add_attribute(cell, "text", 0)
605 self.treeview.append_column(column)
606
607 vbox.pack_start(hpan, expand=True, fill=True)
608 hpan.show()
609
610 def _treeview_clicked(self, *args):
611 """Callback for when the treeview cursor changes."""
612 (path, col) = self.treeview.get_cursor()
613 specific_file = self.model[path][1]
614 commit_sha1 = self.model[path][2]
615 if specific_file == None :
616 return
617 elif specific_file == "" :
618 specific_file = None
619
620 window = AnnotateWindow();
621 window.annotate(specific_file, commit_sha1, 1)
622
623
624 def commit_files(self, commit_sha1, parent_sha1):
625 self.model.clear()
626 add = self.model.append(None, [ "Added", None, None])
627 dele = self.model.append(None, [ "Deleted", None, None])
628 mod = self.model.append(None, [ "Modified", None, None])
629 diff_tree = re.compile('^(:.{6}) (.{6}) (.{40}) (.{40}) (A|D|M)\s(.+)$')
630 fp = os.popen("git diff-tree -r --no-commit-id " + parent_sha1 + " " + commit_sha1)
631 while 1:
632 line = string.strip(fp.readline())
633 if line == '':
634 break
635 m = diff_tree.match(line)
636 if not m:
637 continue
638
639 attr = m.group(5)
640 filename = m.group(6)
641 if attr == "A":
642 self.model.append(add, [filename, filename, commit_sha1])
643 elif attr == "D":
644 self.model.append(dele, [filename, filename, commit_sha1])
645 elif attr == "M":
646 self.model.append(mod, [filename, filename, commit_sha1])
647 fp.close()
648
649 self.treeview.expand_all()
650
651 def set_diff(self, commit_sha1, parent_sha1, encoding):
652 """Set the differences showed by this window.
653 Compares the two trees and populates the window with the
654 differences.
655 """
656 # Diff with the first commit or the last commit shows nothing
657 if (commit_sha1 == 0 or parent_sha1 == 0 ):
658 return
659
660 fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
661 self.buffer.set_text(unicode(fp.read(), encoding).encode('utf-8'))
662 fp.close()
663 self.commit_files(commit_sha1, parent_sha1)
664 self.window.show()
665
666 def save_menu_response(self, widget, string):
667 dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
668 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
669 gtk.STOCK_SAVE, gtk.RESPONSE_OK))
670 dialog.set_default_response(gtk.RESPONSE_OK)
671 response = dialog.run()
672 if response == gtk.RESPONSE_OK:
673 patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
674 self.buffer.get_end_iter())
675 fp = open(dialog.get_filename(), "w")
676 fp.write(patch_buffer)
677 fp.close()
678 dialog.destroy()
679
680class GitView(object):
681 """ This is the main class
682 """
683 version = "0.9"
684
685 def __init__(self, with_diff=0):
686 self.with_diff = with_diff
687 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
688 self.window.set_border_width(0)
689 self.window.set_title("Git repository browser")
690
691 self.get_encoding()
692 self.get_bt_sha1()
693
694 # Use three-quarters of the screen by default
695 screen = self.window.get_screen()
696 monitor = screen.get_monitor_geometry(0)
697 width = int(monitor.width * 0.75)
698 height = int(monitor.height * 0.75)
699 self.window.set_default_size(width, height)
700
701 # FIXME AndyFitz!
702 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
703 self.window.set_icon(icon)
704
705 self.accel_group = gtk.AccelGroup()
706 self.window.add_accel_group(self.accel_group)
707 self.accel_group.connect_group(0xffc2, 0, gtk.ACCEL_LOCKED, self.refresh);
708 self.accel_group.connect_group(0xffc1, 0, gtk.ACCEL_LOCKED, self.maximize);
709 self.accel_group.connect_group(0xffc8, 0, gtk.ACCEL_LOCKED, self.fullscreen);
710 self.accel_group.connect_group(0xffc9, 0, gtk.ACCEL_LOCKED, self.unfullscreen);
711
712 self.window.add(self.construct())
713
714 def refresh(self, widget, event=None, *arguments, **keywords):
715 self.get_encoding()
716 self.get_bt_sha1()
717 Commit.children_sha1 = {}
718 self.set_branch(sys.argv[without_diff:])
719 self.window.show()
720 return True
721
722 def maximize(self, widget, event=None, *arguments, **keywords):
723 self.window.maximize()
724 return True
725
726 def fullscreen(self, widget, event=None, *arguments, **keywords):
727 self.window.fullscreen()
728 return True
729
730 def unfullscreen(self, widget, event=None, *arguments, **keywords):
731 self.window.unfullscreen()
732 return True
733
734 def get_bt_sha1(self):
735 """ Update the bt_sha1 dictionary with the
736 respective sha1 details """
737
738 self.bt_sha1 = { }
739 ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
740 fp = os.popen('git ls-remote "${GIT_DIR-.git}"')
741 while 1:
742 line = string.strip(fp.readline())
743 if line == '':
744 break
745 m = ls_remote.match(line)
746 if not m:
747 continue
748 (sha1, name) = (m.group(1), m.group(2))
749 if not self.bt_sha1.has_key(sha1):
750 self.bt_sha1[sha1] = []
751 self.bt_sha1[sha1].append(name)
752 fp.close()
753
754 def get_encoding(self):
755 fp = os.popen("git config --get i18n.commitencoding")
756 self.encoding=string.strip(fp.readline())
757 fp.close()
758 if (self.encoding == ""):
759 self.encoding = "utf-8"
760
761
762 def construct(self):
763 """Construct the window contents."""
764 vbox = gtk.VBox()
765 paned = gtk.VPaned()
766 paned.pack1(self.construct_top(), resize=False, shrink=True)
767 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
768 menu_bar = gtk.MenuBar()
769 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
770 help_menu = gtk.MenuItem("Help")
771 menu = gtk.Menu()
772 about_menu = gtk.MenuItem("About")
773 menu.append(about_menu)
774 about_menu.connect("activate", self.about_menu_response, "about")
775 about_menu.show()
776 help_menu.set_submenu(menu)
777 help_menu.show()
778 menu_bar.append(help_menu)
779 menu_bar.show()
780 vbox.pack_start(menu_bar, expand=False, fill=True)
781 vbox.pack_start(paned, expand=True, fill=True)
782 paned.show()
783 vbox.show()
784 return vbox
785
786
787 def construct_top(self):
788 """Construct the top-half of the window."""
789 vbox = gtk.VBox(spacing=6)
790 vbox.set_border_width(12)
791 vbox.show()
792
793
794 scrollwin = gtk.ScrolledWindow()
795 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
796 scrollwin.set_shadow_type(gtk.SHADOW_IN)
797 vbox.pack_start(scrollwin, expand=True, fill=True)
798 scrollwin.show()
799
800 self.treeview = gtk.TreeView()
801 self.treeview.set_rules_hint(True)
802 self.treeview.set_search_column(4)
803 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
804 scrollwin.add(self.treeview)
805 self.treeview.show()
806
807 cell = CellRendererGraph()
808 column = gtk.TreeViewColumn()
809 column.set_resizable(True)
810 column.pack_start(cell, expand=True)
811 column.add_attribute(cell, "node", 1)
812 column.add_attribute(cell, "in-lines", 2)
813 column.add_attribute(cell, "out-lines", 3)
814 self.treeview.append_column(column)
815
816 cell = gtk.CellRendererText()
817 cell.set_property("width-chars", 65)
818 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
819 column = gtk.TreeViewColumn("Message")
820 column.set_resizable(True)
821 column.pack_start(cell, expand=True)
822 column.add_attribute(cell, "text", 4)
823 self.treeview.append_column(column)
824
825 cell = gtk.CellRendererText()
826 cell.set_property("width-chars", 40)
827 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
828 column = gtk.TreeViewColumn("Author")
829 column.set_resizable(True)
830 column.pack_start(cell, expand=True)
831 column.add_attribute(cell, "text", 5)
832 self.treeview.append_column(column)
833
834 cell = gtk.CellRendererText()
835 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
836 column = gtk.TreeViewColumn("Date")
837 column.set_resizable(True)
838 column.pack_start(cell, expand=True)
839 column.add_attribute(cell, "text", 6)
840 self.treeview.append_column(column)
841
842 return vbox
843
844 def about_menu_response(self, widget, string):
845 dialog = gtk.AboutDialog()
846 dialog.set_name("Gitview")
847 dialog.set_version(GitView.version)
848 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@gmail.com>"])
849 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
850 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
851 dialog.set_wrap_license(True)
852 dialog.run()
853 dialog.destroy()
854
855
856 def construct_bottom(self):
857 """Construct the bottom half of the window."""
858 vbox = gtk.VBox(False, spacing=6)
859 vbox.set_border_width(12)
860 (width, height) = self.window.get_size()
861 vbox.set_size_request(width, int(height / 2.5))
862 vbox.show()
863
864 self.table = gtk.Table(rows=4, columns=4)
865 self.table.set_row_spacings(6)
866 self.table.set_col_spacings(6)
867 vbox.pack_start(self.table, expand=False, fill=True)
868 self.table.show()
869
870 align = gtk.Alignment(0.0, 0.5)
871 label = gtk.Label()
872 label.set_markup("<b>Revision:</b>")
873 align.add(label)
874 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
875 label.show()
876 align.show()
877
878 align = gtk.Alignment(0.0, 0.5)
879 self.revid_label = gtk.Label()
880 self.revid_label.set_selectable(True)
881 align.add(self.revid_label)
882 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
883 self.revid_label.show()
884 align.show()
885
886 align = gtk.Alignment(0.0, 0.5)
887 label = gtk.Label()
888 label.set_markup("<b>Committer:</b>")
889 align.add(label)
890 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
891 label.show()
892 align.show()
893
894 align = gtk.Alignment(0.0, 0.5)
895 self.committer_label = gtk.Label()
896 self.committer_label.set_selectable(True)
897 align.add(self.committer_label)
898 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
899 self.committer_label.show()
900 align.show()
901
902 align = gtk.Alignment(0.0, 0.5)
903 label = gtk.Label()
904 label.set_markup("<b>Timestamp:</b>")
905 align.add(label)
906 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
907 label.show()
908 align.show()
909
910 align = gtk.Alignment(0.0, 0.5)
911 self.timestamp_label = gtk.Label()
912 self.timestamp_label.set_selectable(True)
913 align.add(self.timestamp_label)
914 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
915 self.timestamp_label.show()
916 align.show()
917
918 align = gtk.Alignment(0.0, 0.5)
919 label = gtk.Label()
920 label.set_markup("<b>Parents:</b>")
921 align.add(label)
922 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
923 label.show()
924 align.show()
925 self.parents_widgets = []
926
927 align = gtk.Alignment(0.0, 0.5)
928 label = gtk.Label()
929 label.set_markup("<b>Children:</b>")
930 align.add(label)
931 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
932 label.show()
933 align.show()
934 self.children_widgets = []
935
936 scrollwin = gtk.ScrolledWindow()
937 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
938 scrollwin.set_shadow_type(gtk.SHADOW_IN)
939 vbox.pack_start(scrollwin, expand=True, fill=True)
940 scrollwin.show()
941
942 if have_gtksourceview:
943 self.message_buffer = gtksourceview.SourceBuffer()
944 slm = gtksourceview.SourceLanguagesManager()
945 gsl = slm.get_language_from_mime_type("text/x-patch")
946 self.message_buffer.set_highlight(True)
947 self.message_buffer.set_language(gsl)
948 sourceview = gtksourceview.SourceView(self.message_buffer)
949 else:
950 self.message_buffer = gtk.TextBuffer()
951 sourceview = gtk.TextView(self.message_buffer)
952
953 sourceview.set_editable(False)
954 sourceview.modify_font(pango.FontDescription("Monospace"))
955 scrollwin.add(sourceview)
956 sourceview.show()
957
958 return vbox
959
960 def _treeview_cursor_cb(self, *args):
961 """Callback for when the treeview cursor changes."""
962 (path, col) = self.treeview.get_cursor()
963 commit = self.model[path][0]
964
965 if commit.committer is not None:
966 committer = commit.committer
967 timestamp = commit.commit_date
968 message = commit.get_message(self.with_diff)
969 revid_label = commit.commit_sha1
970 else:
971 committer = ""
972 timestamp = ""
973 message = ""
974 revid_label = ""
975
976 self.revid_label.set_text(revid_label)
977 self.committer_label.set_text(committer)
978 self.timestamp_label.set_text(timestamp)
979 self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
980
981 for widget in self.parents_widgets:
982 self.table.remove(widget)
983
984 self.parents_widgets = []
985 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
986 for idx, parent_id in enumerate(commit.parent_sha1):
987 self.table.set_row_spacing(idx + 3, 0)
988
989 align = gtk.Alignment(0.0, 0.0)
990 self.parents_widgets.append(align)
991 self.table.attach(align, 1, 2, idx + 3, idx + 4,
992 gtk.EXPAND | gtk.FILL, gtk.FILL)
993 align.show()
994
995 hbox = gtk.HBox(False, 0)
996 align.add(hbox)
997 hbox.show()
998
999 label = gtk.Label(parent_id)
1000 label.set_selectable(True)
1001 hbox.pack_start(label, expand=False, fill=True)
1002 label.show()
1003
1004 image = gtk.Image()
1005 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1006 image.show()
1007
1008 button = gtk.Button()
1009 button.add(image)
1010 button.set_relief(gtk.RELIEF_NONE)
1011 button.connect("clicked", self._go_clicked_cb, parent_id)
1012 hbox.pack_start(button, expand=False, fill=True)
1013 button.show()
1014
1015 image = gtk.Image()
1016 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1017 image.show()
1018
1019 button = gtk.Button()
1020 button.add(image)
1021 button.set_relief(gtk.RELIEF_NONE)
1022 button.set_sensitive(True)
1023 button.connect("clicked", self._show_clicked_cb,
1024 commit.commit_sha1, parent_id, self.encoding)
1025 hbox.pack_start(button, expand=False, fill=True)
1026 button.show()
1027
1028 # Populate with child details
1029 for widget in self.children_widgets:
1030 self.table.remove(widget)
1031
1032 self.children_widgets = []
1033 try:
1034 child_sha1 = Commit.children_sha1[commit.commit_sha1]
1035 except KeyError:
1036 # We don't have child
1037 child_sha1 = [ 0 ]
1038
1039 if ( len(child_sha1) > len(commit.parent_sha1)):
1040 self.table.resize(4 + len(child_sha1) - 1, 4)
1041
1042 for idx, child_id in enumerate(child_sha1):
1043 self.table.set_row_spacing(idx + 3, 0)
1044
1045 align = gtk.Alignment(0.0, 0.0)
1046 self.children_widgets.append(align)
1047 self.table.attach(align, 3, 4, idx + 3, idx + 4,
1048 gtk.EXPAND | gtk.FILL, gtk.FILL)
1049 align.show()
1050
1051 hbox = gtk.HBox(False, 0)
1052 align.add(hbox)
1053 hbox.show()
1054
1055 label = gtk.Label(child_id)
1056 label.set_selectable(True)
1057 hbox.pack_start(label, expand=False, fill=True)
1058 label.show()
1059
1060 image = gtk.Image()
1061 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1062 image.show()
1063
1064 button = gtk.Button()
1065 button.add(image)
1066 button.set_relief(gtk.RELIEF_NONE)
1067 button.connect("clicked", self._go_clicked_cb, child_id)
1068 hbox.pack_start(button, expand=False, fill=True)
1069 button.show()
1070
1071 image = gtk.Image()
1072 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1073 image.show()
1074
1075 button = gtk.Button()
1076 button.add(image)
1077 button.set_relief(gtk.RELIEF_NONE)
1078 button.set_sensitive(True)
1079 button.connect("clicked", self._show_clicked_cb,
1080 child_id, commit.commit_sha1, self.encoding)
1081 hbox.pack_start(button, expand=False, fill=True)
1082 button.show()
1083
1084 def _destroy_cb(self, widget):
1085 """Callback for when a window we manage is destroyed."""
1086 self.quit()
1087
1088
1089 def quit(self):
1090 """Stop the GTK+ main loop."""
1091 gtk.main_quit()
1092
1093 def run(self, args):
1094 self.set_branch(args)
1095 self.window.connect("destroy", self._destroy_cb)
1096 self.window.show()
1097 gtk.main()
1098
1099 def set_branch(self, args):
1100 """Fill in different windows with info from the reposiroty"""
1101 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
1102 git_rev_list_cmd = fp.read()
1103 fp.close()
1104 fp = os.popen("git rev-list --header --topo-order --parents " + git_rev_list_cmd)
1105 self.update_window(fp)
1106
1107 def update_window(self, fp):
1108 commit_lines = []
1109
1110 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
1111 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
1112
1113 # used for cursor positioning
1114 self.index = {}
1115
1116 self.colours = {}
1117 self.nodepos = {}
1118 self.incomplete_line = {}
1119 self.commits = []
1120
1121 index = 0
1122 last_colour = 0
1123 last_nodepos = -1
1124 out_line = []
1125 input_line = fp.readline()
1126 while (input_line != ""):
1127 # The commit header ends with '\0'
1128 # This NULL is immediately followed by the sha1 of the
1129 # next commit
1130 if (input_line[0] != '\0'):
1131 commit_lines.append(input_line)
1132 input_line = fp.readline()
1133 continue;
1134
1135 commit = Commit(commit_lines)
1136 if (commit != None ):
1137 self.commits.append(commit)
1138
1139 # Skip the '\0
1140 commit_lines = []
1141 commit_lines.append(input_line[1:])
1142 input_line = fp.readline()
1143
1144 fp.close()
1145
1146 for commit in self.commits:
1147 (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
1148 index, out_line,
1149 last_colour,
1150 last_nodepos)
1151 self.index[commit.commit_sha1] = index
1152 index += 1
1153
1154 self.treeview.set_model(self.model)
1155 self.treeview.show()
1156
1157 def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
1158 in_line=[]
1159
1160 # | -> outline
1161 # X
1162 # |\ <- inline
1163
1164 # Reset nodepostion
1165 if (last_nodepos > 5):
1166 last_nodepos = -1
1167
1168 # Add the incomplete lines of the last cell in this
1169 try:
1170 colour = self.colours[commit.commit_sha1]
1171 except KeyError:
1172 self.colours[commit.commit_sha1] = last_colour+1
1173 last_colour = self.colours[commit.commit_sha1]
1174 colour = self.colours[commit.commit_sha1]
1175
1176 try:
1177 node_pos = self.nodepos[commit.commit_sha1]
1178 except KeyError:
1179 self.nodepos[commit.commit_sha1] = last_nodepos+1
1180 last_nodepos = self.nodepos[commit.commit_sha1]
1181 node_pos = self.nodepos[commit.commit_sha1]
1182
1183 #The first parent always continue on the same line
1184 try:
1185 # check we alreay have the value
1186 tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
1187 except KeyError:
1188 self.colours[commit.parent_sha1[0]] = colour
1189 self.nodepos[commit.parent_sha1[0]] = node_pos
1190
1191 for sha1 in self.incomplete_line.keys():
1192 if (sha1 != commit.commit_sha1):
1193 self.draw_incomplete_line(sha1, node_pos,
1194 out_line, in_line, index)
1195 else:
1196 del self.incomplete_line[sha1]
1197
1198
1199 for parent_id in commit.parent_sha1:
1200 try:
1201 tmp_node_pos = self.nodepos[parent_id]
1202 except KeyError:
1203 self.colours[parent_id] = last_colour+1
1204 last_colour = self.colours[parent_id]
1205 self.nodepos[parent_id] = last_nodepos+1
1206 last_nodepos = self.nodepos[parent_id]
1207
1208 in_line.append((node_pos, self.nodepos[parent_id],
1209 self.colours[parent_id]))
1210 self.add_incomplete_line(parent_id)
1211
1212 try:
1213 branch_tag = self.bt_sha1[commit.commit_sha1]
1214 except KeyError:
1215 branch_tag = [ ]
1216
1217
1218 node = (node_pos, colour, branch_tag)
1219
1220 self.model.append([commit, node, out_line, in_line,
1221 commit.message, commit.author, commit.date])
1222
1223 return (in_line, last_colour, last_nodepos)
1224
1225 def add_incomplete_line(self, sha1):
1226 try:
1227 self.incomplete_line[sha1].append(self.nodepos[sha1])
1228 except KeyError:
1229 self.incomplete_line[sha1] = [self.nodepos[sha1]]
1230
1231 def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
1232 for idx, pos in enumerate(self.incomplete_line[sha1]):
1233 if(pos == node_pos):
1234 #remove the straight line and add a slash
1235 if ((pos, pos, self.colours[sha1]) in out_line):
1236 out_line.remove((pos, pos, self.colours[sha1]))
1237 out_line.append((pos, pos+0.5, self.colours[sha1]))
1238 self.incomplete_line[sha1][idx] = pos = pos+0.5
1239 try:
1240 next_commit = self.commits[index+1]
1241 if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
1242 # join the line back to the node point
1243 # This need to be done only if we modified it
1244 in_line.append((pos, pos-0.5, self.colours[sha1]))
1245 continue;
1246 except IndexError:
1247 pass
1248 in_line.append((pos, pos, self.colours[sha1]))
1249
1250
1251 def _go_clicked_cb(self, widget, revid):
1252 """Callback for when the go button for a parent is clicked."""
1253 try:
1254 self.treeview.set_cursor(self.index[revid])
1255 except KeyError:
1256 dialog = gtk.MessageDialog(parent=None, flags=0,
1257 type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
1258 message_format=None)
1259 dialog.set_markup("Revision <b>%s</b> not present in the list" % revid)
1260 # revid == 0 is the parent of the first commit
1261 if (revid != 0 ):
1262 dialog.format_secondary_text("Try running gitview without any options")
1263 dialog.run()
1264 dialog.destroy()
1265
1266 self.treeview.grab_focus()
1267
1268 def _show_clicked_cb(self, widget, commit_sha1, parent_sha1, encoding):
1269 """Callback for when the show button for a parent is clicked."""
1270 window = DiffWindow()
1271 window.set_diff(commit_sha1, parent_sha1, encoding)
1272 self.treeview.grab_focus()
1273
1274without_diff = 0
1275if __name__ == "__main__":
1276
1277 if (len(sys.argv) > 1 ):
1278 if (sys.argv[1] == "--without-diff"):
1279 without_diff = 1
1280
1281 view = GitView( without_diff != 1)
1282 view.run(sys.argv[without_diff:])