contrib / gitview / gitviewon commit Import branch 'git-p4' of git://repo.or.cz/fast-export (610f043)
   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__copyright__ = "Copyright (C) 2007 Aneesh Kumar K.V <aneesh.kumar@gmail.com"
  14__author__    = "Aneesh Kumar K.V <aneesh.kumar@gmail.com>"
  15
  16
  17import sys
  18import os
  19import gtk
  20import pygtk
  21import pango
  22import re
  23import time
  24import gobject
  25import cairo
  26import math
  27import string
  28import fcntl
  29
  30try:
  31    import gtksourceview
  32    have_gtksourceview = True
  33except ImportError:
  34    have_gtksourceview = False
  35    print "Running without gtksourceview module"
  36
  37re_ident = re.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
  38
  39def list_to_string(args, skip):
  40        count = len(args)
  41        i = skip
  42        str_arg=" "
  43        while (i < count ):
  44                str_arg = str_arg + args[i]
  45                str_arg = str_arg + " "
  46                i = i+1
  47
  48        return str_arg
  49
  50def show_date(epoch, tz):
  51        secs = float(epoch)
  52        tzsecs = float(tz[1:3]) * 3600
  53        tzsecs += float(tz[3:5]) * 60
  54        if (tz[0] == "+"):
  55                secs += tzsecs
  56        else:
  57                secs -= tzsecs
  58
  59        return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs))
  60
  61
  62class CellRendererGraph(gtk.GenericCellRenderer):
  63        """Cell renderer for directed graph.
  64
  65        This module contains the implementation of a custom GtkCellRenderer that
  66        draws part of the directed graph based on the lines suggested by the code
  67        in graph.py.
  68
  69        Because we're shiny, we use Cairo to do this, and because we're naughty
  70        we cheat and draw over the bits of the TreeViewColumn that are supposed to
  71        just be for the background.
  72
  73        Properties:
  74        node              (column, colour, [ names ]) tuple to draw revision node,
  75        in_lines          (start, end, colour) tuple list to draw inward lines,
  76        out_lines         (start, end, colour) tuple list to draw outward lines.
  77        """
  78
  79        __gproperties__ = {
  80        "node":         ( gobject.TYPE_PYOBJECT, "node",
  81                          "revision node instruction",
  82                          gobject.PARAM_WRITABLE
  83                        ),
  84        "in-lines":     ( gobject.TYPE_PYOBJECT, "in-lines",
  85                          "instructions to draw lines into the cell",
  86                          gobject.PARAM_WRITABLE
  87                        ),
  88        "out-lines":    ( gobject.TYPE_PYOBJECT, "out-lines",
  89                          "instructions to draw lines out of the cell",
  90                          gobject.PARAM_WRITABLE
  91                        ),
  92        }
  93
  94        def do_set_property(self, property, value):
  95                """Set properties from GObject properties."""
  96                if property.name == "node":
  97                        self.node = value
  98                elif property.name == "in-lines":
  99                        self.in_lines = value
 100                elif property.name == "out-lines":
 101                        self.out_lines = value
 102                else:
 103                        raise AttributeError, "no such property: '%s'" % property.name
 104
 105        def box_size(self, widget):
 106                """Calculate box size based on widget's font.
 107
 108                Cache this as it's probably expensive to get.  It ensures that we
 109                draw the graph at least as large as the text.
 110                """
 111                try:
 112                        return self._box_size
 113                except AttributeError:
 114                        pango_ctx = widget.get_pango_context()
 115                        font_desc = widget.get_style().font_desc
 116                        metrics = pango_ctx.get_metrics(font_desc)
 117
 118                        ascent = pango.PIXELS(metrics.get_ascent())
 119                        descent = pango.PIXELS(metrics.get_descent())
 120
 121                        self._box_size = ascent + descent + 6
 122                        return self._box_size
 123
 124        def set_colour(self, ctx, colour, bg, fg):
 125                """Set the context source colour.
 126
 127                Picks a distinct colour based on an internal wheel; the bg
 128                parameter provides the value that should be assigned to the 'zero'
 129                colours and the fg parameter provides the multiplier that should be
 130                applied to the foreground colours.
 131                """
 132                colours = [
 133                    ( 1.0, 0.0, 0.0 ),
 134                    ( 1.0, 1.0, 0.0 ),
 135                    ( 0.0, 1.0, 0.0 ),
 136                    ( 0.0, 1.0, 1.0 ),
 137                    ( 0.0, 0.0, 1.0 ),
 138                    ( 1.0, 0.0, 1.0 ),
 139                    ]
 140
 141                colour %= len(colours)
 142                red   = (colours[colour][0] * fg) or bg
 143                green = (colours[colour][1] * fg) or bg
 144                blue  = (colours[colour][2] * fg) or bg
 145
 146                ctx.set_source_rgb(red, green, blue)
 147
 148        def on_get_size(self, widget, cell_area):
 149                """Return the size we need for this cell.
 150
 151                Each cell is drawn individually and is only as wide as it needs
 152                to be, we let the TreeViewColumn take care of making them all
 153                line up.
 154                """
 155                box_size = self.box_size(widget)
 156
 157                cols = self.node[0]
 158                for start, end, colour in self.in_lines + self.out_lines:
 159                        cols = int(max(cols, start, end))
 160
 161                (column, colour, names) = self.node
 162                names_len = 0
 163                if (len(names) != 0):
 164                        for item in names:
 165                                names_len += len(item)
 166
 167                width = box_size * (cols + 1 ) + names_len
 168                height = box_size
 169
 170                # FIXME I have no idea how to use cell_area properly
 171                return (0, 0, width, height)
 172
 173        def on_render(self, window, widget, bg_area, cell_area, exp_area, flags):
 174                """Render an individual cell.
 175
 176                Draws the cell contents using cairo, taking care to clip what we
 177                do to within the background area so we don't draw over other cells.
 178                Note that we're a bit naughty there and should really be drawing
 179                in the cell_area (or even the exposed area), but we explicitly don't
 180                want any gutter.
 181
 182                We try and be a little clever, if the line we need to draw is going
 183                to cross other columns we actually draw it as in the .---' style
 184                instead of a pure diagonal ... this reduces confusion by an
 185                incredible amount.
 186                """
 187                ctx = window.cairo_create()
 188                ctx.rectangle(bg_area.x, bg_area.y, bg_area.width, bg_area.height)
 189                ctx.clip()
 190
 191                box_size = self.box_size(widget)
 192
 193                ctx.set_line_width(box_size / 8)
 194                ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
 195
 196                # Draw lines into the cell
 197                for start, end, colour in self.in_lines:
 198                        ctx.move_to(cell_area.x + box_size * start + box_size / 2,
 199                                        bg_area.y - bg_area.height / 2)
 200
 201                        if start - end > 1:
 202                                ctx.line_to(cell_area.x + box_size * start, bg_area.y)
 203                                ctx.line_to(cell_area.x + box_size * end + box_size, bg_area.y)
 204                        elif start - end < -1:
 205                                ctx.line_to(cell_area.x + box_size * start + box_size,
 206                                                bg_area.y)
 207                                ctx.line_to(cell_area.x + box_size * end, bg_area.y)
 208
 209                        ctx.line_to(cell_area.x + box_size * end + box_size / 2,
 210                                        bg_area.y + bg_area.height / 2)
 211
 212                        self.set_colour(ctx, colour, 0.0, 0.65)
 213                        ctx.stroke()
 214
 215                # Draw lines out of the cell
 216                for start, end, colour in self.out_lines:
 217                        ctx.move_to(cell_area.x + box_size * start + box_size / 2,
 218                                        bg_area.y + bg_area.height / 2)
 219
 220                        if start - end > 1:
 221                                ctx.line_to(cell_area.x + box_size * start,
 222                                                bg_area.y + bg_area.height)
 223                                ctx.line_to(cell_area.x + box_size * end + box_size,
 224                                                bg_area.y + bg_area.height)
 225                        elif start - end < -1:
 226                                ctx.line_to(cell_area.x + box_size * start + box_size,
 227                                                bg_area.y + bg_area.height)
 228                                ctx.line_to(cell_area.x + box_size * end,
 229                                                bg_area.y + bg_area.height)
 230
 231                        ctx.line_to(cell_area.x + box_size * end + box_size / 2,
 232                                        bg_area.y + bg_area.height / 2 + bg_area.height)
 233
 234                        self.set_colour(ctx, colour, 0.0, 0.65)
 235                        ctx.stroke()
 236
 237                # Draw the revision node in the right column
 238                (column, colour, names) = self.node
 239                ctx.arc(cell_area.x + box_size * column + box_size / 2,
 240                                cell_area.y + cell_area.height / 2,
 241                                box_size / 4, 0, 2 * math.pi)
 242
 243
 244                self.set_colour(ctx, colour, 0.0, 0.5)
 245                ctx.stroke_preserve()
 246
 247                self.set_colour(ctx, colour, 0.5, 1.0)
 248                ctx.fill_preserve()
 249
 250                if (len(names) != 0):
 251                        name = " "
 252                        for item in names:
 253                                name = name + item + " "
 254
 255                        ctx.set_font_size(13)
 256                        if (flags & 1):
 257                                self.set_colour(ctx, colour, 0.5, 1.0)
 258                        else:
 259                                self.set_colour(ctx, colour, 0.0, 0.5)
 260                        ctx.show_text(name)
 261
 262class Commit(object):
 263        """ This represent a commit object obtained after parsing the git-rev-list
 264        output """
 265
 266        __slots__ = ['children_sha1', 'message', 'author', 'date', 'committer',
 267                                 'commit_date', 'commit_sha1', 'parent_sha1']
 268
 269        children_sha1 = {}
 270
 271        def __init__(self, commit_lines):
 272                self.message            = ""
 273                self.author             = ""
 274                self.date               = ""
 275                self.committer          = ""
 276                self.commit_date        = ""
 277                self.commit_sha1        = ""
 278                self.parent_sha1        = [ ]
 279                self.parse_commit(commit_lines)
 280
 281
 282        def parse_commit(self, commit_lines):
 283
 284                # First line is the sha1 lines
 285                line = string.strip(commit_lines[0])
 286                sha1 = re.split(" ", line)
 287                self.commit_sha1 = sha1[0]
 288                self.parent_sha1 = sha1[1:]
 289
 290                #build the child list
 291                for parent_id in self.parent_sha1:
 292                        try:
 293                                Commit.children_sha1[parent_id].append(self.commit_sha1)
 294                        except KeyError:
 295                                Commit.children_sha1[parent_id] = [self.commit_sha1]
 296
 297                # IF we don't have parent
 298                if (len(self.parent_sha1) == 0):
 299                        self.parent_sha1 = [0]
 300
 301                for line in commit_lines[1:]:
 302                        m = re.match("^ ", line)
 303                        if (m != None):
 304                                # First line of the commit message used for short log
 305                                if self.message == "":
 306                                        self.message = string.strip(line)
 307                                continue
 308
 309                        m = re.match("tree", line)
 310                        if (m != None):
 311                                continue
 312
 313                        m = re.match("parent", line)
 314                        if (m != None):
 315                                continue
 316
 317                        m = re_ident.match(line)
 318                        if (m != None):
 319                                date = show_date(m.group('epoch'), m.group('tz'))
 320                                if m.group(1) == "author":
 321                                        self.author = m.group('ident')
 322                                        self.date = date
 323                                elif m.group(1) == "committer":
 324                                        self.committer = m.group('ident')
 325                                        self.commit_date = date
 326
 327                                continue
 328
 329        def get_message(self, with_diff=0):
 330                if (with_diff == 1):
 331                        message = self.diff_tree()
 332                else:
 333                        fp = os.popen("git cat-file commit " + self.commit_sha1)
 334                        message = fp.read()
 335                        fp.close()
 336
 337                return message
 338
 339        def diff_tree(self):
 340                fp = os.popen("git diff-tree --pretty --cc  -v -p --always " +  self.commit_sha1)
 341                diff = fp.read()
 342                fp.close()
 343                return diff
 344
 345class AnnotateWindow(object):
 346        """Annotate window.
 347        This object represents and manages a single window containing the
 348        annotate information of the file
 349        """
 350
 351        def __init__(self):
 352                self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
 353                self.window.set_border_width(0)
 354                self.window.set_title("Git repository browser annotation window")
 355                self.prev_read = ""
 356
 357                # Use two thirds of the screen by default
 358                screen = self.window.get_screen()
 359                monitor = screen.get_monitor_geometry(0)
 360                width = int(monitor.width * 0.66)
 361                height = int(monitor.height * 0.66)
 362                self.window.set_default_size(width, height)
 363
 364        def add_file_data(self, filename, commit_sha1, line_num):
 365                fp = os.popen("git cat-file blob " + commit_sha1 +":"+filename)
 366                i = 1;
 367                for line in fp.readlines():
 368                        line = string.rstrip(line)
 369                        self.model.append(None, ["HEAD", filename, line, i])
 370                        i = i+1
 371                fp.close()
 372
 373                # now set the cursor position
 374                self.treeview.set_cursor(line_num-1)
 375                self.treeview.grab_focus()
 376
 377        def _treeview_cursor_cb(self, *args):
 378                """Callback for when the treeview cursor changes."""
 379                (path, col) = self.treeview.get_cursor()
 380                commit_sha1 = self.model[path][0]
 381                commit_msg = ""
 382                fp = os.popen("git cat-file commit " + commit_sha1)
 383                for line in fp.readlines():
 384                        commit_msg =  commit_msg + line
 385                fp.close()
 386
 387                self.commit_buffer.set_text(commit_msg)
 388
 389        def _treeview_row_activated(self, *args):
 390                """Callback for when the treeview row gets selected."""
 391                (path, col) = self.treeview.get_cursor()
 392                commit_sha1 = self.model[path][0]
 393                filename    = self.model[path][1]
 394                line_num    = self.model[path][3]
 395
 396                window = AnnotateWindow();
 397                fp = os.popen("git rev-parse "+ commit_sha1 + "~1")
 398                commit_sha1 = string.strip(fp.readline())
 399                fp.close()
 400                window.annotate(filename, commit_sha1, line_num)
 401
 402        def data_ready(self, source, condition):
 403                while (1):
 404                        try :
 405                                # A simple readline doesn't work
 406                                # a readline bug ??
 407                                buffer = source.read(100)
 408
 409                        except:
 410                                # resource temporary not available
 411                                return True
 412
 413                        if (len(buffer) == 0):
 414                                gobject.source_remove(self.io_watch_tag)
 415                                source.close()
 416                                return False
 417
 418                        if (self.prev_read != ""):
 419                                buffer = self.prev_read + buffer
 420                                self.prev_read = ""
 421
 422                        if (buffer[len(buffer) -1] != '\n'):
 423                                try:
 424                                        newline_index = buffer.rindex("\n")
 425                                except ValueError:
 426                                        newline_index = 0
 427
 428                                self.prev_read = buffer[newline_index:(len(buffer))]
 429                                buffer = buffer[0:newline_index]
 430
 431                        for buff in buffer.split("\n"):
 432                                annotate_line = re.compile('^([0-9a-f]{40}) (.+) (.+) (.+)$')
 433                                m = annotate_line.match(buff)
 434                                if not m:
 435                                        annotate_line = re.compile('^(filename) (.+)$')
 436                                        m = annotate_line.match(buff)
 437                                        if not m:
 438                                                continue
 439                                        filename = m.group(2)
 440                                else:
 441                                        self.commit_sha1 = m.group(1)
 442                                        self.source_line = int(m.group(2))
 443                                        self.result_line = int(m.group(3))
 444                                        self.count          = int(m.group(4))
 445                                        #set the details only when we have the file name
 446                                        continue
 447
 448                                while (self.count > 0):
 449                                        # set at result_line + count-1 the sha1 as commit_sha1
 450                                        self.count = self.count - 1
 451                                        iter = self.model.iter_nth_child(None, self.result_line + self.count-1)
 452                                        self.model.set(iter, 0, self.commit_sha1, 1, filename, 3, self.source_line)
 453
 454
 455        def annotate(self, filename, commit_sha1, line_num):
 456                # verify the commit_sha1 specified has this filename
 457
 458                fp = os.popen("git ls-tree "+ commit_sha1 + " -- " + filename)
 459                line = string.strip(fp.readline())
 460                if line == '':
 461                        # pop up the message the file is not there as a part of the commit
 462                        fp.close()
 463                        dialog = gtk.MessageDialog(parent=None, flags=0,
 464                                        type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
 465                                        message_format=None)
 466                        dialog.set_markup("The file %s is not present in the parent commit %s" % (filename, commit_sha1))
 467                        dialog.run()
 468                        dialog.destroy()
 469                        return
 470
 471                fp.close()
 472
 473                vpan = gtk.VPaned();
 474                self.window.add(vpan);
 475                vpan.show()
 476
 477                scrollwin = gtk.ScrolledWindow()
 478                scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 479                scrollwin.set_shadow_type(gtk.SHADOW_IN)
 480                vpan.pack1(scrollwin, True, True);
 481                scrollwin.show()
 482
 483                self.model = gtk.TreeStore(str, str, str, int)
 484                self.treeview = gtk.TreeView(self.model)
 485                self.treeview.set_rules_hint(True)
 486                self.treeview.set_search_column(0)
 487                self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
 488                self.treeview.connect("row-activated", self._treeview_row_activated)
 489                scrollwin.add(self.treeview)
 490                self.treeview.show()
 491
 492                cell = gtk.CellRendererText()
 493                cell.set_property("width-chars", 10)
 494                cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 495                column = gtk.TreeViewColumn("Commit")
 496                column.set_resizable(True)
 497                column.pack_start(cell, expand=True)
 498                column.add_attribute(cell, "text", 0)
 499                self.treeview.append_column(column)
 500
 501                cell = gtk.CellRendererText()
 502                cell.set_property("width-chars", 20)
 503                cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 504                column = gtk.TreeViewColumn("File Name")
 505                column.set_resizable(True)
 506                column.pack_start(cell, expand=True)
 507                column.add_attribute(cell, "text", 1)
 508                self.treeview.append_column(column)
 509
 510                cell = gtk.CellRendererText()
 511                cell.set_property("width-chars", 20)
 512                cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 513                column = gtk.TreeViewColumn("Data")
 514                column.set_resizable(True)
 515                column.pack_start(cell, expand=True)
 516                column.add_attribute(cell, "text", 2)
 517                self.treeview.append_column(column)
 518
 519                # The commit message window
 520                scrollwin = gtk.ScrolledWindow()
 521                scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 522                scrollwin.set_shadow_type(gtk.SHADOW_IN)
 523                vpan.pack2(scrollwin, True, True);
 524                scrollwin.show()
 525
 526                commit_text = gtk.TextView()
 527                self.commit_buffer = gtk.TextBuffer()
 528                commit_text.set_buffer(self.commit_buffer)
 529                scrollwin.add(commit_text)
 530                commit_text.show()
 531
 532                self.window.show()
 533
 534                self.add_file_data(filename, commit_sha1, line_num)
 535
 536                fp = os.popen("git blame --incremental -C -C -- " + filename + " " + commit_sha1)
 537                flags = fcntl.fcntl(fp.fileno(), fcntl.F_GETFL)
 538                fcntl.fcntl(fp.fileno(), fcntl.F_SETFL, flags | os.O_NONBLOCK)
 539                self.io_watch_tag = gobject.io_add_watch(fp, gobject.IO_IN, self.data_ready)
 540
 541
 542class DiffWindow(object):
 543        """Diff window.
 544        This object represents and manages a single window containing the
 545        differences between two revisions on a branch.
 546        """
 547
 548        def __init__(self):
 549                self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
 550                self.window.set_border_width(0)
 551                self.window.set_title("Git repository browser diff window")
 552
 553                # Use two thirds of the screen by default
 554                screen = self.window.get_screen()
 555                monitor = screen.get_monitor_geometry(0)
 556                width = int(monitor.width * 0.66)
 557                height = int(monitor.height * 0.66)
 558                self.window.set_default_size(width, height)
 559
 560
 561                self.construct()
 562
 563        def construct(self):
 564                """Construct the window contents."""
 565                vbox = gtk.VBox()
 566                self.window.add(vbox)
 567                vbox.show()
 568
 569                menu_bar = gtk.MenuBar()
 570                save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
 571                save_menu.connect("activate", self.save_menu_response, "save")
 572                save_menu.show()
 573                menu_bar.append(save_menu)
 574                vbox.pack_start(menu_bar, expand=False, fill=True)
 575                menu_bar.show()
 576
 577                hpan = gtk.HPaned()
 578
 579                scrollwin = gtk.ScrolledWindow()
 580                scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 581                scrollwin.set_shadow_type(gtk.SHADOW_IN)
 582                hpan.pack1(scrollwin, True, True)
 583                scrollwin.show()
 584
 585                if have_gtksourceview:
 586                        self.buffer = gtksourceview.SourceBuffer()
 587                        slm = gtksourceview.SourceLanguagesManager()
 588                        gsl = slm.get_language_from_mime_type("text/x-patch")
 589                        self.buffer.set_highlight(True)
 590                        self.buffer.set_language(gsl)
 591                        sourceview = gtksourceview.SourceView(self.buffer)
 592                else:
 593                        self.buffer = gtk.TextBuffer()
 594                        sourceview = gtk.TextView(self.buffer)
 595
 596
 597                sourceview.set_editable(False)
 598                sourceview.modify_font(pango.FontDescription("Monospace"))
 599                scrollwin.add(sourceview)
 600                sourceview.show()
 601
 602                # The file hierarchy: a scrollable treeview
 603                scrollwin = gtk.ScrolledWindow()
 604                scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 605                scrollwin.set_shadow_type(gtk.SHADOW_IN)
 606                scrollwin.set_size_request(20, -1)
 607                hpan.pack2(scrollwin, True, True)
 608                scrollwin.show()
 609
 610                self.model = gtk.TreeStore(str, str, str)
 611                self.treeview = gtk.TreeView(self.model)
 612                self.treeview.set_search_column(1)
 613                self.treeview.connect("cursor-changed", self._treeview_clicked)
 614                scrollwin.add(self.treeview)
 615                self.treeview.show()
 616
 617                cell = gtk.CellRendererText()
 618                cell.set_property("width-chars", 20)
 619                column = gtk.TreeViewColumn("Select to annotate")
 620                column.pack_start(cell, expand=True)
 621                column.add_attribute(cell, "text", 0)
 622                self.treeview.append_column(column)
 623
 624                vbox.pack_start(hpan, expand=True, fill=True)
 625                hpan.show()
 626
 627        def _treeview_clicked(self, *args):
 628                """Callback for when the treeview cursor changes."""
 629                (path, col) = self.treeview.get_cursor()
 630                specific_file = self.model[path][1]
 631                commit_sha1 =  self.model[path][2]
 632                if specific_file ==  None :
 633                        return
 634                elif specific_file ==  "" :
 635                        specific_file =  None
 636
 637                window = AnnotateWindow();
 638                window.annotate(specific_file, commit_sha1, 1)
 639
 640
 641        def commit_files(self, commit_sha1, parent_sha1):
 642                self.model.clear()
 643                add  = self.model.append(None, [ "Added", None, None])
 644                dele = self.model.append(None, [ "Deleted", None, None])
 645                mod  = self.model.append(None, [ "Modified", None, None])
 646                diff_tree = re.compile('^(:.{6}) (.{6}) (.{40}) (.{40}) (A|D|M)\s(.+)$')
 647                fp = os.popen("git diff-tree -r --no-commit-id " + parent_sha1 + " " + commit_sha1)
 648                while 1:
 649                        line = string.strip(fp.readline())
 650                        if line == '':
 651                                break
 652                        m = diff_tree.match(line)
 653                        if not m:
 654                                continue
 655
 656                        attr = m.group(5)
 657                        filename = m.group(6)
 658                        if attr == "A":
 659                                self.model.append(add,  [filename, filename, commit_sha1])
 660                        elif attr == "D":
 661                                self.model.append(dele, [filename, filename, commit_sha1])
 662                        elif attr == "M":
 663                                self.model.append(mod,  [filename, filename, commit_sha1])
 664                fp.close()
 665
 666                self.treeview.expand_all()
 667
 668        def set_diff(self, commit_sha1, parent_sha1, encoding):
 669                """Set the differences showed by this window.
 670                Compares the two trees and populates the window with the
 671                differences.
 672                """
 673                # Diff with the first commit or the last commit shows nothing
 674                if (commit_sha1 == 0 or parent_sha1 == 0 ):
 675                        return
 676
 677                fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
 678                self.buffer.set_text(unicode(fp.read(), encoding).encode('utf-8'))
 679                fp.close()
 680                self.commit_files(commit_sha1, parent_sha1)
 681                self.window.show()
 682
 683        def save_menu_response(self, widget, string):
 684                dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
 685                                (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
 686                                        gtk.STOCK_SAVE, gtk.RESPONSE_OK))
 687                dialog.set_default_response(gtk.RESPONSE_OK)
 688                response = dialog.run()
 689                if response == gtk.RESPONSE_OK:
 690                        patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
 691                                        self.buffer.get_end_iter())
 692                        fp = open(dialog.get_filename(), "w")
 693                        fp.write(patch_buffer)
 694                        fp.close()
 695                dialog.destroy()
 696
 697class GitView(object):
 698        """ This is the main class
 699        """
 700        version = "0.9"
 701
 702        def __init__(self, with_diff=0):
 703                self.with_diff = with_diff
 704                self.window =   gtk.Window(gtk.WINDOW_TOPLEVEL)
 705                self.window.set_border_width(0)
 706                self.window.set_title("Git repository browser")
 707
 708                self.get_encoding()
 709                self.get_bt_sha1()
 710
 711                # Use three-quarters of the screen by default
 712                screen = self.window.get_screen()
 713                monitor = screen.get_monitor_geometry(0)
 714                width = int(monitor.width * 0.75)
 715                height = int(monitor.height * 0.75)
 716                self.window.set_default_size(width, height)
 717
 718                # FIXME AndyFitz!
 719                icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
 720                self.window.set_icon(icon)
 721
 722                self.accel_group = gtk.AccelGroup()
 723                self.window.add_accel_group(self.accel_group)
 724                self.accel_group.connect_group(0xffc2, 0, gtk.ACCEL_LOCKED, self.refresh);
 725                self.accel_group.connect_group(0xffc1, 0, gtk.ACCEL_LOCKED, self.maximize);
 726                self.accel_group.connect_group(0xffc8, 0, gtk.ACCEL_LOCKED, self.fullscreen);
 727                self.accel_group.connect_group(0xffc9, 0, gtk.ACCEL_LOCKED, self.unfullscreen);
 728
 729                self.window.add(self.construct())
 730
 731        def refresh(self, widget, event=None, *arguments, **keywords):
 732                self.get_encoding()
 733                self.get_bt_sha1()
 734                Commit.children_sha1 = {}
 735                self.set_branch(sys.argv[without_diff:])
 736                self.window.show()
 737                return True
 738
 739        def maximize(self, widget, event=None, *arguments, **keywords):
 740                self.window.maximize()
 741                return True
 742
 743        def fullscreen(self, widget, event=None, *arguments, **keywords):
 744                self.window.fullscreen()
 745                return True
 746
 747        def unfullscreen(self, widget, event=None, *arguments, **keywords):
 748                self.window.unfullscreen()
 749                return True
 750
 751        def get_bt_sha1(self):
 752                """ Update the bt_sha1 dictionary with the
 753                respective sha1 details """
 754
 755                self.bt_sha1 = { }
 756                ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
 757                fp = os.popen('git ls-remote "${GIT_DIR-.git}"')
 758                while 1:
 759                        line = string.strip(fp.readline())
 760                        if line == '':
 761                                break
 762                        m = ls_remote.match(line)
 763                        if not m:
 764                                continue
 765                        (sha1, name) = (m.group(1), m.group(2))
 766                        if not self.bt_sha1.has_key(sha1):
 767                                self.bt_sha1[sha1] = []
 768                        self.bt_sha1[sha1].append(name)
 769                fp.close()
 770
 771        def get_encoding(self):
 772                fp = os.popen("git config --get i18n.commitencoding")
 773                self.encoding=string.strip(fp.readline())
 774                fp.close()
 775                if (self.encoding == ""):
 776                        self.encoding = "utf-8"
 777
 778
 779        def construct(self):
 780                """Construct the window contents."""
 781                vbox = gtk.VBox()
 782                paned = gtk.VPaned()
 783                paned.pack1(self.construct_top(), resize=False, shrink=True)
 784                paned.pack2(self.construct_bottom(), resize=False, shrink=True)
 785                menu_bar = gtk.MenuBar()
 786                menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
 787                help_menu = gtk.MenuItem("Help")
 788                menu = gtk.Menu()
 789                about_menu = gtk.MenuItem("About")
 790                menu.append(about_menu)
 791                about_menu.connect("activate", self.about_menu_response, "about")
 792                about_menu.show()
 793                help_menu.set_submenu(menu)
 794                help_menu.show()
 795                menu_bar.append(help_menu)
 796                menu_bar.show()
 797                vbox.pack_start(menu_bar, expand=False, fill=True)
 798                vbox.pack_start(paned, expand=True, fill=True)
 799                paned.show()
 800                vbox.show()
 801                return vbox
 802
 803
 804        def construct_top(self):
 805                """Construct the top-half of the window."""
 806                vbox = gtk.VBox(spacing=6)
 807                vbox.set_border_width(12)
 808                vbox.show()
 809
 810
 811                scrollwin = gtk.ScrolledWindow()
 812                scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 813                scrollwin.set_shadow_type(gtk.SHADOW_IN)
 814                vbox.pack_start(scrollwin, expand=True, fill=True)
 815                scrollwin.show()
 816
 817                self.treeview = gtk.TreeView()
 818                self.treeview.set_rules_hint(True)
 819                self.treeview.set_search_column(4)
 820                self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
 821                scrollwin.add(self.treeview)
 822                self.treeview.show()
 823
 824                cell = CellRendererGraph()
 825                column = gtk.TreeViewColumn()
 826                column.set_resizable(True)
 827                column.pack_start(cell, expand=True)
 828                column.add_attribute(cell, "node", 1)
 829                column.add_attribute(cell, "in-lines", 2)
 830                column.add_attribute(cell, "out-lines", 3)
 831                self.treeview.append_column(column)
 832
 833                cell = gtk.CellRendererText()
 834                cell.set_property("width-chars", 65)
 835                cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 836                column = gtk.TreeViewColumn("Message")
 837                column.set_resizable(True)
 838                column.pack_start(cell, expand=True)
 839                column.add_attribute(cell, "text", 4)
 840                self.treeview.append_column(column)
 841
 842                cell = gtk.CellRendererText()
 843                cell.set_property("width-chars", 40)
 844                cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 845                column = gtk.TreeViewColumn("Author")
 846                column.set_resizable(True)
 847                column.pack_start(cell, expand=True)
 848                column.add_attribute(cell, "text", 5)
 849                self.treeview.append_column(column)
 850
 851                cell = gtk.CellRendererText()
 852                cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 853                column = gtk.TreeViewColumn("Date")
 854                column.set_resizable(True)
 855                column.pack_start(cell, expand=True)
 856                column.add_attribute(cell, "text", 6)
 857                self.treeview.append_column(column)
 858
 859                return vbox
 860
 861        def about_menu_response(self, widget, string):
 862                dialog = gtk.AboutDialog()
 863                dialog.set_name("Gitview")
 864                dialog.set_version(GitView.version)
 865                dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@gmail.com>"])
 866                dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
 867                dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
 868                dialog.set_wrap_license(True)
 869                dialog.run()
 870                dialog.destroy()
 871
 872
 873        def construct_bottom(self):
 874                """Construct the bottom half of the window."""
 875                vbox = gtk.VBox(False, spacing=6)
 876                vbox.set_border_width(12)
 877                (width, height) = self.window.get_size()
 878                vbox.set_size_request(width, int(height / 2.5))
 879                vbox.show()
 880
 881                self.table = gtk.Table(rows=4, columns=4)
 882                self.table.set_row_spacings(6)
 883                self.table.set_col_spacings(6)
 884                vbox.pack_start(self.table, expand=False, fill=True)
 885                self.table.show()
 886
 887                align = gtk.Alignment(0.0, 0.5)
 888                label = gtk.Label()
 889                label.set_markup("<b>Revision:</b>")
 890                align.add(label)
 891                self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
 892                label.show()
 893                align.show()
 894
 895                align = gtk.Alignment(0.0, 0.5)
 896                self.revid_label = gtk.Label()
 897                self.revid_label.set_selectable(True)
 898                align.add(self.revid_label)
 899                self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
 900                self.revid_label.show()
 901                align.show()
 902
 903                align = gtk.Alignment(0.0, 0.5)
 904                label = gtk.Label()
 905                label.set_markup("<b>Committer:</b>")
 906                align.add(label)
 907                self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
 908                label.show()
 909                align.show()
 910
 911                align = gtk.Alignment(0.0, 0.5)
 912                self.committer_label = gtk.Label()
 913                self.committer_label.set_selectable(True)
 914                align.add(self.committer_label)
 915                self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
 916                self.committer_label.show()
 917                align.show()
 918
 919                align = gtk.Alignment(0.0, 0.5)
 920                label = gtk.Label()
 921                label.set_markup("<b>Timestamp:</b>")
 922                align.add(label)
 923                self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
 924                label.show()
 925                align.show()
 926
 927                align = gtk.Alignment(0.0, 0.5)
 928                self.timestamp_label = gtk.Label()
 929                self.timestamp_label.set_selectable(True)
 930                align.add(self.timestamp_label)
 931                self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
 932                self.timestamp_label.show()
 933                align.show()
 934
 935                align = gtk.Alignment(0.0, 0.5)
 936                label = gtk.Label()
 937                label.set_markup("<b>Parents:</b>")
 938                align.add(label)
 939                self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
 940                label.show()
 941                align.show()
 942                self.parents_widgets = []
 943
 944                align = gtk.Alignment(0.0, 0.5)
 945                label = gtk.Label()
 946                label.set_markup("<b>Children:</b>")
 947                align.add(label)
 948                self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
 949                label.show()
 950                align.show()
 951                self.children_widgets = []
 952
 953                scrollwin = gtk.ScrolledWindow()
 954                scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 955                scrollwin.set_shadow_type(gtk.SHADOW_IN)
 956                vbox.pack_start(scrollwin, expand=True, fill=True)
 957                scrollwin.show()
 958
 959                if have_gtksourceview:
 960                        self.message_buffer = gtksourceview.SourceBuffer()
 961                        slm = gtksourceview.SourceLanguagesManager()
 962                        gsl = slm.get_language_from_mime_type("text/x-patch")
 963                        self.message_buffer.set_highlight(True)
 964                        self.message_buffer.set_language(gsl)
 965                        sourceview = gtksourceview.SourceView(self.message_buffer)
 966                else:
 967                        self.message_buffer = gtk.TextBuffer()
 968                        sourceview = gtk.TextView(self.message_buffer)
 969
 970                sourceview.set_editable(False)
 971                sourceview.modify_font(pango.FontDescription("Monospace"))
 972                scrollwin.add(sourceview)
 973                sourceview.show()
 974
 975                return vbox
 976
 977        def _treeview_cursor_cb(self, *args):
 978                """Callback for when the treeview cursor changes."""
 979                (path, col) = self.treeview.get_cursor()
 980                commit = self.model[path][0]
 981
 982                if commit.committer is not None:
 983                        committer = commit.committer
 984                        timestamp = commit.commit_date
 985                        message   =  commit.get_message(self.with_diff)
 986                        revid_label = commit.commit_sha1
 987                else:
 988                        committer = ""
 989                        timestamp = ""
 990                        message = ""
 991                        revid_label = ""
 992
 993                self.revid_label.set_text(revid_label)
 994                self.committer_label.set_text(committer)
 995                self.timestamp_label.set_text(timestamp)
 996                self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
 997
 998                for widget in self.parents_widgets:
 999                        self.table.remove(widget)
