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