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