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