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