1d042e3aa74dce29e6ed67b1b15d3ccefc8de6c3
   1#! /usr/bin/env python
   2
   3# This program is free software; you can redistribute it and/or modify
   4# it under the terms of the GNU General Public License as published by
   5# the Free Software Foundation; either version 2 of the License, or
   6# (at your option) any later version.
   7
   8""" gitview
   9GUI browser for git repository
  10This program is based on bzrk by Scott James Remnant <scott@ubuntu.com>
  11"""
  12__copyright__ = "Copyright (C) 2006 Hewlett-Packard Development Company, L.P."
  13__author__    = "Aneesh Kumar K.V <aneesh.kumar@hp.com>"
  14
  15
  16import sys
  17import os
  18import gtk
  19import pygtk
  20import pango
  21import re
  22import time
  23import gobject
  24import cairo
  25import math
  26import string
  27
  28try:
  29    import gtksourceview
  30    have_gtksourceview = True
  31except ImportError:
  32    have_gtksourceview = False
  33    print "Running without gtksourceview module"
  34
  35re_ident = re.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
  36
  37def list_to_string(args, skip):
  38        count = len(args)
  39        i = skip
  40        str_arg=" "
  41        while (i < count ):
  42                str_arg = str_arg + args[i]
  43                str_arg = str_arg + " "
  44                i = i+1
  45
  46        return str_arg
  47
  48def show_date(epoch, tz):
  49        secs = float(epoch)
  50        tzsecs = float(tz[1:3]) * 3600
  51        tzsecs += float(tz[3:5]) * 60
  52        if (tz[0] == "+"):
  53                secs += tzsecs
  54        else:
  55                secs -= tzsecs
  56
  57        return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs))
  58
  59
  60class CellRendererGraph(gtk.GenericCellRenderer):
  61        """Cell renderer for directed graph.
  62
  63        This module contains the implementation of a custom GtkCellRenderer that
  64        draws part of the directed graph based on the lines suggested by the code
  65        in graph.py.
  66
  67        Because we're shiny, we use Cairo to do this, and because we're naughty
  68        we cheat and draw over the bits of the TreeViewColumn that are supposed to
  69        just be for the background.
  70
  71        Properties:
  72        node              (column, colour, [ names ]) tuple to draw revision node,
  73        in_lines          (start, end, colour) tuple list to draw inward lines,
  74        out_lines         (start, end, colour) tuple list to draw outward lines.
  75        """
  76
  77        __gproperties__ = {
  78        "node":         ( gobject.TYPE_PYOBJECT, "node",
  79                          "revision node instruction",
  80                          gobject.PARAM_WRITABLE
  81                        ),
  82        "in-lines":     ( gobject.TYPE_PYOBJECT, "in-lines",
  83                          "instructions to draw lines into the cell",
  84                          gobject.PARAM_WRITABLE
  85                        ),
  86        "out-lines":    ( gobject.TYPE_PYOBJECT, "out-lines",
  87                          "instructions to draw lines out of the cell",
  88                          gobject.PARAM_WRITABLE
  89                        ),
  90        }
  91
  92        def do_set_property(self, property, value):
  93                """Set properties from GObject properties."""
  94                if property.name == "node":
  95                        self.node = value
  96                elif property.name == "in-lines":
  97                        self.in_lines = value
  98                elif property.name == "out-lines":
  99                        self.out_lines = value
 100                else:
 101                        raise AttributeError, "no such property: '%s'" % property.name
 102
 103        def box_size(self, widget):
 104                """Calculate box size based on widget's font.
 105
 106                Cache this as it's probably expensive to get.  It ensures that we
 107                draw the graph at least as large as the text.
 108                """
 109                try:
 110                        return self._box_size
 111                except AttributeError:
 112                        pango_ctx = widget.get_pango_context()
 113                        font_desc = widget.get_style().font_desc
 114                        metrics = pango_ctx.get_metrics(font_desc)
 115
 116                        ascent = pango.PIXELS(metrics.get_ascent())
 117                        descent = pango.PIXELS(metrics.get_descent())
 118
 119                        self._box_size = ascent + descent + 6
 120                        return self._box_size
 121
 122        def set_colour(self, ctx, colour, bg, fg):
 123                """Set the context source colour.
 124
 125                Picks a distinct colour based on an internal wheel; the bg
 126                parameter provides the value that should be assigned to the 'zero'
 127                colours and the fg parameter provides the multiplier that should be
 128                applied to the foreground colours.
 129                """
 130                colours = [
 131                    ( 1.0, 0.0, 0.0 ),
 132                    ( 1.0, 1.0, 0.0 ),
 133                    ( 0.0, 1.0, 0.0 ),
 134                    ( 0.0, 1.0, 1.0 ),
 135                    ( 0.0, 0.0, 1.0 ),
 136                    ( 1.0, 0.0, 1.0 ),
 137                    ]
 138
 139                colour %= len(colours)
 140                red   = (colours[colour][0] * fg) or bg
 141                green = (colours[colour][1] * fg) or bg
 142                blue  = (colours[colour][2] * fg) or bg
 143
 144                ctx.set_source_rgb(red, green, blue)
 145
 146        def on_get_size(self, widget, cell_area):
 147                """Return the size we need for this cell.
 148
 149                Each cell is drawn individually and is only as wide as it needs
 150                to be, we let the TreeViewColumn take care of making them all
 151                line up.
 152                """
 153                box_size = self.box_size(widget)
 154
 155                cols = self.node[0]
 156                for start, end, colour in self.in_lines + self.out_lines:
 157                        cols = int(max(cols, start, end))
 158
 159                (column, colour, names) = self.node
 160                names_len = 0
 161                if (len(names) != 0):
 162                        for item in names:
 163                                names_len += len(item)
 164
 165                width = box_size * (cols + 1 ) + names_len 
 166                height = box_size
 167
 168                # FIXME I have no idea how to use cell_area properly
 169                return (0, 0, width, height)
 170
 171        def on_render(self, window, widget, bg_area, cell_area, exp_area, flags):
 172                """Render an individual cell.
 173
 174                Draws the cell contents using cairo, taking care to clip what we
 175                do to within the background area so we don't draw over other cells.
 176                Note that we're a bit naughty there and should really be drawing
 177                in the cell_area (or even the exposed area), but we explicitly don't
 178                want any gutter.
 179
 180                We try and be a little clever, if the line we need to draw is going
 181                to cross other columns we actually draw it as in the .---' style
 182                instead of a pure diagonal ... this reduces confusion by an
 183                incredible amount.
 184                """
 185                ctx = window.cairo_create()
 186                ctx.rectangle(bg_area.x, bg_area.y, bg_area.width, bg_area.height)
 187                ctx.clip()
 188
 189                box_size = self.box_size(widget)
 190
 191                ctx.set_line_width(box_size / 8)
 192                ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
 193
 194                # Draw lines into the cell
 195                for start, end, colour in self.in_lines:
 196                        ctx.move_to(cell_area.x + box_size * start + box_size / 2,
 197                                        bg_area.y - bg_area.height / 2)
 198
 199                        if start - end > 1:
 200                                ctx.line_to(cell_area.x + box_size * start, bg_area.y)
 201                                ctx.line_to(cell_area.x + box_size * end + box_size, bg_area.y)
 202                        elif start - end < -1:
 203                                ctx.line_to(cell_area.x + box_size * start + box_size,
 204                                                bg_area.y)
 205                                ctx.line_to(cell_area.x + box_size * end, bg_area.y)
 206
 207                        ctx.line_to(cell_area.x + box_size * end + box_size / 2,
 208                                        bg_area.y + bg_area.height / 2)
 209
 210                        self.set_colour(ctx, colour, 0.0, 0.65)
 211                        ctx.stroke()
 212
 213                # Draw lines out of the cell
 214                for start, end, colour in self.out_lines:
 215                        ctx.move_to(cell_area.x + box_size * start + box_size / 2,
 216                                        bg_area.y + bg_area.height / 2)
 217
 218                        if start - end > 1:
 219                                ctx.line_to(cell_area.x + box_size * start,
 220                                                bg_area.y + bg_area.height)
 221                                ctx.line_to(cell_area.x + box_size * end + box_size,
 222                                                bg_area.y + bg_area.height)
 223                        elif start - end < -1:
 224                                ctx.line_to(cell_area.x + box_size * start + box_size,
 225                                                bg_area.y + bg_area.height)
 226                                ctx.line_to(cell_area.x + box_size * end,
 227                                                bg_area.y + bg_area.height)
 228
 229                        ctx.line_to(cell_area.x + box_size * end + box_size / 2,
 230                                        bg_area.y + bg_area.height / 2 + bg_area.height)
 231
 232                        self.set_colour(ctx, colour, 0.0, 0.65)
 233                        ctx.stroke()
 234
 235                # Draw the revision node in the right column
 236                (column, colour, names) = self.node
 237                ctx.arc(cell_area.x + box_size * column + box_size / 2,
 238                                cell_area.y + cell_area.height / 2,
 239                                box_size / 4, 0, 2 * math.pi)
 240
 241
 242                if (len(names) != 0):
 243                        name = " "
 244                        for item in names:
 245                                name = name + item + " "
 246
 247                        ctx.select_font_face("Monospace")
 248                        ctx.set_font_size(13)
 249                        ctx.text_path(name)
 250
 251                self.set_colour(ctx, colour, 0.0, 0.5)
 252                ctx.stroke_preserve()
 253
 254                self.set_colour(ctx, colour, 0.5, 1.0)
 255                ctx.fill()
 256
 257class Commit:
 258        """ This represent a commit object obtained after parsing the git-rev-list
 259        output """
 260
 261        children_sha1 = {}
 262
 263        def __init__(self, commit_lines):
 264                self.message            = ""
 265                self.author             = ""
 266                self.date               = ""
 267                self.committer          = ""
 268                self.commit_date        = ""
 269                self.commit_sha1        = ""
 270                self.parent_sha1        = [ ]
 271                self.parse_commit(commit_lines)
 272
 273
 274        def parse_commit(self, commit_lines):
 275
 276                # First line is the sha1 lines
 277                line = string.strip(commit_lines[0])
 278                sha1 = re.split(" ", line)
 279                self.commit_sha1 = sha1[0]
 280                self.parent_sha1 = sha1[1:]
 281
 282                #build the child list
 283                for parent_id in self.parent_sha1:
 284                        try:
 285                                Commit.children_sha1[parent_id].append(self.commit_sha1)
 286                        except KeyError:
 287                                Commit.children_sha1[parent_id] = [self.commit_sha1]
 288
 289                # IF we don't have parent
 290                if (len(self.parent_sha1) == 0):
 291                        self.parent_sha1 = [0]
 292
 293                for line in commit_lines[1:]:
 294                        m = re.match("^ ", line)
 295                        if (m != None):
 296                                # First line of the commit message used for short log
 297                                if self.message == "":
 298                                        self.message = string.strip(line)
 299                                continue
 300
 301                        m = re.match("tree", line)
 302                        if (m != None):
 303                                continue
 304
 305                        m = re.match("parent", line)
 306                        if (m != None):
 307                                continue
 308
 309                        m = re_ident.match(line)
 310                        if (m != None):
 311                                date = show_date(m.group('epoch'), m.group('tz'))
 312                                if m.group(1) == "author":
 313                                        self.author = m.group('ident')
 314                                        self.date = date
 315                                elif m.group(1) == "committer":
 316                                        self.committer = m.group('ident')
 317                                        self.commit_date = date
 318
 319                                continue
 320
 321        def get_message(self, with_diff=0):
 322                if (with_diff == 1):
 323                        message = self.diff_tree()
 324                else:
 325                        fp = os.popen("git cat-file commit " + self.commit_sha1)
 326                        message = fp.read()
 327                        fp.close()
 328
 329                return message
 330
 331        def diff_tree(self):
 332                fp = os.popen("git diff-tree --pretty --cc  -v -p --always " +  self.commit_sha1)
 333                diff = fp.read()
 334                fp.close()
 335                return diff
 336
 337class DiffWindow:
 338        """Diff window.
 339        This object represents and manages a single window containing the
 340        differences between two revisions on a branch.
 341        """
 342
 343        def __init__(self):
 344                self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
 345                self.window.set_border_width(0)
 346                self.window.set_title("Git repository browser diff window")
 347
 348                # Use two thirds of the screen by default
 349                screen = self.window.get_screen()
 350                monitor = screen.get_monitor_geometry(0)
 351                width = int(monitor.width * 0.66)
 352                height = int(monitor.height * 0.66)
 353                self.window.set_default_size(width, height)
 354
 355                self.construct()
 356
 357        def construct(self):
 358                """Construct the window contents."""
 359                vbox = gtk.VBox()
 360                self.window.add(vbox)
 361                vbox.show()
 362
 363                menu_bar = gtk.MenuBar()
 364                save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
 365                save_menu.connect("activate", self.save_menu_response, "save")
 366                save_menu.show()
 367                menu_bar.append(save_menu)
 368                vbox.pack_start(menu_bar, False, False, 2)
 369                menu_bar.show()
 370
 371                scrollwin = gtk.ScrolledWindow()
 372                scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 373                scrollwin.set_shadow_type(gtk.SHADOW_IN)
 374                vbox.pack_start(scrollwin, expand=True, fill=True)
 375                scrollwin.show()
 376
 377                if have_gtksourceview:
 378                        self.buffer = gtksourceview.SourceBuffer()
 379                        slm = gtksourceview.SourceLanguagesManager()
 380                        gsl = slm.get_language_from_mime_type("text/x-patch")
 381                        self.buffer.set_highlight(True)
 382                        self.buffer.set_language(gsl)
 383                        sourceview = gtksourceview.SourceView(self.buffer)
 384                else:
 385                        self.buffer = gtk.TextBuffer()
 386                        sourceview = gtk.TextView(self.buffer)
 387
 388                sourceview.set_editable(False)
 389                sourceview.modify_font(pango.FontDescription("Monospace"))
 390                scrollwin.add(sourceview)
 391                sourceview.show()
 392
 393
 394        def set_diff(self, commit_sha1, parent_sha1, encoding):
 395                """Set the differences showed by this window.
 396                Compares the two trees and populates the window with the
 397                differences.
 398                """
 399                # Diff with the first commit or the last commit shows nothing
 400                if (commit_sha1 == 0 or parent_sha1 == 0 ):
 401                        return
 402
 403                fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
 404                self.buffer.set_text(unicode(fp.read(), encoding).encode('utf-8'))
 405                fp.close()
 406                self.window.show()
 407
 408        def save_menu_response(self, widget, string):
 409                dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
 410                                (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
 411                                        gtk.STOCK_SAVE, gtk.RESPONSE_OK))
 412                dialog.set_default_response(gtk.RESPONSE_OK)
 413                response = dialog.run()
 414                if response == gtk.RESPONSE_OK:
 415                        patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
 416                                        self.buffer.get_end_iter())
 417                        fp = open(dialog.get_filename(), "w")
 418                        fp.write(patch_buffer)
 419                        fp.close()
 420                dialog.destroy()
 421
 422class GitView:
 423        """ This is the main class
 424        """
 425        version = "0.7"
 426
 427        def __init__(self, with_diff=0):
 428                self.with_diff = with_diff
 429                self.window =   gtk.Window(gtk.WINDOW_TOPLEVEL)
 430                self.window.set_border_width(0)
 431                self.window.set_title("Git repository browser")
 432
 433                self.get_encoding()
 434                self.get_bt_sha1()
 435
 436                # Use three-quarters of the screen by default
 437                screen = self.window.get_screen()
 438                monitor = screen.get_monitor_geometry(0)
 439                width = int(monitor.width * 0.75)
 440                height = int(monitor.height * 0.75)
 441                self.window.set_default_size(width, height)
 442
 443                # FIXME AndyFitz!
 444                icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
 445                self.window.set_icon(icon)
 446
 447                self.accel_group = gtk.AccelGroup()
 448                self.window.add_accel_group(self.accel_group)
 449
 450                self.construct()
 451
 452        def get_bt_sha1(self):
 453                """ Update the bt_sha1 dictionary with the
 454                respective sha1 details """
 455
 456                self.bt_sha1 = { }
 457                ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
 458                fp = os.popen('git ls-remote "${GIT_DIR-.git}"')
 459                while 1:
 460                        line = string.strip(fp.readline())
 461                        if line == '':
 462                                break
 463                        m = ls_remote.match(line)
 464                        if not m:
 465                                continue
 466                        (sha1, name) = (m.group(1), m.group(2))
 467                        if not self.bt_sha1.has_key(sha1):
 468                                self.bt_sha1[sha1] = []
 469                        self.bt_sha1[sha1].append(name)
 470                fp.close()
 471
 472        def get_encoding(self):
 473                fp = os.popen("git repo-config --get i18n.commitencoding")
 474                self.encoding=string.strip(fp.readline())
 475                fp.close()
 476                if (self.encoding == ""):
 477                        self.encoding = "utf-8"
 478
 479
 480        def construct(self):
 481                """Construct the window contents."""
 482                paned = gtk.VPaned()
 483                paned.pack1(self.construct_top(), resize=False, shrink=True)
 484                paned.pack2(self.construct_bottom(), resize=False, shrink=True)
 485                self.window.add(paned)
 486                paned.show()
 487
 488
 489        def construct_top(self):
 490                """Construct the top-half of the window."""
 491                vbox = gtk.VBox(spacing=6)
 492                vbox.set_border_width(12)
 493                vbox.show()
 494
 495                menu_bar = gtk.MenuBar()
 496                menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
 497                help_menu = gtk.MenuItem("Help")
 498                menu = gtk.Menu()
 499                about_menu = gtk.MenuItem("About")
 500                menu.append(about_menu)
 501                about_menu.connect("activate", self.about_menu_response, "about")
 502                about_menu.show()
 503                help_menu.set_submenu(menu)
 504                help_menu.show()
 505                menu_bar.append(help_menu)
 506                vbox.pack_start(menu_bar, False, False, 2)
 507                menu_bar.show()
 508
 509                scrollwin = gtk.ScrolledWindow()
 510                scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
 511                scrollwin.set_shadow_type(gtk.SHADOW_IN)
 512                vbox.pack_start(scrollwin, expand=True, fill=True)
 513                scrollwin.show()
 514
 515                self.treeview = gtk.TreeView()
 516                self.treeview.set_rules_hint(True)
 517                self.treeview.set_search_column(4)
 518                self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
 519                scrollwin.add(self.treeview)
 520                self.treeview.show()
 521
 522                cell = CellRendererGraph()
 523                column = gtk.TreeViewColumn()
 524                column.set_resizable(True)
 525                column.pack_start(cell, expand=True)
 526                column.add_attribute(cell, "node", 1)
 527                column.add_attribute(cell, "in-lines", 2)
 528                column.add_attribute(cell, "out-lines", 3)
 529                self.treeview.append_column(column)
 530
 531                cell = gtk.CellRendererText()
 532                cell.set_property("width-chars", 65)
 533                cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 534                column = gtk.TreeViewColumn("Message")
 535                column.set_resizable(True)
 536                column.pack_start(cell, expand=True)
 537                column.add_attribute(cell, "text", 4)
 538                self.treeview.append_column(column)
 539
 540                cell = gtk.CellRendererText()
 541                cell.set_property("width-chars", 40)
 542                cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 543                column = gtk.TreeViewColumn("Author")
 544                column.set_resizable(True)
 545                column.pack_start(cell, expand=True)
 546                column.add_attribute(cell, "text", 5)
 547                self.treeview.append_column(column)
 548
 549                cell = gtk.CellRendererText()
 550                cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 551                column = gtk.TreeViewColumn("Date")
 552                column.set_resizable(True)
 553                column.pack_start(cell, expand=True)
 554                column.add_attribute(cell, "text", 6)
 555                self.treeview.append_column(column)
 556
 557                return vbox
 558
 559        def about_menu_response(self, widget, string):
 560                dialog = gtk.AboutDialog()
 561                dialog.set_name("Gitview")
 562                dialog.set_version(GitView.version)
 563                dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
 564                dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
 565                dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
 566                dialog.set_wrap_license(True)
 567                dialog.run()
 568                dialog.destroy()
 569
 570
 571        def construct_bottom(self):
 572                """Construct the bottom half of the window."""
 573                vbox = gtk.VBox(False, spacing=6)
 574                vbox.set_border_width(12)
 575                (width, height) = self.window.get_size()
 576                vbox.set_size_request(width, int(height / 2.5))
 577                vbox.show()
 578
 579                self.table = gtk.Table(rows=4, columns=4)
 580                self.table.set_row_spacings(6)
 581                self.table.set_col_spacings(6)
 582                vbox.pack_start(self.table, expand=False, fill=True)
 583                self.table.show()
 584
 585                align = gtk.Alignment(0.0, 0.5)
 586                label = gtk.Label()
 587                label.set_markup("<b>Revision:</b>")
 588                align.add(label)
 589                self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
 590                label.show()
 591                align.show()
 592
 593                align = gtk.Alignment(0.0, 0.5)
 594                self.revid_label = gtk.Label()
 595                self.revid_label.set_selectable(True)
 596                align.add(self.revid_label)
 597                self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
 598                self.revid_label.show()
 599                align.show()
 600
 601                align = gtk.Alignment(0.0, 0.5)
 602                label = gtk.Label()
 603                label.set_markup("<b>Committer:</b>")
 604                align.add(label)
 605                self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
 606                label.show()
 607                align.show()
 608
 609                align = gtk.Alignment(0.0, 0.5)
 610                self.committer_label = gtk.Label()
 611                self.committer_label.set_selectable(True)
 612                align.add(self.committer_label)
 613                self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
 614                self.committer_label.show()
 615                align.show()
 616
 617                align = gtk.Alignment(0.0, 0.5)
 618                label = gtk.Label()
 619                label.set_markup("<b>Timestamp:</b>")
 620                align.add(label)
 621                self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
 622                label.show()
 623                align.show()
 624
 625                align = gtk.Alignment(0.0, 0.5)
 626                self.timestamp_label = gtk.Label()
 627                self.timestamp_label.set_selectable(True)
 628                align.add(self.timestamp_label)
 629                self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
 630                self.timestamp_label.show()
 631                align.show()
 632
 633                align = gtk.Alignment(0.0, 0.5)
 634                label = gtk.Label()
 635                label.set_markup("<b>Parents:</b>")
 636                align.add(label)
 637                self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
 638                label.show()
 639                align.show()
 640                self.parents_widgets = []
 641
 642                align = gtk.Alignment(0.0, 0.5)
 643                label = gtk.Label()
 644                label.set_markup("<b>Children:</b>")
 645                align.add(label)
 646                self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
 647                label.show()
 648                align.show()
 649                self.children_widgets = []
 650
 651                scrollwin = gtk.ScrolledWindow()
 652                scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 653                scrollwin.set_shadow_type(gtk.SHADOW_IN)
 654                vbox.pack_start(scrollwin, expand=True, fill=True)
 655                scrollwin.show()
 656
 657                if have_gtksourceview:
 658                        self.message_buffer = gtksourceview.SourceBuffer()
 659                        slm = gtksourceview.SourceLanguagesManager()
 660                        gsl = slm.get_language_from_mime_type("text/x-patch")
 661                        self.message_buffer.set_highlight(True)
 662                        self.message_buffer.set_language(gsl)
 663                        sourceview = gtksourceview.SourceView(self.message_buffer)
 664                else:
 665                        self.message_buffer = gtk.TextBuffer()
 666                        sourceview = gtk.TextView(self.message_buffer)
 667
 668                sourceview.set_editable(False)
 669                sourceview.modify_font(pango.FontDescription("Monospace"))
 670                scrollwin.add(sourceview)
 671                sourceview.show()
 672
 673                return vbox
 674
 675        def _treeview_cursor_cb(self, *args):
 676                """Callback for when the treeview cursor changes."""
 677                (path, col) = self.treeview.get_cursor()
 678                commit = self.model[path][0]
 679
 680                if commit.committer is not None:
 681                        committer = commit.committer
 682                        timestamp = commit.commit_date
 683                        message   =  commit.get_message(self.with_diff)
 684                        revid_label = commit.commit_sha1
 685                else:
 686                        committer = ""
 687                        timestamp = ""
 688                        message = ""
 689                        revid_label = ""
 690
 691                self.revid_label.set_text(revid_label)
 692                self.committer_label.set_text(committer)
 693                self.timestamp_label.set_text(timestamp)
 694                self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
 695
 696                for widget in self.parents_widgets:
 697                        self.table.remove(widget)
 698
 699                self.parents_widgets = []
 700                self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
 701                for idx, parent_id in enumerate(commit.parent_sha1):
 702                        self.table.set_row_spacing(idx + 3, 0)
 703
 704                        align = gtk.Alignment(0.0, 0.0)
 705                        self.parents_widgets.append(align)
 706                        self.table.attach(align, 1, 2, idx + 3, idx + 4,
 707                                        gtk.EXPAND | gtk.FILL, gtk.FILL)
 708                        align.show()
 709
 710                        hbox = gtk.HBox(False, 0)
 711                        align.add(hbox)
 712                        hbox.show()
 713
 714                        label = gtk.Label(parent_id)
 715                        label.set_selectable(True)
 716                        hbox.pack_start(label, expand=False, fill=True)
 717                        label.show()
 718
 719                        image = gtk.Image()
 720                        image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
 721                        image.show()
 722
 723                        button = gtk.Button()
 724                        button.add(image)
 725                        button.set_relief(gtk.RELIEF_NONE)
 726                        button.connect("clicked", self._go_clicked_cb, parent_id)
 727                        hbox.pack_start(button, expand=False, fill=True)
 728                        button.show()
 729
 730                        image = gtk.Image()
 731                        image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
 732                        image.show()
 733
 734                        button = gtk.Button()
 735                        button.add(image)
 736                        button.set_relief(gtk.RELIEF_NONE)
 737                        button.set_sensitive(True)
 738                        button.connect("clicked", self._show_clicked_cb,
 739                                        commit.commit_sha1, parent_id, self.encoding)
 740                        hbox.pack_start(button, expand=False, fill=True)
 741                        button.show()
 742
 743                # Populate with child details
 744                for widget in self.children_widgets:
 745                        self.table.remove(widget)
 746
 747                self.children_widgets = []
 748                try:
 749                        child_sha1 = Commit.children_sha1[commit.commit_sha1]
 750                except KeyError:
 751                        # We don't have child
 752                        child_sha1 = [ 0 ]
 753
 754                if ( len(child_sha1) > len(commit.parent_sha1)):
 755                        self.table.resize(4 + len(child_sha1) - 1, 4)
 756
 757                for idx, child_id in enumerate(child_sha1):
 758                        self.table.set_row_spacing(idx + 3, 0)
 759
 760                        align = gtk.Alignment(0.0, 0.0)
 761                        self.children_widgets.append(align)
 762                        self.table.attach(align, 3, 4, idx + 3, idx + 4,
 763                                        gtk.EXPAND | gtk.FILL, gtk.FILL)
 764                        align.show()
 765
 766                        hbox = gtk.HBox(False, 0)
 767                        align.add(hbox)
 768                        hbox.show()
 769
 770                        label = gtk.Label(child_id)
 771                        label.set_selectable(True)
 772                        hbox.pack_start(label, expand=False, fill=True)
 773                        label.show()
 774
 775                        image = gtk.Image()
 776                        image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
 777                        image.show()
 778
 779                        button = gtk.Button()
 780                        button.add(image)
 781                        button.set_relief(gtk.RELIEF_NONE)
 782                        button.connect("clicked", self._go_clicked_cb, child_id)
 783                        hbox.pack_start(button, expand=False, fill=True)
 784                        button.show()
 785
 786                        image = gtk.Image()
 787                        image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
 788                        image.show()
 789
 790                        button = gtk.Button()
 791                        button.add(image)
 792                        button.set_relief(gtk.RELIEF_NONE)
 793                        button.set_sensitive(True)
 794                        button.connect("clicked", self._show_clicked_cb,
 795                                        child_id, commit.commit_sha1)
 796                        hbox.pack_start(button, expand=False, fill=True)
 797                        button.show()
 798
 799        def _destroy_cb(self, widget):
 800                """Callback for when a window we manage is destroyed."""
 801                self.quit()
 802
 803
 804        def quit(self):
 805                """Stop the GTK+ main loop."""
 806                gtk.main_quit()
 807
 808        def run(self, args):
 809                self.set_branch(args)
 810                self.window.connect("destroy", self._destroy_cb)
 811                self.window.show()
 812                gtk.main()
 813
 814        def set_branch(self, args):
 815                """Fill in different windows with info from the reposiroty"""
 816                fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
 817                git_rev_list_cmd = fp.read()
 818                fp.close()
 819                fp = os.popen("git rev-list  --header --topo-order --parents " + git_rev_list_cmd)
 820                self.update_window(fp)
 821
 822        def update_window(self, fp):
 823                commit_lines = []
 824
 825                self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
 826                                gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
 827
 828                # used for cursor positioning
 829                self.index = {}
 830
 831                self.colours = {}
 832                self.nodepos = {}
 833                self.incomplete_line = {}
 834                self.commits = []
 835
 836                index = 0
 837                last_colour = 0
 838                last_nodepos = -1
 839                out_line = []
 840                input_line = fp.readline()
 841                while (input_line != ""):
 842                        # The commit header ends with '\0'
 843                        # This NULL is immediately followed by the sha1 of the
 844                        # next commit
 845                        if (input_line[0] != '\0'):
 846                                commit_lines.append(input_line)
 847                                input_line = fp.readline()
 848                                continue;
 849
 850                        commit = Commit(commit_lines)
 851                        if (commit != None ):
 852                                self.commits.append(commit)
 853
 854                        # Skip the '\0
 855                        commit_lines = []
 856                        commit_lines.append(input_line[1:])
 857                        input_line = fp.readline()
 858
 859                fp.close()
 860
 861                for commit in self.commits:
 862                        (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
 863                                                                                index, out_line,
 864                                                                                last_colour,
 865                                                                                last_nodepos)
 866                        self.index[commit.commit_sha1] = index
 867                        index += 1
 868
 869                self.treeview.set_model(self.model)
 870                self.treeview.show()
 871
 872        def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
 873                in_line=[]
 874
 875                #   |   -> outline
 876                #   X
 877                #   |\  <- inline
 878
 879                # Reset nodepostion
 880                if (last_nodepos > 5):
 881                        last_nodepos = -1 
 882
 883                # Add the incomplete lines of the last cell in this
 884                try:
 885                        colour = self.colours[commit.commit_sha1]
 886                except KeyError:
 887                        self.colours[commit.commit_sha1] = last_colour+1
 888                        last_colour = self.colours[commit.commit_sha1] 
 889                        colour =   self.colours[commit.commit_sha1] 
 890
 891                try:
 892                        node_pos = self.nodepos[commit.commit_sha1]
 893                except KeyError:
 894                        self.nodepos[commit.commit_sha1] = last_nodepos+1
 895                        last_nodepos = self.nodepos[commit.commit_sha1]
 896                        node_pos =  self.nodepos[commit.commit_sha1]
 897
 898                #The first parent always continue on the same line
 899                try:
 900                        # check we alreay have the value
 901                        tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
 902                except KeyError:
 903                        self.colours[commit.parent_sha1[0]] = colour
 904                        self.nodepos[commit.parent_sha1[0]] = node_pos
 905
 906                for sha1 in self.incomplete_line.keys():
 907                        if (sha1 != commit.commit_sha1):
 908                                self.draw_incomplete_line(sha1, node_pos,
 909                                                out_line, in_line, index)
 910                        else:
 911                                del self.incomplete_line[sha1]
 912
 913
 914                for parent_id in commit.parent_sha1:
 915                        try:
 916                                tmp_node_pos = self.nodepos[parent_id]
 917                        except KeyError:
 918                                self.colours[parent_id] = last_colour+1
 919                                last_colour = self.colours[parent_id]
 920                                self.nodepos[parent_id] = last_nodepos+1
 921                                last_nodepos = self.nodepos[parent_id] 
 922
 923                        in_line.append((node_pos, self.nodepos[parent_id],
 924                                                self.colours[parent_id]))
 925                        self.add_incomplete_line(parent_id)
 926
 927                try:
 928                        branch_tag = self.bt_sha1[commit.commit_sha1]
 929                except KeyError:
 930                        branch_tag = [ ]
 931
 932
 933                node = (node_pos, colour, branch_tag)
 934
 935                self.model.append([commit, node, out_line, in_line,
 936                                commit.message, commit.author, commit.date])
 937
 938                return (in_line, last_colour, last_nodepos)
 939
 940        def add_incomplete_line(self, sha1):
 941                try:
 942                        self.incomplete_line[sha1].append(self.nodepos[sha1])
 943                except KeyError:
 944                        self.incomplete_line[sha1] = [self.nodepos[sha1]]
 945
 946        def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
 947                for idx, pos in enumerate(self.incomplete_line[sha1]):
 948                        if(pos == node_pos):
 949                                #remove the straight line and add a slash
 950                                if ((pos, pos, self.colours[sha1]) in out_line):
 951                                        out_line.remove((pos, pos, self.colours[sha1]))
 952                                out_line.append((pos, pos+0.5, self.colours[sha1]))
 953                                self.incomplete_line[sha1][idx] = pos = pos+0.5
 954                        try:
 955                                next_commit = self.commits[index+1]
 956                                if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
 957                                # join the line back to the node point 
 958                                # This need to be done only if we modified it
 959                                        in_line.append((pos, pos-0.5, self.colours[sha1]))
 960                                        continue;
 961                        except IndexError:
 962                                pass
 963                        in_line.append((pos, pos, self.colours[sha1]))
 964
 965
 966        def _go_clicked_cb(self, widget, revid):
 967                """Callback for when the go button for a parent is clicked."""
 968                try:
 969                        self.treeview.set_cursor(self.index[revid])
 970                except KeyError:
 971                        print "Revision %s not present in the list" % revid
 972                        # revid == 0 is the parent of the first commit
 973                        if (revid != 0 ):
 974                                print "Try running gitview without any options"
 975
 976                self.treeview.grab_focus()
 977
 978        def _show_clicked_cb(self, widget,  commit_sha1, parent_sha1, encoding):
 979                """Callback for when the show button for a parent is clicked."""
 980                window = DiffWindow()
 981                window.set_diff(commit_sha1, parent_sha1, encoding)
 982                self.treeview.grab_focus()
 983
 984if __name__ == "__main__":
 985        without_diff = 0
 986
 987        if (len(sys.argv) > 1 ):
 988                if (sys.argv[1] == "--without-diff"):
 989                        without_diff = 1
 990
 991        view = GitView( without_diff != 1)
 992        view.run(sys.argv[without_diff:])
 993
 994