#!/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, core, 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
    self.process_branches_and_tags();

    # Now, traverse the branches, starting at the trunk and spit out
    # XML as we go
    sys.stdout.write("<?xml version=\"1.0\"?>\n")
    self.traverse_branches("/trunk:0",0)

  # This loop takes roughly N^2*T where N is the number of branches
  # and T is the number of tags
  def traverse_branches(self, branchkey, depth):
    spacing = self.spaces(depth)
    sys.stdout.write(spacing+"<branch name=\"%s\">\n" % self.branches[branchkey]["name"])
    sys.stdout.write(spacing+"  <start>%d</start>\n"  % self.branches[branchkey]["start"])
    if self.branches[branchkey].has_key("end"):
      sys.stdout.write(spacing+"  <end>%d</end>\n" % self.branches[branchkey]["end"])

    # First, show all the tags made along this branch
    for tag in self.tags:
      if self.tags[tag]["fromkey"] == branchkey:
        sys.stdout.write(spacing+"  <tag name=\"%s\" rev=\"%d\"/>\n" % (self.tags[tag]["name"], self.tags[tag]["at"]))

    # Iterate over each branch and determine if it came from
    # *this* branch (either via a tag, or direct from a branch)
    # Once we find one, we don't look further
    for subbranchkey in self.branches:
      if self.branches[subbranchkey]["fromkey"] == branchkey:
        self.traverse_branches(subbranchkey, depth+2)
      else:
        for tag in self.tags:
          if self.branches[subbranchkey]["fromkey"] == tag and self.tags[tag]["fromkey"] == branchkey:
	    self.traverse_branches(subbranchkey, depth+2)
	    break

    sys.stdout.write(spacing+"</branch>\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):
    revs = fs.revisions_changed(self.root, ["/branches", "/tags"], 1, self.pool)
    subpool = core.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)
      keys = changes.keys()
      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;
	  # If it's a branch, record the details in the branches table
	  if (string.count(changedpath,"/branches/") == 1):
	    branchname = self.dirname(changedpath,"/branches/")
#	    sys.stdout.write("Adding %s:%d to branches\n" % (changedpath, rev))
	    self.branches["%s:%d" % (changedpath, rev)] = {"start":  rev}
	    self.branches["%s:%d" % (changedpath, rev)]["name"] = branchname;
	    self.branches["%s:%d" % (changedpath, rev)]["frompath"] = ("%s") % (source[1]);
	    self.branches["%s:%d" % (changedpath, rev)]["fromkey"] = ("%s:%d") % (source[1], self.sourcepoints[source[1]] );
	  else:
	    tagname = self.dirname(changedpath,"/tags/");
	    self.tags["%s:%d" % (changedpath, rev)] = {"at":  rev}
	    self.tags["%s:%d" % (changedpath, rev)]["name"] = tagname;
	    self.tags["%s:%d" % (changedpath, rev)]["frompath"] = ("%s") % (source[1]);
	    self.tags["%s:%d" % (changedpath, rev)]["fromkey"] = ("%s:%d") % (source[1], self.sourcepoints[source[1]] );
	  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

    core.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():
  core.run_app(Branch, "/home/danpat/repos")

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


