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