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