gitk-like graph visualisation in python

Tristan Wibberley tristan at wibberley.org
Tue Aug 23 16:37:48 CDT 2005


Tristan Wibberley wrote:
> Hi
> 
> I've painstakingly (I hate tcl) converted the gitk visualisation
> algorithm to python using pygtk 2.7 and py cairo.
> 
> It needs lots of debugging, and lots of performance improvement - some
> of it cairo lovin, but some of it needs heavy caching (on disk) of
> children lists and more rapid lookup of changeset summaries.

I didn't really need the heavy caching on disk. Though I think it could
do with processing the graph concurrently with the gui.

Here's an updated version that starts quite a bit faster on a big repo.

-- 
Tristan Wibberley
-------------- next part --------------
# HG changeset patch
# User tricky at localhost.localdomain
# Node ID b4c9302d7b35ec7c92e485c7943c68e4bb3514b6
# Parent  0bc72d91aedae896d57afb3b7b1c26b956bfdb65
Added basic GTK/Cairo gui, it is really just a graph visualisation based heavily on gitk

diff -r 0bc72d91aeda -r b4c9302d7b35 mercurial/commands.py
--- a/mercurial/commands.py	Sat Aug 20 08:46:57 2005
+++ b/mercurial/commands.py	Sat Aug 20 10:58:39 2005
@@ -704,6 +704,11 @@
             if not exact: ui.status('forgetting ', rel, '\n')
     repo.forget(forget)
 
+def gui(ui, repo):
+    """start gui"""
+    import gui
+    gui.start_gui(ui, repo)
+    
 def heads(ui, repo, **opts):
     """show current repository heads"""
     heads = repo.changelog.heads()
@@ -1328,6 +1333,8 @@
          [('I', 'include', [], 'include path in search'),
           ('X', 'exclude', [], 'exclude path from search')],
          "hg forget [OPTION]... FILE..."),
