b75b39e5fc94bcf96e368ff20ac14e4afb90726f
   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)/3
 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.text_path(name)
 248
 249                self.set_colour(ctx, colour, 0.0, 0.5)
 250                ctx.stroke_preserve()
 251
 252                self.set_colour(ctx, colour, 0.5, 1.0)
 253                ctx.fill()
 254
 255class Commit:
 256        """ This represent a commit object obtained after parsing the git-rev-list
 257        output """
 258
 259        children_sha1 = {}
 260
 261        def __init__(self, commit_lines):
 262                self.message            = ""
 263                self.author             = ""
 264                self.date               = ""
 265                self.committer          = ""
 266                self.commit_date        = ""
 267                self.commit_sha1        = ""
 268                self.parent_sha1        = [ ]
 269                self.parse_commit(commit_lines)
 270
 271
 272        def parse_commit(self, commit_lines):
 273
 274                # First line is the sha1 lines
 275                line = string.strip(commit_lines[0])
 276                sha1 = re.split(" ", line)
 277                self.commit_sha1 = sha1[0]
 278                self.parent_sha1 = sha1[1:]
 279
 280                #build the child list
 281                for parent_id in self.parent_sha1:
 282                        try:
 283                                Commit.children_sha1[parent_id].append(self.commit_sha1)
 284                        except KeyError:
 285                                Commit.children_sha1[parent_id] = [self.commit_sha1]
 286
 287                # IF we don't have parent
 288                if (len(self.parent_sha1) == 0):
 289                        self.parent_sha1 = [0]
 290
 291                for line in commit_lines[1:]:
 292                        m = re.match("^ ", line)
 293                        if (m != None):
 294                                # First line of the commit message used for short log
 295                                if self.message == "":
 296                                        self.message = string.strip(line)
 297                                continue
 298
 299                        m = re.match("tree", line)
 300                        if (m != None):
 301                                continue
 302
 303                        m = re.match("parent", line)
 304                        if (m != None):
 305                                continue
 306
 307                        m = re_ident.match(line)
 308                        if (m != None):
 309                                date = show_date(m.group('epoch'), m.group('tz'))
 310                                if m.group(1) == "author":
 311                                        self.author = m.group('ident')
 312                                        self.date = date
 313                                elif m.group(1) == "committer":
 314                                        self.committer = m.group('ident')
 315                                        self.commit_date = date
 316
 317                                continue
 318
 319        def get_message(self, with_diff=0):
 320                if (with_diff == 1):
 321                        message = self.diff_tree()
 322                else:
 323                        fp = os.popen("git cat-file commit " + self.commit_sha1)
 324                        message = fp.read()
 325                        fp.close()
 326
 327                return message
 328
 329        def diff_tree(self):
 330                fp = os.popen("git diff-tree --pretty --cc  -v -p --always " +  self.commit_sha1)
 331                diff = fp.read()
 332                fp.close()
 333                return diff
 334
 335class DiffWindow:
 336        """Diff window.
 337        This object represents and manages a single window containing the
 338        differences between two revisions on a branch.
 339        """
 340
 341        def __init__(self):
 342                self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
 343                self.window.set_border_width(0)
 344                self.window.set_title("Git repository browser diff window")
 345
 346                # Use two thirds of the screen by default
 347                screen = self.window.get_screen()
 348                monitor = screen.get_monitor_geometry(0)
 349                width = int(monitor.width * 0.66)
 350                height = int(monitor.height * 0.66)
 351                self.window.set_default_size(width, height)
 352
 353                self.construct()
 354
 355        def construct(self):
 356                """Construct the window contents."""
 357                vbox = gtk.VBox()
 358                self.window.add(vbox)
 359                vbox.show()
 360
 361                menu_bar = gtk.MenuBar()
 362                save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
 363                save_menu.connect("activate", self.save_menu_response, "save")
 364                save_menu.show()
 365                menu_bar.append(save_menu)
 366                vbox.pack_start(menu_bar, False, False, 2)
 367                menu_bar.show()
 368
 369                scrollwin = gtk.ScrolledWindow()
 370                scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 371                scrollwin.set_shadow_type(gtk.SHADOW_IN)
 372                vbox.pack_start(scrollwin, expand=True, fill=True)
 373                scrollwin.show()
 374
 375                if have_gtksourceview:
 376                        self.buffer = gtksourceview.SourceBuffer()
 377                        slm = gtksourceview.SourceLanguagesManager()
 378                        gsl = slm.get_language_from_mime_type("text/x-patch")
 379                        self.buffer.set_highlight(True)
 380                        self.buffer.set_language(gsl)
 381                        sourceview = gtksourceview.SourceView(self.buffer)
 382                else:
 383                        self.buffer = gtk.TextBuffer()
 384                        sourceview = gtk.TextView(self.buffer)
 385
 386                sourceview.set_editable(False)
 387                sourceview.modify_font(pango.FontDescription("Monospace"))
 388                scrollwin.add(sourceview)
 389                sourceview.show()
 390
 391
 392        def set_diff(self, commit_sha1, parent_sha1):
 393                """Set the differences showed by this window.
 394                Compares the two trees and populates the window with the
 395                differences.
 396                """
 397                # Diff with the first commit or the last commit shows nothing
 398                if (commit_sha1 == 0 or parent_sha1 == 0 ):
 399                        return
 400
 401                fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
 402                self.buffer.set_text(fp.read())
 403                fp.close()
 404                self.window.show()
 405
 406        def save_menu_response(self, widget, string):
 407                dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
 408                                (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
 409                                        gtk.STOCK_SAVE, gtk.RESPONSE_OK))
 410                dialog.set_default_response(gtk.RESPONSE_OK)
 411                response = dialog.run()
 412                if response == gtk.RESPONSE_OK:
 413                        patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
 414                                        self.buffer.get_end_iter())
 415                        fp = open(dialog.get_filename(), "w")
 416                        fp.write(patch_buffer)
 417                        fp.close()
 418                dialog.destroy()
 419
 420class GitView:
 421        """ This is the main class
 422        """
 423        version = "0.6"
 424
 425        def __init__(self, with_diff=0):
 426                self.with_diff = with_diff
 427                self.window =   gtk.Window(gtk.WINDOW_TOPLEVEL)
 428                self.window.set_border_width(0)
 429                self.window.set_title("Git repository browser")
 430
 431                self.get_bt_sha1()
 432
 433                # Use three-quarters of the screen by default
 434                screen = self.window.get_screen()
 435                monitor = screen.get_monitor_geometry(0)
 436                width = int(monitor.width * 0.75)
 437                height = int(monitor.height * 0.75)
 438                self.window.set_default_size(width, height)
 439
 440                # FIXME AndyFitz!
 441                icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
 442                self.window.set_icon(icon)
 443
 444                self.accel_group = gtk.AccelGroup()
 445                self.window.add_accel_group(self.accel_group)
 446
 447                self.construct()
 448
 449        def get_bt_sha1(self):
 450                """ Update the bt_sha1 dictionary with the
 451                respective sha1 details """
 452
 453                self.bt_sha1 = { }
 454                ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
 455                git_dir = os.getenv("GIT_DIR")
 456                if (git_dir == None):
 457                        git_dir = ".git"
 458
 459                fp = os.popen('git ls-remote ' + git_dir)
 460                while 1:
 461                        line = string.strip(fp.readline())
 462                        if line == '':
 463                                break
 464                        m = ls_remote.match(line)
 465                        if not m:
 466                                continue
 467                        (sha1, name) = (m.group(1), m.group(2))
 468                        if not self.bt_sha1.has_key(sha1):
 469                                self.bt_sha1[sha1] = []
 470                        self.bt_sha1[sha1].append(name)
 471                fp.close()
 472
 473
 474        def construct(self):
 475                """Construct the window contents."""
 476                paned = gtk.VPaned()
 477                paned.pack1(self.construct_top(), resize=False, shrink=True)
 478                paned.pack2(self.construct_bottom(), resize=False, shrink=True)
 479                self.window.add(paned)
 480                paned.show()
 481
 482
 483        def construct_top(self):
 484                """Construct the top-half of the window."""
 485                vbox = gtk.VBox(spacing=6)
 486                vbox.set_border_width(12)
 487                vbox.show()
 488
 489                menu_bar = gtk.MenuBar()
 490                menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
 491                help_menu = gtk.MenuItem("Help")
 492                menu = gtk.Menu()
 493                about_menu = gtk.MenuItem("About")
 494                menu.append(about_menu)
 495                about_menu.connect("activate", self.about_menu_response, "about")
 496                about_menu.show()
 497                help_menu.set_submenu(menu)
 498                help_menu.show()
 499                menu_bar.append(help_menu)
 500                vbox.pack_start(menu_bar, False, False, 2)
 501                menu_bar.show()
 502
 503                scrollwin = gtk.ScrolledWindow()
 504                scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
 505                scrollwin.set_shadow_type(gtk.SHADOW_IN)
 506                vbox.pack_start(scrollwin, expand=True, fill=True)
 507                scrollwin.show()
 508
 509                self.treeview = gtk.TreeView()
 510                self.treeview.set_rules_hint(True)
 511                self.treeview.set_search_column(4)
 512                self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
 513                scrollwin.add(self.treeview)
 514                self.treeview.show()
 515
 516                cell = CellRendererGraph()
 517                column = gtk.TreeViewColumn()
 518                column.set_resizable(False)
 519                column.pack_start(cell, expand=False)
 520                column.add_attribute(cell, "node", 1)
 521                column.add_attribute(cell, "in-lines", 2)
 522                column.add_attribute(cell, "out-lines", 3)
 523                self.treeview.append_column(column)
 524
 525                cell = gtk.CellRendererText()
 526                cell.set_property("width-chars", 65)
 527                cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 528                column = gtk.TreeViewColumn("Message")
 529                column.set_resizable(True)
 530                column.pack_start(cell, expand=True)
 531                column.add_attribute(cell, "text", 4)
 532                self.treeview.append_column(column)
 533
 534                cell = gtk.CellRendererText()
 535                cell.set_property("width-chars", 40)
 536                cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 537                column = gtk.TreeViewColumn("Author")
 538                column.set_resizable(True)
 539                column.pack_start(cell, expand=True)
 540                column.add_attribute(cell, "text", 5)
 541                self.treeview.append_column(column)
 542
 543                cell = gtk.CellRendererText()
 544                cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 545                column = gtk.TreeViewColumn("Date")
 546                column.set_resizable(True)
 547                column.pack_start(cell, expand=True)
 548                column.add_attribute(cell, "text", 6)
 549                self.treeview.append_column(column)
 550
 551                return vbox
 552
 553        def about_menu_response(self, widget, string):
 554                dialog = gtk.AboutDialog()
 555                dialog.set_name("Gitview")
 556                dialog.set_version(GitView.version)
 557                dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
 558                dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
 559                dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
 560                dialog.set_wrap_license(True)
 561                dialog.run()
 562                dialog.destroy()
 563
 564
 565        def construct_bottom(self):
 566                """Construct the bottom half of the window."""
 567                vbox = gtk.VBox(False, spacing=6)
 568                vbox.set_border_width(12)
 569                (width, height) = self.window.get_size()
 570                vbox.set_size_request(width, int(height / 2.5))
 571                vbox.show()
 572
 573                self.table = gtk.Table(rows=4, columns=4)
 574                self.table.set_row_spacings(6)
 575                self.table.set_col_spacings(6)
 576                vbox.pack_start(self.table, expand=False, fill=True)
 577                self.table.show()
 578
 579                align = gtk.Alignment(0.0, 0.5)
 580                label = gtk.Label()
 581                label.set_markup("<b>Revision:</b>")
 582                align.add(label)
 583                self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
 584                label.show()
 585                align.show()
 586
 587                align = gtk.Alignment(0.0, 0.5)
 588                self.revid_label = gtk.Label()
 589                self.revid_label.set_selectable(True)
 590                align.add(self.revid_label)
 591                self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
 592                self.revid_label.show()
 593                align.show()
 594
 595                align = gtk.Alignment(0.0, 0.5)
 596                label = gtk.Label()
 597                label.set_markup("<b>Committer:</b>")
 598                align.add(label)
 599                self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
 600                label.show()
 601                align.show()
 602
 603                align = gtk.Alignment(0.0, 0.5)
 604                self.committer_label = gtk.Label()
 605                self.committer_label.set_selectable(True)
 606                align.add(self.committer_label)
 607                self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
 608                self.committer_label.show()
 609                align.show()
 610
 611                align = gtk.Alignment(0.0, 0.5)
 612                label = gtk.Label()
 613                label.set_markup("<b>Timestamp:</b>")
 614                align.add(label)
 615                self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
 616                label.show()
 617                align.show()
 618
 619                align = gtk.Alignment(0.0, 0.5)
 620                self.timestamp_label = gtk.Label()
 621                self.timestamp_label.set_selectable(True)
 622                align.add(self.timestamp_label)
 623                self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
 624                self.timestamp_label.show()
 625                align.show()
 626
 627                align = gtk.Alignment(0.0, 0.5)
 628                label = gtk.Label()
 629                label.set_markup("<b>Parents:</b>")
 630                align.add(label)
 631                self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
 632                label.show()
 633                align.show()
 634                self.parents_widgets = []
 635
 636                align = gtk.Alignment(0.0, 0.5)
 637                label = gtk.Label()
 638                label.set_markup("<b>Children:</b>")
 639                align.add(label)
 640                self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
 641                label.show()
 642                align.show()
 643                self.children_widgets = []
 644
 645                scrollwin = gtk.ScrolledWindow()
 646                scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 647                scrollwin.set_shadow_type(gtk.SHADOW_IN)
 648                vbox.pack_start(scrollwin, expand=True, fill=True)
 649                scrollwin.show()
 650
 651                if have_gtksourceview:
 652                        self.message_buffer = gtksourceview.SourceBuffer()
 653                        slm = gtksourceview.SourceLanguagesManager()
 654                        gsl = slm.get_language_from_mime_type("text/x-patch")
 655                        self.message_buffer.set_highlight(True)
 656                        self.message_buffer.set_language(gsl)
 657                        sourceview = gtksourceview.SourceView(self.message_buffer)
 658                else:
 659                        self.message_buffer = gtk.TextBuffer()
 660                        sourceview = gtk.TextView(self.message_buffer)
 661
 662                sourceview.set_editable(False)
 663                sourceview.modify_font(pango.FontDescription("Monospace"))
 664                scrollwin.add(sourceview)
 665                sourceview.show()
 666
 667                return vbox
 668
 669        def _treeview_cursor_cb(self, *args):
 670                """Callback for when the treeview cursor changes."""
 671                (path, col) = self.treeview.get_cursor()
 672                commit = self.model[path][0]
 673
 674                if commit.committer is not None:
 675                        committer = commit.committer
 676                        timestamp = commit.commit_date
 677                        message   =  commit.get_message(self.with_diff)
 678                        revid_label = commit.commit_sha1
 679                else:
 680                        committer = ""
 681                        timestamp = ""
 682                        message = ""
 683                        revid_label = ""
 684
 685                self.revid_label.set_text(revid_label)
 686                self.committer_label.set_text(committer)
 687                self.timestamp_label.set_text(timestamp)
 688                self.message_buffer.set_text(message)
 689
 690                for widget in self.parents_widgets:
 691                        self.table.remove(widget)
 692
 693                self.parents_widgets = []
 694                self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
 695                for idx, parent_id in enumerate(commit.parent_sha1):
 696                        self.table.set_row_spacing(idx + 3, 0)
 697
 698                        align = gtk.Alignment(0.0, 0.0)
 699                        self.parents_widgets.append(align)
 700                        self.table.attach(align, 1, 2, idx + 3, idx + 4,
 701                                        gtk.EXPAND | gtk.FILL, gtk.FILL)
 702                        align.show()
 703
 704                        hbox = gtk.HBox(False, 0)
 705                        align.add(hbox)
 706                        hbox.show()
 707
 708                        label = gtk.Label(parent_id)
 709                        label.set_selectable(True)
 710                        hbox.pack_start(label, expand=False, fill=True)
 711                        label.show()
 712
 713                        image = gtk.Image()
 714                        image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
 715                        image.show()
 716
 717                        button = gtk.Button()
 718                        button.add(image)
 719                        button.set_relief(gtk.RELIEF_NONE)
 720                        button.connect("clicked", self._go_clicked_cb, parent_id)
 721                        hbox.pack_start(button, expand=False, fill=True)
 722                        button.show()
 723
 724                        image = gtk.Image()
 725                        image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
 726                        image.show()
 727
 728                        button = gtk.Button()
 729                        button.add(image)
 730                        button.set_relief(gtk.RELIEF_NONE)
 731                        button.set_sensitive(True)
 732                        button.connect("clicked", self._show_clicked_cb,
 733                                        commit.commit_sha1, parent_id)
 734                        hbox.pack_start(button, expand=False, fill=True)
 735                        button.show()
 736
 737                # Populate with child details
 738                for widget in self.children_widgets:
 739                        self.table.remove(widget)
 740
 741                self.children_widgets = []
 742                try:
 743                        child_sha1 = Commit.children_sha1[commit.commit_sha1]
 744                except KeyError:
 745                        # We don't have child
 746                        child_sha1 = [ 0 ]
 747
 748                if ( len(child_sha1) > len(commit.parent_sha1)):
 749                        self.table.resize(4 + len(child_sha1) - 1, 4)
 750
 751                for idx, child_id in enumerate(child_sha1):
 752                        self.table.set_row_spacing(idx + 3, 0)
 753
 754                        align = gtk.Alignment(0.0, 0.0)
 755                        self.children_widgets.append(align)
 756                        self.table.attach(align, 3, 4, idx + 3, idx + 4,
 757                                        gtk.EXPAND | gtk.FILL, gtk.FILL)
 758                        align.show()
 759
 760                        hbox = gtk.HBox(False, 0)
 761                        align.add(hbox)
 762                        hbox.show()
 763
 764                        label = gtk.Label(child_id)
 765                        label.set_selectable(True)
 766                        hbox.pack_start(label, expand=False, fill=True)
 767                        label.show()
 768
 769                        image = gtk.Image()
 770                        image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
 771                        image.show()
 772
 773                        button = gtk.Button()
 774                        button.add(image)
 775                        button.set_relief(gtk.RELIEF_NONE)
 776                        button.connect("clicked", self._go_clicked_cb, child_id)
 777                        hbox.pack_start(button, expand=False, fill=True)
 778                        button.show()
 779
 780                        image = gtk.Image()
 781                        image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
 782                        image.show()
 783
 784                        button = gtk.Button()
 785                        button.add(image)
 786                        button.set_relief(gtk.RELIEF_NONE)
 787                        button.set_sensitive(True)
 788                        button.connect("clicked", self._show_clicked_cb,
 789                                        child_id, commit.commit_sha1)
 790                        hbox.pack_start(button, expand=False, fill=True)
 791                        button.show()
 792
 793        def _destroy_cb(self, widget):
 794                """Callback for when a window we manage is destroyed."""
 795                self.quit()
 796
 797
 798        def quit(self):
 799                """Stop the GTK+ main loop."""
 800                gtk.main_quit()
 801
 802        def run(self, args):
 803                self.set_branch(args)
 804                self.window.connect("destroy", self._destroy_cb)
 805                self.window.show()
 806                gtk.main()
 807
 808        def set_branch(self, args):
 809                """Fill in different windows with info from the reposiroty"""
 810                fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
 811                git_rev_list_cmd = fp.read()
 812                fp.close()
 813                fp = os.popen("git rev-list  --header --topo-order --parents " + git_rev_list_cmd)
 814                self.update_window(fp)
 815
 816        def update_window(self, fp):
 817                commit_lines = []
 818
 819                self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
 820                                gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
 821
 822                # used for cursor positioning
 823                self.index = {}
 824
 825                self.colours = {}
 826                self.nodepos = {}
 827                self.incomplete_line = {}
 828
 829                index = 0
 830                last_colour = 0
 831                last_nodepos = -1
 832                out_line = []
 833                input_line = fp.readline()
 834                while (input_line != ""):
 835                        # The commit header ends with '\0'
 836                        # This NULL is immediately followed by the sha1 of the
 837                        # next commit
 838                        if (input_line[0] != '\0'):
 839                                commit_lines.append(input_line)
 840                                input_line = fp.readline()
 841                                continue;
 842
 843                        commit = Commit(commit_lines)
 844                        if (commit != None ):
 845                                (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
 846                                                                                index, out_line,
 847                                                                                last_colour,
 848                                                                                last_nodepos)
 849                                self.index[commit.commit_sha1] = index
 850                                index += 1
 851
 852                        # Skip the '\0
 853                        commit_lines = []
 854                        commit_lines.append(input_line[1:])
 855                        input_line = fp.readline()
 856
 857                fp.close()
 858
 859                self.treeview.set_model(self.model)
 860                self.treeview.show()
 861
 862        def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
 863                in_line=[]
 864
 865                #   |   -> outline
 866                #   X
 867                #   |\  <- inline
 868
 869                # Reset nodepostion
 870                if (last_nodepos > 5):
 871                        last_nodepos = 0
 872
 873                # Add the incomplete lines of the last cell in this
 874                for sha1 in self.incomplete_line.keys():
 875                        if ( sha1 != commit.commit_sha1):
 876                                for pos in self.incomplete_line[sha1]:
 877                                        in_line.append((pos, pos, self.colours[sha1]))
 878                        else:
 879                                del self.incomplete_line[sha1]
 880
 881                try:
 882                        colour = self.colours[commit.commit_sha1]
 883                except KeyError:
 884                        last_colour +=1
 885                        self.colours[commit.commit_sha1] = last_colour
 886                        colour =  last_colour
 887                try:
 888                        node_pos = self.nodepos[commit.commit_sha1]
 889                except KeyError:
 890                        last_nodepos +=1
 891                        self.nodepos[commit.commit_sha1] = last_nodepos
 892                        node_pos = last_nodepos
 893
 894                #The first parent always continue on the same line
 895                try:
 896                        # check we alreay have the value
 897                        tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
 898                except KeyError:
 899                        self.colours[commit.parent_sha1[0]] = colour
 900                        self.nodepos[commit.parent_sha1[0]] = node_pos
 901
 902                in_line.append((node_pos, self.nodepos[commit.parent_sha1[0]],
 903                                        self.colours[commit.parent_sha1[0]]))
 904
 905                self.add_incomplete_line(commit.parent_sha1[0], index+1)
 906
 907                if (len(commit.parent_sha1) > 1):
 908                        for parent_id in commit.parent_sha1[1:]:
 909                                try:
 910                                        tmp_node_pos = self.nodepos[parent_id]
 911                                except KeyError:
 912                                        last_colour += 1;
 913                                        self.colours[parent_id] = last_colour
 914                                        last_nodepos +=1
 915                                        self.nodepos[parent_id] = last_nodepos
 916
 917                                in_line.append((node_pos, self.nodepos[parent_id],
 918                                                        self.colours[parent_id]))
 919                                self.add_incomplete_line(parent_id, index+1)
 920
 921
 922                try:
 923                        branch_tag = self.bt_sha1[commit.commit_sha1]
 924                except KeyError:
 925                        branch_tag = [ ]
 926
 927
 928                node = (node_pos, colour, branch_tag)
 929
 930                self.model.append([commit, node, out_line, in_line,
 931                                commit.message, commit.author, commit.date])
 932
 933                return (in_line, last_colour, last_nodepos)
 934
 935        def add_incomplete_line(self, sha1, index):
 936                try:
 937                        self.incomplete_line[sha1].append(self.nodepos[sha1])
 938                except KeyError:
 939                        self.incomplete_line[sha1] = [self.nodepos[sha1]]
 940
 941
 942        def _go_clicked_cb(self, widget, revid):
 943                """Callback for when the go button for a parent is clicked."""
 944                try:
 945                        self.treeview.set_cursor(self.index[revid])
 946                except KeyError:
 947                        print "Revision %s not present in the list" % revid
 948                        # revid == 0 is the parent of the first commit
 949                        if (revid != 0 ):
 950                                print "Try running gitview without any options"
 951
 952                self.treeview.grab_focus()
 953
 954        def _show_clicked_cb(self, widget,  commit_sha1, parent_sha1):
 955                """Callback for when the show button for a parent is clicked."""
 956                window = DiffWindow()
 957                window.set_diff(commit_sha1, parent_sha1)
 958                self.treeview.grab_focus()
 959
 960if __name__ == "__main__":
 961        without_diff = 0
 962
 963        if (len(sys.argv) > 1 ):
 964                if (sys.argv[1] == "--without-diff"):
 965                        without_diff = 1
 966
 967        view = GitView( without_diff != 1)
 968        view.run(sys.argv[without_diff:])
 969
 970