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 git_dir = os.getenv("GIT_DIR")
458 if (git_dir == None):
459 git_dir = ".git"
460
461 fp = os.popen('git ls-remote ' + git_dir)
462 while 1:
463 line = string.strip(fp.readline())
464 if line == '':
465 break
466 m = ls_remote.match(line)
467 if not m:
468 continue
469 (sha1, name) = (m.group(1), m.group(2))
470 if not self.bt_sha1.has_key(sha1):
471 self.bt_sha1[sha1] = []
472 self.bt_sha1[sha1].append(name)
473 fp.close()
474
475
476 def construct(self):
477 """Construct the window contents."""
478 paned = gtk.VPaned()
479 paned.pack1(self.construct_top(), resize=False, shrink=True)
480 paned.pack2(self.construct_bottom(), resize=False, shrink=True)
481 self.window.add(paned)
482 paned.show()
483
484
485 def construct_top(self):
486 """Construct the top-half of the window."""
487 vbox = gtk.VBox(spacing=6)
488 vbox.set_border_width(12)
489 vbox.show()
490
491 menu_bar = gtk.MenuBar()
492 menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
493 help_menu = gtk.MenuItem("Help")
494 menu = gtk.Menu()
495 about_menu = gtk.MenuItem("About")
496 menu.append(about_menu)
497 about_menu.connect("activate", self.about_menu_response, "about")
498 about_menu.show()
499 help_menu.set_submenu(menu)
500 help_menu.show()
501 menu_bar.append(help_menu)
502 vbox.pack_start(menu_bar, False, False, 2)
503 menu_bar.show()
504
505 scrollwin = gtk.ScrolledWindow()
506 scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
507 scrollwin.set_shadow_type(gtk.SHADOW_IN)
508 vbox.pack_start(scrollwin, expand=True, fill=True)
509 scrollwin.show()
510
511 self.treeview = gtk.TreeView()
512 self.treeview.set_rules_hint(True)
513 self.treeview.set_search_column(4)
514 self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
515 scrollwin.add(self.treeview)
516 self.treeview.show()
517
518 cell = CellRendererGraph()
519 column = gtk.TreeViewColumn()
520 column.set_resizable(True)
521 column.pack_start(cell, expand=True)
522 column.add_attribute(cell, "node", 1)
523 column.add_attribute(cell, "in-lines", 2)
524 column.add_attribute(cell, "out-lines", 3)
525 self.treeview.append_column(column)
526
527 cell = gtk.CellRendererText()
528 cell.set_property("width-chars", 65)
529 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
530 column = gtk.TreeViewColumn("Message")
531 column.set_resizable(True)
532 column.pack_start(cell, expand=True)
533 column.add_attribute(cell, "text", 4)
534 self.treeview.append_column(column)
535
536 cell = gtk.CellRendererText()
537 cell.set_property("width-chars", 40)
538 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
539 column = gtk.TreeViewColumn("Author")
540 column.set_resizable(True)
541 column.pack_start(cell, expand=True)
542 column.add_attribute(cell, "text", 5)
543 self.treeview.append_column(column)
544
545 cell = gtk.CellRendererText()
546 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
547 column = gtk.TreeViewColumn("Date")
548 column.set_resizable(True)
549 column.pack_start(cell, expand=True)
550 column.add_attribute(cell, "text", 6)
551 self.treeview.append_column(column)
552
553 return vbox
554
555 def about_menu_response(self, widget, string):
556 dialog = gtk.AboutDialog()
557 dialog.set_name("Gitview")
558 dialog.set_version(GitView.version)
559 dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
560 dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
561 dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
562 dialog.set_wrap_license(True)
563 dialog.run()
564 dialog.destroy()
565
566
567 def construct_bottom(self):
568 """Construct the bottom half of the window."""
569 vbox = gtk.VBox(False, spacing=6)
570 vbox.set_border_width(12)
571 (width, height) = self.window.get_size()
572 vbox.set_size_request(width, int(height / 2.5))
573 vbox.show()
574
575 self.table = gtk.Table(rows=4, columns=4)
576 self.table.set_row_spacings(6)
577 self.table.set_col_spacings(6)
578 vbox.pack_start(self.table, expand=False, fill=True)
579 self.table.show()
580
581 align = gtk.Alignment(0.0, 0.5)
582 label = gtk.Label()
583 label.set_markup("<b>Revision:</b>")
584 align.add(label)
585 self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
586 label.show()
587 align.show()
588
589 align = gtk.Alignment(0.0, 0.5)
590 self.revid_label = gtk.Label()
591 self.revid_label.set_selectable(True)
592 align.add(self.revid_label)
593 self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
594 self.revid_label.show()
595 align.show()
596
597 align = gtk.Alignment(0.0, 0.5)
598 label = gtk.Label()
599 label.set_markup("<b>Committer:</b>")
600 align.add(label)
601 self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
602 label.show()
603 align.show()
604
605 align = gtk.Alignment(0.0, 0.5)
606 self.committer_label = gtk.Label()
607 self.committer_label.set_selectable(True)
608 align.add(self.committer_label)
609 self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
610 self.committer_label.show()
611 align.show()
612
613 align = gtk.Alignment(0.0, 0.5)
614 label = gtk.Label()
615 label.set_markup("<b>Timestamp:</b>")
616 align.add(label)
617 self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
618 label.show()
619 align.show()
620
621 align = gtk.Alignment(0.0, 0.5)
622 self.timestamp_label = gtk.Label()
623 self.timestamp_label.set_selectable(True)
624 align.add(self.timestamp_label)
625 self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
626 self.timestamp_label.show()
627 align.show()
628
629 align = gtk.Alignment(0.0, 0.5)
630 label = gtk.Label()
631 label.set_markup("<b>Parents:</b>")
632 align.add(label)
633 self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
634 label.show()
635 align.show()
636 self.parents_widgets = []
637
638 align = gtk.Alignment(0.0, 0.5)
639 label = gtk.Label()
640 label.set_markup("<b>Children:</b>")
641 align.add(label)
642 self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
643 label.show()
644 align.show()
645 self.children_widgets = []
646
647 scrollwin = gtk.ScrolledWindow()
648 scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
649 scrollwin.set_shadow_type(gtk.SHADOW_IN)
650 vbox.pack_start(scrollwin, expand=True, fill=True)
651 scrollwin.show()
652
653 if have_gtksourceview:
654 self.message_buffer = gtksourceview.SourceBuffer()
655 slm = gtksourceview.SourceLanguagesManager()
656 gsl = slm.get_language_from_mime_type("text/x-patch")
657 self.message_buffer.set_highlight(True)
658 self.message_buffer.set_language(gsl)
659 sourceview = gtksourceview.SourceView(self.message_buffer)
660 else:
661 self.message_buffer = gtk.TextBuffer()
662 sourceview = gtk.TextView(self.message_buffer)
663
664 sourceview.set_editable(False)
665 sourceview.modify_font(pango.FontDescription("Monospace"))
666 scrollwin.add(sourceview)
667 sourceview.show()
668
669 return vbox
670
671 def _treeview_cursor_cb(self, *args):
672 """Callback for when the treeview cursor changes."""
673 (path, col) = self.treeview.get_cursor()
674 commit = self.model[path][0]
675
676 if commit.committer is not None:
677 committer = commit.committer
678 timestamp = commit.commit_date
679 message = commit.get_message(self.with_diff)
680 revid_label = commit.commit_sha1
681 else:
682 committer = ""
683 timestamp = ""
684 message = ""
685 revid_label = ""
686
687 self.revid_label.set_text(revid_label)
688 self.committer_label.set_text(committer)
689 self.timestamp_label.set_text(timestamp)
690 self.message_buffer.set_text(message)
691
692 for widget in self.parents_widgets:
693 self.table.remove(widget)
694
695 self.parents_widgets = []
696 self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
697 for idx, parent_id in enumerate(commit.parent_sha1):
698 self.table.set_row_spacing(idx + 3, 0)
699
700 align = gtk.Alignment(0.0, 0.0)
701 self.parents_widgets.append(align)
702 self.table.attach(align, 1, 2, idx + 3, idx + 4,
703 gtk.EXPAND | gtk.FILL, gtk.FILL)
704 align.show()
705
706 hbox = gtk.HBox(False, 0)
707 align.add(hbox)
708 hbox.show()
709
710 label = gtk.Label(parent_id)
711 label.set_selectable(True)
712 hbox.pack_start(label, expand=False, fill=True)
713 label.show()
714
715 image = gtk.Image()
716 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
717 image.show()
718
719 button = gtk.Button()
720 button.add(image)
721 button.set_relief(gtk.RELIEF_NONE)
722 button.connect("clicked", self._go_clicked_cb, parent_id)
723 hbox.pack_start(button, expand=False, fill=True)
724 button.show()
725
726 image = gtk.Image()
727 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
728 image.show()
729
730 button = gtk.Button()
731 button.add(image)
732 button.set_relief(gtk.RELIEF_NONE)
733 button.set_sensitive(True)
734 button.connect("clicked", self._show_clicked_cb,
735 commit.commit_sha1, parent_id)
736 hbox.pack_start(button, expand=False, fill=True)
737 button.show()
738
739 # Populate with child details
740 for widget in self.children_widgets:
741 self.table.remove(widget)
742
743 self.children_widgets = []
744 try:
745 child_sha1 = Commit.children_sha1[commit.commit_sha1]
746 except KeyError:
747 # We don't have child
748 child_sha1 = [ 0 ]
749
750 if ( len(child_sha1) > len(commit.parent_sha1)):
751 self.table.resize(4 + len(child_sha1) - 1, 4)
752
753 for idx, child_id in enumerate(child_sha1):
754 self.table.set_row_spacing(idx + 3, 0)
755
756 align = gtk.Alignment(0.0, 0.0)
757 self.children_widgets.append(align)
758 self.table.attach(align, 3, 4, idx + 3, idx + 4,
759 gtk.EXPAND | gtk.FILL, gtk.FILL)
760 align.show()
761
762 hbox = gtk.HBox(False, 0)
763 align.add(hbox)
764 hbox.show()
765
766 label = gtk.Label(child_id)
767 label.set_selectable(True)
768 hbox.pack_start(label, expand=False, fill=True)
769 label.show()
770
771 image = gtk.Image()
772 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
773 image.show()
774
775 button = gtk.Button()
776 button.add(image)
777 button.set_relief(gtk.RELIEF_NONE)
778 button.connect("clicked", self._go_clicked_cb, child_id)
779 hbox.pack_start(button, expand=False, fill=True)
780 button.show()
781
782 image = gtk.Image()
783 image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
784 image.show()
785
786 button = gtk.Button()
787 button.add(image)
788 button.set_relief(gtk.RELIEF_NONE)
789 button.set_sensitive(True)
790 button.connect("clicked", self._show_clicked_cb,
791 child_id, commit.commit_sha1)
792 hbox.pack_start(button, expand=False, fill=True)
793 button.show()
794
795 def _destroy_cb(self, widget):
796 """Callback for when a window we manage is destroyed."""
797 self.quit()
798
799
800 def quit(self):
801 """Stop the GTK+ main loop."""
802 gtk.main_quit()
803
804 def run(self, args):
805 self.set_branch(args)
806 self.window.connect("destroy", self._destroy_cb)
807 self.window.show()
808 gtk.main()
809
810 def set_branch(self, args):
811 """Fill in different windows with info from the reposiroty"""
812 fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
813 git_rev_list_cmd = fp.read()
814 fp.close()
815 fp = os.popen("git rev-list --header --topo-order --parents " + git_rev_list_cmd)
816 self.update_window(fp)
817
818 def update_window(self, fp):
819 commit_lines = []
820
821 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
822 gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
823
824 # used for cursor positioning
825 self.index = {}
826
827 self.colours = {}
828 self.nodepos = {}
829 self.incomplete_line = {}
830
831 index = 0
832 last_colour = 0
833 last_nodepos = -1
834 out_line = []
835 input_line = fp.readline()
836 while (input_line != ""):
837 # The commit header ends with '\0'
838 # This NULL is immediately followed by the sha1 of the
839 # next commit
840 if (input_line[0] != '\0'):
841 commit_lines.append(input_line)
842 input_line = fp.readline()
843 continue;
844
845 commit = Commit(commit_lines)
846 if (commit != None ):
847 (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
848 index, out_line,
849 last_colour,
850 last_nodepos)
851 self.index[commit.commit_sha1] = index
852 index += 1
853
854 # Skip the '\0
855 commit_lines = []
856 commit_lines.append(input_line[1:])
857 input_line = fp.readline()
858
859 fp.close()
860
861 self.treeview.set_model(self.model)
862 self.treeview.show()
863
864 def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
865 in_line=[]
866
867 # | -> outline
868 # X
869 # |\ <- inline
870
871 # Reset nodepostion
872 if (last_nodepos > 5):
873 last_nodepos = 0
874
875 # Add the incomplete lines of the last cell in this
876 for sha1 in self.incomplete_line.keys():
877 if ( sha1 != commit.commit_sha1):
878 for pos in self.incomplete_line[sha1]:
879 in_line.append((pos, pos, self.colours[sha1]))
880 else:
881 del self.incomplete_line[sha1]
882
883 try:
884 colour = self.colours[commit.commit_sha1]
885 except KeyError:
886 last_colour +=1
887 self.colours[commit.commit_sha1] = last_colour
888 colour = last_colour
889 try:
890 node_pos = self.nodepos[commit.commit_sha1]
891 except KeyError:
892 last_nodepos +=1
893 self.nodepos[commit.commit_sha1] = last_nodepos
894 node_pos = last_nodepos
895
896 #The first parent always continue on the same line
897 try:
898 # check we alreay have the value
899 tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
900 except KeyError:
901 self.colours[commit.parent_sha1[0]] = colour
902 self.nodepos[commit.parent_sha1[0]] = node_pos
903
904 in_line.append((node_pos, self.nodepos[commit.parent_sha1[0]],
905 self.colours[commit.parent_sha1[0]]))
906
907 self.add_incomplete_line(commit.parent_sha1[0], index+1)
908
909 if (len(commit.parent_sha1) > 1):
910 for parent_id in commit.parent_sha1[1:]:
911 try:
912 tmp_node_pos = self.nodepos[parent_id]
913 except KeyError:
914 last_colour += 1;
915 self.colours[parent_id] = last_colour
916 last_nodepos +=1
917 self.nodepos[parent_id] = last_nodepos
918
919 in_line.append((node_pos, self.nodepos[parent_id],
920 self.colours[parent_id]))
921 self.add_incomplete_line(parent_id, index+1)
922
923
924 try:
925 branch_tag = self.bt_sha1[commit.commit_sha1]
926 except KeyError:
927 branch_tag = [ ]
928
929
930 node = (node_pos, colour, branch_tag)
931
932 self.model.append([commit, node, out_line, in_line,
933 commit.message, commit.author, commit.date])
934
935 return (in_line, last_colour, last_nodepos)
936
937 def add_incomplete_line(self, sha1, index):
938 try:
939 self.incomplete_line[sha1].append(self.nodepos[sha1])
940 except KeyError:
941 self.incomplete_line[sha1] = [self.nodepos[sha1]]
942
943
944 def _go_clicked_cb(self, widget, revid):
945 """Callback for when the go button for a parent is clicked."""
946 try:
947 self.treeview.set_cursor(self.index[revid])
948 except KeyError:
949 print "Revision %s not present in the list" % revid
950 # revid == 0 is the parent of the first commit
951 if (revid != 0 ):
952 print "Try running gitview without any options"
953
954 self.treeview.grab_focus()
955
956 def _show_clicked_cb(self, widget, commit_sha1, parent_sha1):
957 """Callback for when the show button for a parent is clicked."""
958 window = DiffWindow()
959 window.set_diff(commit_sha1, parent_sha1)
960 self.treeview.grab_focus()
961
962if __name__ == "__main__":
963 without_diff = 0
964
965 if (len(sys.argv) > 1 ):
966 if (sys.argv[1] == "--without-diff"):
967 without_diff = 1
968
969 view = GitView( without_diff != 1)
970 view.run(sys.argv[without_diff:])
971
972