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