Code

Merge branch 'jc/attr'
[git.git] / contrib / gitview / gitview
index 4e3847d8bf38db1497f6d48ce1fcaeaad8b9592a..2d80e2bad2e6f322d7ff7e9f03a6897a11f74231 100755 (executable)
@@ -10,7 +10,8 @@ GUI browser for git repository
 This program is based on bzrk by Scott James Remnant <scott@ubuntu.com>
 """
 __copyright__ = "Copyright (C) 2006 Hewlett-Packard Development Company, L.P."
-__author__    = "Aneesh Kumar K.V <aneesh.kumar@hp.com>"
+__copyright__ = "Copyright (C) 2007 Aneesh Kumar K.V <aneesh.kumar@gmail.com"
+__author__    = "Aneesh Kumar K.V <aneesh.kumar@gmail.com>"
 
 
 import sys
@@ -24,6 +25,7 @@ import gobject
 import cairo
 import math
 import string
+import fcntl
 
 try:
     import gtksourceview
@@ -162,7 +164,7 @@ class CellRendererGraph(gtk.GenericCellRenderer):
                        for item in names:
                                names_len += len(item)
 
-               width = box_size * (cols + 1 ) + names_len 
+               width = box_size * (cols + 1 ) + names_len
                height = box_size
 
                # FIXME I have no idea how to use cell_area properly
@@ -239,20 +241,23 @@ class CellRendererGraph(gtk.GenericCellRenderer):
                                box_size / 4, 0, 2 * math.pi)
 
 
+               self.set_colour(ctx, colour, 0.0, 0.5)
+               ctx.stroke_preserve()
+
+               self.set_colour(ctx, colour, 0.5, 1.0)
+               ctx.fill_preserve()
+
                if (len(names) != 0):
                        name = " "
                        for item in names:
                                name = name + item + " "
 
-                       ctx.select_font_face("Monospace")
                        ctx.set_font_size(13)
-                       ctx.text_path(name)
-
-               self.set_colour(ctx, colour, 0.0, 0.5)
-               ctx.stroke_preserve()
-
-               self.set_colour(ctx, colour, 0.5, 1.0)
-               ctx.fill()
+                       if (flags & 1):
+                               self.set_colour(ctx, colour, 0.5, 1.0)
+                       else:
+                               self.set_colour(ctx, colour, 0.0, 0.5)
+                       ctx.show_text(name)
 
 class Commit:
        """ This represent a commit object obtained after parsing the git-rev-list
@@ -261,11 +266,11 @@ class Commit:
        children_sha1 = {}
 
        def __init__(self, commit_lines):
-               self.message            = ""
+               self.message            = ""
                self.author             = ""
-               self.date               = ""
-               self.committer          = ""
-               self.commit_date        = ""
+               self.date               = ""
+               self.committer          = ""
+               self.commit_date        = ""
                self.commit_sha1        = ""
                self.parent_sha1        = [ ]
                self.parse_commit(commit_lines)
@@ -334,6 +339,186 @@ class Commit:
                fp.close()
                return diff
 
