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