1000
1001                self.parents_widgets = []
1002                self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
1003                for idx, parent_id in enumerate(commit.parent_sha1):
1004                        self.table.set_row_spacing(idx + 3, 0)
1005
1006                        align = gtk.Alignment(0.0, 0.0)
1007                        self.parents_widgets.append(align)
1008                        self.table.attach(align, 1, 2, idx + 3, idx + 4,
1009                                        gtk.EXPAND | gtk.FILL, gtk.FILL)
1010                        align.show()
1011
1012                        hbox = gtk.HBox(False, 0)
1013                        align.add(hbox)
1014                        hbox.show()
1015
1016                        label = gtk.Label(parent_id)
1017                        label.set_selectable(True)
1018                        hbox.pack_start(label, expand=False, fill=True)
1019                        label.show()
1020
1021                        image = gtk.Image()
1022                        image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1023                        image.show()
1024
1025                        button = gtk.Button()
1026                        button.add(image)
1027                        button.set_relief(gtk.RELIEF_NONE)
1028                        button.connect("clicked", self._go_clicked_cb, parent_id)
1029                        hbox.pack_start(button, expand=False, fill=True)
1030                        button.show()
1031
1032                        image = gtk.Image()
1033                        image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1034                        image.show()
1035
1036                        button = gtk.Button()
1037                        button.add(image)
1038                        button.set_relief(gtk.RELIEF_NONE)
1039                        button.set_sensitive(True)
1040                        button.connect("clicked", self._show_clicked_cb,
1041                                        commit.commit_sha1, parent_id, self.encoding)
1042                        hbox.pack_start(button, expand=False, fill=True)
1043                        button.show()
1044
1045                # Populate with child details
1046                for widget in self.children_widgets:
1047                        self.table.remove(widget)
1048
1049                self.children_widgets = []
1050                try:
1051                        child_sha1 = Commit.children_sha1[commit.commit_sha1]
1052                except KeyError:
1053                        # We don't have child
1054                        child_sha1 = [ 0 ]
1055
1056                if ( len(child_sha1) > len(commit.parent_sha1)):
1057                        self.table.resize(4 + len(child_sha1) - 1, 4)
1058
1059                for idx, child_id in enumerate(child_sha1):
1060                        self.table.set_row_spacing(idx + 3, 0)
1061
1062                        align = gtk.Alignment(0.0, 0.0)
1063                        self.children_widgets.append(align)
1064                        self.table.attach(align, 3, 4, idx + 3, idx + 4,
1065                                        gtk.EXPAND | gtk.FILL, gtk.FILL)
1066                        align.show()
1067
1068                        hbox = gtk.HBox(False, 0)
1069                        align.add(hbox)
1070                        hbox.show()
1071
1072                        label = gtk.Label(child_id)
1073                        label.set_selectable(True)
1074                        hbox.pack_start(label, expand=False, fill=True)
1075                        label.show()
1076
1077                        image = gtk.Image()
1078                        image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1079                        image.show()
1080
1081                        button = gtk.Button()
1082                        button.add(image)
1083                        button.set_relief(gtk.RELIEF_NONE)
1084                        button.connect("clicked", self._go_clicked_cb, child_id)
1085                        hbox.pack_start(button, expand=False, fill=True)
1086                        button.show()
1087
1088                        image = gtk.Image()
1089                        image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1090                        image.show()
1091
1092                        button = gtk.Button()
1093                        button.add(image)
1094                        button.set_relief(gtk.RELIEF_NONE)
1095                        button.set_sensitive(True)
1096                        button.connect("clicked", self._show_clicked_cb,
1097                                        child_id, commit.commit_sha1, self.encoding)
1098                        hbox.pack_start(button, expand=False, fill=True)
1099                        button.show()
1100
1101        def _destroy_cb(self, widget):
1102                """Callback for when a window we manage is destroyed."""
1103                self.quit()
1104
1105
1106        def quit(self):
1107                """Stop the GTK+ main loop."""
1108                gtk.main_quit()
1109
1110        def run(self, args):
1111                self.set_branch(args)
1112                self.window.connect("destroy", self._destroy_cb)
1113                self.window.show()
1114                gtk.main()
1115
1116        def set_branch(self, args):
1117                """Fill in different windows with info from the reposiroty"""
1118                fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
1119                git_rev_list_cmd = fp.read()
1120                fp.close()
1121                fp = os.popen("git rev-list  --header --topo-order --parents " + git_rev_list_cmd)
1122                self.update_window(fp)
1123
1124        def update_window(self, fp):
1125                commit_lines = []
1126
1127                self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
1128                                gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
1129
1130                # used for cursor positioning
1131                self.index = {}
1132
1133                self.colours = {}
1134                self.nodepos = {}
1135                self.incomplete_line = {}
1136                self.commits = []
1137
1138                index = 0
1139                last_colour = 0
1140                last_nodepos = -1
1141                out_line = []
1142                input_line = fp.readline()
1143                while (input_line != ""):
1144                        # The commit header ends with '\0'
1145                        # This NULL is immediately followed by the sha1 of the
1146                        # next commit
1147                        if (input_line[0] != '\0'):
1148                                commit_lines.append(input_line)
1149                                input_line = fp.readline()
1150                                continue;
1151
1152                        commit = Commit(commit_lines)
1153                        if (commit != None ):
1154                                self.commits.append(commit)
1155
1156                        # Skip the '\0
1157                        commit_lines = []
1158                        commit_lines.append(input_line[1:])
1159                        input_line = fp.readline()
1160
1161                fp.close()
1162
1163                for commit in self.commits:
1164                        (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
1165                                                                                index, out_line,
1166                                                                                last_colour,
1167                                                                                last_nodepos)
1168                        self.index[commit.commit_sha1] = index
1169                        index += 1
1170
1171                self.treeview.set_model(self.model)
1172                self.treeview.show()
1173
1174        def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
1175                in_line=[]
1176
1177                #   |   -> outline
1178                #   X
1179                #   |\  <- inline
1180
1181                # Reset nodepostion
1182                if (last_nodepos > 5):
1183                        last_nodepos = -1
1184
1185                # Add the incomplete lines of the last cell in this
1186                try:
1187                        colour = self.colours[commit.commit_sha1]
1188                except KeyError:
1189                        self.colours[commit.commit_sha1] = last_colour+1
1190                        last_colour = self.colours[commit.commit_sha1]
1191                        colour =   self.colours[commit.commit_sha1]
1192
1193                try:
1194                        node_pos = self.nodepos[commit.commit_sha1]
1195                except KeyError:
1196                        self.nodepos[commit.commit_sha1] = last_nodepos+1
1197                        last_nodepos = self.nodepos[commit.commit_sha1]
1198                        node_pos =  self.nodepos[commit.commit_sha1]
1199
1200                #The first parent always continue on the same line
1201                try:
1202                        # check we alreay have the value
1203                        tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
1204                except KeyError:
1205                        self.colours[commit.parent_sha1[0]] = colour
1206                        self.nodepos[commit.parent_sha1[0]] = node_pos
1207
1208                for sha1 in self.incomplete_line.keys():
1209                        if (sha1 != commit.commit_sha1):
1210                                self.draw_incomplete_line(sha1, node_pos,
1211                                                out_line, in_line, index)
1212                        else:
1213                                del self.incomplete_line[sha1]
1214
1215
1216                for parent_id in commit.parent_sha1:
1217                        try:
1218                                tmp_node_pos = self.nodepos[parent_id]
1219                        except KeyError:
1220                                self.colours[parent_id] = last_colour+1
1221                                last_colour = self.colours[parent_id]
1222                                self.nodepos[parent_id] = last_nodepos+1
1223                                last_nodepos = self.nodepos[parent_id]
1224
1225                        in_line.append((node_pos, self.nodepos[parent_id],
1226                                                self.colours[parent_id]))
1227                        self.add_incomplete_line(parent_id)
1228
1229                try:
1230                        branch_tag = self.bt_sha1[commit.commit_sha1]
1231                except KeyError:
1232                        branch_tag = [ ]
1233
1234
1235                node = (node_pos, colour, branch_tag)
1236
1237                self.model.append([commit, node, out_line, in_line,
1238                                commit.message, commit.author, commit.date])
1239
1240                return (in_line, last_colour, last_nodepos)
1241
1242        def add_incomplete_line(self, sha1):
1243                try:
1244                        self.incomplete_line[sha1].append(self.nodepos[sha1])
1245                except KeyError:
1246                        self.incomplete_line[sha1] = [self.nodepos[sha1]]
1247
1248        def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
1249                for idx, pos in enumerate(self.incomplete_line[sha1]):
1250                        if(pos == node_pos):
1251                                #remove the straight line and add a slash
1252                                if ((pos, pos, self.colours[sha1]) in out_line):
1253                                        out_line.remove((pos, pos, self.colours[sha1]))
1254                                out_line.append((pos, pos+0.5, self.colours[sha1]))
1255                                self.incomplete_line[sha1][idx] = pos = pos+0.5
1256                        try:
1257                                next_commit = self.commits[index+1]
1258                                if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
1259                                # join the line back to the node point
1260                                # This need to be done only if we modified it
1261                                        in_line.append((pos, pos-0.5, self.colours[sha1]))
1262                                        continue;
1263                        except IndexError:
1264                                pass
1265                        in_line.append((pos, pos, self.colours[sha1]))
1266
1267
1268        def _go_clicked_cb(self, widget, revid):
1269                """Callback for when the go button for a parent is clicked."""
1270                try:
1271                        self.treeview.set_cursor(self.index[revid])
1272                except KeyError:
1273                        dialog = gtk.MessageDialog(parent=None, flags=0,
1274                                        type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
1275                                        message_format=None)
1276                        dialog.set_markup("Revision <b>%s</b> not present in the list" % revid)
1277                        # revid == 0 is the parent of the first commit
1278                        if (revid != 0 ):
1279                                dialog.format_secondary_text("Try running gitview without any options")
1280                        dialog.run()
1281                        dialog.destroy()
1282
1283                self.treeview.grab_focus()
1284
1285        def _show_clicked_cb(self, widget,  commit_sha1, parent_sha1, encoding):
1286                """Callback for when the show button for a parent is clicked."""
1287                window = DiffWindow()
1288                window.set_diff(commit_sha1, parent_sha1, encoding)
1289                self.treeview.grab_focus()
1290
1291without_diff = 0
1292if __name__ == "__main__":
1293
1294        if (len(sys.argv) > 1 ):
1295                if (sys.argv[1] == "--without-diff"):
1296                        without_diff = 1
1297
1298        view = GitView( without_diff != 1)
1299        view.run(sys.argv[without_diff:])