1#! /usr/bin/env python
   2# 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""" 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>"
  15import sys
  18import os
  19import gtk
  20import pygtk
  21import pango
  22import re
  23import time
  24import gobject
  25import cairo
  26import math
  27import string
  28import fcntl
  29try:
  31    import gtksourceview2
  32    have_gtksourceview2 = True
  33except ImportError:
  34    have_gtksourceview2 = False
  35try:
  37    import gtksourceview
  38    have_gtksourceview = True
  39except ImportError:
  40    have_gtksourceview = False
  41if not have_gtksourceview2 and not have_gtksourceview:
  43    print "Running without gtksourceview2 or gtksourceview module"
  44re_ident = re.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
  46def 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        return str_arg
  57def 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        return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs))
  68def 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)
  88class CellRendererGraph(gtk.GenericCellRenderer):
  91        """Cell renderer for directed graph.
  92        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        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        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        __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        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        def box_size(self, widget):
 134                """Calculate box size based on widget's font.
 135                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                        ascent = pango.PIXELS(metrics.get_ascent())
 147                        descent = pango.PIXELS(metrics.get_descent())
 148                        self._box_size = ascent + descent + 6
 150                        return self._box_size
 151        def set_colour(self, ctx, colour, bg, fg):
 153                """Set the context source colour.
 154                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                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                ctx.set_source_rgb(red, green, blue)
 175        def on_get_size(self, widget, cell_area):
 177                """Return the size we need for this cell.
 178                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                cols = self.node[0]
 186                for start, end, colour in self.in_lines + self.out_lines:
 187                        cols = int(max(cols, start, end))
 188                (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                width = box_size * (cols + 1 ) + names_len
 196                height = box_size
 197                # FIXME I have no idea how to use cell_area properly
 199                return (0, 0, width, height)
 200        def on_render(self, window, widget, bg_area, cell_area, exp_area, flags):
 202                """Render an individual cell.
 203                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                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                box_size = self.box_size(widget)
 220                ctx.set_line_width(box_size / 8)
 222                ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
 223                # 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                        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                        ctx.line_to(cell_area.x + box_size * end + box_size / 2,
 238                                        bg_area.y + bg_area.height / 2)
 239                        self.set_colour(ctx, colour, 0.0, 0.65)
 241                        ctx.stroke()
 242                # 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                        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                        ctx.line_to(cell_area.x + box_size * end + box_size / 2,
 260                                        bg_area.y + bg_area.height / 2 + bg_area.height)
 261                        self.set_colour(ctx, colour, 0.0, 0.65)
 263                        ctx.stroke()
 264                # 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                self.set_colour(ctx, colour, 0.0, 0.5)
 273                ctx.stroke_preserve()
 274                self.set_colour(ctx, colour, 0.5, 1.0)
 276                ctx.fill_preserve()
 277                if (len(names) != 0):
 279                        name = " "
 280                        for item in names:
 281                                name = name + item + " "
 282                        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)
 289class Commit(object):
 291        """ This represent a commit object obtained after parsing the git-rev-list
 292        output """
 293        __slots__ = ['children_sha1', 'message', 'author', 'date', 'committer',
 295                                 'commit_date', 'commit_sha1', 'parent_sha1']
 296        children_sha1 = {}
 298        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        def parse_commit(self, commit_lines):
 311                # 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                #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                # IF we don't have parent
 326                if (len(self.parent_sha1) == 0):
 327                        self.parent_sha1 = [0]
 328                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                        m = re.match("tree", line)
 338                        if (m != None):
 339                                continue
 340                        m = re.match("parent", line)
 342                        if (m != None):
 343                                continue
 344                        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                                continue
 356        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                return message
 366        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
 372class AnnotateWindow(object):
 374        """Annotate window.
 375        This object represents and manages a single window containing the
 376        annotate information of the file
 377        """
 378        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                # 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        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                # now set the cursor position
 402                self.treeview.set_cursor(line_num-1)
 403                self.treeview.grab_focus()
 404        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                self.commit_buffer.set_text(commit_msg)
 416        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                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        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                        except:
 438                                # resource temporary not available
 439                                return True
 440                        if (len(buffer) == 0):
 442                                gobject.source_remove(self.io_watch_tag)
 443                                source.close()
 444                                return False
 445                        if (self.prev_read != ""):
 447                                buffer = self.prev_read + buffer
 448                                self.prev_read = ""
 449                        if (buffer[len(buffer) -1] != '\n'):
 451                                try:
 452                                        newline_index = buffer.rindex("\n")
 453                                except ValueError:
 454                                        newline_index = 0
 455                                self.prev_read = buffer[newline_index:(len(buffer))]
 457                                buffer = buffer[0:newline_index]
 458                        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                                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        def annotate(self, filename, commit_sha1, line_num):
 484                # verify the commit_sha1 specified has this filename
 485                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                fp.close()
 500                vpan = gtk.VPaned();
 502                self.window.add(vpan);
 503                vpan.show()
 504                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                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                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                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                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                # 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                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                self.window.show()
 561                self.add_file_data(filename, commit_sha1, line_num)
 563                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)
 568class 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        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                # 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                self.construct()
 590        def construct(self):
 592                """Construct the window contents."""
 593                vbox = gtk.VBox()
 594                self.window.add(vbox)
 595                vbox.show()
 596                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                hpan = gtk.HPaned()
 606                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                (self.buffer, sourceview) = get_source_buffer_and_view()
 614                sourceview.set_editable(False)
 616                sourceview.modify_font(pango.FontDescription("Monospace"))
 617                scrollwin.add(sourceview)
 618                sourceview.show()
 619                # 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                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                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                vbox.pack_start(hpan, expand=True, fill=True)
 643                hpan.show()
 644        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                window = AnnotateWindow();
 656                window.annotate(specific_file, commit_sha1, 1)
 657        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                        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                self.treeview.expand_all()
 685        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                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        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()
 714class GitView(object):
 716        """ This is the main class
 717        """
 718        version = "0.9"
 719        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                self.get_encoding()
 727                self.get_bt_sha1()
 728                # 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                # FIXME AndyFitz!
 737                icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
 738                self.window.set_icon(icon)
 739                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                self.window.add(self.construct())
 748        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        def maximize(self, widget, event=None, *arguments, **keywords):
 758                self.window.maximize()
 759                return True
 760        def fullscreen(self, widget, event=None, *arguments, **keywords):
 762                self.window.fullscreen()
 763                return True
 764        def unfullscreen(self, widget, event=None, *arguments, **keywords):
 766                self.window.unfullscreen()
 767                return True
 768        def get_bt_sha1(self):
 770                """ Update the bt_sha1 dictionary with the
 771                respective sha1 details """
 772                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        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        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        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                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                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                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                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                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                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                return vbox
 878        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        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                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                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                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                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                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                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                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                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                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                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                (self.message_buffer, sourceview) = get_source_buffer_and_view()
 978                sourceview.set_editable(False)
 980                sourceview.modify_font(pango.FontDescription("Monospace"))
 981                scrollwin.add(sourceview)
 982                sourceview.show()
 983                return vbox
 985        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                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                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                for widget in self.parents_widgets:
1008                        self.table.remove(widget)
1009                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                        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                        hbox = gtk.HBox(False, 0)
1022                        align.add(hbox)
1023                        hbox.show()
1024                        label = gtk.Label(parent_id)
1026                        label.set_selectable(True)
1027                        hbox.pack_start(label, expand=False, fill=True)
1028                        label.show()
1029                        image = gtk.Image()
1031                        image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1032                        image.show()
1033                        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                        image = gtk.Image()
1042                        image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1043                        image.show()
1044                        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                # Populate with child details
1055                for widget in self.children_widgets:
1056                        self.table.remove(widget)
1057                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                if ( len(child_sha1) > len(commit.parent_sha1)):
1066                        self.table.resize(4 + len(child_sha1) - 1, 4)
1067                for idx, child_id in enumerate(child_sha1):
1069                        self.table.set_row_spacing(idx + 3, 0)
1070                        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                        hbox = gtk.HBox(False, 0)
1078                        align.add(hbox)
1079                        hbox.show()
1080                        label = gtk.Label(child_id)
1082                        label.set_selectable(True)
1083                        hbox.pack_start(label, expand=False, fill=True)
1084                        label.show()
1085                        image = gtk.Image()
1087                        image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
1088                        image.show()
1089                        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                        image = gtk.Image()
1098                        image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
1099                        image.show()
1100                        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        def _destroy_cb(self, widget):
1111                """Callback for when a window we manage is destroyed."""
1112                self.quit()
1113        def quit(self):
1116                """Stop the GTK+ main loop."""
1117                gtk.main_quit()
1118        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        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        def update_window(self, fp):
1134                commit_lines = []
1135                self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
1137                                gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
1138                # used for cursor positioning
1140                self.index = {}
1141                self.colours = {}
1143                self.nodepos = {}
1144                self.incomplete_line = {}
1145                self.commits = []
1146                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                        commit = Commit(commit_lines)
1162                        if (commit != None ):
1163                                self.commits.append(commit)
1164                        # Skip the '\0
1166                        commit_lines = []
1167                        commit_lines.append(input_line[1:])
1168                        input_line = fp.readline()
1169                fp.close()
1171                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                self.treeview.set_model(self.model)
1181                self.treeview.show()
1182        def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
1184                in_line=[]
1185                #   |   -> outline
1187                #   X
1188                #   |\  <- inline
1189                # Reset nodepostion
1191                if (last_nodepos > 5):
1192                        last_nodepos = -1
1193                # 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                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                #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                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                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                        in_line.append((node_pos, self.nodepos[parent_id],
1235                                                self.colours[parent_id]))
1236                        self.add_incomplete_line(parent_id)
1237                try:
1239                        branch_tag = self.bt_sha1[commit.commit_sha1]
1240                except KeyError:
1241                        branch_tag = [ ]
1242                node = (node_pos, colour, branch_tag)
1245                self.model.append([commit, node, out_line, in_line,
1247                                commit.message, commit.author, commit.date])
1248                return (in_line, last_colour, last_nodepos)
1250        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        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        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                self.treeview.grab_focus()
1293        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()
1299without_diff = 0
1301if __name__ == "__main__":
1302        if (len(sys.argv) > 1 ):
1304                if (sys.argv[1] == "--without-diff"):
1305                        without_diff = 1
1306        view = GitView( without_diff != 1)
1308        view.run(sys.argv[without_diff:])