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