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