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>"
32 have_gtksourceview
= True
34 have_gtksourceview
= False
35 print "Running without gtksourceview module"
37 re_ident
= re
.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
39 def list_to_string(args
, skip
):
44 str_arg
= str_arg
+ args
[i
]
45 str_arg
= str_arg
+ " "
50 def show_date(epoch
, tz
):
52 tzsecs
= float(tz
[1:3]) * 3600
53 tzsecs
+= float(tz
[3:5]) * 60
59 return time
.strftime("%Y-%m-%d %H:%M:%S", time
.gmtime(secs
))
62 class CellRendererGraph(gtk
.GenericCellRenderer
):
63 """Cell renderer for directed graph.
65 This module contains the implementation of a custom GtkCellRenderer that
66 draws part of the directed graph based on the lines suggested by the code
69 Because we're shiny, we use Cairo to do this, and because we're naughty
70 we cheat and draw over the bits of the TreeViewColumn that are supposed to
71 just be for the background.
74 node (column, colour, [ names ]) tuple to draw revision node,
75 in_lines (start, end, colour) tuple list to draw inward lines,
76 out_lines (start, end, colour) tuple list to draw outward lines.
80 "node": ( gobject
.TYPE_PYOBJECT
, "node",
81 "revision node instruction",
82 gobject
.PARAM_WRITABLE
84 "in-lines": ( gobject
.TYPE_PYOBJECT
, "in-lines",
85 "instructions to draw lines into the cell",
86 gobject
.PARAM_WRITABLE
88 "out-lines": ( gobject
.TYPE_PYOBJECT
, "out-lines",
89 "instructions to draw lines out of the cell",
90 gobject
.PARAM_WRITABLE
94 def do_set_property(self
, property, value
):
95 """Set properties from GObject properties."""
96 if property.name
== "node":
98 elif property.name
== "in-lines":
100 elif property.name
== "out-lines":
101 self
.out_lines
= value
103 raise AttributeError, "no such property: '%s'" % property.name
105 def box_size(self
, widget
):
106 """Calculate box size based on widget's font.
108 Cache this as it's probably expensive to get. It ensures that we
109 draw the graph at least as large as the text.
112 return self
._box
_size
113 except AttributeError:
114 pango_ctx
= widget
.get_pango_context()
115 font_desc
= widget
.get_style().font_desc
116 metrics
= pango_ctx
.get_metrics(font_desc
)
118 ascent
= pango
.PIXELS(metrics
.get_ascent())
119 descent
= pango
.PIXELS(metrics
.get_descent())
121 self
._box
_size
= ascent
+ descent
+ 6
122 return self
._box
_size
124 def set_colour(self
, ctx
, colour
, bg
, fg
):
125 """Set the context source colour.
127 Picks a distinct colour based on an internal wheel; the bg
128 parameter provides the value that should be assigned to the 'zero'
129 colours and the fg parameter provides the multiplier that should be
130 applied to the foreground colours.
141 colour
%= len(colours
)
142 red
= (colours
[colour
][0] * fg
) or bg
143 green
= (colours
[colour
][1] * fg
) or bg
144 blue
= (colours
[colour
][2] * fg
) or bg
146 ctx
.set_source_rgb(red
, green
, blue
)
148 def on_get_size(self
, widget
, cell_area
):
149 """Return the size we need for this cell.
151 Each cell is drawn individually and is only as wide as it needs
152 to be, we let the TreeViewColumn take care of making them all
155 box_size
= self
.box_size(widget
)
158 for start
, end
, colour
in self
.in_lines
+ self
.out_lines
:
159 cols
= int(max(cols
, start
, end
))
161 (column
, colour
, names
) = self
.node
163 if (len(names
) != 0):
165 names_len
+= len(item
)
167 width
= box_size
* (cols
+ 1 ) + names_len
170 # FIXME I have no idea how to use cell_area properly
171 return (0, 0, width
, height
)
173 def on_render(self
, window
, widget
, bg_area
, cell_area
, exp_area
, flags
):
174 """Render an individual cell.
176 Draws the cell contents using cairo, taking care to clip what we
177 do to within the background area so we don't draw over other cells.
178 Note that we're a bit naughty there and should really be drawing
179 in the cell_area (or even the exposed area), but we explicitly don't
182 We try and be a little clever, if the line we need to draw is going
183 to cross other columns we actually draw it as in the .---' style
184 instead of a pure diagonal ... this reduces confusion by an
187 ctx
= window
.cairo_create()
188 ctx
.rectangle(bg_area
.x
, bg_area
.y
, bg_area
.width
, bg_area
.height
)
191 box_size
= self
.box_size(widget
)
193 ctx
.set_line_width(box_size
/ 8)
194 ctx
.set_line_cap(cairo
.LINE_CAP_SQUARE
)
196 # Draw lines into the cell
197 for start
, end
, colour
in self
.in_lines
:
198 ctx
.move_to(cell_area
.x
+ box_size
* start
+ box_size
/ 2,
199 bg_area
.y
- bg_area
.height
/ 2)
202 ctx
.line_to(cell_area
.x
+ box_size
* start
, bg_area
.y
)
203 ctx
.line_to(cell_area
.x
+ box_size
* end
+ box_size
, bg_area
.y
)
204 elif start
- end
< -1:
205 ctx
.line_to(cell_area
.x
+ box_size
* start
+ box_size
,
207 ctx
.line_to(cell_area
.x
+ box_size
* end
, bg_area
.y
)
209 ctx
.line_to(cell_area
.x
+ box_size
* end
+ box_size
/ 2,
210 bg_area
.y
+ bg_area
.height
/ 2)
212 self
.set_colour(ctx
, colour
, 0.0, 0.65)
215 # Draw lines out of the cell
216 for start
, end
, colour
in self
.out_lines
:
217 ctx
.move_to(cell_area
.x
+ box_size
* start
+ box_size
/ 2,
218 bg_area
.y
+ bg_area
.height
/ 2)
221 ctx
.line_to(cell_area
.x
+ box_size
* start
,
222 bg_area
.y
+ bg_area
.height
)
223 ctx
.line_to(cell_area
.x
+ box_size
* end
+ box_size
,
224 bg_area
.y
+ bg_area
.height
)
225 elif start
- end
< -1:
226 ctx
.line_to(cell_area
.x
+ box_size
* start
+ box_size
,
227 bg_area
.y
+ bg_area
.height
)
228 ctx
.line_to(cell_area
.x
+ box_size
* end
,
229 bg_area
.y
+ bg_area
.height
)
231 ctx
.line_to(cell_area
.x
+ box_size
* end
+ box_size
/ 2,
232 bg_area
.y
+ bg_area
.height
/ 2 + bg_area
.height
)
234 self
.set_colour(ctx
, colour
, 0.0, 0.65)
237 # Draw the revision node in the right column
238 (column
, colour
, names
) = self
.node
239 ctx
.arc(cell_area
.x
+ box_size
* column
+ box_size
/ 2,
240 cell_area
.y
+ cell_area
.height
/ 2,
241 box_size
/ 4, 0, 2 * math
.pi
)
244 self
.set_colour(ctx
, colour
, 0.0, 0.5)
245 ctx
.stroke_preserve()
247 self
.set_colour(ctx
, colour
, 0.5, 1.0)
250 if (len(names
) != 0):
253 name
= name
+ item
+ " "
255 ctx
.set_font_size(13)
257 self
.set_colour(ctx
, colour
, 0.5, 1.0)
259 self
.set_colour(ctx
, colour
, 0.0, 0.5)
263 """ This represent a commit object obtained after parsing the git-rev-list
268 def __init__(self
, commit_lines
):
273 self
.commit_date
= ""
274 self
.commit_sha1
= ""
275 self
.parent_sha1
= [ ]
276 self
.parse_commit(commit_lines
)
279 def parse_commit(self
, commit_lines
):
281 # First line is the sha1 lines
282 line
= string
.strip(commit_lines
[0])
283 sha1
= re
.split(" ", line
)
284 self
.commit_sha1
= sha1
[0]
285 self
.parent_sha1
= sha1
[1:]
287 #build the child list
288 for parent_id
in self
.parent_sha1
:
290 Commit
.children_sha1
[parent_id
].append(self
.commit_sha1
)
292 Commit
.children_sha1
[parent_id
] = [self
.commit_sha1
]
294 # IF we don't have parent
295 if (len(self
.parent_sha1
) == 0):
296 self
.parent_sha1
= [0]
298 for line
in commit_lines
[1:]:
299 m
= re
.match("^ ", line
)
301 # First line of the commit message used for short log
302 if self
.message
== "":
303 self
.message
= string
.strip(line
)
306 m
= re
.match("tree", line
)
310 m
= re
.match("parent", line
)
314 m
= re_ident
.match(line
)
316 date
= show_date(m
.group('epoch'), m
.group('tz'))
317 if m
.group(1) == "author":
318 self
.author
= m
.group('ident')
320 elif m
.group(1) == "committer":
321 self
.committer
= m
.group('ident')
322 self
.commit_date
= date
326 def get_message(self
, with_diff
=0):
328 message
= self
.diff_tree()
330 fp
= os
.popen("git cat-file commit " + self
.commit_sha1
)
337 fp
= os
.popen("git diff-tree --pretty --cc -v -p --always " + self
.commit_sha1
)
342 class AnnotateWindow
:
344 This object represents and manages a single window containing the
345 annotate information of the file
349 self
.window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
350 self
.window
.set_border_width(0)
351 self
.window
.set_title("Git repository browser annotation window")
353 # Use two thirds of the screen by default
354 screen
= self
.window
.get_screen()
355 monitor
= screen
.get_monitor_geometry(0)
356 width
= int(monitor
.width
* 0.66)
357 height
= int(monitor
.height
* 0.66)
358 self
.window
.set_default_size(width
, height
)
360 def add_file_data(self
, filename
, commit_sha1
, line_num
):
361 fp
= os
.popen("git cat-file blob " + commit_sha1
+":"+filename
)
363 for line
in fp
.readlines():
364 line
= string
.rstrip(line
)
365 self
.model
.append(None, ["HEAD", filename
, line
, i
])
369 # now set the cursor position
370 self
.treeview
.set_cursor(line_num
-1)
371 self
.treeview
.grab_focus()
373 def _treeview_cursor_cb(self
, *args
):
374 """Callback for when the treeview cursor changes."""
375 (path
, col
) = self
.treeview
.get_cursor()
376 commit_sha1
= self
.model
[path
][0]
378 fp
= os
.popen("git cat-file commit " + commit_sha1
)
379 for line
in fp
.readlines():
380 commit_msg
= commit_msg
+ line
383 self
.commit_buffer
.set_text(commit_msg
)
385 def _treeview_row_activated(self
, *args
):
386 """Callback for when the treeview row gets selected."""
387 (path
, col
) = self
.treeview
.get_cursor()
388 commit_sha1
= self
.model
[path
][0]
389 filename
= self
.model
[path
][1]
390 line_num
= self
.model
[path
][3]
392 window
= AnnotateWindow();
393 fp
= os
.popen("git rev-parse "+ commit_sha1
+ "~1")
394 commit_sha1
= string
.strip(fp
.readline())
396 window
.annotate(filename
, commit_sha1
, line_num
)
398 def data_ready(self
, source
, condition
):
401 buffer = source
.read(8192)
403 # resource temporary not available
406 if (len(buffer) == 0):
407 gobject
.source_remove(self
.io_watch_tag
)
411 for buff
in buffer.split("\n"):
412 annotate_line
= re
.compile('^([0-9a-f]{40}) (.+) (.+) (.+)$')
413 m
= annotate_line
.match(buff
)
415 annotate_line
= re
.compile('^(filename) (.+)$')
416 m
= annotate_line
.match(buff
)
419 filename
= m
.group(2)
421 self
.commit_sha1
= m
.group(1)
422 self
.source_line
= int(m
.group(2))
423 self
.result_line
= int(m
.group(3))
424 self
.count
= int(m
.group(4))
425 #set the details only when we have the file name
428 while (self
.count
> 0):
429 # set at result_line + count-1 the sha1 as commit_sha1
430 self
.count
= self
.count
- 1
431 iter = self
.model
.iter_nth_child(None, self
.result_line
+ self
.count
-1)
432 self
.model
.set(iter, 0, self
.commit_sha1
, 1, filename
, 3, self
.source_line
)
435 def annotate(self
, filename
, commit_sha1
, line_num
):
436 # verify the commit_sha1 specified has this filename
438 fp
= os
.popen("git ls-tree "+ commit_sha1
+ " -- " + filename
)
439 line
= string
.strip(fp
.readline())
441 # pop up the message the file is not there as a part of the commit
443 dialog
= gtk
.MessageDialog(parent
=None, flags
=0,
444 type=gtk
.MESSAGE_WARNING
, buttons
=gtk
.BUTTONS_CLOSE
,
446 dialog
.set_markup("The file %s is not present in the parent commit %s" % (filename
, commit_sha1
))
454 self
.window
.add(vpan
);
457 scrollwin
= gtk
.ScrolledWindow()
458 scrollwin
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
459 scrollwin
.set_shadow_type(gtk
.SHADOW_IN
)
460 vpan
.pack1(scrollwin
, True, True);
463 self
.model
= gtk
.TreeStore(str, str, str, int)
464 self
.treeview
= gtk
.TreeView(self
.model
)
465 self
.treeview
.set_rules_hint(True)
466 self
.treeview
.set_search_column(0)
467 self
.treeview
.connect("cursor-changed", self
._treeview
_cursor
_cb
)
468 self
.treeview
.connect("row-activated", self
._treeview
_row
_activated
)
469 scrollwin
.add(self
.treeview
)
472 cell
= gtk
.CellRendererText()
473 cell
.set_property("width-chars", 10)
474 cell
.set_property("ellipsize", pango
.ELLIPSIZE_END
)
475 column
= gtk
.TreeViewColumn("Commit")
476 column
.set_resizable(True)
477 column
.pack_start(cell
, expand
=True)
478 column
.add_attribute(cell
, "text", 0)
479 self
.treeview
.append_column(column
)
481 cell
= gtk
.CellRendererText()
482 cell
.set_property("width-chars", 20)
483 cell
.set_property("ellipsize", pango
.ELLIPSIZE_END
)
484 column
= gtk
.TreeViewColumn("File Name")
485 column
.set_resizable(True)
486 column
.pack_start(cell
, expand
=True)
487 column
.add_attribute(cell
, "text", 1)
488 self
.treeview
.append_column(column
)
490 cell
= gtk
.CellRendererText()
491 cell
.set_property("width-chars", 20)
492 cell
.set_property("ellipsize", pango
.ELLIPSIZE_END
)
493 column
= gtk
.TreeViewColumn("Data")
494 column
.set_resizable(True)
495 column
.pack_start(cell
, expand
=True)
496 column
.add_attribute(cell
, "text", 2)
497 self
.treeview
.append_column(column
)
499 # The commit message window
500 scrollwin
= gtk
.ScrolledWindow()
501 scrollwin
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
502 scrollwin
.set_shadow_type(gtk
.SHADOW_IN
)
503 vpan
.pack2(scrollwin
, True, True);
506 commit_text
= gtk
.TextView()
507 self
.commit_buffer
= gtk
.TextBuffer()
508 commit_text
.set_buffer(self
.commit_buffer
)
509 scrollwin
.add(commit_text
)
514 self
.add_file_data(filename
, commit_sha1
, line_num
)
516 fp
= os
.popen("git blame --incremental -- " + filename
+ " " + commit_sha1
)
517 flags
= fcntl
.fcntl(fp
.fileno(), fcntl
.F_GETFL
)
518 fcntl
.fcntl(fp
.fileno(), fcntl
.F_SETFL
, flags | os
.O_NONBLOCK
)
519 self
.io_watch_tag
= gobject
.io_add_watch(fp
, gobject
.IO_IN
, self
.data_ready
)
524 This object represents and manages a single window containing the
525 differences between two revisions on a branch.
529 self
.window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
530 self
.window
.set_border_width(0)
531 self
.window
.set_title("Git repository browser diff window")
533 # Use two thirds of the screen by default
534 screen
= self
.window
.get_screen()
535 monitor
= screen
.get_monitor_geometry(0)
536 width
= int(monitor
.width
* 0.66)
537 height
= int(monitor
.height
* 0.66)
538 self
.window
.set_default_size(width
, height
)
544 """Construct the window contents."""
546 self
.window
.add(vbox
)
549 menu_bar
= gtk
.MenuBar()
550 save_menu
= gtk
.ImageMenuItem(gtk
.STOCK_SAVE
)
551 save_menu
.connect("activate", self
.save_menu_response
, "save")
553 menu_bar
.append(save_menu
)
554 vbox
.pack_start(menu_bar
, expand
=False, fill
=True)
559 scrollwin
= gtk
.ScrolledWindow()
560 scrollwin
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
561 scrollwin
.set_shadow_type(gtk
.SHADOW_IN
)
562 hpan
.pack1(scrollwin
, True, True)
565 if have_gtksourceview
:
566 self
.buffer = gtksourceview
.SourceBuffer()
567 slm
= gtksourceview
.SourceLanguagesManager()
568 gsl
= slm
.get_language_from_mime_type("text/x-patch")
569 self
.buffer.set_highlight(True)
570 self
.buffer.set_language(gsl
)
571 sourceview
= gtksourceview
.SourceView(self
.buffer)
573 self
.buffer = gtk
.TextBuffer()
574 sourceview
= gtk
.TextView(self
.buffer)
577 sourceview
.set_editable(False)
578 sourceview
.modify_font(pango
.FontDescription("Monospace"))
579 scrollwin
.add(sourceview
)
582 # The file hierarchy: a scrollable treeview
583 scrollwin
= gtk
.ScrolledWindow()
584 scrollwin
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
585 scrollwin
.set_shadow_type(gtk
.SHADOW_IN
)
586 scrollwin
.set_size_request(20, -1)
587 hpan
.pack2(scrollwin
, True, True)
590 self
.model
= gtk
.TreeStore(str, str, str)
591 self
.treeview
= gtk
.TreeView(self
.model
)
592 self
.treeview
.set_search_column(1)
593 self
.treeview
.connect("cursor-changed", self
._treeview
_clicked
)
594 scrollwin
.add(self
.treeview
)
597 cell
= gtk
.CellRendererText()
598 cell
.set_property("width-chars", 20)
599 column
= gtk
.TreeViewColumn("Select to annotate")
600 column
.pack_start(cell
, expand
=True)
601 column
.add_attribute(cell
, "text", 0)
602 self
.treeview
.append_column(column
)
604 vbox
.pack_start(hpan
, expand
=True, fill
=True)
607 def _treeview_clicked(self
, *args
):
608 """Callback for when the treeview cursor changes."""
609 (path
, col
) = self
.treeview
.get_cursor()
610 specific_file
= self
.model
[path
][1]
611 commit_sha1
= self
.model
[path
][2]
612 if specific_file
== None :
614 elif specific_file
== "" :
617 window
= AnnotateWindow();
618 window
.annotate(specific_file
, commit_sha1
, 1)
621 def commit_files(self
, commit_sha1
, parent_sha1
):
623 add
= self
.model
.append(None, [ "Added", None, None])
624 dele
= self
.model
.append(None, [ "Deleted", None, None])
625 mod
= self
.model
.append(None, [ "Modified", None, None])
626 diff_tree
= re
.compile('^(:.{6}) (.{6}) (.{40}) (.{40}) (A|D|M)\s(.+)$')
627 fp
= os
.popen("git diff-tree -r --no-commit-id " + parent_sha1
+ " " + commit_sha1
)
629 line
= string
.strip(fp
.readline())
632 m
= diff_tree
.match(line
)
637 filename
= m
.group(6)
639 self
.model
.append(add
, [filename
, filename
, commit_sha1
])
641 self
.model
.append(dele
, [filename
, filename
, commit_sha1
])
643 self
.model
.append(mod
, [filename
, filename
, commit_sha1
])
646 self
.treeview
.expand_all()
648 def set_diff(self
, commit_sha1
, parent_sha1
, encoding
):
649 """Set the differences showed by this window.
650 Compares the two trees and populates the window with the
653 # Diff with the first commit or the last commit shows nothing
654 if (commit_sha1
== 0 or parent_sha1
== 0 ):
657 fp
= os
.popen("git diff-tree -p " + parent_sha1
+ " " + commit_sha1
)
658 self
.buffer.set_text(unicode(fp
.read(), encoding
).encode('utf-8'))
660 self
.commit_files(commit_sha1
, parent_sha1
)
663 def save_menu_response(self
, widget
, string
):
664 dialog
= gtk
.FileChooserDialog("Save..", None, gtk
.FILE_CHOOSER_ACTION_SAVE
,
665 (gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
,
666 gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
))
667 dialog
.set_default_response(gtk
.RESPONSE_OK
)
668 response
= dialog
.run()
669 if response
== gtk
.RESPONSE_OK
:
670 patch_buffer
= self
.buffer.get_text(self
.buffer.get_start_iter(),
671 self
.buffer.get_end_iter())
672 fp
= open(dialog
.get_filename(), "w")
673 fp
.write(patch_buffer
)
678 """ This is the main class
682 def __init__(self
, with_diff
=0):
683 self
.with_diff
= with_diff
684 self
.window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
685 self
.window
.set_border_width(0)
686 self
.window
.set_title("Git repository browser")
691 # Use three-quarters of the screen by default
692 screen
= self
.window
.get_screen()
693 monitor
= screen
.get_monitor_geometry(0)
694 width
= int(monitor
.width
* 0.75)
695 height
= int(monitor
.height
* 0.75)
696 self
.window
.set_default_size(width
, height
)
699 icon
= self
.window
.render_icon(gtk
.STOCK_INDEX
, gtk
.ICON_SIZE_BUTTON
)
700 self
.window
.set_icon(icon
)
702 self
.accel_group
= gtk
.AccelGroup()
703 self
.window
.add_accel_group(self
.accel_group
)
704 self
.accel_group
.connect_group(0xffc2, 0, gtk
.ACCEL_LOCKED
, self
.refresh
);
705 self
.accel_group
.connect_group(0xffc1, 0, gtk
.ACCEL_LOCKED
, self
.maximize
);
706 self
.accel_group
.connect_group(0xffc8, 0, gtk
.ACCEL_LOCKED
, self
.fullscreen
);
707 self
.accel_group
.connect_group(0xffc9, 0, gtk
.ACCEL_LOCKED
, self
.unfullscreen
);
709 self
.window
.add(self
.construct())
711 def refresh(self
, widget
, event
=None, *arguments
, **keywords
):
714 Commit
.children_sha1
= {}
715 self
.set_branch(sys
.argv
[without_diff
:])
719 def maximize(self
, widget
, event
=None, *arguments
, **keywords
):
720 self
.window
.maximize()
723 def fullscreen(self
, widget
, event
=None, *arguments
, **keywords
):
724 self
.window
.fullscreen()
727 def unfullscreen(self
, widget
, event
=None, *arguments
, **keywords
):
728 self
.window
.unfullscreen()
731 def get_bt_sha1(self
):
732 """ Update the bt_sha1 dictionary with the
733 respective sha1 details """
736 ls_remote
= re
.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
737 fp
= os
.popen('git ls-remote "${GIT_DIR-.git}"')
739 line
= string
.strip(fp
.readline())
742 m
= ls_remote
.match(line
)
745 (sha1
, name
) = (m
.group(1), m
.group(2))
746 if not self
.bt_sha1
.has_key(sha1
):
747 self
.bt_sha1
[sha1
] = []
748 self
.bt_sha1
[sha1
].append(name
)
751 def get_encoding(self
):
752 fp
= os
.popen("git config --get i18n.commitencoding")
753 self
.encoding
=string
.strip(fp
.readline())
755 if (self
.encoding
== ""):
756 self
.encoding
= "utf-8"
760 """Construct the window contents."""
763 paned
.pack1(self
.construct_top(), resize
=False, shrink
=True)
764 paned
.pack2(self
.construct_bottom(), resize
=False, shrink
=True)
765 menu_bar
= gtk
.MenuBar()
766 menu_bar
.set_pack_direction(gtk
.PACK_DIRECTION_RTL
)
767 help_menu
= gtk
.MenuItem("Help")
769 about_menu
= gtk
.MenuItem("About")
770 menu
.append(about_menu
)
771 about_menu
.connect("activate", self
.about_menu_response
, "about")
773 help_menu
.set_submenu(menu
)
775 menu_bar
.append(help_menu
)
777 vbox
.pack_start(menu_bar
, expand
=False, fill
=True)
778 vbox
.pack_start(paned
, expand
=True, fill
=True)
784 def construct_top(self
):
785 """Construct the top-half of the window."""
786 vbox
= gtk
.VBox(spacing
=6)
787 vbox
.set_border_width(12)
791 scrollwin
= gtk
.ScrolledWindow()
792 scrollwin
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
793 scrollwin
.set_shadow_type(gtk
.SHADOW_IN
)
794 vbox
.pack_start(scrollwin
, expand
=True, fill
=True)
797 self
.treeview
= gtk
.TreeView()
798 self
.treeview
.set_rules_hint(True)
799 self
.treeview
.set_search_column(4)
800 self
.treeview
.connect("cursor-changed", self
._treeview
_cursor
_cb
)
801 scrollwin
.add(self
.treeview
)
804 cell
= CellRendererGraph()
805 column
= gtk
.TreeViewColumn()
806 column
.set_resizable(True)
807 column
.pack_start(cell
, expand
=True)
808 column
.add_attribute(cell
, "node", 1)
809 column
.add_attribute(cell
, "in-lines", 2)
810 column
.add_attribute(cell
, "out-lines", 3)
811 self
.treeview
.append_column(column
)
813 cell
= gtk
.CellRendererText()
814 cell
.set_property("width-chars", 65)
815 cell
.set_property("ellipsize", pango
.ELLIPSIZE_END
)
816 column
= gtk
.TreeViewColumn("Message")
817 column
.set_resizable(True)
818 column
.pack_start(cell
, expand
=True)
819 column
.add_attribute(cell
, "text", 4)
820 self
.treeview
.append_column(column
)
822 cell
= gtk
.CellRendererText()
823 cell
.set_property("width-chars", 40)
824 cell
.set_property("ellipsize", pango
.ELLIPSIZE_END
)
825 column
= gtk
.TreeViewColumn("Author")
826 column
.set_resizable(True)
827 column
.pack_start(cell
, expand
=True)
828 column
.add_attribute(cell
, "text", 5)
829 self
.treeview
.append_column(column
)
831 cell
= gtk
.CellRendererText()
832 cell
.set_property("ellipsize", pango
.ELLIPSIZE_END
)
833 column
= gtk
.TreeViewColumn("Date")
834 column
.set_resizable(True)
835 column
.pack_start(cell
, expand
=True)
836 column
.add_attribute(cell
, "text", 6)
837 self
.treeview
.append_column(column
)
841 def about_menu_response(self
, widget
, string
):
842 dialog
= gtk
.AboutDialog()
843 dialog
.set_name("Gitview")
844 dialog
.set_version(GitView
.version
)
845 dialog
.set_authors(["Aneesh Kumar K.V <aneesh.kumar@gmail.com>"])
846 dialog
.set_website("http://www.kernel.org/pub/software/scm/git/")
847 dialog
.set_copyright("Use and distribute under the terms of the GNU General Public License")
848 dialog
.set_wrap_license(True)
853 def construct_bottom(self
):
854 """Construct the bottom half of the window."""
855 vbox
= gtk
.VBox(False, spacing
=6)
856 vbox
.set_border_width(12)
857 (width
, height
) = self
.window
.get_size()
858 vbox
.set_size_request(width
, int(height
/ 2.5))
861 self
.table
= gtk
.Table(rows
=4, columns
=4)
862 self
.table
.set_row_spacings(6)
863 self
.table
.set_col_spacings(6)
864 vbox
.pack_start(self
.table
, expand
=False, fill
=True)
867 align
= gtk
.Alignment(0.0, 0.5)
869 label
.set_markup("<b>Revision:</b>")
871 self
.table
.attach(align
, 0, 1, 0, 1, gtk
.FILL
, gtk
.FILL
)
875 align
= gtk
.Alignment(0.0, 0.5)
876 self
.revid_label
= gtk
.Label()
877 self
.revid_label
.set_selectable(True)
878 align
.add(self
.revid_label
)
879 self
.table
.attach(align
, 1, 2, 0, 1, gtk
.EXPAND | gtk
.FILL
, gtk
.FILL
)
880 self
.revid_label
.show()
883 align
= gtk
.Alignment(0.0, 0.5)
885 label
.set_markup("<b>Committer:</b>")
887 self
.table
.attach(align
, 0, 1, 1, 2, gtk
.FILL
, gtk
.FILL
)
891 align
= gtk
.Alignment(0.0, 0.5)
892 self
.committer_label
= gtk
.Label()
893 self
.committer_label
.set_selectable(True)
894 align
.add(self
.committer_label
)
895 self
.table
.attach(align
, 1, 2, 1, 2, gtk
.EXPAND | gtk
.FILL
, gtk
.FILL
)
896 self
.committer_label
.show()
899 align
= gtk
.Alignment(0.0, 0.5)
901 label
.set_markup("<b>Timestamp:</b>")
903 self
.table
.attach(align
, 0, 1, 2, 3, gtk
.FILL
, gtk
.FILL
)
907 align
= gtk
.Alignment(0.0, 0.5)
908 self
.timestamp_label
= gtk
.Label()
909 self
.timestamp_label
.set_selectable(True)
910 align
.add(self
.timestamp_label
)
911 self
.table
.attach(align
, 1, 2, 2, 3, gtk
.EXPAND | gtk
.FILL
, gtk
.FILL
)
912 self
.timestamp_label
.show()
915 align
= gtk
.Alignment(0.0, 0.5)
917 label
.set_markup("<b>Parents:</b>")
919 self
.table
.attach(align
, 0, 1, 3, 4, gtk
.FILL
, gtk
.FILL
)
922 self
.parents_widgets
= []
924 align
= gtk
.Alignment(0.0, 0.5)
926 label
.set_markup("<b>Children:</b>")
928 self
.table
.attach(align
, 2, 3, 3, 4, gtk
.FILL
, gtk
.FILL
)
931 self
.children_widgets
= []
933 scrollwin
= gtk
.ScrolledWindow()
934 scrollwin
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
935 scrollwin
.set_shadow_type(gtk
.SHADOW_IN
)
936 vbox
.pack_start(scrollwin
, expand
=True, fill
=True)
939 if have_gtksourceview
:
940 self
.message_buffer
= gtksourceview
.SourceBuffer()
941 slm
= gtksourceview
.SourceLanguagesManager()
942 gsl
= slm
.get_language_from_mime_type("text/x-patch")
943 self
.message_buffer
.set_highlight(True)
944 self
.message_buffer
.set_language(gsl
)
945 sourceview
= gtksourceview
.SourceView(self
.message_buffer
)
947 self
.message_buffer
= gtk
.TextBuffer()
948 sourceview
= gtk
.TextView(self
.message_buffer
)
950 sourceview
.set_editable(False)
951 sourceview
.modify_font(pango
.FontDescription("Monospace"))
952 scrollwin
.add(sourceview
)
957 def _treeview_cursor_cb(self
, *args
):
958 """Callback for when the treeview cursor changes."""
959 (path
, col
) = self
.treeview
.get_cursor()
960 commit
= self
.model
[path
][0]
962 if commit
.committer
is not None:
963 committer
= commit
.committer
964 timestamp
= commit
.commit_date
965 message
= commit
.get_message(self
.with_diff
)
966 revid_label
= commit
.commit_sha1
973 self
.revid_label
.set_text(revid_label
)
974 self
.committer_label
.set_text(committer
)
975 self
.timestamp_label
.set_text(timestamp
)
976 self
.message_buffer
.set_text(unicode(message
, self
.encoding
).encode('utf-8'))
978 for widget
in self
.parents_widgets
:
979 self
.table
.remove(widget
)
981 self
.parents_widgets
= []
982 self
.table
.resize(4 + len(commit
.parent_sha1
) - 1, 4)
983 for idx
, parent_id
in enumerate(commit
.parent_sha1
):
984 self
.table
.set_row_spacing(idx
+ 3, 0)
986 align
= gtk
.Alignment(0.0, 0.0)
987 self
.parents_widgets
.append(align
)
988 self
.table
.attach(align
, 1, 2, idx
+ 3, idx
+ 4,
989 gtk
.EXPAND | gtk
.FILL
, gtk
.FILL
)
992 hbox
= gtk
.HBox(False, 0)
996 label
= gtk
.Label(parent_id
)
997 label
.set_selectable(True)
998 hbox
.pack_start(label
, expand
=False, fill
=True)
1002 image
.set_from_stock(gtk
.STOCK_JUMP_TO
, gtk
.ICON_SIZE_MENU
)
1005 button
= gtk
.Button()
1007 button
.set_relief(gtk
.RELIEF_NONE
)
1008 button
.connect("clicked", self
._go
_clicked
_cb
, parent_id
)
1009 hbox
.pack_start(button
, expand
=False, fill
=True)
1013 image
.set_from_stock(gtk
.STOCK_FIND
, gtk
.ICON_SIZE_MENU
)
1016 button
= gtk
.Button()
1018 button
.set_relief(gtk
.RELIEF_NONE
)
1019 button
.set_sensitive(True)
1020 button
.connect("clicked", self
._show
_clicked
_cb
,
1021 commit
.commit_sha1
, parent_id
, self
.encoding
)
1022 hbox
.pack_start(button
, expand
=False, fill
=True)
1025 # Populate with child details
1026 for widget
in self
.children_widgets
:
1027 self
.table
.remove(widget
)
1029 self
.children_widgets
= []
1031 child_sha1
= Commit
.children_sha1
[commit
.commit_sha1
]
1033 # We don't have child
1036 if ( len(child_sha1
) > len(commit
.parent_sha1
)):
1037 self
.table
.resize(4 + len(child_sha1
) - 1, 4)
1039 for idx
, child_id
in enumerate(child_sha1
):
1040 self
.table
.set_row_spacing(idx
+ 3, 0)
1042 align
= gtk
.Alignment(0.0, 0.0)
1043 self
.children_widgets
.append(align
)
1044 self
.table
.attach(align
, 3, 4, idx
+ 3, idx
+ 4,
1045 gtk
.EXPAND | gtk
.FILL
, gtk
.FILL
)
1048 hbox
= gtk
.HBox(False, 0)
1052 label
= gtk
.Label(child_id
)
1053 label
.set_selectable(True)
1054 hbox
.pack_start(label
, expand
=False, fill
=True)
1058 image
.set_from_stock(gtk
.STOCK_JUMP_TO
, gtk
.ICON_SIZE_MENU
)
1061 button
= gtk
.Button()
1063 button
.set_relief(gtk
.RELIEF_NONE
)
1064 button
.connect("clicked", self
._go
_clicked
_cb
, child_id
)
1065 hbox
.pack_start(button
, expand
=False, fill
=True)
1069 image
.set_from_stock(gtk
.STOCK_FIND
, gtk
.ICON_SIZE_MENU
)
1072 button
= gtk
.Button()
1074 button
.set_relief(gtk
.RELIEF_NONE
)
1075 button
.set_sensitive(True)
1076 button
.connect("clicked", self
._show
_clicked
_cb
,
1077 child_id
, commit
.commit_sha1
, self
.encoding
)
1078 hbox
.pack_start(button
, expand
=False, fill
=True)
1081 def _destroy_cb(self
, widget
):
1082 """Callback for when a window we manage is destroyed."""
1087 """Stop the GTK+ main loop."""
1090 def run(self
, args
):
1091 self
.set_branch(args
)
1092 self
.window
.connect("destroy", self
._destroy
_cb
)
1096 def set_branch(self
, args
):
1097 """Fill in different windows with info from the reposiroty"""
1098 fp
= os
.popen("git rev-parse --sq --default HEAD " + list_to_string(args
, 1))
1099 git_rev_list_cmd
= fp
.read()
1101 fp
= os
.popen("git rev-list --header --topo-order --parents " + git_rev_list_cmd
)
1102 self
.update_window(fp
)
1104 def update_window(self
, fp
):
1107 self
.model
= gtk
.ListStore(gobject
.TYPE_PYOBJECT
, gobject
.TYPE_PYOBJECT
,
1108 gobject
.TYPE_PYOBJECT
, gobject
.TYPE_PYOBJECT
, str, str, str)
1110 # used for cursor positioning
1115 self
.incomplete_line
= {}
1122 input_line
= fp
.readline()
1123 while (input_line
!= ""):
1124 # The commit header ends with '\0'
1125 # This NULL is immediately followed by the sha1 of the
1127 if (input_line
[0] != '\0'):
1128 commit_lines
.append(input_line
)
1129 input_line
= fp
.readline()
1132 commit
= Commit(commit_lines
)
1133 if (commit
!= None ):
1134 self
.commits
.append(commit
)
1138 commit_lines
.append(input_line
[1:])
1139 input_line
= fp
.readline()
1143 for commit
in self
.commits
:
1144 (out_line
, last_colour
, last_nodepos
) = self
.draw_graph(commit
,
1148 self
.index
[commit
.commit_sha1
] = index
1151 self
.treeview
.set_model(self
.model
)
1152 self
.treeview
.show()
1154 def draw_graph(self
, commit
, index
, out_line
, last_colour
, last_nodepos
):
1162 if (last_nodepos
> 5):
1165 # Add the incomplete lines of the last cell in this
1167 colour
= self
.colours
[commit
.commit_sha1
]
1169 self
.colours
[commit
.commit_sha1
] = last_colour
+1
1170 last_colour
= self
.colours
[commit
.commit_sha1
]
1171 colour
= self
.colours
[commit
.commit_sha1
]
1174 node_pos
= self
.nodepos
[commit
.commit_sha1
]
1176 self
.nodepos
[commit
.commit_sha1
] = last_nodepos
+1
1177 last_nodepos
= self
.nodepos
[commit
.commit_sha1
]
1178 node_pos
= self
.nodepos
[commit
.commit_sha1
]
1180 #The first parent always continue on the same line
1182 # check we alreay have the value
1183 tmp_node_pos
= self
.nodepos
[commit
.parent_sha1
[0]]
1185 self
.colours
[commit
.parent_sha1
[0]] = colour
1186 self
.nodepos
[commit
.parent_sha1
[0]] = node_pos
1188 for sha1
in self
.incomplete_line
.keys():
1189 if (sha1
!= commit
.commit_sha1
):
1190 self
.draw_incomplete_line(sha1
, node_pos
,
1191 out_line
, in_line
, index
)
1193 del self
.incomplete_line
[sha1
]
1196 for parent_id
in commit
.parent_sha1
:
1198 tmp_node_pos
= self
.nodepos
[parent_id
]
1200 self
.colours
[parent_id
] = last_colour
+1
1201 last_colour
= self
.colours
[parent_id
]
1202 self
.nodepos
[parent_id
] = last_nodepos
+1
1203 last_nodepos
= self
.nodepos
[parent_id
]
1205 in_line
.append((node_pos
, self
.nodepos
[parent_id
],
1206 self
.colours
[parent_id
]))
1207 self
.add_incomplete_line(parent_id
)
1210 branch_tag
= self
.bt_sha1
[commit
.commit_sha1
]
1215 node
= (node_pos
, colour
, branch_tag
)
1217 self
.model
.append([commit
, node
, out_line
, in_line
,
1218 commit
.message
, commit
.author
, commit
.date
])
1220 return (in_line
, last_colour
, last_nodepos
)
1222 def add_incomplete_line(self
, sha1
):
1224 self
.incomplete_line
[sha1
].append(self
.nodepos
[sha1
])
1226 self
.incomplete_line
[sha1
] = [self
.nodepos
[sha1
]]
1228 def draw_incomplete_line(self
, sha1
, node_pos
, out_line
, in_line
, index
):
1229 for idx
, pos
in enumerate(self
.incomplete_line
[sha1
]):
1230 if(pos
== node_pos
):
1231 #remove the straight line and add a slash
1232 if ((pos
, pos
, self
.colours
[sha1
]) in out_line
):
1233 out_line
.remove((pos
, pos
, self
.colours
[sha1
]))
1234 out_line
.append((pos
, pos
+0.5, self
.colours
[sha1
]))
1235 self
.incomplete_line
[sha1
][idx
] = pos
= pos
+0.5
1237 next_commit
= self
.commits
[index
+1]
1238 if (next_commit
.commit_sha1
== sha1
and pos
!= int(pos
)):
1239 # join the line back to the node point
1240 # This need to be done only if we modified it
1241 in_line
.append((pos
, pos
-0.5, self
.colours
[sha1
]))
1245 in_line
.append((pos
, pos
, self
.colours
[sha1
]))
1248 def _go_clicked_cb(self
, widget
, revid
):
1249 """Callback for when the go button for a parent is clicked."""
1251 self
.treeview
.set_cursor(self
.index
[revid
])
1253 dialog
= gtk
.MessageDialog(parent
=None, flags
=0,
1254 type=gtk
.MESSAGE_WARNING
, buttons
=gtk
.BUTTONS_CLOSE
,
1255 message_format
=None)
1256 dialog
.set_markup("Revision <b>%s</b> not present in the list" % revid
)
1257 # revid == 0 is the parent of the first commit
1259 dialog
.format_secondary_text("Try running gitview without any options")
1263 self
.treeview
.grab_focus()
1265 def _show_clicked_cb(self
, widget
, commit_sha1
, parent_sha1
, encoding
):
1266 """Callback for when the show button for a parent is clicked."""
1267 window
= DiffWindow()
1268 window
.set_diff(commit_sha1
, parent_sha1
, encoding
)
1269 self
.treeview
.grab_focus()
1272 if __name__
== "__main__":
1274 if (len(sys
.argv
) > 1 ):
1275 if (sys
.argv
[1] == "--without-diff"):
1278 view
= GitView( without_diff
!= 1)
1279 view
.run(sys
.argv
[without_diff
:])