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 __copyright__
= "Copyright (C) 2007 Aneesh Kumar K.V <aneesh.kumar@gmail.com"
14 __author__
= "Aneesh Kumar K.V <aneesh.kumar@gmail.com>"
30 have_gtksourceview2
= False
31 have_gtksourceview
= False
34 have_gtksourceview2
= True
38 have_gtksourceview
= True
40 print "Running without gtksourceview2 or gtksourceview module"
42 re_ident
= re
.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
44 def list_to_string(args
, skip
):
49 str_arg
= str_arg
+ args
[i
]
50 str_arg
= str_arg
+ " "
55 def show_date(epoch
, tz
):
57 tzsecs
= float(tz
[1:3]) * 3600
58 tzsecs
+= float(tz
[3:5]) * 60
64 return time
.strftime("%Y-%m-%d %H:%M:%S", time
.gmtime(secs
))
66 def get_source_buffer_and_view():
67 if have_gtksourceview2
:
68 buffer = gtksourceview2
.Buffer()
69 slm
= gtksourceview2
.LanguageManager()
70 gsl
= slm
.get_language("diff")
71 buffer.set_highlight_syntax(True)
72 buffer.set_language(gsl
)
73 view
= gtksourceview2
.View(buffer)
74 elif have_gtksourceview
:
75 buffer = gtksourceview
.SourceBuffer()
76 slm
= gtksourceview
.SourceLanguagesManager()
77 gsl
= slm
.get_language_from_mime_type("text/x-patch")
78 buffer.set_highlight(True)
79 buffer.set_language(gsl
)
80 view
= gtksourceview
.SourceView(buffer)
82 buffer = gtk
.TextBuffer()
83 view
= gtk
.TextView(buffer)
87 class CellRendererGraph(gtk
.GenericCellRenderer
):
88 """Cell renderer for directed graph.
90 This module contains the implementation of a custom GtkCellRenderer that
91 draws part of the directed graph based on the lines suggested by the code
94 Because we're shiny, we use Cairo to do this, and because we're naughty
95 we cheat and draw over the bits of the TreeViewColumn that are supposed to
96 just be for the background.
99 node (column, colour, [ names ]) tuple to draw revision node,
100 in_lines (start, end, colour) tuple list to draw inward lines,
101 out_lines (start, end, colour) tuple list to draw outward lines.
105 "node": ( gobject
.TYPE_PYOBJECT
, "node",
106 "revision node instruction",
107 gobject
.PARAM_WRITABLE
109 "in-lines": ( gobject
.TYPE_PYOBJECT
, "in-lines",
110 "instructions to draw lines into the cell",
111 gobject
.PARAM_WRITABLE
113 "out-lines": ( gobject
.TYPE_PYOBJECT
, "out-lines",
114 "instructions to draw lines out of the cell",
115 gobject
.PARAM_WRITABLE
119 def do_set_property(self
, property, value
):
120 """Set properties from GObject properties."""
121 if property.name
== "node":
123 elif property.name
== "in-lines":
124 self
.in_lines
= value
125 elif property.name
== "out-lines":
126 self
.out_lines
= value
128 raise AttributeError, "no such property: '%s'" % property.name
130 def box_size(self
, widget
):
131 """Calculate box size based on widget's font.
133 Cache this as it's probably expensive to get. It ensures that we
134 draw the graph at least as large as the text.
137 return self
._box
_size
138 except AttributeError:
139 pango_ctx
= widget
.get_pango_context()
140 font_desc
= widget
.get_style().font_desc
141 metrics
= pango_ctx
.get_metrics(font_desc
)
143 ascent
= pango
.PIXELS(metrics
.get_ascent())
144 descent
= pango
.PIXELS(metrics
.get_descent())
146 self
._box
_size
= ascent
+ descent
+ 6
147 return self
._box
_size
149 def set_colour(self
, ctx
, colour
, bg
, fg
):
150 """Set the context source colour.
152 Picks a distinct colour based on an internal wheel; the bg
153 parameter provides the value that should be assigned to the 'zero'
154 colours and the fg parameter provides the multiplier that should be
155 applied to the foreground colours.
166 colour
%= len(colours
)
167 red
= (colours
[colour
][0] * fg
) or bg
168 green
= (colours
[colour
][1] * fg
) or bg
169 blue
= (colours
[colour
][2] * fg
) or bg
171 ctx
.set_source_rgb(red
, green
, blue
)
173 def on_get_size(self
, widget
, cell_area
):
174 """Return the size we need for this cell.
176 Each cell is drawn individually and is only as wide as it needs
177 to be, we let the TreeViewColumn take care of making them all
180 box_size
= self
.box_size(widget
)
183 for start
, end
, colour
in self
.in_lines
+ self
.out_lines
:
184 cols
= int(max(cols
, start
, end
))
186 (column
, colour
, names
) = self
.node
188 if (len(names
) != 0):
190 names_len
+= len(item
)
192 width
= box_size
* (cols
+ 1 ) + names_len
195 # FIXME I have no idea how to use cell_area properly
196 return (0, 0, width
, height
)
198 def on_render(self
, window
, widget
, bg_area
, cell_area
, exp_area
, flags
):
199 """Render an individual cell.
201 Draws the cell contents using cairo, taking care to clip what we
202 do to within the background area so we don't draw over other cells.
203 Note that we're a bit naughty there and should really be drawing
204 in the cell_area (or even the exposed area), but we explicitly don't
207 We try and be a little clever, if the line we need to draw is going
208 to cross other columns we actually draw it as in the .---' style
209 instead of a pure diagonal ... this reduces confusion by an
212 ctx
= window
.cairo_create()
213 ctx
.rectangle(bg_area
.x
, bg_area
.y
, bg_area
.width
, bg_area
.height
)
216 box_size
= self
.box_size(widget
)
218 ctx
.set_line_width(box_size
/ 8)
219 ctx
.set_line_cap(cairo
.LINE_CAP_SQUARE
)
221 # Draw lines into the cell
222 for start
, end
, colour
in self
.in_lines
:
223 ctx
.move_to(cell_area
.x
+ box_size
* start
+ box_size
/ 2,
224 bg_area
.y
- bg_area
.height
/ 2)
227 ctx
.line_to(cell_area
.x
+ box_size
* start
, bg_area
.y
)
228 ctx
.line_to(cell_area
.x
+ box_size
* end
+ box_size
, bg_area
.y
)
229 elif start
- end
< -1:
230 ctx
.line_to(cell_area
.x
+ box_size
* start
+ box_size
,
232 ctx
.line_to(cell_area
.x
+ box_size
* end
, bg_area
.y
)
234 ctx
.line_to(cell_area
.x
+ box_size
* end
+ box_size
/ 2,
235 bg_area
.y
+ bg_area
.height
/ 2)
237 self
.set_colour(ctx
, colour
, 0.0, 0.65)
240 # Draw lines out of the cell
241 for start
, end
, colour
in self
.out_lines
:
242 ctx
.move_to(cell_area
.x
+ box_size
* start
+ box_size
/ 2,
243 bg_area
.y
+ bg_area
.height
/ 2)
246 ctx
.line_to(cell_area
.x
+ box_size
* start
,
247 bg_area
.y
+ bg_area
.height
)
248 ctx
.line_to(cell_area
.x
+ box_size
* end
+ box_size
,
249 bg_area
.y
+ bg_area
.height
)
250 elif start
- end
< -1:
251 ctx
.line_to(cell_area
.x
+ box_size
* start
+ box_size
,
252 bg_area
.y
+ bg_area
.height
)
253 ctx
.line_to(cell_area
.x
+ box_size
* end
,
254 bg_area
.y
+ bg_area
.height
)
256 ctx
.line_to(cell_area
.x
+ box_size
* end
+ box_size
/ 2,
257 bg_area
.y
+ bg_area
.height
/ 2 + bg_area
.height
)
259 self
.set_colour(ctx
, colour
, 0.0, 0.65)
262 # Draw the revision node in the right column
263 (column
, colour
, names
) = self
.node
264 ctx
.arc(cell_area
.x
+ box_size
* column
+ box_size
/ 2,
265 cell_area
.y
+ cell_area
.height
/ 2,
266 box_size
/ 4, 0, 2 * math
.pi
)
269 self
.set_colour(ctx
, colour
, 0.0, 0.5)
270 ctx
.stroke_preserve()
272 self
.set_colour(ctx
, colour
, 0.5, 1.0)
275 if (len(names
) != 0):
278 name
= name
+ item
+ " "
280 ctx
.set_font_size(13)
282 self
.set_colour(ctx
, colour
, 0.5, 1.0)
284 self
.set_colour(ctx
, colour
, 0.0, 0.5)
287 class Commit(object):
288 """ This represent a commit object obtained after parsing the git-rev-list
291 __slots__
= ['children_sha1', 'message', 'author', 'date', 'committer',
292 'commit_date', 'commit_sha1', 'parent_sha1']
296 def __init__(self
, commit_lines
):
301 self
.commit_date
= ""
302 self
.commit_sha1
= ""
303 self
.parent_sha1
= [ ]
304 self
.parse_commit(commit_lines
)
307 def parse_commit(self
, commit_lines
):
309 # First line is the sha1 lines
310 line
= string
.strip(commit_lines
[0])
311 sha1
= re
.split(" ", line
)
312 self
.commit_sha1
= sha1
[0]
313 self
.parent_sha1
= sha1
[1:]
315 #build the child list
316 for parent_id
in self
.parent_sha1
:
318 Commit
.children_sha1
[parent_id
].append(self
.commit_sha1
)
320 Commit
.children_sha1
[parent_id
] = [self
.commit_sha1
]
322 # IF we don't have parent
323 if (len(self
.parent_sha1
) == 0):
324 self
.parent_sha1
= [0]
326 for line
in commit_lines
[1:]:
327 m
= re
.match("^ ", line
)
329 # First line of the commit message used for short log
330 if self
.message
== "":
331 self
.message
= string
.strip(line
)
334 m
= re
.match("tree", line
)
338 m
= re
.match("parent", line
)
342 m
= re_ident
.match(line
)
344 date
= show_date(m
.group('epoch'), m
.group('tz'))
345 if m
.group(1) == "author":
346 self
.author
= m
.group('ident')
348 elif m
.group(1) == "committer":
349 self
.committer
= m
.group('ident')
350 self
.commit_date
= date
354 def get_message(self
, with_diff
=0):
356 message
= self
.diff_tree()
358 fp
= os
.popen("git cat-file commit " + self
.commit_sha1
)
365 fp
= os
.popen("git diff-tree --pretty --cc -v -p --always " + self
.commit_sha1
)
370 class AnnotateWindow(object):
372 This object represents and manages a single window containing the
373 annotate information of the file
377 self
.window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
378 self
.window
.set_border_width(0)
379 self
.window
.set_title("Git repository browser annotation window")
382 # Use two thirds of the screen by default
383 screen
= self
.window
.get_screen()
384 monitor
= screen
.get_monitor_geometry(0)
385 width
= int(monitor
.width
* 0.66)
386 height
= int(monitor
.height
* 0.66)
387 self
.window
.set_default_size(width
, height
)
389 def add_file_data(self
, filename
, commit_sha1
, line_num
):
390 fp
= os
.popen("git cat-file blob " + commit_sha1
+":"+filename
)
392 for line
in fp
.readlines():
393 line
= string
.rstrip(line
)
394 self
.model
.append(None, ["HEAD", filename
, line
, i
])
398 # now set the cursor position
399 self
.treeview
.set_cursor(line_num
-1)
400 self
.treeview
.grab_focus()
402 def _treeview_cursor_cb(self
, *args
):
403 """Callback for when the treeview cursor changes."""
404 (path
, col
) = self
.treeview
.get_cursor()
405 commit_sha1
= self
.model
[path
][0]
407 fp
= os
.popen("git cat-file commit " + commit_sha1
)
408 for line
in fp
.readlines():
409 commit_msg
= commit_msg
+ line
412 self
.commit_buffer
.set_text(commit_msg
)
414 def _treeview_row_activated(self
, *args
):
415 """Callback for when the treeview row gets selected."""
416 (path
, col
) = self
.treeview
.get_cursor()
417 commit_sha1
= self
.model
[path
][0]
418 filename
= self
.model
[path
][1]
419 line_num
= self
.model
[path
][3]
421 window
= AnnotateWindow();
422 fp
= os
.popen("git rev-parse "+ commit_sha1
+ "~1")
423 commit_sha1
= string
.strip(fp
.readline())
425 window
.annotate(filename
, commit_sha1
, line_num
)
427 def data_ready(self
, source
, condition
):
430 # A simple readline doesn't work
432 buffer = source
.read(100)
435 # resource temporary not available
438 if (len(buffer) == 0):
439 gobject
.source_remove(self
.io_watch_tag
)
443 if (self
.prev_read
!= ""):
444 buffer = self
.prev_read
+ buffer
447 if (buffer[len(buffer) -1] != '\n'):
449 newline_index
= buffer.rindex("\n")
453 self
.prev_read
= buffer[newline_index
:(len(buffer))]
454 buffer = buffer[0:newline_index
]
456 for buff
in buffer.split("\n"):
457 annotate_line
= re
.compile('^([0-9a-f]{40}) (.+) (.+) (.+)$')
458 m
= annotate_line
.match(buff
)
460 annotate_line
= re
.compile('^(filename) (.+)$')
461 m
= annotate_line
.match(buff
)
464 filename
= m
.group(2)
466 self
.commit_sha1
= m
.group(1)
467 self
.source_line
= int(m
.group(2))
468 self
.result_line
= int(m
.group(3))
469 self
.count
= int(m
.group(4))
470 #set the details only when we have the file name
473 while (self
.count
> 0):
474 # set at result_line + count-1 the sha1 as commit_sha1
475 self
.count
= self
.count
- 1
476 iter = self
.model
.iter_nth_child(None, self
.result_line
+ self
.count
-1)
477 self
.model
.set(iter, 0, self
.commit_sha1
, 1, filename
, 3, self
.source_line
)
480 def annotate(self
, filename
, commit_sha1
, line_num
):
481 # verify the commit_sha1 specified has this filename
483 fp
= os
.popen("git ls-tree "+ commit_sha1
+ " -- " + filename
)
484 line
= string
.strip(fp
.readline())
486 # pop up the message the file is not there as a part of the commit
488 dialog
= gtk
.MessageDialog(parent
=None, flags
=0,
489 type=gtk
.MESSAGE_WARNING
, buttons
=gtk
.BUTTONS_CLOSE
,
491 dialog
.set_markup("The file %s is not present in the parent commit %s" % (filename
, commit_sha1
))
499 self
.window
.add(vpan
);
502 scrollwin
= gtk
.ScrolledWindow()
503 scrollwin
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
504 scrollwin
.set_shadow_type(gtk
.SHADOW_IN
)
505 vpan
.pack1(scrollwin
, True, True);
508 self
.model
= gtk
.TreeStore(str, str, str, int)
509 self
.treeview
= gtk
.TreeView(self
.model
)
510 self
.treeview
.set_rules_hint(True)
511 self
.treeview
.set_search_column(0)
512 self
.treeview
.connect("cursor-changed", self
._treeview
_cursor
_cb
)
513 self
.treeview
.connect("row-activated", self
._treeview
_row
_activated
)
514 scrollwin
.add(self
.treeview
)
517 cell
= gtk
.CellRendererText()
518 cell
.set_property("width-chars", 10)
519 cell
.set_property("ellipsize", pango
.ELLIPSIZE_END
)
520 column
= gtk
.TreeViewColumn("Commit")
521 column
.set_resizable(True)
522 column
.pack_start(cell
, expand
=True)
523 column
.add_attribute(cell
, "text", 0)
524 self
.treeview
.append_column(column
)
526 cell
= gtk
.CellRendererText()
527 cell
.set_property("width-chars", 20)
528 cell
.set_property("ellipsize", pango
.ELLIPSIZE_END
)
529 column
= gtk
.TreeViewColumn("File Name")
530 column
.set_resizable(True)
531 column
.pack_start(cell
, expand
=True)
532 column
.add_attribute(cell
, "text", 1)
533 self
.treeview
.append_column(column
)
535 cell
= gtk
.CellRendererText()
536 cell
.set_property("width-chars", 20)
537 cell
.set_property("ellipsize", pango
.ELLIPSIZE_END
)
538 column
= gtk
.TreeViewColumn("Data")
539 column
.set_resizable(True)
540 column
.pack_start(cell
, expand
=True)
541 column
.add_attribute(cell
, "text", 2)
542 self
.treeview
.append_column(column
)
544 # The commit message window
545 scrollwin
= gtk
.ScrolledWindow()
546 scrollwin
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
547 scrollwin
.set_shadow_type(gtk
.SHADOW_IN
)
548 vpan
.pack2(scrollwin
, True, True);
551 commit_text
= gtk
.TextView()
552 self
.commit_buffer
= gtk
.TextBuffer()
553 commit_text
.set_buffer(self
.commit_buffer
)
554 scrollwin
.add(commit_text
)
559 self
.add_file_data(filename
, commit_sha1
, line_num
)
561 fp
= os
.popen("git blame --incremental -C -C -- " + filename
+ " " + commit_sha1
)
562 flags
= fcntl
.fcntl(fp
.fileno(), fcntl
.F_GETFL
)
563 fcntl
.fcntl(fp
.fileno(), fcntl
.F_SETFL
, flags | os
.O_NONBLOCK
)
564 self
.io_watch_tag
= gobject
.io_add_watch(fp
, gobject
.IO_IN
, self
.data_ready
)
567 class DiffWindow(object):
569 This object represents and manages a single window containing the
570 differences between two revisions on a branch.
574 self
.window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
575 self
.window
.set_border_width(0)
576 self
.window
.set_title("Git repository browser diff window")
578 # Use two thirds of the screen by default
579 screen
= self
.window
.get_screen()
580 monitor
= screen
.get_monitor_geometry(0)
581 width
= int(monitor
.width
* 0.66)
582 height
= int(monitor
.height
* 0.66)
583 self
.window
.set_default_size(width
, height
)
589 """Construct the window contents."""
591 self
.window
.add(vbox
)
594 menu_bar
= gtk
.MenuBar()
595 save_menu
= gtk
.ImageMenuItem(gtk
.STOCK_SAVE
)
596 save_menu
.connect("activate", self
.save_menu_response
, "save")
598 menu_bar
.append(save_menu
)
599 vbox
.pack_start(menu_bar
, expand
=False, fill
=True)
604 scrollwin
= gtk
.ScrolledWindow()
605 scrollwin
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
606 scrollwin
.set_shadow_type(gtk
.SHADOW_IN
)
607 hpan
.pack1(scrollwin
, True, True)
610 (self
.buffer, sourceview
) = get_source_buffer_and_view()
612 sourceview
.set_editable(False)
613 sourceview
.modify_font(pango
.FontDescription("Monospace"))
614 scrollwin
.add(sourceview
)
617 # The file hierarchy: a scrollable treeview
618 scrollwin
= gtk
.ScrolledWindow()
619 scrollwin
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
620 scrollwin
.set_shadow_type(gtk
.SHADOW_IN
)
621 scrollwin
.set_size_request(20, -1)
622 hpan
.pack2(scrollwin
, True, True)
625 self
.model
= gtk
.TreeStore(str, str, str)
626 self
.treeview
= gtk
.TreeView(self
.model
)
627 self
.treeview
.set_search_column(1)
628 self
.treeview
.connect("cursor-changed", self
._treeview
_clicked
)
629 scrollwin
.add(self
.treeview
)
632 cell
= gtk
.CellRendererText()
633 cell
.set_property("width-chars", 20)
634 column
= gtk
.TreeViewColumn("Select to annotate")
635 column
.pack_start(cell
, expand
=True)
636 column
.add_attribute(cell
, "text", 0)
637 self
.treeview
.append_column(column
)
639 vbox
.pack_start(hpan
, expand
=True, fill
=True)
642 def _treeview_clicked(self
, *args
):
643 """Callback for when the treeview cursor changes."""
644 (path
, col
) = self
.treeview
.get_cursor()
645 specific_file
= self
.model
[path
][1]
646 commit_sha1
= self
.model
[path
][2]
647 if specific_file
== None :
649 elif specific_file
== "" :
652 window
= AnnotateWindow();
653 window
.annotate(specific_file
, commit_sha1
, 1)
656 def commit_files(self
, commit_sha1
, parent_sha1
):
658 add
= self
.model
.append(None, [ "Added", None, None])
659 dele
= self
.model
.append(None, [ "Deleted", None, None])
660 mod
= self
.model
.append(None, [ "Modified", None, None])
661 diff_tree
= re
.compile('^(:.{6}) (.{6}) (.{40}) (.{40}) (A|D|M)\s(.+)$')
662 fp
= os
.popen("git diff-tree -r --no-commit-id " + parent_sha1
+ " " + commit_sha1
)
664 line
= string
.strip(fp
.readline())
667 m
= diff_tree
.match(line
)
672 filename
= m
.group(6)
674 self
.model
.append(add
, [filename
, filename
, commit_sha1
])
676 self
.model
.append(dele
, [filename
, filename
, commit_sha1
])
678 self
.model
.append(mod
, [filename
, filename
, commit_sha1
])
681 self
.treeview
.expand_all()
683 def set_diff(self
, commit_sha1
, parent_sha1
, encoding
):
684 """Set the differences showed by this window.
685 Compares the two trees and populates the window with the
688 # Diff with the first commit or the last commit shows nothing
689 if (commit_sha1
== 0 or parent_sha1
== 0 ):
692 fp
= os
.popen("git diff-tree -p " + parent_sha1
+ " " + commit_sha1
)
693 self
.buffer.set_text(unicode(fp
.read(), encoding
).encode('utf-8'))
695 self
.commit_files(commit_sha1
, parent_sha1
)
698 def save_menu_response(self
, widget
, string
):
699 dialog
= gtk
.FileChooserDialog("Save..", None, gtk
.FILE_CHOOSER_ACTION_SAVE
,
700 (gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
,
701 gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
))
702 dialog
.set_default_response(gtk
.RESPONSE_OK
)
703 response
= dialog
.run()
704 if response
== gtk
.RESPONSE_OK
:
705 patch_buffer
= self
.buffer.get_text(self
.buffer.get_start_iter(),
706 self
.buffer.get_end_iter())
707 fp
= open(dialog
.get_filename(), "w")
708 fp
.write(patch_buffer
)
712 class GitView(object):
713 """ This is the main class
717 def __init__(self
, with_diff
=0):
718 self
.with_diff
= with_diff
719 self
.window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
720 self
.window
.set_border_width(0)
721 self
.window
.set_title("Git repository browser")
726 # Use three-quarters of the screen by default
727 screen
= self
.window
.get_screen()
728 monitor
= screen
.get_monitor_geometry(0)
729 width
= int(monitor
.width
* 0.75)
730 height
= int(monitor
.height
* 0.75)
731 self
.window
.set_default_size(width
, height
)
734 icon
= self
.window
.render_icon(gtk
.STOCK_INDEX
, gtk
.ICON_SIZE_BUTTON
)
735 self
.window
.set_icon(icon
)
737 self
.accel_group
= gtk
.AccelGroup()
738 self
.window
.add_accel_group(self
.accel_group
)
739 self
.accel_group
.connect_group(0xffc2, 0, gtk
.ACCEL_LOCKED
, self
.refresh
);
740 self
.accel_group
.connect_group(0xffc1, 0, gtk
.ACCEL_LOCKED
, self
.maximize
);
741 self
.accel_group
.connect_group(0xffc8, 0, gtk
.ACCEL_LOCKED
, self
.fullscreen
);
742 self
.accel_group
.connect_group(0xffc9, 0, gtk
.ACCEL_LOCKED
, self
.unfullscreen
);
744 self
.window
.add(self
.construct())
746 def refresh(self
, widget
, event
=None, *arguments
, **keywords
):
749 Commit
.children_sha1
= {}
750 self
.set_branch(sys
.argv
[without_diff
:])
754 def maximize(self
, widget
, event
=None, *arguments
, **keywords
):
755 self
.window
.maximize()
758 def fullscreen(self
, widget
, event
=None, *arguments
, **keywords
):
759 self
.window
.fullscreen()
762 def unfullscreen(self
, widget
, event
=None, *arguments
, **keywords
):
763 self
.window
.unfullscreen()
766 def get_bt_sha1(self
):
767 """ Update the bt_sha1 dictionary with the
768 respective sha1 details """
771 ls_remote
= re
.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
772 fp
= os
.popen('git ls-remote "${GIT_DIR-.git}"')
774 line
= string
.strip(fp
.readline())
777 m
= ls_remote
.match(line
)
780 (sha1
, name
) = (m
.group(1), m
.group(2))
781 if not self
.bt_sha1
.has_key(sha1
):
782 self
.bt_sha1
[sha1
] = []
783 self
.bt_sha1
[sha1
].append(name
)
786 def get_encoding(self
):
787 fp
= os
.popen("git config --get i18n.commitencoding")
788 self
.encoding
=string
.strip(fp
.readline())
790 if (self
.encoding
== ""):
791 self
.encoding
= "utf-8"
795 """Construct the window contents."""
798 paned
.pack1(self
.construct_top(), resize
=False, shrink
=True)
799 paned
.pack2(self
.construct_bottom(), resize
=False, shrink
=True)
800 menu_bar
= gtk
.MenuBar()
801 menu_bar
.set_pack_direction(gtk
.PACK_DIRECTION_RTL
)
802 help_menu
= gtk
.MenuItem("Help")
804 about_menu
= gtk
.MenuItem("About")
805 menu
.append(about_menu
)
806 about_menu
.connect("activate", self
.about_menu_response
, "about")
808 help_menu
.set_submenu(menu
)
810 menu_bar
.append(help_menu
)
812 vbox
.pack_start(menu_bar
, expand
=False, fill
=True)
813 vbox
.pack_start(paned
, expand
=True, fill
=True)
819 def construct_top(self
):
820 """Construct the top-half of the window."""
821 vbox
= gtk
.VBox(spacing
=6)
822 vbox
.set_border_width(12)
826 scrollwin
= gtk
.ScrolledWindow()
827 scrollwin
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
828 scrollwin
.set_shadow_type(gtk
.SHADOW_IN
)
829 vbox
.pack_start(scrollwin
, expand
=True, fill
=True)
832 self
.treeview
= gtk
.TreeView()
833 self
.treeview
.set_rules_hint(True)
834 self
.treeview
.set_search_column(4)
835 self
.treeview
.connect("cursor-changed", self
._treeview
_cursor
_cb
)
836 scrollwin
.add(self
.treeview
)
839 cell
= CellRendererGraph()
840 column
= gtk
.TreeViewColumn()
841 column
.set_resizable(True)
842 column
.pack_start(cell
, expand
=True)
843 column
.add_attribute(cell
, "node", 1)
844 column
.add_attribute(cell
, "in-lines", 2)
845 column
.add_attribute(cell
, "out-lines", 3)
846 self
.treeview
.append_column(column
)
848 cell
= gtk
.CellRendererText()
849 cell
.set_property("width-chars", 65)
850 cell
.set_property("ellipsize", pango
.ELLIPSIZE_END
)
851 column
= gtk
.TreeViewColumn("Message")
852 column
.set_resizable(True)
853 column
.pack_start(cell
, expand
=True)
854 column
.add_attribute(cell
, "text", 4)
855 self
.treeview
.append_column(column
)
857 cell
= gtk
.CellRendererText()
858 cell
.set_property("width-chars", 40)
859 cell
.set_property("ellipsize", pango
.ELLIPSIZE_END
)
860 column
= gtk
.TreeViewColumn("Author")
861 column
.set_resizable(True)
862 column
.pack_start(cell
, expand
=True)
863 column
.add_attribute(cell
, "text", 5)
864 self
.treeview
.append_column(column
)
866 cell
= gtk
.CellRendererText()
867 cell
.set_property("ellipsize", pango
.ELLIPSIZE_END
)
868 column
= gtk
.TreeViewColumn("Date")
869 column
.set_resizable(True)
870 column
.pack_start(cell
, expand
=True)
871 column
.add_attribute(cell
, "text", 6)
872 self
.treeview
.append_column(column
)
876 def about_menu_response(self
, widget
, string
):
877 dialog
= gtk
.AboutDialog()
878 dialog
.set_name("Gitview")
879 dialog
.set_version(GitView
.version
)
880 dialog
.set_authors(["Aneesh Kumar K.V <aneesh.kumar@gmail.com>"])
881 dialog
.set_website("http://www.kernel.org/pub/software/scm/git/")
882 dialog
.set_copyright("Use and distribute under the terms of the GNU General Public License")
883 dialog
.set_wrap_license(True)
888 def construct_bottom(self
):
889 """Construct the bottom half of the window."""
890 vbox
= gtk
.VBox(False, spacing
=6)
891 vbox
.set_border_width(12)
892 (width
, height
) = self
.window
.get_size()
893 vbox
.set_size_request(width
, int(height
/ 2.5))
896 self
.table
= gtk
.Table(rows
=4, columns
=4)
897 self
.table
.set_row_spacings(6)
898 self
.table
.set_col_spacings(6)
899 vbox
.pack_start(self
.table
, expand
=False, fill
=True)
902 align
= gtk
.Alignment(0.0, 0.5)
904 label
.set_markup("<b>Revision:</b>")
906 self
.table
.attach(align
, 0, 1, 0, 1, gtk
.FILL
, gtk
.FILL
)
910 align
= gtk
.Alignment(0.0, 0.5)
911 self
.revid_label
= gtk
.Label()
912 self
.revid_label
.set_selectable(True)
913 align
.add(self
.revid_label
)
914 self
.table
.attach(align
, 1, 2, 0, 1, gtk
.EXPAND | gtk
.FILL
, gtk
.FILL
)
915 self
.revid_label
.show()
918 align
= gtk
.Alignment(0.0, 0.5)
920 label
.set_markup("<b>Committer:</b>")
922 self
.table
.attach(align
, 0, 1, 1, 2, gtk
.FILL
, gtk
.FILL
)
926 align
= gtk
.Alignment(0.0, 0.5)
927 self
.committer_label
= gtk
.Label()
928 self
.committer_label
.set_selectable(True)
929 align
.add(self
.committer_label
)
930 self
.table
.attach(align
, 1, 2, 1, 2, gtk
.EXPAND | gtk
.FILL
, gtk
.FILL
)
931 self
.committer_label
.show()
934 align
= gtk
.Alignment(0.0, 0.5)
936 label
.set_markup("<b>Timestamp:</b>")
938 self
.table
.attach(align
, 0, 1, 2, 3, gtk
.FILL
, gtk
.FILL
)
942 align
= gtk
.Alignment(0.0, 0.5)
943 self
.timestamp_label
= gtk
.Label()
944 self
.timestamp_label
.set_selectable(True)
945 align
.add(self
.timestamp_label
)
946 self
.table
.attach(align
, 1, 2, 2, 3, gtk
.EXPAND | gtk
.FILL
, gtk
.FILL
)
947 self
.timestamp_label
.show()
950 align
= gtk
.Alignment(0.0, 0.5)
952 label
.set_markup("<b>Parents:</b>")
954 self
.table
.attach(align
, 0, 1, 3, 4, gtk
.FILL
, gtk
.FILL
)
957 self
.parents_widgets
= []
959 align
= gtk
.Alignment(0.0, 0.5)
961 label
.set_markup("<b>Children:</b>")
963 self
.table
.attach(align
, 2, 3, 3, 4, gtk
.FILL
, gtk
.FILL
)
966 self
.children_widgets
= []
968 scrollwin
= gtk
.ScrolledWindow()
969 scrollwin
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
970 scrollwin
.set_shadow_type(gtk
.SHADOW_IN
)
971 vbox
.pack_start(scrollwin
, expand
=True, fill
=True)
974 (self
.message_buffer
, sourceview
) = get_source_buffer_and_view()
976 sourceview
.set_editable(False)
977 sourceview
.modify_font(pango
.FontDescription("Monospace"))
978 scrollwin
.add(sourceview
)
983 def _treeview_cursor_cb(self
, *args
):
984 """Callback for when the treeview cursor changes."""
985 (path
, col
) = self
.treeview
.get_cursor()
986 commit
= self
.model
[path
][0]
988 if commit
.committer
is not None:
989 committer
= commit
.committer
990 timestamp
= commit
.commit_date
991 message
= commit
.get_message(self
.with_diff
)
992 revid_label
= commit
.commit_sha1
999 self
.revid_label
.set_text(revid_label
)
1000 self
.committer_label
.set_text(committer
)
1001 self
.timestamp_label
.set_text(timestamp
)
1002 self
.message_buffer
.set_text(unicode(message
, self
.encoding
).encode('utf-8'))
1004 for widget
in self
.parents_widgets
:
1005 self
.table
.remove(widget
)
1007 self
.parents_widgets
= []
1008 self
.table
.resize(4 + len(commit
.parent_sha1
) - 1, 4)
1009 for idx
, parent_id
in enumerate(commit
.parent_sha1
):
1010 self
.table
.set_row_spacing(idx
+ 3, 0)
1012 align
= gtk
.Alignment(0.0, 0.0)
1013 self
.parents_widgets
.append(align
)
1014 self
.table
.attach(align
, 1, 2, idx
+ 3, idx
+ 4,
1015 gtk
.EXPAND | gtk
.FILL
, gtk
.FILL
)
1018 hbox
= gtk
.HBox(False, 0)
1022 label
= gtk
.Label(parent_id
)
1023 label
.set_selectable(True)
1024 hbox
.pack_start(label
, expand
=False, fill
=True)
1028 image
.set_from_stock(gtk
.STOCK_JUMP_TO
, gtk
.ICON_SIZE_MENU
)
1031 button
= gtk
.Button()
1033 button
.set_relief(gtk
.RELIEF_NONE
)
1034 button
.connect("clicked", self
._go
_clicked
_cb
, parent_id
)
1035 hbox
.pack_start(button
, expand
=False, fill
=True)
1039 image
.set_from_stock(gtk
.STOCK_FIND
, gtk
.ICON_SIZE_MENU
)
1042 button
= gtk
.Button()
1044 button
.set_relief(gtk
.RELIEF_NONE
)
1045 button
.set_sensitive(True)
1046 button
.connect("clicked", self
._show
_clicked
_cb
,
1047 commit
.commit_sha1
, parent_id
, self
.encoding
)
1048 hbox
.pack_start(button
, expand
=False, fill
=True)
1051 # Populate with child details
1052 for widget
in self
.children_widgets
:
1053 self
.table
.remove(widget
)
1055 self
.children_widgets
= []
1057 child_sha1
= Commit
.children_sha1
[commit
.commit_sha1
]
1059 # We don't have child
1062 if ( len(child_sha1
) > len(commit
.parent_sha1
)):
1063 self
.table
.resize(4 + len(child_sha1
) - 1, 4)
1065 for idx
, child_id
in enumerate(child_sha1
):
1066 self
.table
.set_row_spacing(idx
+ 3, 0)
1068 align
= gtk
.Alignment(0.0, 0.0)
1069 self
.children_widgets
.append(align
)
1070 self
.table
.attach(align
, 3, 4, idx
+ 3, idx
+ 4,
1071 gtk
.EXPAND | gtk
.FILL
, gtk
.FILL
)
1074 hbox
= gtk
.HBox(False, 0)
1078 label
= gtk
.Label(child_id
)
1079 label
.set_selectable(True)
1080 hbox
.pack_start(label
, expand
=False, fill
=True)
1084 image
.set_from_stock(gtk
.STOCK_JUMP_TO
, gtk
.ICON_SIZE_MENU
)
1087 button
= gtk
.Button()
1089 button
.set_relief(gtk
.RELIEF_NONE
)
1090 button
.connect("clicked", self
._go
_clicked
_cb
, child_id
)
1091 hbox
.pack_start(button
, expand
=False, fill
=True)
1095 image
.set_from_stock(gtk
.STOCK_FIND
, gtk
.ICON_SIZE_MENU
)
1098 button
= gtk
.Button()
1100 button
.set_relief(gtk
.RELIEF_NONE
)
1101 button
.set_sensitive(True)
1102 button
.connect("clicked", self
._show
_clicked
_cb
,
1103 child_id
, commit
.commit_sha1
, self
.encoding
)
1104 hbox
.pack_start(button
, expand
=False, fill
=True)
1107 def _destroy_cb(self
, widget
):
1108 """Callback for when a window we manage is destroyed."""
1113 """Stop the GTK+ main loop."""
1116 def run(self
, args
):
1117 self
.set_branch(args
)
1118 self
.window
.connect("destroy", self
._destroy
_cb
)
1122 def set_branch(self
, args
):
1123 """Fill in different windows with info from the reposiroty"""
1124 fp
= os
.popen("git rev-parse --sq --default HEAD " + list_to_string(args
, 1))
1125 git_rev_list_cmd
= fp
.read()
1127 fp
= os
.popen("git rev-list --header --topo-order --parents " + git_rev_list_cmd
)
1128 self
.update_window(fp
)
1130 def update_window(self
, fp
):
1133 self
.model
= gtk
.ListStore(gobject
.TYPE_PYOBJECT
, gobject
.TYPE_PYOBJECT
,
1134 gobject
.TYPE_PYOBJECT
, gobject
.TYPE_PYOBJECT
, str, str, str)
1136 # used for cursor positioning
1141 self
.incomplete_line
= {}
1148 input_line
= fp
.readline()
1149 while (input_line
!= ""):
1150 # The commit header ends with '\0'
1151 # This NULL is immediately followed by the sha1 of the
1153 if (input_line
[0] != '\0'):
1154 commit_lines
.append(input_line
)
1155 input_line
= fp
.readline()
1158 commit
= Commit(commit_lines
)
1159 if (commit
!= None ):
1160 self
.commits
.append(commit
)
1164 commit_lines
.append(input_line
[1:])
1165 input_line
= fp
.readline()
1169 for commit
in self
.commits
:
1170 (out_line
, last_colour
, last_nodepos
) = self
.draw_graph(commit
,
1174 self
.index
[commit
.commit_sha1
] = index
1177 self
.treeview
.set_model(self
.model
)
1178 self
.treeview
.show()
1180 def draw_graph(self
, commit
, index
, out_line
, last_colour
, last_nodepos
):
1188 if (last_nodepos
> 5):
1191 # Add the incomplete lines of the last cell in this
1193 colour
= self
.colours
[commit
.commit_sha1
]
1195 self
.colours
[commit
.commit_sha1
] = last_colour
+1
1196 last_colour
= self
.colours
[commit
.commit_sha1
]
1197 colour
= self
.colours
[commit
.commit_sha1
]
1200 node_pos
= self
.nodepos
[commit
.commit_sha1
]
1202 self
.nodepos
[commit
.commit_sha1
] = last_nodepos
+1
1203 last_nodepos
= self
.nodepos
[commit
.commit_sha1
]
1204 node_pos
= self
.nodepos
[commit
.commit_sha1
]
1206 #The first parent always continue on the same line
1208 # check we already have the value
1209 tmp_node_pos
= self
.nodepos
[commit
.parent_sha1
[0]]
1211 self
.colours
[commit
.parent_sha1
[0]] = colour
1212 self
.nodepos
[commit
.parent_sha1
[0]] = node_pos
1214 for sha1
in self
.incomplete_line
.keys():
1215 if (sha1
!= commit
.commit_sha1
):
1216 self
.draw_incomplete_line(sha1
, node_pos
,
1217 out_line
, in_line
, index
)
1219 del self
.incomplete_line
[sha1
]
1222 for parent_id
in commit
.parent_sha1
:
1224 tmp_node_pos
= self
.nodepos
[parent_id
]
1226 self
.colours
[parent_id
] = last_colour
+1
1227 last_colour
= self
.colours
[parent_id
]
1228 self
.nodepos
[parent_id
] = last_nodepos
+1
1229 last_nodepos
= self
.nodepos
[parent_id
]
1231 in_line
.append((node_pos
, self
.nodepos
[parent_id
],
1232 self
.colours
[parent_id
]))
1233 self
.add_incomplete_line(parent_id
)
1236 branch_tag
= self
.bt_sha1
[commit
.commit_sha1
]
1241 node
= (node_pos
, colour
, branch_tag
)
1243 self
.model
.append([commit
, node
, out_line
, in_line
,
1244 commit
.message
, commit
.author
, commit
.date
])
1246 return (in_line
, last_colour
, last_nodepos
)
1248 def add_incomplete_line(self
, sha1
):
1250 self
.incomplete_line
[sha1
].append(self
.nodepos
[sha1
])
1252 self
.incomplete_line
[sha1
] = [self
.nodepos
[sha1
]]
1254 def draw_incomplete_line(self
, sha1
, node_pos
, out_line
, in_line
, index
):
1255 for idx
, pos
in enumerate(self
.incomplete_line
[sha1
]):
1256 if(pos
== node_pos
):
1257 #remove the straight line and add a slash
1258 if ((pos
, pos
, self
.colours
[sha1
]) in out_line
):
1259 out_line
.remove((pos
, pos
, self
.colours
[sha1
]))
1260 out_line
.append((pos
, pos
+0.5, self
.colours
[sha1
]))
1261 self
.incomplete_line
[sha1
][idx
] = pos
= pos
+0.5
1263 next_commit
= self
.commits
[index
+1]
1264 if (next_commit
.commit_sha1
== sha1
and pos
!= int(pos
)):
1265 # join the line back to the node point
1266 # This need to be done only if we modified it
1267 in_line
.append((pos
, pos
-0.5, self
.colours
[sha1
]))
1271 in_line
.append((pos
, pos
, self
.colours
[sha1
]))
1274 def _go_clicked_cb(self
, widget
, revid
):
1275 """Callback for when the go button for a parent is clicked."""
1277 self
.treeview
.set_cursor(self
.index
[revid
])
1279 dialog
= gtk
.MessageDialog(parent
=None, flags
=0,
1280 type=gtk
.MESSAGE_WARNING
, buttons
=gtk
.BUTTONS_CLOSE
,
1281 message_format
=None)
1282 dialog
.set_markup("Revision <b>%s</b> not present in the list" % revid
)
1283 # revid == 0 is the parent of the first commit
1285 dialog
.format_secondary_text("Try running gitview without any options")
1289 self
.treeview
.grab_focus()
1291 def _show_clicked_cb(self
, widget
, commit_sha1
, parent_sha1
, encoding
):
1292 """Callback for when the show button for a parent is clicked."""
1293 window
= DiffWindow()
1294 window
.set_diff(commit_sha1
, parent_sha1
, encoding
)
1295 self
.treeview
.grab_focus()
1298 if __name__
== "__main__":
1300 if (len(sys
.argv
) > 1 ):
1301 if (sys
.argv
[1] == "--without-diff"):
1304 view
= GitView( without_diff
!= 1)
1305 view
.run(sys
.argv
[without_diff
:])