b75b39e5fc94bcf96e368ff20ac14e4afb90726f
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 = 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)/3
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 if (len(names) != 0):
243 name = " "
244 for item in names:
245 name = name + item + " "
246
247 ctx.text_path(name)
248
249 self.set_colour(ctx, colour, 0.0, 0.5)
250 ctx.stroke_preserve()
251
252 self.set_colour(ctx, colour, 0.5, 1.0)
253 ctx.fill()
254
255class Commit:
256 """ This represent a commit object obtained after parsing the git-rev-list
257 output """
258
259 children_sha1 = {}
260
261 def __init__(self, commit_lines):
262 self.message = ""
263 self.author = ""
264 self.date = ""
265 self.committer = ""
266 self.commit_date = ""
267 self.commit_sha1 = ""
268 self.parent_sha1 = [ ]
269 self.parse_commit(commit_lines)
270
271
272 def parse_commit(self, commit_lines):
273
274 # First line is the sha1 lines
275 line = string.strip(commit_lines[0])
276 sha1 = re.split(" ", line)
277 self.commit_sha1 = sha1[0]
278 self.parent_sha1 = sha1[1:]
279
280 #build the child list
281 for parent_id in self.parent_sha1:
282 try:
283 Commit.children_sha1[parent_id].append(self.commit_sha1)
284 except KeyError:
285 Commit.children_sha1[parent_id] = [self.commit_sha1]
286
287 # IF we don't have parent
288 if (len(self.parent_sha1) == 0):
289 self.parent_sha1 = [0]
290
291 for line in commit_lines[1:]:
292 m = re.match("^ ", line)
293 if (m != None):
294 # First line of the commit message used for short log
295 if self.message == "":
296 self.message = string.strip(line)
297 continue
298
299 m = re.match("tree", line)
300 if (m != None):
301 continue
302
303 m = re.match("parent", line)
304 if (m != None):
305 continue
306
307 m = re_ident.match(line)
308 if (m != None):
309 date = show_date(m.group('epoch'), m.group('tz'))
310 if m.group(1) == "author":
311 self.author = m.group('ident')
312 self.date = date
313 elif m.group(1) == "committer":
314 self.committer = m.group('ident')
315 self.commit_date = date
316
317 continue
318
319 def get_message(self, with_diff=0):
320 if (with_diff == 1):
321 message = self.diff_tree()
322 else:
323 fp = os.popen("git cat-file commit " + self.commit_sha1)
324 message = fp.read()
325 fp.close()
326
327 return message
328
329 def diff_tree(self):
330 fp = os.popen("git diff-tree --pretty --cc -v -p --always " + self.commit_sha1)
331 diff = fp.read()
332 fp.close()
333 return diff
334
335class DiffWindow:
336 """Diff window.
337 This object represents and manages a single window containing the
338 differences between two revisions on a branch.
339 """
340
341 def __init__(self):
342 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
343 self.window.set_border_width(0)
344 self.window.set_title("Git repository browser diff window")
345
346 # Use two thirds of the screen by default
347 screen = self.window.get_screen()
348 monitor = screen.get_monitor_geometry(0)
349 width = int(monitor.width * 0.66)
350 height = int(monitor.height * 0.66)
351 self.window.set_default_size(width, height)
352
353 self.construct()
354
355 def construct(self):
356 """Construct the window contents."""
357 vbox = gtk.VBox()
358 self.window.add(vbox)
359 vbox.show()
360
361 menu_bar = gtk.MenuBar()
362 save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
363 save_menu.connect("activate", self.save_menu_response, "save")
364 save_menu.show()
365 menu_bar.append(save_menu)
366 vbox.pack_start(menu_bar, False, False, 2)
367 menu_bar.show()
368
369 scrollwin = gtk.ScrolledWindow()
370 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
371 scrollwin.set_shadow_type(gtk.SHADOW_IN)
372 vbox.pack_start(scrollwin, expand=True, fill=True)
373 scrollwin.show()
374
375 if have_gtksourceview:
376 self.buffer = gtksourceview.SourceBuffer()
377 slm = gtksourceview.SourceLanguagesManager()
378 gsl = slm.get_language_from_mime_type("text/x-patch")
379 self.buffer.set_highlight(True)
380 self.buffer.set_language(gsl)
381 sourceview = gtksourceview.SourceView(self.buffer)
382 else:
383 self.buffer = gtk.TextBuffer()
384 sourceview = gtk.TextView(self.buffer)
385
386 sourceview.set_editable(False)
387 sourceview.modify_font(pango.FontDescription("Monospace"))
388 scrollwin.add(sourceview)
389 sourceview.show()
390
391
392 def set_diff(self, commit_sha1, parent_sha1):
393 """Set the differences showed by this window.
394 Compares the two trees and populates the window with the
395 differences.
396 """
397 # Diff with the first commit or the last commit shows nothing
398 if (commit_sha1 == 0 or parent_sha1 == 0 ):
399 return
400
401 fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
402 self.buffer.set_text(fp.read())
403 fp.close()
404 self.window.show()
405
406 def save_menu_response(self, widget, string):
407 dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
408 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
409 gtk.STOCK_SAVE, gtk.RESPONSE_OK))
410 dialog.set_default_response(gtk.RESPONSE_OK)
411 response = dialog.run()
412 if response == gtk.RESPONSE_OK:
413 patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
414 self.buffer.get_end_iter())
415 fp = open(dialog.get_filename(), "w")
416 fp.write(patch_buffer)
417 fp.close()
418 dialog.destroy()
419
420class GitView:
421 """ This is the main class
422 """
423 version = "0.6"
424
425 def __init__(self, with_diff=0):
426 self.with_diff = with_diff
427 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
428 self.window.set_border_width(0)
429 self.window.set_title("Git repository browser")
430
431 self.get_bt_sha1()
432
433 # Use three-quarters of the screen by default
434 screen = self.window.get_screen()
435 monitor = screen.get_monitor_geometry(0)
436 width = int(monitor.width * 0.75)
437 height = int(monitor.height * 0.75)
438 self.window.set_default_size(width, height)
439
440 # FIXME AndyFitz!
441 icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
442 self.window.set_icon(icon)
443
444 self.accel_group = gtk.AccelGroup()
445 self.window.add_accel_group(self.accel_group)
446
447 self.construct()
448
449 def get_bt_sha1(self):
450 """ Update the bt_sha1 dictionary with the
451 respective sha1 details """
452
453 self.bt_sha1 = { }
454 ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
455 git_dir = os.getenv("GIT_DIR")
456 if (git_dir == None):
457 git_dir = ".git"
458
459 fp = os.popen('git ls-remote ' + git_dir)
460 while 1:
461 line = string.strip(fp.readline())
462 if line == '':
463 break
464 m = ls_remote.match(line)
465 if not m:
466 continue
467 (sha1, name) = (m.group(1), m.group(2))
468 if not self.bt_sha1.has_key(sha1):
469 self.bt_sha1[sha1] = []
470 self.bt_sha1[sha1].append(name)
471 fp.close()
472
473
474 def construct(self):
475 """Construct the window contents."""
476 paned = gtk.VPaned()
477 paned.pack1(self.construct_top(), resize=False, shrink=True)
478 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
479 self.window.add(paned)
480 paned.show()
481
482
483 def construct_top(self):
484 """Construct the top-half of the window."""
485 vbox = gtk.VBox(spacing=6)
486 vbox.set_border_width(12)
487 vbox.show()
488
489 menu_bar = gtk.MenuBar()
490 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
491 help_menu = gtk.MenuItem("Help")
492 menu = gtk.Menu()
493 about_menu = gtk.MenuItem("About")
494 menu.append(about_menu)
495 about_menu.connect("activate", self.about_menu_response, "about")
496 about_menu.show()
497 help_menu.set_submenu(menu)
498 help_menu.show()
499 menu_bar.append(help_menu)
500 vbox.pack_start(menu_bar, False, False, 2)
501 menu_bar.show()
502
503 scrollwin = gtk.ScrolledWindow()
504 scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
505 scrollwin.set_shadow_type(gtk.SHADOW_IN)
506 vbox.pack_start(scrollwin, expand=True, fill=True)
507 scrollwin.show()
508
509 self.treeview = gtk.TreeView()
510 self.treeview.set_rules_hint(True)
511 self.treeview.set_search_column(4)
512 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
513 scrollwin.add(self.treeview)
514 self.treeview.show()
515
516 cell = CellRendererGraph()
517 column = gtk.TreeViewColumn()
518 column.set_resizable(False)
519 column.pack_start(cell, expand=False)
520 column.add_attribute(cell, "node", 1)
521 column.add_attribute(cell, "in-lines", 2)
522 column.add_attribute(cell, "out-lines", 3)
523 self.treeview.append_column(column)
524
525 cell = gtk.CellRendererText()
526 cell.set_property("width-chars", 65)
527 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
528 column = gtk.TreeViewColumn("Message")
529 column.set_resizable(True)
530 column.pack_start(cell, expand=True)
531 column.add_attribute(cell, "text", 4)
532 self.treeview.append_column(column)
533
534 cell = gtk.CellRendererText()
535 cell.set_property("width-chars", 40)
536 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
537 column = gtk.TreeViewColumn("Author")
538 column.set_resizable(True)
539 column.pack_start(cell, expand=True)
540 column.add_attribute(cell, "text", 5)
541 self.treeview.append_column(column)
542
543 cell = gtk.CellRendererText()
544 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
545 column = gtk.TreeViewColumn("Date")
546 column.set_resizable(True)
547 column.pack_start(cell, expand=True)
548 column.add_attribute(cell, "text", 6)
549 self.treeview.append_column(column)
550
551 return vbox
552
553 def about_menu_response(self, widget, string):
554 dialog = gtk.AboutDialog()
555 dialog.set_name("Gitview")
556 dialog.set_version(GitView.version)
557 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
558 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
559 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
560 dialog.set_wrap_license(True)
561 dialog.run()
562 dialog.destroy()
563
564
565 def construct_bottom(self):
566 """Construct the bottom half of the window."""
567 vbox = gtk.VBox(False, spacing=6)
568 vbox.set_border_width(12)
569 (width, height) = self.window.get_size()
570 vbox.set_size_request(width, int(height / 2.5))
571 vbox.show()
572
573 self.table = gtk.Table(rows=4, columns=4)
574 self.table.set_row_spacings(6)
575 self.table.set_col_spacings(6)
576 vbox.pack_start(self.table, expand=False, fill=True)
577 self.table.show()
578
579 align = gtk.Alignment(0.0, 0.5)
580 label = gtk.Label()
581 label.set_markup("<b>Revision:</b>")
582 align.add(label)
583 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
584 label.show()
585 align.show()
586
587 align = gtk.Alignment(0.0, 0.5)
588 self.revid_label = gtk.Label()
589 self.revid_label.set_selectable(True)
590 align.add(self.revid_label)
591 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
592 self.revid_label.show()
593 align.show()
594
595 align = gtk.Alignment(0.0, 0.5)
596 label = gtk.Label()
597 label.set_markup("<b>Committer:</b>")
598 align.add(label)
599 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
600 label.show()
601 align.show()
602
603 align = gtk.Alignment(0.0, 0.5)
604 self.committer_label = gtk.Label()
605 self.committer_label.set_selectable(True)
606 align.add(self.committer_label)
607 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
608 self.committer_label.show()
609 align.show()
610
611 align = gtk.Alignment(0.0, 0.5)
612 label = gtk.Label()
613 label.set_markup("<b>Timestamp:</b>")
614 align.add(label)
615 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
616 label.show()
617 align.show()
618
619 align = gtk.Alignment(0.0, 0.5)
620 self.timestamp_label = gtk.Label()
621 self.timestamp_label.set_selectable(True)
622 align.add(self.timestamp_label)
623 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
624 self.timestamp_label.show()
625 align.show()
626
627 align = gtk.Alignment(0.0, 0.5)
628 label = gtk.Label()
629 label.set_markup("<b>Parents:</b>")
630 align.add(label)
631 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
632 label.show()
633 align.show()
634 self.parents_widgets = []
635
636 align = gtk.Alignment(0.0, 0.5)
637 label = gtk.Label()
638 label.set_markup("<b>Children:</b>")
639 align.add(label)
640 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
641 label.show()
642 align.show()
643 self.children_widgets = []
644
645 scrollwin = gtk.ScrolledWindow()
646 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
647 scrollwin.set_shadow_type(gtk.SHADOW_IN)
648 vbox.pack_start(scrollwin, expand=True, fill=True)
649 scrollwin.show()
650
651 if have_gtksourceview:
652 self.message_buffer = gtksourceview.SourceBuffer()
653 slm = gtksourceview.SourceLanguagesManager()
654 gsl = slm.get_language_from_mime_type("text/x-patch")
655 self.message_buffer.set_highlight(True)
656 self.message_buffer.set_language(gsl)
657 sourceview = gtksourceview.SourceView(self.message_buffer)
658 else:
659 self.message_buffer = gtk.TextBuffer()
660 sourceview = gtk.TextView(self.message_buffer)
661
662 sourceview.set_editable(False)
663 sourceview.modify_font(pango.FontDescription("Monospace"))
664 scrollwin.add(sourceview)
665 sourceview.show()
666
667 return vbox
668
669 def _treeview_cursor_cb(self, *args):
670 """Callback for when the treeview cursor changes."""
671 (path, col) = self.treeview.get_cursor()
672 commit = self.model[path][0]
673
674 if commit.committer is not None:
675 committer = commit.committer
676 timestamp = commit.commit_date
677 message = commit.get_message(self.with_diff)
678 revid_label = commit.commit_sha1
679 else:
680 committer = ""
681 timestamp = ""
682 message = ""
683 revid_label = ""
684
685 self.revid_label.set_text(revid_label)
686 self.committer_label.set_text(committer)
687 self.timestamp_label.set_text(timestamp)
688 self.message_buffer.set_text(message)
689
690 for widget in self.parents_widgets:
691 self.table.remove(widget)
692
693 self.parents_widgets = []
694 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
695 for idx, parent_id in enumerate(commit.parent_sha1):
696 self.table.set_row_spacing(idx + 3, 0)
697
698 align = gtk.Alignment(0.0, 0.0)
699 self.parents_widgets.append(align)
700 self.table.attach(align, 1, 2, idx + 3, idx + 4,
701 gtk.EXPAND | gtk.FILL, gtk.FILL)
702 align.show()
703
704 hbox = gtk.HBox(False, 0)
705 align.add(hbox)
706 hbox.show()
707
708 label = gtk.Label(parent_id)
709 label.set_selectable(True)
710 hbox.pack_start(label, expand=False, fill=True)
711 label.show()
712
713 image = gtk.Image()
714 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
715 image.show()
716
717 button = gtk.Button()
718 button.add(image)
719 button.set_relief(gtk.RELIEF_NONE)
720 button.connect("clicked", self._go_clicked_cb, parent_id)
721 hbox.pack_start(button, expand=False, fill=True)
722 button.show()
723
724 image = gtk.Image()
725 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
726 image.show()
727
728 button = gtk.Button()
729 button.add(image)
730 button.set_relief(gtk.RELIEF_NONE)
731 button.set_sensitive(True)
732 button.connect("clicked", self._show_clicked_cb,
733 commit.commit_sha1, parent_id)
734 hbox.pack_start(button, expand=False, fill=True)
735 button.show()
736
737 # Populate with child details
738 for widget in self.children_widgets:
739 self.table.remove(widget)
740
741 self.children_widgets = []
742 try:
743 child_sha1 = Commit.children_sha1[commit.commit_sha1]
744 except KeyError:
745 # We don't have child
746 child_sha1 = [ 0 ]
747
748 if ( len(child_sha1) > len(commit.parent_sha1)):
749 self.table.resize(4 + len(child_sha1) - 1, 4)
750
751 for idx, child_id in enumerate(child_sha1):
752 self.table.set_row_spacing(idx + 3, 0)
753
754 align = gtk.Alignment(0.0, 0.0)
755 self.children_widgets.append(align)
756 self.table.attach(align, 3, 4, idx + 3, idx + 4,
757 gtk.EXPAND | gtk.FILL, gtk.FILL)
758 align.show()
759
760 hbox = gtk.HBox(False, 0)
761 align.add(hbox)
762 hbox.show()
763
764 label = gtk.Label(child_id)
765 label.set_selectable(True)
766 hbox.pack_start(label, expand=False, fill=True)
767 label.show()
768
769 image = gtk.Image()
770 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
771 image.show()
772
773 button = gtk.Button()
774 button.add(image)
775 button.set_relief(gtk.RELIEF_NONE)
776 button.connect("clicked", self._go_clicked_cb, child_id)
777 hbox.pack_start(button, expand=False, fill=True)
778 button.show()
779
780 image = gtk.Image()
781 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
782 image.show()
783
784 button = gtk.Button()
785 button.add(image)
786 button.set_relief(gtk.RELIEF_NONE)
787 button.set_sensitive(True)
788 button.connect("clicked", self._show_clicked_cb,
789 child_id, commit.commit_sha1)
790 hbox.pack_start(button, expand=False, fill=True)
791 button.show()
792
793 def _destroy_cb(self, widget):
794 """Callback for when a window we manage is destroyed."""
795 self.quit()
796
797
798 def quit(self):
799 """Stop the GTK+ main loop."""
800 gtk.main_quit()
801
802 def run(self, args):
803 self.set_branch(args)
804 self.window.connect("destroy", self._destroy_cb)
805 self.window.show()
806 gtk.main()
807
808 def set_branch(self, args):
809 """Fill in different windows with info from the reposiroty"""
810 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
811 git_rev_list_cmd = fp.read()
812 fp.close()
813 fp = os.popen("git rev-list --header --topo-order --parents " + git_rev_list_cmd)
814 self.update_window(fp)
815
816 def update_window(self, fp):
817 commit_lines = []
818
819 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
820 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
821
822 # used for cursor positioning
823 self.index = {}
824
825 self.colours = {}
826 self.nodepos = {}
827 self.incomplete_line = {}
828
829 index = 0
830 last_colour = 0
831 last_nodepos = -1
832 out_line = []
833 input_line = fp.readline()
834 while (input_line != ""):
835 # The commit header ends with '\0'
836 # This NULL is immediately followed by the sha1 of the
837 # next commit
838 if (input_line[0] != '\0'):
839 commit_lines.append(input_line)
840 input_line = fp.readline()
841 continue;
842
843 commit = Commit(commit_lines)
844 if (commit != None ):
845 (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
846 index, out_line,
847 last_colour,
848 last_nodepos)
849 self.index[commit.commit_sha1] = index
850 index += 1
851
852 # Skip the '\0
853 commit_lines = []
854 commit_lines.append(input_line[1:])
855 input_line = fp.readline()
856
857 fp.close()
858
859 self.treeview.set_model(self.model)
860 self.treeview.show()
861
862 def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
863 in_line=[]
864
865 # | -> outline
866 # X
867 # |\ <- inline
868
869 # Reset nodepostion
870 if (last_nodepos > 5):
871 last_nodepos = 0
872
873 # Add the incomplete lines of the last cell in this
874 for sha1 in self.incomplete_line.keys():
875 if ( sha1 != commit.commit_sha1):
876 for pos in self.incomplete_line[sha1]:
877 in_line.append((pos, pos, self.colours[sha1]))
878 else:
879 del self.incomplete_line[sha1]
880
881 try:
882 colour = self.colours[commit.commit_sha1]
883 except KeyError:
884 last_colour +=1
885 self.colours[commit.commit_sha1] = last_colour
886 colour = last_colour
887 try:
888 node_pos = self.nodepos[commit.commit_sha1]
889 except KeyError:
890 last_nodepos +=1
891 self.nodepos[commit.commit_sha1] = last_nodepos
892 node_pos = last_nodepos
893
894 #The first parent always continue on the same line
895 try:
896 # check we alreay have the value
897 tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
898 except KeyError:
899 self.colours[commit.parent_sha1[0]] = colour
900 self.nodepos[commit.parent_sha1[0]] = node_pos
901
902 in_line.append((node_pos, self.nodepos[commit.parent_sha1[0]],
903 self.colours[commit.parent_sha1[0]]))
904
905 self.add_incomplete_line(commit.parent_sha1[0], index+1)
906
907 if (len(commit.parent_sha1) > 1):
908 for parent_id in commit.parent_sha1[1:]:
909 try:
910 tmp_node_pos = self.nodepos[parent_id]
911 except KeyError:
912 last_colour += 1;
913 self.colours[parent_id] = last_colour
914 last_nodepos +=1
915 self.nodepos[parent_id] = last_nodepos
916
917 in_line.append((node_pos, self.nodepos[parent_id],
918 self.colours[parent_id]))
919 self.add_incomplete_line(parent_id, index+1)
920
921
922 try:
923 branch_tag = self.bt_sha1[commit.commit_sha1]
924 except KeyError:
925 branch_tag = [ ]
926
927
928 node = (node_pos, colour, branch_tag)
929
930 self.model.append([commit, node, out_line, in_line,
931 commit.message, commit.author, commit.date])
932
933 return (in_line, last_colour, last_nodepos)
934
935 def add_incomplete_line(self, sha1, index):
936 try:
937 self.incomplete_line[sha1].append(self.nodepos[sha1])
938 except KeyError:
939 self.incomplete_line[sha1] = [self.nodepos[sha1]]
940
941
942 def _go_clicked_cb(self, widget, revid):
943 """Callback for when the go button for a parent is clicked."""
944 try:
945 self.treeview.set_cursor(self.index[revid])
946 except KeyError:
947 print "Revision %s not present in the list" % revid
948 # revid == 0 is the parent of the first commit
949 if (revid != 0 ):
950 print "Try running gitview without any options"
951
952 self.treeview.grab_focus()
953
954 def _show_clicked_cb(self, widget, commit_sha1, parent_sha1):
955 """Callback for when the show button for a parent is clicked."""
956 window = DiffWindow()
957 window.set_diff(commit_sha1, parent_sha1)
958 self.treeview.grab_focus()
959
960if __name__ == "__main__":
961 without_diff = 0
962
963 if (len(sys.argv) > 1 ):
964 if (sys.argv[1] == "--without-diff"):
965 without_diff = 1
966
967 view = GitView( without_diff != 1)
968 view.run(sys.argv[without_diff:])
969
970