+class AnnotateWindow:
+       """Annotate window.
+       This object represents and manages a single window containing the
+       annotate information of the file
+       """
+
+       def __init__(self):
+               self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+               self.window.set_border_width(0)
+               self.window.set_title("Git repository browser annotation window")
+
+               # Use two thirds of the screen by default
+               screen = self.window.get_screen()
+               monitor = screen.get_monitor_geometry(0)
+               width = int(monitor.width * 0.66)
+               height = int(monitor.height * 0.66)
+               self.window.set_default_size(width, height)
+
+       def add_file_data(self, filename, commit_sha1, line_num):
+               fp = os.popen("git cat-file blob " + commit_sha1 +":"+filename)
+               i = 1;
+               for line in fp.readlines():
+                       line = string.rstrip(line)
+                       self.model.append(None, ["HEAD", filename, line, i])
+                       i = i+1
+               fp.close()
+
+               # now set the cursor position
+               self.treeview.set_cursor(line_num-1)
+               self.treeview.grab_focus()
+
+       def _treeview_cursor_cb(self, *args):
+               """Callback for when the treeview cursor changes."""
+               (path, col) = self.treeview.get_cursor()
+               commit_sha1 = self.model[path][0]
+               commit_msg = ""
+               fp = os.popen("git cat-file commit " + commit_sha1)
+               for line in fp.readlines():
+                       commit_msg =  commit_msg + line
+               fp.close()
+
+               self.commit_buffer.set_text(commit_msg)
+
+       def _treeview_row_activated(self, *args):
+               """Callback for when the treeview row gets selected."""
+               (path, col) = self.treeview.get_cursor()
+               commit_sha1 = self.model[path][0]
+               filename    = self.model[path][1]
+               line_num    = self.model[path][3]
+
+               window = AnnotateWindow();
+               fp = os.popen("git rev-parse "+ commit_sha1 + "~1")
+               commit_sha1 = string.strip(fp.readline())
+               fp.close()
+               window.annotate(filename, commit_sha1, line_num)
+
+       def data_ready(self, source, condition):
+               while (1):
+                       try :
+                               buffer = source.read(8192)
+                       except:
+                               # resource temporary not available
+                               return True
+
+                       if (len(buffer) == 0):
+                               gobject.source_remove(self.io_watch_tag)
+                               source.close()
+                               return False
+
+                       for buff in buffer.split("\n"):
+                               annotate_line = re.compile('^([0-9a-f]{40}) (.+) (.+) (.+)$')
+                               m = annotate_line.match(buff)
+                               if not m:
+                                       annotate_line = re.compile('^(filename) (.+)$')
+                                       m = annotate_line.match(buff)
+                                       if not m:
+                                               continue
+                                       filename = m.group(2)
+                               else:
+                                       self.commit_sha1 = m.group(1)
+                                       self.source_line = int(m.group(2))
+                                       self.result_line = int(m.group(3))
+                                       self.count          = int(m.group(4))
+                                       #set the details only when we have the file name
+                                       continue
+
+                               while (self.count > 0):
+                                       # set at result_line + count-1 the sha1 as commit_sha1
+                                       self.count = self.count - 1
+                                       iter = self.model.iter_nth_child(None, self.result_line + self.count-1)
+                                       self.model.set(iter, 0, self.commit_sha1, 1, filename, 3, self.source_line)
+
+
+       def annotate(self, filename, commit_sha1, line_num):
+               # verify the commit_sha1 specified has this filename
+
+               fp = os.popen("git ls-tree "+ commit_sha1 + " -- " + filename)
+               line = string.strip(fp.readline())
+               if line == '':
+                       # pop up the message the file is not there as a part of the commit
+                       fp.close()
+                       dialog = gtk.MessageDialog(parent=None, flags=0,
+                                       type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
+                                       message_format=None)
+                       dialog.set_markup("The file %s is not present in the parent commit %s" % (filename, commit_sha1))
+                       dialog.run()
+                       dialog.destroy()
+                       return
+
+               fp.close()
+
+               vpan = gtk.VPaned();
+               self.window.add(vpan);
+               vpan.show()
+
+               scrollwin = gtk.ScrolledWindow()
+               scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+               scrollwin.set_shadow_type(gtk.SHADOW_IN)
+               vpan.pack1(scrollwin, True, True);
+               scrollwin.show()
+
+               self.model = gtk.TreeStore(str, str, str, int)
+               self.treeview = gtk.TreeView(self.model)
+               self.treeview.set_rules_hint(True)
+               self.treeview.set_search_column(0)
+               self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
+               self.treeview.connect("row-activated", self._treeview_row_activated)
+               scrollwin.add(self.treeview)
+               self.treeview.show()
+
+               cell = gtk.CellRendererText()
+               cell.set_property("width-chars", 10)
+               cell.set_property("ellipsize", pango.ELLIPSIZE_END)
+               column = gtk.TreeViewColumn("Commit")
+               column.set_resizable(True)
+               column.pack_start(cell, expand=True)
+               column.add_attribute(cell, "text", 0)
+               self.treeview.append_column(column)
+
+               cell = gtk.CellRendererText()
+               cell.set_property("width-chars", 20)
+               cell.set_property("ellipsize", pango.ELLIPSIZE_END)
+               column = gtk.TreeViewColumn("File Name")
+               column.set_resizable(True)
+               column.pack_start(cell, expand=True)
+               column.add_attribute(cell, "text", 1)
+               self.treeview.append_column(column)
+
+               cell = gtk.CellRendererText()
+               cell.set_property("width-chars", 20)
+               cell.set_property("ellipsize", pango.ELLIPSIZE_END)
+               column = gtk.TreeViewColumn("Data")
+               column.set_resizable(True)
+               column.pack_start(cell, expand=True)
+               column.add_attribute(cell, "text", 2)
+               self.treeview.append_column(column)
+
+               # The commit message window
+               scrollwin = gtk.ScrolledWindow()
+               scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+               scrollwin.set_shadow_type(gtk.SHADOW_IN)
+               vpan.pack2(scrollwin, True, True);
+               scrollwin.show()
+
+               commit_text = gtk.TextView()
+               self.commit_buffer = gtk.TextBuffer()
+               commit_text.set_buffer(self.commit_buffer)
+               scrollwin.add(commit_text)
+               commit_text.show()
+
+               self.window.show()
+
+               self.add_file_data(filename, commit_sha1, line_num)
+
+               fp = os.popen("git blame --incremental -- " + filename + " " + commit_sha1)
+               flags = fcntl.fcntl(fp.fileno(), fcntl.F_GETFL)
+               fcntl.fcntl(fp.fileno(), fcntl.F_SETFL, flags | os.O_NONBLOCK)
+               self.io_watch_tag = gobject.io_add_watch(fp, gobject.IO_IN, self.data_ready)
+
+
 class DiffWindow:
        """Diff window.
        This object represents and manages a single window containing the
@@ -352,6 +537,7 @@ class DiffWindow:
                height = int(monitor.height * 0.66)
                self.window.set_default_size(width, height)
 
+
                self.construct()
 
        def construct(self):
@@ -365,13 +551,15 @@ class DiffWindow:
                save_menu.connect("activate", self.save_menu_response, "save")
                save_menu.show()
                menu_bar.append(save_menu)
-               vbox.pack_start(menu_bar, False, False, 2)
+               vbox.pack_start(menu_bar, expand=False, fill=True)
                menu_bar.show()
 
+               hpan = gtk.HPaned()
+
                scrollwin = gtk.ScrolledWindow()
                scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
                scrollwin.set_shadow_type(gtk.SHADOW_IN)
-               vbox.pack_start(scrollwin, expand=True, fill=True)
+               hpan.pack1(scrollwin, True, True)
                scrollwin.show()
 
                if have_gtksourceview:
@@ -385,13 +573,79 @@ class DiffWindow:
                        self.buffer = gtk.TextBuffer()
                        sourceview = gtk.TextView(self.buffer)
 
+
                sourceview.set_editable(False)
                sourceview.modify_font(pango.FontDescription("Monospace"))
                scrollwin.add(sourceview)
                sourceview.show()
 
+               # The file hierarchy: a scrollable treeview
+               scrollwin = gtk.ScrolledWindow()
+               scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+               scrollwin.set_shadow_type(gtk.SHADOW_IN)
+               scrollwin.set_size_request(20, -1)
+               hpan.pack2(scrollwin, True, True)
+               scrollwin.show()
 
-       def set_diff(self, commit_sha1, parent_sha1):
+               self.model = gtk.TreeStore(str, str, str)
+               self.treeview = gtk.TreeView(self.model)
+               self.treeview.set_search_column(1)
+               self.treeview.connect("cursor-changed", self._treeview_clicked)
+               scrollwin.add(self.treeview)
+               self.treeview.show()
+
+               cell = gtk.CellRendererText()
+               cell.set_property("width-chars", 20)
+               column = gtk.TreeViewColumn("Select to annotate")
+               column.pack_start(cell, expand=True)
+               column.add_attribute(cell, "text", 0)
+               self.treeview.append_column(column)
+
+               vbox.pack_start(hpan, expand=True, fill=True)
+               hpan.show()
+
+       def _treeview_clicked(self, *args):
+               """Callback for when the treeview cursor changes."""
+               (path, col) = self.treeview.get_cursor()
+               specific_file = self.model[path][1]
+               commit_sha1 =  self.model[path][2]
+               if specific_file ==  None :
+                       return
+               elif specific_file ==  "" :
+                       specific_file =  None
+
+               window = AnnotateWindow();
+               window.annotate(specific_file, commit_sha1, 1)
+
+
+       def commit_files(self, commit_sha1, parent_sha1):
+               self.model.clear()
+               add  = self.model.append(None, [ "Added", None, None])
+               dele = self.model.append(None, [ "Deleted", None, None])
+               mod  = self.model.append(None, [ "Modified", None, None])
+               diff_tree = re.compile('^(:.{6}) (.{6}) (.{40}) (.{40}) (A|D|M)\s(.+)$')
+               fp = os.popen("git diff-tree -r --no-commit-id " + parent_sha1 + " " + commit_sha1)
+               while 1:
+                       line = string.strip(fp.readline())
+                       if line == '':
+                               break
+                       m = diff_tree.match(line)
+                       if not m:
+                               continue
+
+                       attr = m.group(5)
+                       filename = m.group(6)
+                       if attr == "A":
+                               self.model.append(add,  [filename, filename, commit_sha1])
+                       elif attr == "D":
+                               self.model.append(dele, [filename, filename, commit_sha1])
+                       elif attr == "M":
+                               self.model.append(mod,  [filename, filename, commit_sha1])
+               fp.close()
+
+               self.treeview.expand_all()
+
+       def set_diff(self, commit_sha1, parent_sha1, encoding):
                """Set the differences showed by this window.
                Compares the two trees and populates the window with the
                differences.