+    "^gui":
+        (gui, [], 'hg gui'),
     "heads":
         (heads,
          [('b', 'branches', None, 'find branch info')],
diff -r 0bc72d91aeda -r b4c9302d7b35 mercurial/gui.py
--- /dev/null	Sat Aug 20 08:46:57 2005
+++ b/mercurial/gui.py	Sat Aug 20 10:58:39 2005
@@ -0,0 +1,344 @@
+#!/usr/bin/env python
+# gui.py - gui classes for mercurial
+#
+# Copyright (C) 2005 Tristan Wibberley <tristan at wibberley.com>. All rights reserved.
+# Copyright (C) 2005 Paul Mackerras.  All rights reserved.
+#
+# This software may be used and distributed according to the terms
+# of the GNU General Public License, incorporated herein by reference.
+
+# This was translated from tcl/tk (gitk) to python
+
+import hg
+import ui
+from itertools import *
+import revlog
+from revlog import nullid
+
+import pygtk
+pygtk.require('2.0')
+import gtk
+import gobject
+import time
+
+def parents_of(repo, node):
+    (p1,p2) = repo.changelog.parents(node)
+    if (p1 != nullid) and (p2 == nullid):
+        return [p1]
+    if (p2 != nullid) and (p1 == nullid):
+        return [p2]
+    if (p2 == nullid) and (p1 == nullid):
+        return []
+    return [p1,p2]
+
+def parents_of_rev(repo,rev):
+    return map(repo.changelog.rev,parents_of(repo,repo.changelog.node(rev)))
+
+class RevGraph(gtk.Bin):
+
+    __gsignals__ = {'set-scroll-adjustments' :
+                    (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+                     (gtk.Adjustment,gtk.Adjustment))}
+    
+    def __init__(self, repo, hadjustment, vadjustment):
+        gtk.Bin.__init__(self)
+        self.draw = gtk.DrawingArea()
+        
+        self.connect('expose-event', self.paint)
+        self.connect('set-scroll-adjustments', self.do_my_adjust)
+        
+        self.repo = repo
+
+        start = repo.heads()
+        ncleft = {} # number of children left to do for a given node
+        self.nchildren = {} # total number of children for a given node
+        self.nparents = {} # total number of parents for a given node
+        self.x = {} # for a given node
+        self.rowid = {} # mapping of row to node
+        self.idrow = {} # mapping of node to row
+        self.rowlines = {} # mapping of row to list of lines
+        self.rownlines = {} # mapping of row to number of lines
+        self.rowtext = {} # mapping of row to text
+
+        # calculate nparents and nchildren for each node
+        for rev in xrange(repo.changelog.count()):
+
+            node = repo.changelog.node(rev)
+            ps = repo.changelog.parents(node)
+            for p in ps:
+                if p not in self.nchildren:
+                    self.nchildren[p] = 0
+                self.nchildren[p] += 1
+            self.nparents[node] = len(ps)
+
+        # initialise ncleft for each node
+        for rev in xrange(repo.changelog.count()):
+            node = repo.changelog.node(rev)
+            if node not in self.nchildren:
+                self.nchildren[node] = 0
+            ncleft[node] = self.nchildren[node]
+            
+        todo = start[:] # None is a blank column
+        level = len(todo) - 1 # column of the node being worked with
+        nullentry = -1 # next column to be eradicate when it is determined that one should be
+        rowno = -1
+        numcommits = 0
+        linestarty = {}
+        datemode = False
+
+        while(todo != []):
+
+            numcommits += 1
+            rowno += 1
+
+            self.rownlines[rowno] = len(todo)
+            id = todo[level]
+            self.rowid[rowno] = id
+            self.idrow[id] = rowno
+            (_,_,_,_,text) = repo.changelog.read(id)
+            self.rowtext[rowno] = text.splitlines()[0]
+            actualparents = []
+
+            for p in parents_of(repo,id):
+                ncleft[p] -= 1
+                actualparents.append(p)
+            
+            self.x[id] = level
+
+            # linestarty is top of line at each level
+            if level in linestarty and linestarty[level] < rowno:
+                # add line from (x, linestarty[level]) to (x, rowno)
+                for r in xrange(min(linestarty[level],rowno),max(linestarty[level],rowno)+1):
+                    if r not in self.rowlines:
+                        self.rowlines[r] = set()
+                    self.rowlines[r].update([((level,linestarty[level]),(level,rowno))])
+            linestarty[level] = rowno # starting a new line
+
+            # TODO tags
+            #
+
+            # if only one parent and last child,
+            # replace with parent in todo
+            if (not datemode) and (len(actualparents) == 1):
+                p = actualparents[0]
+                if (ncleft[p] == 0) and (p not in todo):
+                    todo[level] = p
+                    continue
+
+            # otherwise obliterate a sensible gap choice
+            oldtodo = todo[:]
+            oldlevel = level
+            lines = []
+            oldstarty = {}
+            
+            for i in xrange(self.rownlines[rowno]):
+                if todo[i] == None: continue
+                if i in linestarty:
+                    oldstarty[i] = linestarty[i]
+                    del linestarty[i]
+                if i != level:
+                    lines.append((i,todo[i]))
+            if nullentry >= 0:
+                del todo[nullentry]
+                if nullentry < level:
+                    level -= 1
+
+            # delete the done id
+            del todo[level]
+            if nullentry > level:
+                nullentry -= 1
+            # and insert the parents
+            i = level
+            for p in actualparents:
+                if p not in todo:
+                    #assigncolor(p)
+                    todo.insert(i,p)
+                    if nullentry >= i:
+                        nullentry += 1
+                    i += 1
+                lines.append((oldlevel,p))
+
+            # then choose a new level
+            todol = len(todo)
+            level = -1
+            latest = None
+
+            for k in xrange(todol-1,-1,-1):
+                p = todo[k]
+                if p == None:
+                    continue
+
+                if ncleft[p] == 0:
+                    if datemode:
+                        if (latest == None) or (cdate[p] > latest):
+                            level = k
+                            latest = cdate[p]
+                    else:
+                        level = k
+                        break
+                        
+            if level < 0:
+                if todo != []:
+                    print "ERROR: none of the pending commits can be done yet"
+                    for p in todo:
+                        print "  " + revlog.hex(p)
+                break
+
+            # if we are reducing, put in a null entry
+            if todol < self.rownlines[rowno]:
+                if nullentry >= 0:
+                    i = nullentry
+                    while (i < todol and oldtodo[i] == todo[i]):
+                        i += 1
+                else:
+                    i = oldlevel
+                    if level >= i:
+                        i += 1
+                if i >= todol:
+                    nullentry = -1
+                else:
+                    nullentry = i
+                    todo.insert(nullentry, None)
+                    if level >= i:
+                        level += 1
+            else:
+                nullentry = -1
+
+            # j is x at the bottom of a horizontalish line
+            # i is x at the top of a horizontalish
+            for (i,dst) in lines:
+                j = todo.index(dst)
+                if i == j:
+                    if i in oldstarty:
+                        linestarty[i] = oldstarty[i]
+                    continue
+                coords = []
+                if (i in oldstarty) and (oldstarty[i] < rowno):
+                    coords.append((i,oldstarty[i]))
+                coords.append((i, rowno))
+                if j < i - 1:
+                    coords.append((j + 1, rowno))
+                elif j > i + 1:
+                    coords.append((j - 1, rowno))
+                coords.append((j, rowno + 1))
+
+
+                # add line from (x1, y1) to (x2, y2)
+                (x1,y1) = coords[0]
+                for (x2,y2) in coords[1:]:
+                    
+                    for r in xrange(min(y1,y2),max(y1,y2)+1):
+                        if r not in self.rowlines:
+                            self.rowlines[r] = set()
+                        self.rowlines[r].update([((x1,y1),(x2,y2))])
+                    (x1,y1) = (x2,y2)
+
+                if j not in linestarty:
+                    linestarty[j] = rowno + 1
+                
+
+    def do_my_adjust(self, widget, adj1, adj2, data=None):
+        self.hadjustment = adj1
+        self.vadjustment = adj2
+
+        adj2.lower = 0
+        adj2.upper = self.repo.changelog.count() - 1
+        adj2.step_increment = 1
+        adj2.emit('changed')
+
+        self.vadjustment.connect('value-changed', self.do_value_changed)
+        self.top = 0
+
+    def do_value_changed(self, adj, data=None):
+        self.top = self.vadjustment.value
+        self.queue_draw()
+    
+    def get_child(self):
+        return self.draw
+
+    def paint(self, area, event):
+
+        size = 5
+        
+        ctx = area.window.cairo_create()
+	(x,y,w,h) = event.area
+	ctx.rectangle(x,y,w,h)
+	ctx.clip()
+
+        (w,h) = area.window.get_size()
+
+        stop = int(self.top)
+        
+        lines = set()
+
+        for srow in xrange(0, h / (size * 4) + 1):
+            row = srow + stop
+
+            if row in self.rowlines:
+                lines.update(self.rowlines[row])
+
+        ctx.set_line_width(size / 5.0)
+
+        for ((x1,y1),(x2,y2)) in lines:
+            x1 = size * 4 * x1 + size * 2
+            x2 = size * 4 * x2 + size * 2
+            y1 = size * 4 * (y1 - stop) + size * 2
+            y2 = size * 4 * (y2 - stop) + size * 2
+            
+            if x1 < 0: x1 = 0
+            if x2 < 0: x2 = 0
+            if y1 < 0: y1 = 0
+            if y2 < 0: y2 = 0
+            if x1 > w: x1 = w
+            if x2 > w: x2 = w
+            if y1 > h: y1 = h
+            if y2 > h: y2 = h
+            
+            ctx.move_to(x1, y1)
+            ctx.line_to(x2, y2)
+            ctx.stroke()
+
+        for srow in xrange(0, h / (size * 4) + 1):
+            row = srow + stop
+
+            if row not in self.rowid:
+                continue
+            id = self.rowid[row]
+            ctx.arc(size * 4 * self.x[id] + size * 2,
+                    size * 4 * srow + size * 2, size, 0, 2*3.1415)
+            ctx.set_source_rgba(1,1,1,1)
+            ctx.fill_preserve()
+            ctx.set_source_rgba(0,0,0,1)
+            ctx.stroke()
+            
+            ctx.move_to(size * 4 * self.rownlines[row] + size * 2,
+                        size * 4 * srow + size * 2)
+            ctx.text_path(self.rowtext[row])
+            ctx.stroke()
+            
+RevGraph.set_set_scroll_adjustments_signal('set-scroll-adjustments')
+
+
+def destroy(widget, data=None):
+    gtk.main_quit()
+
+def start_gui(ui, repo):
+    w = gtk.Window(gtk.WINDOW_TOPLEVEL)
+    r = RevGraph(repo, gtk.Adjustment(), gtk.Adjustment())
+    s = gtk.ScrolledWindow()
+    w.add(s)
+    s.add(r)
+
+    w.connect('destroy', destroy)
+    r.show()
+    s.show()
+    w.show()
+    
+    gtk.main()
+    
+
+if __name__ == '__main__':
+    ui_ = ui.ui()
+    repo = hg.repository(ui.ui())
+
+    start_gui(ui_, repo)
-------------- next part --------------
# HG changeset patch
# User tricky at localhost.localdomain
# Node ID 71e838ce20421e84eb51cee7b55f447bb5f5f0a6
# Parent  b4c9302d7b35ec7c92e485c7943c68e4bb3514b6
Startup performance improvement

diff -r b4c9302d7b35 -r 71e838ce2042 mercurial/gui.py
--- a/mercurial/gui.py	Sat Aug 20 10:58:39 2005
+++ b/mercurial/gui.py	Tue Aug 23 21:14:55 2005
@@ -58,7 +58,7 @@
         self.idrow = {} # mapping of node to row
         self.rowlines = {} # mapping of row to list of lines
         self.rownlines = {} # mapping of row to number of lines
-        self.rowtext = {} # mapping of row to text
+        self.idtext = {} # mapping of id to text
 
         # calculate nparents and nchildren for each node
         for rev in xrange(repo.changelog.count()):
@@ -70,6 +70,7 @@
                     self.nchildren[p] = 0
                 self.nchildren[p] += 1
             self.nparents[node] = len(ps)
+            self.idtext[node] = repo.changelog.read(node)[4].splitlines()[0]
 
         # initialise ncleft for each node
         for rev in xrange(repo.changelog.count()):
@@ -95,8 +96,6 @@
             id = todo[level]
             self.rowid[rowno] = id
             self.idrow[id] = rowno
-            (_,_,_,_,text) = repo.changelog.read(id)
-            self.rowtext[rowno] = text.splitlines()[0]
             actualparents = []
 
             for p in parents_of(repo,id):
@@ -313,7 +312,7 @@
             
             ctx.move_to(size * 4 * self.rownlines[row] + size * 2,
                         size * 4 * srow + size * 2)
-            ctx.text_path(self.rowtext[row])
+            ctx.text_path(self.idtext[id])
             ctx.stroke()
             
 RevGraph.set_set_scroll_adjustments_signal('set-scroll-adjustments')


More information about the Mercurial mailing list