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