contrib / gitview / gitviewon commit contrib/git-svn: add Makefile, test, and associated ignores (96a40b2)
   1#! /usr/bin/env python
   2
   3# This program is free software; you can redistribute it and/or modify
   4# it under the terms of the GNU General Public License as published by
   5# the Free Software Foundation; either version 2 of the License, or
   6# (at your option) any later version.
   7
   8""" gitview
   9GUI browser for git repository
  10This program is based on bzrk by Scott James Remnant <scott@ubuntu.com>
  11"""
  12__copyright__ = "Copyright (C) 2006 Hewlett-Packard Development Company, L.P."
  13__author__    = "Aneesh Kumar K.V <aneesh.kumar@hp.com>"
  14
  15
  16import sys
  17import os
  18import gtk
  19import pygtk
  20import pango
  21import re
  22import time
  23import gobject
  24import cairo
  25import math
  26import string
  27
  28try:
  29    import gtksourceview
  30    have_gtksourceview = True
  31except ImportError:
  32    have_gtksourceview = False
  33    print "Running without gtksourceview module"
  34
  35re_ident = re.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
  36
  37def list_to_string(args, skip):
  38        count = len(args)
  39        i = skip
  40        str_arg=" "
  41        while (i < count ):
  42                str_arg = str_arg + args[i]
  43                str_arg = str_arg + " "
  44                i = i+1
  45
  46        return str_arg
  47
  48def show_date(epoch, tz):
  49        secs = float(epoch)
  50        tzsecs = float(tz[1:3]) * 3600
  51        tzsecs += float(tz[3:5]) * 60
  52        if (tz[0] == "+"):
  53                secs += tzsecs
  54        else:
  55                secs -= tzsecs
  56
  57        return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs))
  58
  59def get_sha1_from_tags(line):
  60        fp = os.popen("git cat-file -t " + line)
  61        entry = string.strip(fp.readline())
  62        fp.close()
  63        if (entry == "commit"):
  64                return line
  65        elif (entry == "tag"):
  66                fp = os.popen("git cat-file tag "+ line)
  67                entry = string.strip(fp.readline())
  68                fp.close()
  69                obj = re.split(" ", entry)
  70                if (obj[0] == "object"):
  71                        return obj[1]
  72        return None
  73
  74class CellRendererGraph(gtk.GenericCellRenderer):
  75        """Cell renderer for directed graph.
  76
  77        This module contains the implementation of a custom GtkCellRenderer that
  78        draws part of the directed graph based on the lines suggested by the code
  79        in graph.py.
  80
  81        Because we're shiny, we use Cairo to do this, and because we're naughty
  82        we cheat and draw over the bits of the TreeViewColumn that are supposed to
  83        just be for the background.
  84
  85        Properties:
  86        node              (column, colour, [ names ]) tuple to draw revision node,
  87        in_lines          (start, end, colour) tuple list to draw inward lines,
  88        out_lines         (start, end, colour) tuple list to draw outward lines.
  89        """
  90
  91        __gproperties__ = {
  92        "node":         ( gobject.TYPE_PYOBJECT, "node",
  93                          "revision node instruction",
  94                          gobject.PARAM_WRITABLE
  95                        ),
  96        "in-lines":     ( gobject.TYPE_PYOBJECT, "in-lines",
  97                          "instructions to draw lines into the cell",
  98                          gobject.PARAM_WRITABLE
  99                        ),
 100        "out-lines":    ( gobject.TYPE_PYOBJECT, "out-lines",
 101                          "instructions to draw lines out of the cell",
 102                          gobject.PARAM_WRITABLE
 103                        ),
 104        }
 105
 106        def do_set_property(self, property, value):
 107                """Set properties from GObject properties."""
 108                if property.name == "node":
 109                        self.node = value
 110                elif property.name == "in-lines":
 111                        self.in_lines = value
 112                elif property.name == "out-lines":
 113                        self.out_lines = value
 114                else:
 115                        raise AttributeError, "no such property: '%s'" % property.name
 116
 117        def box_size(self, widget):
 118                """Calculate box size based on widget's font.
 119
 120                Cache this as it's probably expensive to get.  It ensures that we
 121                draw the graph at least as large as the text.
 122                """
 123                try:
 124                        return self._box_size
 125                except AttributeError:
 126                        pango_ctx = widget.get_pango_context()
 127                        font_desc = widget.get_style().font_desc
 128                        metrics = pango_ctx.get_metrics(font_desc)
 129
 130                        ascent = pango.PIXELS(metrics.get_ascent())
 131                        descent = pango.PIXELS(metrics.get_descent())
 132
 133                        self._box_size = ascent + descent + 6
 134                        return self._box_size
 135
 136        def set_colour(self, ctx, colour, bg, fg):
 137                """Set the context source colour.
 138
 139                Picks a distinct colour based on an internal wheel; the bg
 140                parameter provides the value that should be assigned to the 'zero'
 141                colours and the fg parameter provides the multiplier that should be
 142                applied to the foreground colours.
 143                """
 144                colours = [
 145                    ( 1.0, 0.0, 0.0 ),
 146                    ( 1.0, 1.0, 0.0 ),
 147                    ( 0.0, 1.0, 0.0 ),
 148                    ( 0.0, 1.0, 1.0 ),
 149                    ( 0.0, 0.0, 1.0 ),
 150                    ( 1.0, 0.0, 1.0 ),
 151                    ]
 152
 153                colour %= len(colours)
 154                red   = (colours[colour][0] * fg) or bg
 155                green = (colours[colour][1] * fg) or bg
 156                blue  = (colours[colour][2] * fg) or bg
 157
 158                ctx.set_source_rgb(red, green, blue)
 159
 160        def on_get_size(self, widget, cell_area):
 161                """Return the size we need for this cell.
 162
 163                Each cell is drawn individually and is only as wide as it needs
 164                to be, we let the TreeViewColumn take care of making them all
 165                line up.
 166                """
 167                box_size = self.box_size(widget)
 168
 169                cols = self.node[0]
 170                for start, end, colour in self.in_lines + self.out_lines:
 171                        cols = max(cols, start, end)
 172
 173                (column, colour, names) = self.node
 174                names_len = 0
 175                if (len(names) != 0):
 176                        for item in names:
 177                                names_len += len(item)/3
 178
 179                width = box_size * (cols + 1 + names_len )
 180                height = box_size
 181
 182                # FIXME I have no idea how to use cell_area properly
 183                return (0, 0, width, height)
 184
 185        def on_render(self, window, widget, bg_area, cell_area, exp_area, flags):
 186                """Render an individual cell.
 187
 188                Draws the cell contents using cairo, taking care to clip what we
 189                do to within the background area so we don't draw over other cells.
 190                Note that we're a bit naughty there and should really be drawing
 191                in the cell_area (or even the exposed area), but we explicitly don't
 192                want any gutter.
 193
 194                We try and be a little clever, if the line we need to draw is going
 195                to cross other columns we actually draw it as in the .---' style
 196                instead of a pure diagonal ... this reduces confusion by an
 197                incredible amount.
 198                """
 199                ctx = window.cairo_create()
 200                ctx.rectangle(bg_area.x, bg_area.y, bg_area.width, bg_area.height)
 201                ctx.clip()
 202
 203                box_size = self.box_size(widget)
 204
 205                ctx.set_line_width(box_size / 8)
 206                ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
 207
 208                # Draw lines into the cell
 209                for start, end, colour in self.in_lines:
 210                        ctx.move_to(cell_area.x + box_size * start + box_size / 2,
 211                                        bg_area.y - bg_area.height / 2)
 212
 213                        if start - end > 1:
 214                                ctx.line_to(cell_area.x + box_size * start, bg_area.y)
 215                                ctx.line_to(cell_area.x + box_size * end + box_size, bg_area.y)
 216                        elif start - end < -1:
 217                                ctx.line_to(cell_area.x + box_size * start + box_size,
 218                                                bg_area.y)
 219                                ctx.line_to(cell_area.x + box_size * end, bg_area.y)
 220
 221                        ctx.line_to(cell_area.x + box_size * end + box_size / 2,
 222                                        bg_area.y + bg_area.height / 2)
 223
 224                        self.set_colour(ctx, colour, 0.0, 0.65)
 225                        ctx.stroke()
 226
 227                # Draw lines out of the cell
 228                for start, end, colour in self.out_lines:
 229                        ctx.move_to(cell_area.x + box_size * start + box_size / 2,
 230                                        bg_area.y + bg_area.height / 2)
 231
 232                        if start - end > 1:
 233                                ctx.line_to(cell_area.x + box_size * start,
 234                                                bg_area.y + bg_area.height)
 235                                ctx.line_to(cell_area.x + box_size * end + box_size,
 236                                                bg_area.y + bg_area.height)
 237                        elif start - end < -1:
 238                                ctx.line_to(cell_area.x + box_size * start + box_size,
 239                                                bg_area.y + bg_area.height)
 240                                ctx.line_to(cell_area.x + box_size * end,
 241                                                bg_area.y + bg_area.height)
 242
 243                        ctx.line_to(cell_area.x + box_size * end + box_size / 2,
 244                                        bg_area.y + bg_area.height / 2 + bg_area.height)
 245
 246                        self.set_colour(ctx, colour, 0.0, 0.65)
 247                        ctx.stroke()
 248
 249                # Draw the revision node in the right column
 250                (column, colour, names) = self.node
 251                ctx.arc(cell_area.x + box_size * column + box_size / 2,
 252                                cell_area.y + cell_area.height / 2,
 253                                box_size / 4, 0, 2 * math.pi)
 254
 255
 256                if (len(names) != 0):
 257                        name = " "
 258                        for item in names:
 259                                name = name + item + " "
 260
 261                        ctx.text_path(name)
 262
 263                self.set_colour(ctx, colour, 0.0, 0.5)
 264                ctx.stroke_preserve()
 265
 266                self.set_colour(ctx, colour, 0.5, 1.0)
 267                ctx.fill()
 268
 269class Commit:
 270        """ This represent a commit object obtained after parsing the git-rev-list
 271        output """
 272
 273        children_sha1 = {}
 274
 275        def __init__(self, commit_lines):
 276                self.message            = ""
 277                self.author             = ""
 278                self.date               = ""
 279                self.committer          = ""
 280                self.commit_date        = ""
 281                self.commit_sha1        = ""
 282                self.parent_sha1        = [ ]
 283                self.parse_commit(commit_lines)
 284
 285
 286        def parse_commit(self, commit_lines):
 287
 288                # First line is the sha1 lines
 289                line = string.strip(commit_lines[0])
 290                sha1 = re.split(" ", line)
 291                self.commit_sha1 = sha1[0]
 292                self.parent_sha1 = sha1[1:]
 293
 294                #build the child list
 295                for parent_id in self.parent_sha1:
 296                        try:
 297                                Commit.children_sha1[parent_id].append(self.commit_sha1)
 298                        except KeyError:
 299                                Commit.children_sha1[parent_id] = [self.commit_sha1]
 300
 301                # IF we don't have parent
 302                if (len(self.parent_sha1) == 0):
 303                        self.parent_sha1 = [0]
 304
 305                for line in commit_lines[1:]:
 306                        m = re.match("^ ", line)
 307                        if (m != None):
 308                                # First line of the commit message used for short log
 309                                if self.message == "":
 310                                        self.message = string.strip(line)
 311                                continue
 312
 313                        m = re.match("tree", line)
 314                        if (m != None):
 315                                continue
 316
 317                        m = re.match("parent", line)
 318                        if (m != None):
 319                                continue
 320
 321                        m = re_ident.match(line)
 322                        if (m != None):
 323                                date = show_date(m.group('epoch'), m.group('tz'))
 324                                if m.group(1) == "author":
 325                                        self.author = m.group('ident')
 326                                        self.date = date
 327                                elif m.group(1) == "committer":
 328                                        self.committer = m.group('ident')
 329                                        self.commit_date = date
 330
 331                                continue
 332
 333        def get_message(self, with_diff=0):
 334                if (with_diff == 1):
 335                        message = self.diff_tree()
 336                else:
 337                        fp = os.popen("git cat-file commit " + self.commit_sha1)
 338                        message = fp.read()
 339                        fp.close()
 340
 341                return message
 342
 343        def diff_tree(self):
 344                fp = os.popen("git diff-tree --pretty --cc  -v -p --always " +  self.commit_sha1)
 345                diff = fp.read()
 346                fp.close()
 347                return diff
 348
 349class DiffWindow:
 350        """Diff window.
 351        This object represents and manages a single window containing the
 352        differences between two revisions on a branch.
 353        """
 354
 355        def __init__(self):
 356                self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
 357                self.window.set_border_width(0)
 358                self.window.set_title("Git repository browser diff window")
 359
 360                # Use two thirds of the screen by default
 361                screen = self.window.get_screen()
 362                monitor = screen.get_monitor_geometry(0)
 363                width = int(monitor.width * 0.66)
 364                height = int(monitor.height * 0.66)
 365                self.window.set_default_size(width, height)
 366
 367                self.construct()
 368
 369        def construct(self):
 370                """Construct the window contents."""
 371                vbox = gtk.VBox()
 372                self.window.add(vbox)
 373                vbox.show()
 374
 375                menu_bar = gtk.MenuBar()
 376                save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
 377                save_menu.connect("activate", self.save_menu_response, "save")
 378                save_menu.show()
 379                menu_bar.append(save_menu)
 380                vbox.pack_start(menu_bar, False, False, 2)
 381                menu_bar.show()
 382
 383                scrollwin = gtk.ScrolledWindow()
 384                scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 385                scrollwin.set_shadow_type(gtk.SHADOW_IN)
 386                vbox.pack_start(scrollwin, expand=True, fill=True)
 387                scrollwin.show()
 388
 389                if have_gtksourceview:
 390                        self.buffer = gtksourceview.SourceBuffer()
 391                        slm = gtksourceview.SourceLanguagesManager()
 392                        gsl = slm.get_language_from_mime_type("text/x-patch")
 393                        self.buffer.set_highlight(True)
 394                        self.buffer.set_language(gsl)
 395                        sourceview = gtksourceview.SourceView(self.buffer)
 396                else:
 397                        self.buffer = gtk.TextBuffer()
 398                        sourceview = gtk.TextView(self.buffer)
 399
 400                sourceview.set_editable(False)
 401                sourceview.modify_font(pango.FontDescription("Monospace"))
 402                scrollwin.add(sourceview)
 403                sourceview.show()
 404
 405
 406        def set_diff(self, commit_sha1, parent_sha1):
 407                """Set the differences showed by this window.
 408                Compares the two trees and populates the window with the
 409                differences.
 410                """
 411                # Diff with the first commit or the last commit shows nothing
 412                if (commit_sha1 == 0 or parent_sha1 == 0 ):
 413                        return
 414
 415                fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
 416                self.buffer.set_text(fp.read())
 417                fp.close()
 418                self.window.show()
 419
 420        def save_menu_response(self, widget, string):
 421                dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
 422                                (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
 423                                        gtk.STOCK_SAVE, gtk.RESPONSE_OK))
 424                dialog.set_default_response(gtk.RESPONSE_OK)
 425                response = dialog.run()
 426                if response == gtk.RESPONSE_OK:
 427                        patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
 428                                        self.buffer.get_end_iter())
 429                        fp = open(dialog.get_filename(), "w")
 430                        fp.write(patch_buffer)
 431                        fp.close()
 432                dialog.destroy()
 433
 434class GitView:
 435        """ This is the main class
 436        """
 437        version = "0.6"
 438
 439        def __init__(self, with_diff=0):
 440                self.with_diff = with_diff
 441                self.window =   gtk.Window(gtk.WINDOW_TOPLEVEL)
 442                self.window.set_border_width(0)
 443                self.window.set_title("Git repository browser")
 444
 445                self.get_bt_sha1()
 446
 447                # Use three-quarters of the screen by default
 448                screen = self.window.get_screen()
 449                monitor = screen.get_monitor_geometry(0)
 450                width = int(monitor.width * 0.75)
 451                height = int(monitor.height * 0.75)
 452                self.window.set_default_size(width, height)
 453
 454                # FIXME AndyFitz!
 455                icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
 456                self.window.set_icon(icon)
 457
 458                self.accel_group = gtk.AccelGroup()
 459                self.window.add_accel_group(self.accel_group)
 460
 461                self.construct()
 462
 463        def get_bt_sha1(self):
 464                """ Update the bt_sha1 dictionary with the
 465                respective sha1 details """
 466
 467                self.bt_sha1 = { }
 468                git_dir = os.getenv("GIT_DIR")
 469                if (git_dir == None):
 470                        git_dir = ".git"
 471
 472                #FIXME the path seperator
 473                ref_files = os.listdir(git_dir + "/refs/tags")
 474                for file in ref_files:
 475                        fp = open(git_dir + "/refs/tags/"+file)
 476                        sha1 = get_sha1_from_tags(string.strip(fp.readline()))
 477                        try:
 478                                self.bt_sha1[sha1].append(file)
 479                        except KeyError:
 480                                self.bt_sha1[sha1] = [file]
 481                        fp.close()
 482
 483
 484                #FIXME the path seperator
 485                ref_files = os.listdir(git_dir + "/refs/heads")
 486                for file in ref_files:
 487                        fp = open(git_dir + "/refs/heads/" + file)
 488                        sha1 = get_sha1_from_tags(string.strip(fp.readline()))
 489                        try:
 490                                self.bt_sha1[sha1].append(file)
 491                        except KeyError:
 492                                self.bt_sha1[sha1] = [file]
 493                        fp.close()
 494
 495
 496        def construct(self):
 497                """Construct the window contents."""
 498                paned = gtk.VPaned()
 499                paned.pack1(self.construct_top(), resize=False, shrink=True)
 500                paned.pack2(self.construct_bottom(), resize=False, shrink=True)
 501                self.window.add(paned)
 502                paned.show()
 503
 504
 505        def construct_top(self):
 506                """Construct the top-half of the window."""
 507                vbox = gtk.VBox(spacing=6)
 508                vbox.set_border_width(12)
 509                vbox.show()
 510
 511                menu_bar = gtk.MenuBar()
 512                menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
 513                help_menu = gtk.MenuItem("Help")
 514                menu = gtk.Menu()
 515                about_menu = gtk.MenuItem("About")
 516                menu.append(about_menu)
 517                about_menu.connect("activate", self.about_menu_response, "about")
 518                about_menu.show()
 519                help_menu.set_submenu(menu)
 520                help_menu.show()
 521                menu_bar.append(help_menu)
 522                vbox.pack_start(menu_bar, False, False, 2)
 523                menu_bar.show()
 524
 525                scrollwin = gtk.ScrolledWindow()
 526                scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
 527                scrollwin.set_shadow_type(gtk.SHADOW_IN)
 528                vbox.pack_start(scrollwin, expand=True, fill=True)
 529                scrollwin.show()
 530
 531                self.treeview = gtk.TreeView()
 532                self.treeview.set_rules_hint(True)
 533                self.treeview.set_search_column(4)
 534                self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
 535                scrollwin.add(self.treeview)
 536                self.treeview.show()
 537
 538                cell = CellRendererGraph()
 539                column = gtk.TreeViewColumn()
 540                column.set_resizable(False)
 541                column.pack_start(cell, expand=False)
 542                column.add_attribute(cell, "node", 1)
 543                column.add_attribute(cell, "in-lines", 2)
 544                column.add_attribute(cell, "out-lines", 3)
 545                self.treeview.append_column(column)
 546
 547                cell = gtk.CellRendererText()
 548                cell.set_property("width-chars", 65)
 549                cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 550                column = gtk.TreeViewColumn("Message")
 551                column.set_resizable(True)
 552                column.pack_start(cell, expand=True)
 553                column.add_attribute(cell, "text", 4)
 554                self.treeview.append_column(column)
 555
 556                cell = gtk.CellRendererText()
 557                cell.set_property("width-chars", 40)
 558                cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 559                column = gtk.TreeViewColumn("Author")
 560                column.set_resizable(True)
 561                column.pack_start(cell, expand=True)
 562                column.add_attribute(cell, "text", 5)
 563                self.treeview.append_column(column)
 564
 565                cell = gtk.CellRendererText()
 566                cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 567                column = gtk.TreeViewColumn("Date")
 568                column.set_resizable(True)
 569                column.pack_start(cell, expand=True)
 570                column.add_attribute(cell, "text", 6)
 571                self.treeview.append_column(column)
 572
 573                return vbox
 574
 575        def about_menu_response(self, widget, string):
 576                dialog = gtk.AboutDialog()
 577                dialog.set_name("Gitview")
 578                dialog.set_version(GitView.version)
 579                dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
 580                dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
 581                dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
 582                dialog.set_wrap_license(True)
 583                dialog.run()
 584                dialog.destroy()
 585
 586
 587        def construct_bottom(self):
 588                """Construct the bottom half of the window."""
 589                vbox = gtk.VBox(False, spacing=6)
 590                vbox.set_border_width(12)
 591                (width, height) = self.window.get_size()
 592                vbox.set_size_request(width, int(height / 2.5))
 593                vbox.show()
 594
 595                self.table = gtk.Table(rows=4, columns=4)
 596                self.table.set_row_spacings(6)
 597                self.table.set_col_spacings(6)
 598                vbox.pack_start(self.table, expand=False, fill=True)
 599                self.table.show()
 600
 601                align = gtk.Alignment(0.0, 0.5)
 602                label = gtk.Label()
 603                label.set_markup("<b>Revision:</b>")
 604                align.add(label)
 605                self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
 606                label.show()
 607                align.show()
 608
 609                align = gtk.Alignment(0.0, 0.5)
 610                self.revid_label = gtk.Label()
 611                self.revid_label.set_selectable(True)
 612                align.add(self.revid_label)
 613                self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
 614                self.revid_label.show()
 615                align.show()
 616
 617                align = gtk.Alignment(0.0, 0.5)
 618                label = gtk.Label()
 619                label.set_markup("<b>Committer:</b>")
 620                align.add(label)
 621                self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
 622                label.show()
 623                align.show()
 624
 625                align = gtk.Alignment(0.0, 0.5)
 626                self.committer_label = gtk.Label()
 627                self.committer_label.set_selectable(True)
 628                align.add(self.committer_label)
 629                self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
 630                self.committer_label.show()
 631                align.show()
 632
 633                align = gtk.Alignment(0.0, 0.5)
 634                label = gtk.Label()
 635                label.set_markup("<b>Timestamp:</b>")
 636                align.add(label)
 637                self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
 638                label.show()
 639                align.show()
 640
 641                align = gtk.Alignment(0.0, 0.5)
 642                self.timestamp_label = gtk.Label()
 643                self.timestamp_label.set_selectable(True)
 644                align.add(self.timestamp_label)
 645                self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
 646                self.timestamp_label.show()
 647                align.show()
 648
 649                align = gtk.Alignment(0.0, 0.5)
 650                label = gtk.Label()
 651                label.set_markup("<b>Parents:</b>")
 652                align.add(label)
 653                self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
 654                label.show()
 655                align.show()
 656                self.parents_widgets = []
 657
 658                align = gtk.Alignment(0.0, 0.5)
 659                label = gtk.Label()
 660                label.set_markup("<b>Children:</b>")
 661                align.add(label)
 662                self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
 663                label.show()
 664                align.show()
 665                self.children_widgets = []
 666
 667                scrollwin = gtk.ScrolledWindow()
 668                scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 669                scrollwin.set_shadow_type(gtk.SHADOW_IN)
 670                vbox.pack_start(scrollwin, expand=True, fill=True)
 671                scrollwin.show()
 672
 673                if have_gtksourceview:
 674                        self.message_buffer = gtksourceview.SourceBuffer()
 675                        slm = gtksourceview.SourceLanguagesManager()
 676                        gsl = slm.get_language_from_mime_type("text/x-patch")
 677                        self.message_buffer.set_highlight(True)
 678                        self.message_buffer.set_language(gsl)
 679                        sourceview = gtksourceview.SourceView(self.message_buffer)
 680                else:
 681                        self.message_buffer = gtk.TextBuffer()
 682                        sourceview = gtk.TextView(self.message_buffer)
 683
 684                sourceview.set_editable(False)
 685                sourceview.modify_font(pango.FontDescription("Monospace"))
 686                scrollwin.add(sourceview)
 687                sourceview.show()
 688
 689                return vbox
 690
 691        def _treeview_cursor_cb(self, *args):
 692                """Callback for when the treeview cursor changes."""
 693                (path, col) = self.treeview.get_cursor()
 694                commit = self.model[path][0]
 695
 696                if commit.committer is not None:
 697                        committer = commit.committer
 698                        timestamp = commit.commit_date
 699                        message   =  commit.get_message(self.with_diff)
 700                        revid_label = commit.commit_sha1
 701                else:
 702                        committer = ""
 703                        timestamp = ""
 704                        message = ""
 705                        revid_label = ""
 706
 707                self.revid_label.set_text(revid_label)
 708                self.committer_label.set_text(committer)
 709                self.timestamp_label.set_text(timestamp)
 710                self.message_buffer.set_text(message)
 711
 712                for widget in self.parents_widgets:
 713                        self.table.remove(widget)
 714
 715                self.parents_widgets = []
 716                self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
 717                for idx, parent_id in enumerate(commit.parent_sha1):
 718                        self.table.set_row_spacing(idx + 3, 0)
 719
 720                        align = gtk.Alignment(0.0, 0.0)
 721                        self.parents_widgets.append(align)
 722                        self.table.attach(align, 1, 2, idx + 3, idx + 4,
 723                                        gtk.EXPAND | gtk.FILL, gtk.FILL)
 724                        align.show()
 725
 726                        hbox = gtk.HBox(False, 0)
 727                        align.add(hbox)
 728                        hbox.show()
 729
 730                        label = gtk.Label(parent_id)
 731                        label.set_selectable(True)
 732                        hbox.pack_start(label, expand=False, fill=True)
 733                        label.show()
 734
 735                        image = gtk.Image()
 736                        image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
 737                        image.show()
 738
 739                        button = gtk.Button()
 740                        button.add(image)
 741                        button.set_relief(gtk.RELIEF_NONE)
 742                        button.connect("clicked", self._go_clicked_cb, parent_id)
 743                        hbox.pack_start(button, expand=False, fill=True)
 744                        button.show()
 745
 746                        image = gtk.Image()
 747                        image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
 748                        image.show()
 749
 750                        button = gtk.Button()
 751                        button.add(image)
 752                        button.set_relief(gtk.RELIEF_NONE)
 753                        button.set_sensitive(True)
 754                        button.connect("clicked", self._show_clicked_cb,
 755                                        commit.commit_sha1, parent_id)
 756                        hbox.pack_start(button, expand=False, fill=True)
 757                        button.show()
 758
 759                # Populate with child details
 760                for widget in self.children_widgets:
 761                        self.table.remove(widget)
 762
 763                self.children_widgets = []
 764                try:
 765                        child_sha1 = Commit.children_sha1[commit.commit_sha1]
 766                except KeyError:
 767                        # We don't have child
 768                        child_sha1 = [ 0 ]
 769
 770                if ( len(child_sha1) > len(commit.parent_sha1)):
 771                        self.table.resize(4 + len(child_sha1) - 1, 4)
 772
 773                for idx, child_id in enumerate(child_sha1):
 774                        self.table.set_row_spacing(idx + 3, 0)
 775
 776                        align = gtk.Alignment(0.0, 0.0)
 777                        self.children_widgets.append(align)
 778                        self.table.attach(align, 3, 4, idx + 3, idx + 4,
 779                                        gtk.EXPAND | gtk.FILL, gtk.FILL)
 780                        align.show()
 781
 782                        hbox = gtk.HBox(False, 0)
 783                        align.add(hbox)
 784                        hbox.show()
 785
 786                        label = gtk.Label(child_id)
 787                        label.set_selectable(True)
 788                        hbox.pack_start(label, expand=False, fill=True)
 789                        label.show()
 790
 791                        image = gtk.Image()
 792                        image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
 793                        image.show()
 794
 795                        button = gtk.Button()
 796                        button.add(image)
 797                        button.set_relief(gtk.RELIEF_NONE)
 798                        button.connect("clicked", self._go_clicked_cb, child_id)
 799                        hbox.pack_start(button, expand=False, fill=True)
 800                        button.show()
 801
 802                        image = gtk.Image()
 803                        image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
 804                        image.show()
 805
 806                        button = gtk.Button()
 807                        button.add(image)
 808                        button.set_relief(gtk.RELIEF_NONE)
 809                        button.set_sensitive(True)
 810                        button.connect("clicked", self._show_clicked_cb,
 811                                        child_id, commit.commit_sha1)
 812                        hbox.pack_start(button, expand=False, fill=True)
 813                        button.show()
 814
 815        def _destroy_cb(self, widget):
 816                """Callback for when a window we manage is destroyed."""
 817                self.quit()
 818
 819
 820        def quit(self):
 821                """Stop the GTK+ main loop."""
 822                gtk.main_quit()
 823
 824        def run(self, args):
 825                self.set_branch(args)
 826                self.window.connect("destroy", self._destroy_cb)
 827                self.window.show()
 828                gtk.main()
 829
 830        def set_branch(self, args):
 831                """Fill in different windows with info from the reposiroty"""
 832                fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
 833                git_rev_list_cmd = fp.read()
 834                fp.close()
 835                fp = os.popen("git rev-list  --header --topo-order --parents " + git_rev_list_cmd)
 836                self.update_window(fp)
 837
 838        def update_window(self, fp):
 839                commit_lines = []
 840
 841                self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
 842                                gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
 843
 844                # used for cursor positioning
 845                self.index = {}
 846
 847                self.colours = {}
 848                self.nodepos = {}
 849                self.incomplete_line = {}
 850
 851                index = 0
 852                last_colour = 0
 853                last_nodepos = -1
 854                out_line = []
 855                input_line = fp.readline()
 856                while (input_line != ""):
 857                        # The commit header ends with '\0'
 858                        # This NULL is immediately followed by the sha1 of the
 859                        # next commit
 860                        if (input_line[0] != '\0'):
 861                                commit_lines.append(input_line)
 862                                input_line = fp.readline()
 863                                continue;
 864
 865                        commit = Commit(commit_lines)
 866                        if (commit != None ):
 867                                (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
 868                                                                                index, out_line,
 869                                                                                last_colour,
 870                                                                                last_nodepos)
 871                                self.index[commit.commit_sha1] = index
 872                                index += 1
 873
 874                        # Skip the '\0
 875                        commit_lines = []
 876                        commit_lines.append(input_line[1:])
 877                        input_line = fp.readline()
 878
 879                fp.close()
 880
 881                self.treeview.set_model(self.model)
 882                self.treeview.show()
 883
 884        def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
 885                in_line=[]
 886
 887                #   |   -> outline
 888                #   X
 889                #   |\  <- inline
 890
 891                # Reset nodepostion
 892                if (last_nodepos > 5):
 893                        last_nodepos = 0
 894
 895                # Add the incomplete lines of the last cell in this
 896                for sha1 in self.incomplete_line.keys():
 897                        if ( sha1 != commit.commit_sha1):
 898                                for pos in self.incomplete_line[sha1]:
 899                                        in_line.append((pos, pos, self.colours[sha1]))
 900                        else:
 901                                del self.incomplete_line[sha1]
 902
 903                try:
 904                        colour = self.colours[commit.commit_sha1]
 905                except KeyError:
 906                        last_colour +=1
 907                        self.colours[commit.commit_sha1] = last_colour
 908                        colour =  last_colour
 909                try:
 910                        node_pos = self.nodepos[commit.commit_sha1]
 911                except KeyError:
 912                        last_nodepos +=1
 913                        self.nodepos[commit.commit_sha1] = last_nodepos
 914                        node_pos = last_nodepos
 915
 916                #The first parent always continue on the same line
 917                try:
 918                        # check we alreay have the value
 919                        tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
 920                except KeyError:
 921                        self.colours[commit.parent_sha1[0]] = colour
 922                        self.nodepos[commit.parent_sha1[0]] = node_pos
 923
 924                in_line.append((node_pos, self.nodepos[commit.parent_sha1[0]],
 925                                        self.colours[commit.parent_sha1[0]]))
 926
 927                self.add_incomplete_line(commit.parent_sha1[0], index+1)
 928
 929                if (len(commit.parent_sha1) > 1):
 930                        for parent_id in commit.parent_sha1[1:]:
 931                                try:
 932                                        tmp_node_pos = self.nodepos[parent_id]
 933                                except KeyError:
 934                                        last_colour += 1;
 935                                        self.colours[parent_id] = last_colour
 936                                        last_nodepos +=1
 937                                        self.nodepos[parent_id] = last_nodepos
 938
 939                                in_line.append((node_pos, self.nodepos[parent_id],
 940                                                        self.colours[parent_id]))
 941                                self.add_incomplete_line(parent_id, index+1)
 942
 943
 944                try:
 945                        branch_tag = self.bt_sha1[commit.commit_sha1]
 946                except KeyError:
 947                        branch_tag = [ ]
 948
 949
 950                node = (node_pos, colour, branch_tag)
 951
 952                self.model.append([commit, node, out_line, in_line,
 953                                commit.message, commit.author, commit.date])
 954
 955                return (in_line, last_colour, last_nodepos)
 956
 957        def add_incomplete_line(self, sha1, index):
 958                try:
 959                        self.incomplete_line[sha1].append(self.nodepos[sha1])
 960                except KeyError:
 961                        self.incomplete_line[sha1] = [self.nodepos[sha1]]
 962
 963
 964        def _go_clicked_cb(self, widget, revid):
 965                """Callback for when the go button for a parent is clicked."""
 966                try:
 967                        self.treeview.set_cursor(self.index[revid])
 968                except KeyError:
 969                        print "Revision %s not present in the list" % revid
 970                        # revid == 0 is the parent of the first commit
 971                        if (revid != 0 ):
 972                                print "Try running gitview without any options"
 973
 974                self.treeview.grab_focus()
 975
 976        def _show_clicked_cb(self, widget,  commit_sha1, parent_sha1):
 977                """Callback for when the show button for a parent is clicked."""
 978                window = DiffWindow()
 979                window.set_diff(commit_sha1, parent_sha1)
 980                self.treeview.grab_focus()
 981
 982if __name__ == "__main__":
 983        without_diff = 0
 984
 985        if (len(sys.argv) > 1 ):
 986                if (sys.argv[1] == "--without-diff"):
 987                        without_diff = 1
 988
 989        view = GitView( without_diff != 1)
 990        view.run(sys.argv[without_diff:])
 991
 992