5c338c02206869d4e6dadb2d15212532678c6f12
   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 = 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):
 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(fp.read())
 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.6"
 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_bt_sha1()
 434
 435                # Use three-quarters of the screen by default
 436                screen = self.window.get_screen()
 437                monitor = screen.get_monitor_geometry(0)
 438                width = int(monitor.width * 0.75)
 439                height = int(monitor.height * 0.75)
 440                self.window.set_default_size(width, height)
 441
 442                # FIXME AndyFitz!
 443                icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
 444                self.window.set_icon(icon)
 445
 446                self.accel_group = gtk.AccelGroup()
 447                self.window.add_accel_group(self.accel_group)
 448
 449                self.construct()
 450
 451        def get_bt_sha1(self):
 452                """ Update the bt_sha1 dictionary with the
 453                respective sha1 details """
 454
 455                self.bt_sha1 = { }
 456                ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
 457                git_dir = os.getenv("GIT_DIR")
 458                if (git_dir == None):
 459                        git_dir = ".git"
 460
 461                fp = os.popen('git ls-remote ' + git_dir)
 462                while 1:
 463                        line = string.strip(fp.readline())
 464                        if line == '':
 465                                break
 466                        m = ls_remote.match(line)
 467                        if not m:
 468                                continue
 469                        (sha1, name) = (m.group(1), m.group(2))
 470                        if not self.bt_sha1.has_key(sha1):
 471                                self.bt_sha1[sha1] = []
 472                        self.bt_sha1[sha1].append(name)
 473                fp.close()
 474
 475
 476        def construct(self):
 477                """Construct the window contents."""
 478                paned = gtk.VPaned()
 479                paned.pack1(self.construct_top(), resize=False, shrink=True)
 480                paned.pack2(self.construct_bottom(), resize=False, shrink=True)
 481                self.window.add(paned)
 482                paned.show()
 483
 484
 485        def construct_top(self):
 486                """Construct the top-half of the window."""
 487                vbox = gtk.VBox(spacing=6)
 488                vbox.set_border_width(12)
 489                vbox.show()
 490
 491                menu_bar = gtk.MenuBar()
 492                menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
 493                help_menu = gtk.MenuItem("Help")
 494                menu = gtk.Menu()
 495                about_menu = gtk.MenuItem("About")
 496                menu.append(about_menu)
 497                about_menu.connect("activate", self.about_menu_response, "about")
 498                about_menu.show()
 499                help_menu.set_submenu(menu)
 500                help_menu.show()
 501                menu_bar.append(help_menu)
 502                vbox.pack_start(menu_bar, False, False, 2)
 503                menu_bar.show()
 504
 505                scrollwin = gtk.ScrolledWindow()
 506                scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
 507                scrollwin.set_shadow_type(gtk.SHADOW_IN)
 508                vbox.pack_start(scrollwin, expand=True, fill=True)
 509                scrollwin.show()
 510
 511                self.treeview = gtk.TreeView()
 512                self.treeview.set_rules_hint(True)
 513                self.treeview.set_search_column(4)
 514                self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
 515                scrollwin.add(self.treeview)
 516                self.treeview.show()
 517
 518                cell = CellRendererGraph()
 519                column = gtk.TreeViewColumn()
 520                column.set_resizable(True)
 521                column.pack_start(cell, expand=True)
 522                column.add_attribute(cell, "node", 1)
 523                column.add_attribute(cell, "in-lines", 2)
 524                column.add_attribute(cell, "out-lines", 3)
 525                self.treeview.append_column(column)
 526
 527                cell = gtk.CellRendererText()
 528                cell.set_property("width-chars", 65)
 529                cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 530                column = gtk.TreeViewColumn("Message")
 531                column.set_resizable(True)
 532                column.pack_start(cell, expand=True)
 533                column.add_attribute(cell, "text", 4)
 534                self.treeview.append_column(column)
 535
 536                cell = gtk.CellRendererText()
 537                cell.set_property("width-chars", 40)
 538                cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 539                column = gtk.TreeViewColumn("Author")
 540                column.set_resizable(True)
 541                column.pack_start(cell, expand=True)
 542                column.add_attribute(cell, "text", 5)
 543                self.treeview.append_column(column)
 544
 545                cell = gtk.CellRendererText()
 546                cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 547                column = gtk.TreeViewColumn("Date")
 548                column.set_resizable(True)
 549                column.pack_start(cell, expand=True)
 550                column.add_attribute(cell, "text", 6)
 551                self.treeview.append_column(column)
 552
 553                return vbox
 554
 555        def about_menu_response(self, widget, string):
 556                dialog = gtk.AboutDialog()
 557                dialog.set_name("Gitview")
 558                dialog.set_version(GitView.version)
 559                dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
 560                dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
 561                dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
 562                dialog.set_wrap_license(True)
 563                dialog.run()
 564                dialog.destroy()
 565
 566
 567        def construct_bottom(self):
 568                """Construct the bottom half of the window."""
 569                vbox = gtk.VBox(False, spacing=6)
 570                vbox.set_border_width(12)
 571                (width, height) = self.window.get_size()
 572                vbox.set_size_request(width, int(height / 2.5))
 573                vbox.show()
 574
 575                self.table = gtk.Table(rows=4, columns=4)
 576                self.table.set_row_spacings(6)
 577                self.table.set_col_spacings(6)
 578                vbox.pack_start(self.table, expand=False, fill=True)
 579                self.table.show()
 580
 581                align = gtk.Alignment(0.0, 0.5)
 582                label = gtk.Label()
 583                label.set_markup("<b>Revision:</b>")
 584                align.add(label)
 585                self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
 586                label.show()
 587                align.show()
 588
 589                align = gtk.Alignment(0.0, 0.5)
 590                self.revid_label = gtk.Label()
 591                self.revid_label.set_selectable(True)
 592                align.add(self.revid_label)
 593                self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
 594                self.revid_label.show()
 595                align.show()
 596
 597                align = gtk.Alignment(0.0, 0.5)
 598                label = gtk.Label()
 599                label.set_markup("<b>Committer:</b>")
 600                align.add(label)
 601                self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
 602                label.show()
 603                align.show()
 604
 605                align = gtk.Alignment(0.0, 0.5)
 606                self.committer_label = gtk.Label()
 607                self.committer_label.set_selectable(True)
 608                align.add(self.committer_label)
 609                self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
 610                self.committer_label.show()
 611                align.show()
 612
 613                align = gtk.Alignment(0.0, 0.5)
 614                label = gtk.Label()
 615                label.set_markup("<b>Timestamp:</b>")
 616                align.add(label)
 617                self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
 618                label.show()
 619                align.show()
 620
 621                align = gtk.Alignment(0.0, 0.5)
 622                self.timestamp_label = gtk.Label()
 623                self.timestamp_label.set_selectable(True)
 624                align.add(self.timestamp_label)
 625                self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
 626                self.timestamp_label.show()
 627                align.show()
 628
 629                align = gtk.Alignment(0.0, 0.5)
 630                label = gtk.Label()
 631                label.set_markup("<b>Parents:</b>")
 632                align.add(label)
 633                self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
 634                label.show()
 635                align.show()
 636                self.parents_widgets = []
 637
 638                align = gtk.Alignment(0.0, 0.5)
 639                label = gtk.Label()
 640                label.set_markup("<b>Children:</b>")
 641                align.add(label)
 642                self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
 643                label.show()
 644                align.show()
 645                self.children_widgets = []
 646
 647                scrollwin = gtk.ScrolledWindow()
 648                scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 649                scrollwin.set_shadow_type(gtk.SHADOW_IN)
 650                vbox.pack_start(scrollwin, expand=True, fill=True)
 651                scrollwin.show()
 652
 653                if have_gtksourceview:
 654                        self.message_buffer = gtksourceview.SourceBuffer()
 655                        slm = gtksourceview.SourceLanguagesManager()
 656                        gsl = slm.get_language_from_mime_type("text/x-patch")
 657                        self.message_buffer.set_highlight(True)
 658                        self.message_buffer.set_language(gsl)
 659                        sourceview = gtksourceview.SourceView(self.message_buffer)
 660                else:
 661                        self.message_buffer = gtk.TextBuffer()
 662                        sourceview = gtk.TextView(self.message_buffer)
 663
 664                sourceview.set_editable(False)
 665                sourceview.modify_font(pango.FontDescription("Monospace"))
 666                scrollwin.add(sourceview)
 667                sourceview.show()
 668
 669                return vbox
 670
 671        def _treeview_cursor_cb(self, *args):
 672                """Callback for when the treeview cursor changes."""
 673                (path, col) = self.treeview.get_cursor()
 674                commit = self.model[path][0]
 675
 676                if commit.committer is not None:
 677                        committer = commit.committer
 678                        timestamp = commit.commit_date
 679                        message   =  commit.get_message(self.with_diff)
 680                        revid_label = commit.commit_sha1
 681                else:
 682                        committer = ""
 683                        timestamp = ""
 684                        message = ""
 685                        revid_label = ""
 686
 687                self.revid_label.set_text(revid_label)
 688                self.committer_label.set_text(committer)
 689                self.timestamp_label.set_text(timestamp)
 690                self.message_buffer.set_text(message)
 691
 692                for widget in self.parents_widgets:
 693                        self.table.remove(widget)
 694
 695                self.parents_widgets = []
 696                self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
 697                for idx, parent_id in enumerate(commit.parent_sha1):
 698                        self.table.set_row_spacing(idx + 3, 0)
 699
 700                        align = gtk.Alignment(0.0, 0.0)
 701                        self.parents_widgets.append(align)
 702                        self.table.attach(align, 1, 2, idx + 3, idx + 4,
 703                                        gtk.EXPAND | gtk.FILL, gtk.FILL)
 704                        align.show()
 705
 706                        hbox = gtk.HBox(False, 0)
 707                        align.add(hbox)
 708                        hbox.show()
 709
 710                        label = gtk.Label(parent_id)
 711                        label.set_selectable(True)
 712                        hbox.pack_start(label, expand=False, fill=True)
 713                        label.show()
 714
 715                        image = gtk.Image()
 716                        image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
 717                        image.show()
 718
 719                        button = gtk.Button()
 720                        button.add(image)
 721                        button.set_relief(gtk.RELIEF_NONE)
 722                        button.connect("clicked", self._go_clicked_cb, parent_id)
 723                        hbox.pack_start(button, expand=False, fill=True)
 724                        button.show()
 725
 726                        image = gtk.Image()
 727                        image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
 728                        image.show()
 729
 730                        button = gtk.Button()
 731                        button.add(image)
 732                        button.set_relief(gtk.RELIEF_NONE)
 733                        button.set_sensitive(True)
 734                        button.connect("clicked", self._show_clicked_cb,
 735                                        commit.commit_sha1, parent_id)
 736                        hbox.pack_start(button, expand=False, fill=True)
 737                        button.show()
 738
 739                # Populate with child details
 740                for widget in self.children_widgets:
 741                        self.table.remove(widget)
 742
 743                self.children_widgets = []
 744                try:
 745                        child_sha1 = Commit.children_sha1[commit.commit_sha1]
 746                except KeyError:
 747                        # We don't have child
 748                        child_sha1 = [ 0 ]
 749
 750                if ( len(child_sha1) > len(commit.parent_sha1)):
 751                        self.table.resize(4 + len(child_sha1) - 1, 4)
 752
 753                for idx, child_id in enumerate(child_sha1):
 754                        self.table.set_row_spacing(idx + 3, 0)
 755
 756                        align = gtk.Alignment(0.0, 0.0)
 757                        self.children_widgets.append(align)
 758                        self.table.attach(align, 3, 4, idx + 3, idx + 4,
 759                                        gtk.EXPAND | gtk.FILL, gtk.FILL)
 760                        align.show()
 761
 762                        hbox = gtk.HBox(False, 0)
 763                        align.add(hbox)
 764                        hbox.show()
 765
 766                        label = gtk.Label(child_id)
 767                        label.set_selectable(True)
 768                        hbox.pack_start(label, expand=False, fill=True)
 769                        label.show()
 770
 771                        image = gtk.Image()
 772                        image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
 773                        image.show()
 774
 775                        button = gtk.Button()
 776                        button.add(image)
 777                        button.set_relief(gtk.RELIEF_NONE)
 778                        button.connect("clicked", self._go_clicked_cb, child_id)
 779                        hbox.pack_start(button, expand=False, fill=True)
 780                        button.show()
 781
 782                        image = gtk.Image()
 783                        image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
 784                        image.show()
 785
 786                        button = gtk.Button()
 787                        button.add(image)
 788                        button.set_relief(gtk.RELIEF_NONE)
 789                        button.set_sensitive(True)
 790                        button.connect("clicked", self._show_clicked_cb,
 791                                        child_id, commit.commit_sha1)
 792                        hbox.pack_start(button, expand=False, fill=True)
 793                        button.show()
 794
 795        def _destroy_cb(self, widget):
 796                """Callback for when a window we manage is destroyed."""
 797                self.quit()
 798
 799
 800        def quit(self):
 801                """Stop the GTK+ main loop."""
 802                gtk.main_quit()
 803
 804        def run(self, args):
 805                self.set_branch(args)
 806                self.window.connect("destroy", self._destroy_cb)
 807                self.window.show()
 808                gtk.main()
 809
 810        def set_branch(self, args):
 811                """Fill in different windows with info from the reposiroty"""
 812                fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
 813                git_rev_list_cmd = fp.read()
 814                fp.close()
 815                fp = os.popen("git rev-list  --header --topo-order --parents " + git_rev_list_cmd)
 816                self.update_window(fp)
 817
 818        def update_window(self, fp):
 819                commit_lines = []
 820
 821                self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
 822                                gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
 823
 824                # used for cursor positioning
 825                self.index = {}
 826
 827                self.colours = {}
 828                self.nodepos = {}
 829                self.incomplete_line = {}
 830
 831                index = 0
 832                last_colour = 0
 833                last_nodepos = -1
 834                out_line = []
 835                input_line = fp.readline()
 836                while (input_line != ""):
 837                        # The commit header ends with '\0'
 838                        # This NULL is immediately followed by the sha1 of the
 839                        # next commit
 840                        if (input_line[0] != '\0'):
 841                                commit_lines.append(input_line)
 842                                input_line = fp.readline()
 843                                continue;
 844
 845                        commit = Commit(commit_lines)
 846                        if (commit != None ):
 847                                (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
 848                                                                                index, out_line,
 849                                                                                last_colour,
 850                                                                                last_nodepos)
 851                                self.index[commit.commit_sha1] = index
 852                                index += 1
 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                self.treeview.set_model(self.model)
 862                self.treeview.show()
 863
 864        def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
 865                in_line=[]
 866
 867                #   |   -> outline
 868                #   X
 869                #   |\  <- inline
 870
 871                # Reset nodepostion
 872                if (last_nodepos > 5):
 873                        last_nodepos = 0
 874
 875                # Add the incomplete lines of the last cell in this
 876                for sha1 in self.incomplete_line.keys():
 877                        if ( sha1 != commit.commit_sha1):
 878                                for pos in self.incomplete_line[sha1]:
 879                                        in_line.append((pos, pos, self.colours[sha1]))
 880                        else:
 881                                del self.incomplete_line[sha1]
 882
 883                try:
 884                        colour = self.colours[commit.commit_sha1]
 885                except KeyError:
 886                        last_colour +=1
 887                        self.colours[commit.commit_sha1] = last_colour
 888                        colour =  last_colour
 889                try:
 890                        node_pos = self.nodepos[commit.commit_sha1]
 891                except KeyError:
 892                        last_nodepos +=1
 893                        self.nodepos[commit.commit_sha1] = last_nodepos
 894                        node_pos = last_nodepos
 895
 896                #The first parent always continue on the same line
 897                try:
 898                        # check we alreay have the value
 899                        tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
 900                except KeyError:
 901                        self.colours[commit.parent_sha1[0]] = colour
 902                        self.nodepos[commit.parent_sha1[0]] = node_pos
 903
 904                in_line.append((node_pos, self.nodepos[commit.parent_sha1[0]],
 905                                        self.colours[commit.parent_sha1[0]]))
 906
 907                self.add_incomplete_line(commit.parent_sha1[0], index+1)
 908
 909                if (len(commit.parent_sha1) > 1):
 910                        for parent_id in commit.parent_sha1[1:]:
 911                                try:
 912                                        tmp_node_pos = self.nodepos[parent_id]
 913                                except KeyError:
 914                                        last_colour += 1;
 915                                        self.colours[parent_id] = last_colour
 916                                        last_nodepos +=1
 917                                        self.nodepos[parent_id] = last_nodepos
 918
 919                                in_line.append((node_pos, self.nodepos[parent_id],
 920                                                        self.colours[parent_id]))
 921                                self.add_incomplete_line(parent_id, index+1)
 922
 923
 924                try:
 925                        branch_tag = self.bt_sha1[commit.commit_sha1]
 926                except KeyError:
 927                        branch_tag = [ ]
 928
 929
 930                node = (node_pos, colour, branch_tag)
 931
 932                self.model.append([commit, node, out_line, in_line,
 933                                commit.message, commit.author, commit.date])
 934
 935                return (in_line, last_colour, last_nodepos)
 936
 937        def add_incomplete_line(self, sha1, index):
 938                try:
 939                        self.incomplete_line[sha1].append(self.nodepos[sha1])
 940                except KeyError:
 941                        self.incomplete_line[sha1] = [self.nodepos[sha1]]
 942
 943
 944        def _go_clicked_cb(self, widget, revid):
 945                """Callback for when the go button for a parent is clicked."""
 946                try:
 947                        self.treeview.set_cursor(self.index[revid])
 948                except KeyError:
 949                        print "Revision %s not present in the list" % revid
 950                        # revid == 0 is the parent of the first commit
 951                        if (revid != 0 ):
 952                                print "Try running gitview without any options"
 953
 954                self.treeview.grab_focus()
 955
 956        def _show_clicked_cb(self, widget,  commit_sha1, parent_sha1):
 957                """Callback for when the show button for a parent is clicked."""
 958                window = DiffWindow()
 959                window.set_diff(commit_sha1, parent_sha1)
 960                self.treeview.grab_focus()
 961
 962if __name__ == "__main__":
 963        without_diff = 0
 964
 965        if (len(sys.argv) > 1 ):
 966                if (sys.argv[1] == "--without-diff"):
 967                        without_diff = 1
 968
 969        view = GitView( without_diff != 1)
 970        view.run(sys.argv[without_diff:])
 971
 972