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.
9 GUI browser for git repository
10 This program is based on bzrk by Scott James Remnant <scott@ubuntu.com>
12 __copyright__
= "Copyright (C) 2006 Hewlett-Packard Development Company, L.P."
13 __author__
= "Aneesh Kumar K.V <aneesh.kumar@hp.com>"
30 have_gtksourceview
= True
32 have_gtksourceview
= False
33 print "Running without gtksourceview module"
35 re_ident
= re
.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
37 def list_to_string(args
, skip
):
42 str_arg
= str_arg
+ args
[i
]
43 str_arg
= str_arg
+ " "
48 def show_date(epoch
, tz
):
50 tzsecs
= float(tz
[1:3]) * 3600
51 tzsecs
+= float(tz
[3:5]) * 60
57 return time
.strftime("%Y-%m-%d %H:%M:%S", time
.gmtime(secs
))
60 class CellRendererGraph(gtk
.GenericCellRenderer
):
61 """Cell renderer for directed graph.
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
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.
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.
78 "node": ( gobject
.TYPE_PYOBJECT
, "node",
79 "revision node instruction",
80 gobject
.PARAM_WRITABLE
82 "in-lines": ( gobject
.TYPE_PYOBJECT
, "in-lines",
83 "instructions to draw lines into the cell",
84 gobject
.PARAM_WRITABLE
86 "out-lines": ( gobject
.TYPE_PYOBJECT
, "out-lines",
87 "instructions to draw lines out of the cell",
88 gobject
.PARAM_WRITABLE
92 def do_set_property(self
, property, value
):
93 """Set properties from GObject properties."""
94 if property.name
== "node":
96 elif property.name
== "in-lines":
98 elif property.name
== "out-lines":
99 self
.out_lines
= value
101 raise AttributeError, "no such property: '%s'" % property.name
103 def box_size(self
, widget
):
104 """Calculate box size based on widget's font.
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.
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
)
116 ascent
= pango
.PIXELS(metrics
.get_ascent())
117 descent
= pango
.PIXELS(metrics
.get_descent())
119 self
._box
_size
= ascent
+ descent
+ 6
120 return self
._box
_size
122 def set_colour(self
, ctx
, colour
, bg
, fg
):
123 """Set the context source colour.
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.
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
144 ctx
.set_source_rgb(red
, green
, blue
)
146 def on_get_size(self
, widget
, cell_area
):
147 """Return the size we need for this cell.
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
153 box_size
= self
.box_size(widget
)
156 for start
, end
, colour
in self
.in_lines
+ self
.out_lines
:
157 cols
= int(max(cols
, start
, end
))
159 (column
, colour
, names
) = self
.node
161 if (len(names
) != 0):
163 names_len
+= len(item
)
165 width
= box_size
* (cols
+ 1 ) + names_len
168 # FIXME I have no idea how to use cell_area properly
169 return (0, 0, width
, height
)
171 def on_render(self
, window
, widget
, bg_area
, cell_area
, exp_area
, flags
):
172 """Render an individual cell.
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
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
185 ctx
= window
.cairo_create()
186 ctx
.rectangle(bg_area
.x
, bg_area
.y
, bg_area
.width
, bg_area
.height
)
189 box_size
= self
.box_size(widget
)
191 ctx
.set_line_width(box_size
/ 8)
192 ctx
.set_line_cap(cairo
.LINE_CAP_SQUARE
)
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)
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
,
205 ctx
.line_to(cell_area
.x
+ box_size
* end
, bg_area
.y
)
207 ctx
.line_to(cell_area
.x
+ box_size
* end
+ box_size
/ 2,
208 bg_area
.y
+ bg_area
.height
/ 2)
210 self
.set_colour(ctx
, colour
, 0.0, 0.65)
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)
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
)
229 ctx
.line_to(cell_area
.x
+ box_size
* end
+ box_size
/ 2,
230 bg_area
.y
+ bg_area
.height
/ 2 + bg_area
.height
)
232 self
.set_colour(ctx
, colour
, 0.0, 0.65)
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
)
242 self
.set_colour(ctx
, colour
, 0.0, 0.5)
243 ctx
.stroke_preserve()
245 self
.set_colour(ctx
, colour
, 0.5, 1.0)
248 if (len(names
) != 0):
251 name
= name
+ item
+ " "
253 ctx
.set_font_size(13)
255 self
.set_colour(ctx
, colour
, 0.5, 1.0)
257 self
.set_colour(ctx
, colour
, 0.0, 0.5)
261 """ This represent a commit object obtained after parsing the git-rev-list
266 def __init__(self
, commit_lines
):
271 self
.commit_date
= ""
272 self
.commit_sha1
= ""
273 self
.parent_sha1
= [ ]
274 self
.parse_commit(commit_lines
)
277 def parse_commit(self
, commit_lines
):
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:]
285 #build the child list
286 for parent_id
in self
.parent_sha1
:
288 Commit
.children_sha1
[parent_id
].append(self
.commit_sha1
)
290 Commit
.children_sha1
[parent_id
] = [self
.commit_sha1
]
292 # IF we don't have parent
293 if (len(self
.parent_sha1
) == 0):
294 self
.parent_sha1
= [0]
296 for line
in commit_lines
[1:]:
297 m
= re
.match("^ ", line
)
299 # First line of the commit message used for short log
300 if self
.message
== "":
301 self
.message
= string
.strip(line
)
304 m
= re
.match("tree", line
)
308 m
= re
.match("parent", line
)
312 m
= re_ident
.match(line
)
314 date
= show_date(m
.group('epoch'), m
.group('tz'))
315 if m
.group(1) == "author":
316 self
.author
= m
.group('ident')
318 elif m
.group(1) == "committer":
319 self
.committer
= m
.group('ident')
320 self
.commit_date
= date
324 def get_message(self
, with_diff
=0):
326 message
= self
.diff_tree()
328 fp
= os
.popen("git cat-file commit " + self
.commit_sha1
)
335 fp
= os
.popen("git diff-tree --pretty --cc -v -p --always " + self
.commit_sha1
)
342 This object represents and manages a single window containing the
343 differences between two revisions on a branch.
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")
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
)
361 """Construct the window contents."""
363 self
.window
.add(vbox
)
366 menu_bar
= gtk
.MenuBar()
367 save_menu
= gtk
.ImageMenuItem(gtk
.STOCK_SAVE
)
368 save_menu
.connect("activate", self
.save_menu_response
, "save")
370 menu_bar
.append(save_menu
)
371 vbox
.pack_start(menu_bar
, expand
=False, fill
=True)
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)
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)
388 self
.buffer = gtk
.TextBuffer()
389 sourceview
= gtk
.TextView(self
.buffer)
391 sourceview
.set_editable(False)
392 sourceview
.modify_font(pango
.FontDescription("Monospace"))
393 scrollwin
.add(sourceview
)
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
402 # Diff with the first commit or the last commit shows nothing
403 if (commit_sha1
== 0 or parent_sha1
== 0 ):
406 fp
= os
.popen("git diff-tree -p " + parent_sha1
+ " " + commit_sha1
)
407 self
.buffer.set_text(unicode(fp
.read(), encoding
).encode('utf-8'))
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
)
426 """ This is the main class
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")
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
)
447 icon
= self
.window
.render_icon(gtk
.STOCK_INDEX
, gtk
.ICON_SIZE_BUTTON
)
448 self
.window
.set_icon(icon
)
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 self
.accel_group
.connect_group(0xffc1, 0, gtk
.ACCEL_LOCKED
, self
.maximize
);
454 self
.accel_group
.connect_group(0xffc8, 0, gtk
.ACCEL_LOCKED
, self
.fullscreen
);
455 self
.accel_group
.connect_group(0xffc9, 0, gtk
.ACCEL_LOCKED
, self
.unfullscreen
);
457 self
.window
.add(self
.construct())
459 def refresh(self
, widget
, event
=None, *arguments
, **keywords
):
462 Commit
.children_sha1
= {}
463 self
.set_branch(sys
.argv
[without_diff
:])
467 def maximize(self
, widget
, event
=None, *arguments
, **keywords
):
468 self
.window
.maximize()
471 def fullscreen(self
, widget
, event
=None, *arguments
, **keywords
):
472 self
.window
.fullscreen()
475 def unfullscreen(self
, widget
, event
=None, *arguments
, **keywords
):
476 self
.window
.unfullscreen()
479 def get_bt_sha1(self
):
480 """ Update the bt_sha1 dictionary with the
481 respective sha1 details """
484 ls_remote
= re
.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
485 fp
= os
.popen('git ls-remote "${GIT_DIR-.git}"')
487 line
= string
.strip(fp
.readline())
490 m
= ls_remote
.match(line
)
493 (sha1
, name
) = (m
.group(1), m
.group(2))
494 if not self
.bt_sha1
.has_key(sha1
):
495 self
.bt_sha1
[sha1
] = []
496 self
.bt_sha1
[sha1
].append(name
)
499 def get_encoding(self
):
500 fp
= os
.popen("git repo-config --get i18n.commitencoding")
501 self
.encoding
=string
.strip(fp
.readline())
503 if (self
.encoding
== ""):
504 self
.encoding
= "utf-8"
508 """Construct the window contents."""
511 paned
.pack1(self
.construct_top(), resize
=False, shrink
=True)
512 paned
.pack2(self
.construct_bottom(), resize
=False, shrink
=True)
513 menu_bar
= gtk
.MenuBar()
514 menu_bar
.set_pack_direction(gtk
.PACK_DIRECTION_RTL
)
515 help_menu
= gtk
.MenuItem("Help")
517 about_menu
= gtk
.MenuItem("About")
518 menu
.append(about_menu
)
519 about_menu
.connect("activate", self
.about_menu_response
, "about")
521 help_menu
.set_submenu(menu
)
523 menu_bar
.append(help_menu
)
525 vbox
.pack_start(menu_bar
, expand
=False, fill
=True)
526 vbox
.pack_start(paned
, expand
=True, fill
=True)
532 def construct_top(self
):
533 """Construct the top-half of the window."""
534 vbox
= gtk
.VBox(spacing
=6)
535 vbox
.set_border_width(12)
539 scrollwin
= gtk
.ScrolledWindow()
540 scrollwin
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
541 scrollwin
.set_shadow_type(gtk
.SHADOW_IN
)
542 vbox
.pack_start(scrollwin
, expand
=True, fill
=True)
545 self
.treeview
= gtk
.TreeView()
546 self
.treeview
.set_rules_hint(True)
547 self
.treeview
.set_search_column(4)
548 self
.treeview
.connect("cursor-changed", self
._treeview
_cursor
_cb
)
549 scrollwin
.add(self
.treeview
)
552 cell
= CellRendererGraph()
553 column
= gtk
.TreeViewColumn()
554 column
.set_resizable(True)
555 column
.pack_start(cell
, expand
=True)
556 column
.add_attribute(cell
, "node", 1)
557 column
.add_attribute(cell
, "in-lines", 2)
558 column
.add_attribute(cell
, "out-lines", 3)
559 self
.treeview
.append_column(column
)
561 cell
= gtk
.CellRendererText()
562 cell
.set_property("width-chars", 65)
563 cell
.set_property("ellipsize", pango
.ELLIPSIZE_END
)
564 column
= gtk
.TreeViewColumn("Message")
565 column
.set_resizable(True)
566 column
.pack_start(cell
, expand
=True)
567 column
.add_attribute(cell
, "text", 4)
568 self
.treeview
.append_column(column
)
570 cell
= gtk
.CellRendererText()
571 cell
.set_property("width-chars", 40)
572 cell
.set_property("ellipsize", pango
.ELLIPSIZE_END
)
573 column
= gtk
.TreeViewColumn("Author")
574 column
.set_resizable(True)
575 column
.pack_start(cell
, expand
=True)
576 column
.add_attribute(cell
, "text", 5)
577 self
.treeview
.append_column(column
)
579 cell
= gtk
.CellRendererText()
580 cell
.set_property("ellipsize", pango
.ELLIPSIZE_END
)
581 column
= gtk
.TreeViewColumn("Date")
582 column
.set_resizable(True)
583 column
.pack_start(cell
, expand
=True)
584 column
.add_attribute(cell
, "text", 6)
585 self
.treeview
.append_column(column
)
589 def about_menu_response(self
, widget
, string
):
590 dialog
= gtk
.AboutDialog()
591 dialog
.set_name("Gitview")
592 dialog
.set_version(GitView
.version
)
593 dialog
.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
594 dialog
.set_website("http://www.kernel.org/pub/software/scm/git/")
595 dialog
.set_copyright("Use and distribute under the terms of the GNU General Public License")
596 dialog
.set_wrap_license(True)
601 def construct_bottom(self
):
602 """Construct the bottom half of the window."""
603 vbox
= gtk
.VBox(False, spacing
=6)
604 vbox
.set_border_width(12)
605 (width
, height
) = self
.window
.get_size()
606 vbox
.set_size_request(width
, int(height
/ 2.5))
609 self
.table
= gtk
.Table(rows
=4, columns
=4)
610 self
.table
.set_row_spacings(6)
611 self
.table
.set_col_spacings(6)
612 vbox
.pack_start(self
.table
, expand
=False, fill
=True)
615 align
= gtk
.Alignment(0.0, 0.5)
617 label
.set_markup("<b>Revision:</b>")
619 self
.table
.attach(align
, 0, 1, 0, 1, gtk
.FILL
, gtk
.FILL
)
623 align
= gtk
.Alignment(0.0, 0.5)
624 self
.revid_label
= gtk
.Label()
625 self
.revid_label
.set_selectable(True)
626 align
.add(self
.revid_label
)
627 self
.table
.attach(align
, 1, 2, 0, 1, gtk
.EXPAND | gtk
.FILL
, gtk
.FILL
)
628 self
.revid_label
.show()
631 align
= gtk
.Alignment(0.0, 0.5)
633 label
.set_markup("<b>Committer:</b>")
635 self
.table
.attach(align
, 0, 1, 1, 2, gtk
.FILL
, gtk
.FILL
)
639 align
= gtk
.Alignment(0.0, 0.5)
640 self
.committer_label
= gtk
.Label()
641 self
.committer_label
.set_selectable(True)
642 align
.add(self
.committer_label
)
643 self
.table
.attach(align
, 1, 2, 1, 2, gtk
.EXPAND | gtk
.FILL
, gtk
.FILL
)
644 self
.committer_label
.show()
647 align
= gtk
.Alignment(0.0, 0.5)
649 label
.set_markup("<b>Timestamp:</b>")
651 self
.table
.attach(align
, 0, 1, 2, 3, gtk
.FILL
, gtk
.FILL
)
655 align
= gtk
.Alignment(0.0, 0.5)
656 self
.timestamp_label
= gtk
.Label()
657 self
.timestamp_label
.set_selectable(True)
658 align
.add(self
.timestamp_label
)
659 self
.table
.attach(align
, 1, 2, 2, 3, gtk
.EXPAND | gtk
.FILL
, gtk
.FILL
)
660 self
.timestamp_label
.show()
663 align
= gtk
.Alignment(0.0, 0.5)
665 label
.set_markup("<b>Parents:</b>")
667 self
.table
.attach(align
, 0, 1, 3, 4, gtk
.FILL
, gtk
.FILL
)
670 self
.parents_widgets
= []
672 align
= gtk
.Alignment(0.0, 0.5)
674 label
.set_markup("<b>Children:</b>")
676 self
.table
.attach(align
, 2, 3, 3, 4, gtk
.FILL
, gtk
.FILL
)
679 self
.children_widgets
= []
681 scrollwin
= gtk
.ScrolledWindow()
682 scrollwin
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
683 scrollwin
.set_shadow_type(gtk
.SHADOW_IN
)
684 vbox
.pack_start(scrollwin
, expand
=True, fill
=True)
687 if have_gtksourceview
:
688 self
.message_buffer
= gtksourceview
.SourceBuffer()
689 slm
= gtksourceview
.SourceLanguagesManager()
690 gsl
= slm
.get_language_from_mime_type("text/x-patch")
691 self
.message_buffer
.set_highlight(True)
692 self
.message_buffer
.set_language(gsl
)
693 sourceview
= gtksourceview
.SourceView(self
.message_buffer
)
695 self
.message_buffer
= gtk
.TextBuffer()
696 sourceview
= gtk
.TextView(self
.message_buffer
)
698 sourceview
.set_editable(False)
699 sourceview
.modify_font(pango
.FontDescription("Monospace"))
700 scrollwin
.add(sourceview
)
705 def _treeview_cursor_cb(self
, *args
):
706 """Callback for when the treeview cursor changes."""
707 (path
, col
) = self
.treeview
.get_cursor()
708 commit
= self
.model
[path
][0]
710 if commit
.committer
is not None:
711 committer
= commit
.committer
712 timestamp
= commit
.commit_date
713 message
= commit
.get_message(self
.with_diff
)
714 revid_label
= commit
.commit_sha1
721 self
.revid_label
.set_text(revid_label
)
722 self
.committer_label
.set_text(committer
)
723 self
.timestamp_label
.set_text(timestamp
)
724 self
.message_buffer
.set_text(unicode(message
, self
.encoding
).encode('utf-8'))
726 for widget
in self
.parents_widgets
:
727 self
.table
.remove(widget
)
729 self
.parents_widgets
= []
730 self
.table
.resize(4 + len(commit
.parent_sha1
) - 1, 4)
731 for idx
, parent_id
in enumerate(commit
.parent_sha1
):
732 self
.table
.set_row_spacing(idx
+ 3, 0)
734 align
= gtk
.Alignment(0.0, 0.0)
735 self
.parents_widgets
.append(align
)
736 self
.table
.attach(align
, 1, 2, idx
+ 3, idx
+ 4,
737 gtk
.EXPAND | gtk
.FILL
, gtk
.FILL
)
740 hbox
= gtk
.HBox(False, 0)
744 label
= gtk
.Label(parent_id
)
745 label
.set_selectable(True)
746 hbox
.pack_start(label
, expand
=False, fill
=True)
750 image
.set_from_stock(gtk
.STOCK_JUMP_TO
, gtk
.ICON_SIZE_MENU
)
753 button
= gtk
.Button()
755 button
.set_relief(gtk
.RELIEF_NONE
)
756 button
.connect("clicked", self
._go
_clicked
_cb
, parent_id
)
757 hbox
.pack_start(button
, expand
=False, fill
=True)
761 image
.set_from_stock(gtk
.STOCK_FIND
, gtk
.ICON_SIZE_MENU
)
764 button
= gtk
.Button()
766 button
.set_relief(gtk
.RELIEF_NONE
)
767 button
.set_sensitive(True)
768 button
.connect("clicked", self
._show
_clicked
_cb
,
769 commit
.commit_sha1
, parent_id
, self
.encoding
)
770 hbox
.pack_start(button
, expand
=False, fill
=True)
773 # Populate with child details
774 for widget
in self
.children_widgets
:
775 self
.table
.remove(widget
)
777 self
.children_widgets
= []
779 child_sha1
= Commit
.children_sha1
[commit
.commit_sha1
]
781 # We don't have child
784 if ( len(child_sha1
) > len(commit
.parent_sha1
)):
785 self
.table
.resize(4 + len(child_sha1
) - 1, 4)
787 for idx
, child_id
in enumerate(child_sha1
):
788 self
.table
.set_row_spacing(idx
+ 3, 0)
790 align
= gtk
.Alignment(0.0, 0.0)
791 self
.children_widgets
.append(align
)
792 self
.table
.attach(align
, 3, 4, idx
+ 3, idx
+ 4,
793 gtk
.EXPAND | gtk
.FILL
, gtk
.FILL
)
796 hbox
= gtk
.HBox(False, 0)
800 label
= gtk
.Label(child_id
)
801 label
.set_selectable(True)
802 hbox
.pack_start(label
, expand
=False, fill
=True)
806 image
.set_from_stock(gtk
.STOCK_JUMP_TO
, gtk
.ICON_SIZE_MENU
)
809 button
= gtk
.Button()
811 button
.set_relief(gtk
.RELIEF_NONE
)
812 button
.connect("clicked", self
._go
_clicked
_cb
, child_id
)
813 hbox
.pack_start(button
, expand
=False, fill
=True)
817 image
.set_from_stock(gtk
.STOCK_FIND
, gtk
.ICON_SIZE_MENU
)
820 button
= gtk
.Button()
822 button
.set_relief(gtk
.RELIEF_NONE
)
823 button
.set_sensitive(True)
824 button
.connect("clicked", self
._show
_clicked
_cb
,
825 child_id
, commit
.commit_sha1
, self
.encoding
)
826 hbox
.pack_start(button
, expand
=False, fill
=True)
829 def _destroy_cb(self
, widget
):
830 """Callback for when a window we manage is destroyed."""
835 """Stop the GTK+ main loop."""
839 self
.set_branch(args
)
840 self
.window
.connect("destroy", self
._destroy
_cb
)
844 def set_branch(self
, args
):
845 """Fill in different windows with info from the reposiroty"""
846 fp
= os
.popen("git rev-parse --sq --default HEAD " + list_to_string(args
, 1))
847 git_rev_list_cmd
= fp
.read()
849 fp
= os
.popen("git rev-list --header --topo-order --parents " + git_rev_list_cmd
)
850 self
.update_window(fp
)
852 def update_window(self
, fp
):
855 self
.model
= gtk
.ListStore(gobject
.TYPE_PYOBJECT
, gobject
.TYPE_PYOBJECT
,
856 gobject
.TYPE_PYOBJECT
, gobject
.TYPE_PYOBJECT
, str, str, str)
858 # used for cursor positioning
863 self
.incomplete_line
= {}
870 input_line
= fp
.readline()
871 while (input_line
!= ""):
872 # The commit header ends with '\0'
873 # This NULL is immediately followed by the sha1 of the
875 if (input_line
[0] != '\0'):
876 commit_lines
.append(input_line
)
877 input_line
= fp
.readline()
880 commit
= Commit(commit_lines
)
881 if (commit
!= None ):
882 self
.commits
.append(commit
)
886 commit_lines
.append(input_line
[1:])
887 input_line
= fp
.readline()
891 for commit
in self
.commits
:
892 (out_line
, last_colour
, last_nodepos
) = self
.draw_graph(commit
,
896 self
.index
[commit
.commit_sha1
] = index
899 self
.treeview
.set_model(self
.model
)
902 def draw_graph(self
, commit
, index
, out_line
, last_colour
, last_nodepos
):
910 if (last_nodepos
> 5):
913 # Add the incomplete lines of the last cell in this
915 colour
= self
.colours
[commit
.commit_sha1
]
917 self
.colours
[commit
.commit_sha1
] = last_colour
+1
918 last_colour
= self
.colours
[commit
.commit_sha1
]
919 colour
= self
.colours
[commit
.commit_sha1
]
922 node_pos
= self
.nodepos
[commit
.commit_sha1
]
924 self
.nodepos
[commit
.commit_sha1
] = last_nodepos
+1
925 last_nodepos
= self
.nodepos
[commit
.commit_sha1
]
926 node_pos
= self
.nodepos
[commit
.commit_sha1
]
928 #The first parent always continue on the same line
930 # check we alreay have the value
931 tmp_node_pos
= self
.nodepos
[commit
.parent_sha1
[0]]
933 self
.colours
[commit
.parent_sha1
[0]] = colour
934 self
.nodepos
[commit
.parent_sha1
[0]] = node_pos
936 for sha1
in self
.incomplete_line
.keys():
937 if (sha1
!= commit
.commit_sha1
):
938 self
.draw_incomplete_line(sha1
, node_pos
,
939 out_line
, in_line
, index
)
941 del self
.incomplete_line
[sha1
]
944 for parent_id
in commit
.parent_sha1
:
946 tmp_node_pos
= self
.nodepos
[parent_id
]
948 self
.colours
[parent_id
] = last_colour
+1
949 last_colour
= self
.colours
[parent_id
]
950 self
.nodepos
[parent_id
] = last_nodepos
+1
951 last_nodepos
= self
.nodepos
[parent_id
]
953 in_line
.append((node_pos
, self
.nodepos
[parent_id
],
954 self
.colours
[parent_id
]))
955 self
.add_incomplete_line(parent_id
)
958 branch_tag
= self
.bt_sha1
[commit
.commit_sha1
]
963 node
= (node_pos
, colour
, branch_tag
)
965 self
.model
.append([commit
, node
, out_line
, in_line
,
966 commit
.message
, commit
.author
, commit
.date
])
968 return (in_line
, last_colour
, last_nodepos
)
970 def add_incomplete_line(self
, sha1
):
972 self
.incomplete_line
[sha1
].append(self
.nodepos
[sha1
])
974 self
.incomplete_line
[sha1
] = [self
.nodepos
[sha1
]]
976 def draw_incomplete_line(self
, sha1
, node_pos
, out_line
, in_line
, index
):
977 for idx
, pos
in enumerate(self
.incomplete_line
[sha1
]):
979 #remove the straight line and add a slash
980 if ((pos
, pos
, self
.colours
[sha1
]) in out_line
):
981 out_line
.remove((pos
, pos
, self
.colours
[sha1
]))
982 out_line
.append((pos
, pos
+0.5, self
.colours
[sha1
]))
983 self
.incomplete_line
[sha1
][idx
] = pos
= pos
+0.5
985 next_commit
= self
.commits
[index
+1]
986 if (next_commit
.commit_sha1
== sha1
and pos
!= int(pos
)):
987 # join the line back to the node point
988 # This need to be done only if we modified it
989 in_line
.append((pos
, pos
-0.5, self
.colours
[sha1
]))
993 in_line
.append((pos
, pos
, self
.colours
[sha1
]))
996 def _go_clicked_cb(self
, widget
, revid
):
997 """Callback for when the go button for a parent is clicked."""
999 self
.treeview
.set_cursor(self
.index
[revid
])
1001 dialog
= gtk
.MessageDialog(parent
=None, flags
=0,
1002 type=gtk
.MESSAGE_WARNING
, buttons
=gtk
.BUTTONS_CLOSE
,
1003 message_format
=None)
1004 dialog
.set_markup("Revision <b>%s</b> not present in the list" % revid
)
1005 # revid == 0 is the parent of the first commit
1007 dialog
.format_secondary_text("Try running gitview without any options")
1011 self
.treeview
.grab_focus()
1013 def _show_clicked_cb(self
, widget
, commit_sha1
, parent_sha1
, encoding
):
1014 """Callback for when the show button for a parent is clicked."""
1015 window
= DiffWindow()
1016 window
.set_diff(commit_sha1
, parent_sha1
, encoding
)
1017 self
.treeview
.grab_focus()
1020 if __name__
== "__main__":
1022 if (len(sys
.argv
) > 1 ):
1023 if (sys
.argv
[1] == "--without-diff"):
1026 view
= GitView( without_diff
!= 1)
1027 view
.run(sys
.argv
[without_diff
:])