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