@@ -401,8 +655,9 @@ class DiffWindow:
                        return
 
                fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
-               self.buffer.set_text(fp.read())
+               self.buffer.set_text(unicode(fp.read(), encoding).encode('utf-8'))
                fp.close()
+               self.commit_files(commit_sha1, parent_sha1)
                self.window.show()
 
        def save_menu_response(self, widget, string):
@@ -422,14 +677,15 @@ class DiffWindow:
 class GitView:
        """ This is the main class
        """
-       version = "0.7"
+       version = "0.9"
 
        def __init__(self, with_diff=0):
                self.with_diff = with_diff
-               self.window =   gtk.Window(gtk.WINDOW_TOPLEVEL)
+               self.window =   gtk.Window(gtk.WINDOW_TOPLEVEL)
                self.window.set_border_width(0)
                self.window.set_title("Git repository browser")
 
+               self.get_encoding()
                self.get_bt_sha1()
 
                # Use three-quarters of the screen by default
@@ -445,8 +701,32 @@ class GitView:
 
                self.accel_group = gtk.AccelGroup()
                self.window.add_accel_group(self.accel_group)
+               self.accel_group.connect_group(0xffc2, 0, gtk.ACCEL_LOCKED, self.refresh);
+               self.accel_group.connect_group(0xffc1, 0, gtk.ACCEL_LOCKED, self.maximize);
+               self.accel_group.connect_group(0xffc8, 0, gtk.ACCEL_LOCKED, self.fullscreen);
+               self.accel_group.connect_group(0xffc9, 0, gtk.ACCEL_LOCKED, self.unfullscreen);
 
