#!/usr/bin/python
#
# This script only handles the following directory structure:
#
# /trunk - the mainline, only one and must always exist
# /tags/* - each is a copy of either /trunk or /branches/X
# /branches/* - each is a copy of either /branches/X, /trunk or /tags/X
#
# The algorithm
#
#   Get a list of all the revisions that affect /branches and /tags
#
#   Then, start playing them, starting at the earliest.
#
#   When a branch is created, create a unique key based on the branch path and
#   the revision number.  We need to do this so that we can know where tags
#   and other branches are created from.  A branch may be removed and re-created
#   with the same name, we need to be able to tell the difference.
#
#   As branches are found, record their name and the revision in a hashtable
#   as they're deleted, remove from that hashtable.  Use the hashtable to identify
#   the path/revision combination as we play through the revisions.  This is ok to
#   do, as we're going earliest->latest, so the table will constantly contain
#   the branches that are alive as we move through time.  When we find a new
#   branch, we assume that the live branch is where we copy from (TODO: handle
#   branches and tags that are made on revisions in the past?)
#
#   Then, starting with "trunk", we iterate over our complete branches table, 
#   testing each branch to see if it's a child of the current one (yes, an N^2 loop), 
#   and recurse if it is.  We print branch details as soon as a branch is visited
#   and the list of tags that apply to this branch.
#
#   The "branchkey" is used to uniquely identify each branch (even those of the
#   same name)
import sys
import string
import time
import os
import array
import re

from svn import fs, util, delta, _repos

class Branch:
  def __init__(self, pool, path):
    self.pool = pool

    repos = _repos.svn_repos_open(path,self.pool)
    self.fs_ptr = _repos.svn_repos_fs(repos)
    rev = fs.youngest_rev(self.fs_ptr, self.pool)
    self.root = fs.revision_root(self.fs_ptr, rev, self.pool)

    # Setup the inital branch (trunk) and a blank tags dictionary
    self.branches = {'/trunk:0': {'start': 0, 'name': 'trunk', 'frompath': 'NONE', 'fromkey': 'NONE'}}
    self.tags = {}

    # Sourcepoints are the names and revisions of sources
    # that branches and tags can be made from.  Sometimes,
    # Branches dissapear and reappear, so we uniquely identify 
    # branches not only by their name, but with the revision they
    # were created in
    self.sourcepoints = {'/trunk': 0}

    # Analyse the repository and populate the branches and tags dictionaries
    sys.stdout.write("digraph branch {\n")
    self.process_branches_and_tags();
    sys.stdout.write("}\n")

  # Looks over all the revisions on /tags and /branches and plays through them
  # (oldest to youngest) to determine the branch/tag starts and ends and record
  # what came from what and when.
  def process_branches_and_tags(self):
    # Do some setup for the trunk
    last_item_on_branch = {"/trunk:0": "/trunk:0" };
    items_on_branch = {};
    sys.stdout.write("\"%s:%d\" [shape=box, label=\"Trunk\"];\n" % ("/trunk", 0))

    revs = fs.revisions_changed(self.root, ["/branches", "/tags"], 1, self.pool)
    subpool = util.svn_pool_create(self.pool)
    revs.reverse()
    for rev in revs:
      newroot = fs.revision_root(self.fs_ptr, rev, self.pool)
      changes = fs.paths_changed(newroot, subpool)
      for changedpath in changes.keys():
        if (fs.path_change_t_change_kind_get(changes[changedpath]) == fs.path_change_add and self.is_one_deep(changedpath)):
          source = fs.copied_from(newroot,changedpath, subpool)
          self.sourcepoints[changedpath] = rev;
          thiskey = "%s:%d" % (changedpath, rev)
	  fromkey = "%s:%d" % (source[1], self.sourcepoints[source[1]])
	  # If it's a branch, record the details in the branches table
	  if (string.count(changedpath,"/branches/") == 1):
	    branchname = self.dirname(changedpath,"/branches/")
	    self.branches[thiskey] = {"start":  rev}
	    self.branches[thiskey]["name"] = branchname
	    self.branches[thiskey]["frompath"] = ("%s") % (source[1])
	    self.branches[thiskey]["fromkey"] = fromkey
            sys.stdout.write("\"%s\" [shape=box, rank=2];\n" % (thiskey))
	    # Create a link for where this came from

	    # If this branch is not made from a tag, create an invisible node
	    if string.count(fromkey, "/tags/") == 0:
              sys.stdout.write("\"%s-created\" [label=\"\", height=0, width=0, fixedsize=true]\n" % (thiskey))
              sys.stdout.write("\"%s\" -> \"%s-created\" [weight=100, style=bold]\n" % (last_item_on_branch[fromkey], thiskey))
              sys.stdout.write("\"%s-created\" -> \"%s\" [weight=1, color=red]\n" % (thiskey, thiskey))
              sys.stdout.write("\"%s\" [label=\"%s\"]\n" % (thiskey, branchname))
	      last_item_on_branch["%s" % fromkey] = "%s-created" % thiskey
	    else:
              sys.stdout.write("\"%s\" -> \"%s\" [color=red]\n" % (fromkey, thiskey))
              sys.stdout.write("\"%s\" [label=\"%s\"]\n" % (thiskey, branchname))
	    last_item_on_branch["%s" % thiskey] = "%s" % thiskey
	  else:
	    tagname = self.dirname(changedpath,"/tags/");
	    self.tags[thiskey] = {"at":  rev}
	    self.tags[thiskey]["name"] = tagname
	    self.tags[thiskey]["frompath"] = ("%s") % (source[1])
	    self.tags[thiskey]["fromkey"] = fromkey
            sys.stdout.write("\"%s\" -> \"%s\" [weight=100, style=bold]\n" % (last_item_on_branch[fromkey], thiskey))
            sys.stdout.write("\"%s\" [label=\"%s\"]\n" % (thiskey, tagname))
	    last_item_on_branch["%s" % fromkey] = "%s" % thiskey
	    last_item_on_branch["%s" % thiskey] = "%s" % thiskey
	  break
        elif (fs.path_change_t_change_kind_get(changes[changedpath]) == fs.path_change_delete and self.is_one_deep(changedpath)):
	  # If it's a branch, record it's ending
	  if (string.count(changedpath,"/branches/") == 1):
	    self.branches["%s:%d" % (changedpath, self.sourcepoints[changedpath] )]["end"] = rev;
	  # remove the reference so that we can recognise a new branch with the same name
	  del self.sourcepoints[changedpath]
	  break

    util.svn_pool_clear(subpool)

  def is_one_deep(self, path):
    return string.count(path,"/")==2

  def dirname(self,path,prefix):
    return string.replace(path,prefix,"")

  def spaces(self, num):
    s = ""
    for i in range(num):
      s = s+" "
    return s
    
def main():
  util.run_app(Branch, "/home/danpat/repos")

if __name__ == '__main__':
  main();

