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