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