-               self.construct()
+               self.window.add(self.construct())
+
+       def refresh(self, widget, event=None, *arguments, **keywords):
+               self.get_encoding()
+               self.get_bt_sha1()
+               Commit.children_sha1 = {}
+               self.set_branch(sys.argv[without_diff:])
+               self.window.show()
+               return True
+
+       def maximize(self, widget, event=None, *arguments, **keywords):
+               self.window.maximize()
+               return True
+
+       def fullscreen(self, widget, event=None, *arguments, **keywords):
+               self.window.fullscreen()
+               return True
+
+       def unfullscreen(self, widget, event=None, *arguments, **keywords):
+               self.window.unfullscreen()
+               return True
 
        def get_bt_sha1(self):
                """ Update the bt_sha1 dictionary with the
@@ -468,22 +748,20 @@ class GitView:
                        self.bt_sha1[sha1].append(name)
                fp.close()
 
+       def get_encoding(self):
+               fp = os.popen("git config --get i18n.commitencoding")
+               self.encoding=string.strip(fp.readline())
+               fp.close()
+               if (self.encoding == ""):
+                       self.encoding = "utf-8"
+
 
        def construct(self):
                """Construct the window contents."""
+               vbox = gtk.VBox()
                paned = gtk.VPaned()
                paned.pack1(self.construct_top(), resize=False, shrink=True)
                paned.pack2(self.construct_bottom(), resize=False, shrink=True)
-               self.window.add(paned)
-               paned.show()
-
-
-       def construct_top(self):
-               """Construct the top-half of the window."""
-               vbox = gtk.VBox(spacing=6)
-               vbox.set_border_width(12)
-               vbox.show()
-
                menu_bar = gtk.MenuBar()
                menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
                help_menu = gtk.MenuItem("Help")
@@ -495,11 +773,23 @@ class GitView:
                help_menu.set_submenu(menu)
                help_menu.show()
                menu_bar.append(help_menu)
-               vbox.pack_start(menu_bar, False, False, 2)
                menu_bar.show()
+               vbox.pack_start(menu_bar, expand=False, fill=True)
+               vbox.pack_start(paned, expand=True, fill=True)
+               paned.show()
+               vbox.show()
+               return vbox
+
+
+       def construct_top(self):
+               """Construct the top-half of the window."""
+               vbox = gtk.VBox(spacing=6)
+               vbox.set_border_width(12)
+               vbox.show()
+
 
                scrollwin = gtk.ScrolledWindow()
-               scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+               scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
                scrollwin.set_shadow_type(gtk.SHADOW_IN)
                vbox.pack_start(scrollwin, expand=True, fill=True)
                scrollwin.show()
@@ -552,7 +842,7 @@ class GitView:
                dialog = gtk.AboutDialog()
                dialog.set_name("Gitview")
                dialog.set_version(GitView.version)
-               dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
+               dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@gmail.com>"])
                dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
                dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
                dialog.set_wrap_license(True)
@@ -683,7 +973,7 @@ class GitView:
                self.revid_label.set_text(revid_label)
                self.committer_label.set_text(committer)
                self.timestamp_label.set_text(timestamp)
-               self.message_buffer.set_text(message)
+               self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
 
                for widget in self.parents_widgets:
                        self.table.remove(widget)
@@ -728,7 +1018,7 @@ class GitView:
                        button.set_relief(gtk.RELIEF_NONE)
                        button.set_sensitive(True)
                        button.connect("clicked", self._show_clicked_cb,
-                                       commit.commit_sha1, parent_id)
+                                       commit.commit_sha1, parent_id, self.encoding)
                        hbox.pack_start(button, expand=False, fill=True)
                        button.show()
 
@@ -784,7 +1074,7 @@ class GitView:
                        button.set_relief(gtk.RELIEF_NONE)
                        button.set_sensitive(True)
                        button.connect("clicked", self._show_clicked_cb,
-                                       child_id, commit.commit_sha1)
+                                       child_id, commit.commit_sha1, self.encoding)
                        hbox.pack_start(button, expand=False, fill=True)
                        button.show()
 
@@ -870,15 +1160,15 @@ class GitView:
 
                # Reset nodepostion
                if (last_nodepos > 5):
-                       last_nodepos = -1 
+                       last_nodepos = -1
 
                # Add the incomplete lines of the last cell in this
                try:
                        colour = self.colours[commit.commit_sha1]
                except KeyError:
                        self.colours[commit.commit_sha1] = last_colour+1
-                       last_colour = self.colours[commit.commit_sha1] 
-                       colour =   self.colours[commit.commit_sha1] 
+                       last_colour = self.colours[commit.commit_sha1]
+                       colour =   self.colours[commit.commit_sha1]
 
                try:
                        node_pos = self.nodepos[commit.commit_sha1]
@@ -910,7 +1200,7 @@ class GitView:
                                self.colours[parent_id] = last_colour+1
                                last_colour = self.colours[parent_id]
                                self.nodepos[parent_id] = last_nodepos+1
-                               last_nodepos = self.nodepos[parent_id] 
+                               last_nodepos = self.nodepos[parent_id]
 
                        in_line.append((node_pos, self.nodepos[parent_id],
                                                self.colours[parent_id]))
@@ -946,7 +1236,7 @@ class GitView:
                        try:
                                next_commit = self.commits[index+1]
                                if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
-                               # join the line back to the node point 
+                               # join the line back to the node point
                                # This need to be done only if we modified it
                                        in_line.append((pos, pos-0.5, self.colours[sha1]))
                                        continue;
@@ -960,21 +1250,26 @@ class GitView:
                try:
                        self.treeview.set_cursor(self.index[revid])
                except KeyError:
-                       print "Revision %s not present in the list" % revid
+                       dialog = gtk.MessageDialog(parent=None, flags=0,
+                                       type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
+                                       message_format=None)
+                       dialog.set_markup("Revision <b>%s</b> not present in the list" % revid)
                        # revid == 0 is the parent of the first commit
                        if (revid != 0 ):
-                               print "Try running gitview without any options"
+                               dialog.format_secondary_text("Try running gitview without any options")
+                       dialog.run()
+                       dialog.destroy()
 
                self.treeview.grab_focus()
 
-       def _show_clicked_cb(self, widget,  commit_sha1, parent_sha1):
+       def _show_clicked_cb(self, widget,  commit_sha1, parent_sha1, encoding):
                """Callback for when the show button for a parent is clicked."""
                window = DiffWindow()
-               window.set_diff(commit_sha1, parent_sha1)
+               window.set_diff(commit_sha1, parent_sha1, encoding)
                self.treeview.grab_focus()
 
+without_diff = 0
 if __name__ == "__main__":
-       without_diff = 0
 
        if (len(sys.argv) > 1 ):
                if (sys.argv[1] == "--without-diff"):