#!/usr/bin/python

# Apply a git diff to a subversion repository directly. Its possible
# to do a similar thing using git-svn, but for someone who doesn't
# want the pain or learning git, this is a lot easier.
# This could easily be adapted to any version control system that
# supports rename.
#
# This could probably be made much faster if it
# used the svn library, instead of the command line - its really
# very slow.

import sys
import string
from popen2 import popen3
import os
from os import path
from optparse import OptionParser

optionsParser = OptionParser()
optionsParser.add_option("-p", "--strip", dest="strip",
                  help="""Like patch -p: Strip the smallest prefix
                  containing num leading slashes  from  each
                  file  name found in the patch file""",
                  metavar="STRIP")

(options, args) = optionsParser.parse_args()

if options.has_key("strip"):
    STRIP = int(options["strip"]) + 1
else:
    STRIP = 1



def error(msg):
    print 'Error: %s' % msg


def mode(chunk):
    if chunk and chunk.has_key('mode'):
        return chunk['mode']
    else:
        return "default"

###############################################################
# Chunk processing

# How we handle the various types of chunk
modes = {
    "remove": { "pre"  : [ ],
                "patch": [ "--- a/%(src)s",
                           "+++ /dev/null" ],
                "post" : [ "svn rm --non-interactive %(src)s" ] },
    "add"   : { "pre"  : [ ],
                "patch": [ "--- /dev/null",
                           "+++ b/%(dest)s" ],
                "post" : [ "svn add %(dest)s" ] },
    "rename": { "pre"  : [ "svn rename --non-interactive %(src)s %(dest)s" ],
                "patch": [  "--- a/%(dest)s",
                            "+++ b/%(dest)s" ],
                "post" : [ ] },
    "default":{ "pre"  : [ ],
                "patch": [  "--- a/%(src)s",
                            "+++ b/%(src)s" ],
                "post" : [ ] }
    }

# Execute external commands
def system(cmd):
    # make sure any output happens in a consistent order
    sys.stdout.write(cmd + '\n')
    sys.stdout.flush()
    sys.stderr.flush()
    os.system(cmd)

# Svn expects all parent directories to exist before a
# rename. Hg for example doesn't need this at all
def svnmakedirs(dest):
     destdir = path.dirname(dest)
     if len(destdir) and not(path.exists(destdir)):
         svnmakedirs(destdir)
         os.mkdir(destdir)
         system("svn add %s" % destdir)

# Process a chunk. A chunk is actually a dictionary of properties we
# determined during parsing:
#  mode: ( remove | add | rename | None ) None := default
#  src: the src file (or '/dev/null')
#  dest: the dest file (or '/dev/null')
#  lines: these are the lines of the patch
def processChunk(chunk):
    svnmakedirs(chunk['dest'])

    for command in modes[mode(chunk)]["pre"]:
        system(command % chunk)
    
    if len(chunk['lines']):
        sys.stdout.write("patch -p1\n")
        sys.stdout.writelines((line % chunk) + '\n' for line in modes[mode(chunk)]["patch"])
        (stdout, stdin, stderr) = popen3("patch -p1")
        stdin.writelines((line % chunk) + '\n' for line in modes[mode(chunk)]["patch"])
        stdin.writelines(line + '\n' for line in chunk['lines'])
        stdin.close()
        sys.stdout.write(stdout.read())
        sys.stderr.write(stderr.read())
        stdout.close()
        stderr.close()

    for command in modes[mode(chunk)]["post"]:
        system(command % chunk)

def stripInput(pre, path):
    if path.startswith('/'):
        return path
    elif len(path.split('/')) > STRIP:
        return string.join(path.split('/')[STRIP:], '/')
    else:
        return path

def chunkLine(line):
    return [ '\\', '@', '+', '-', ' ' ].count(line[0])

# The parser, which processes lines, and when it has a chunk, calling
# processChunk above
class Parser:
    def __init__(self):
        self.chunk = None
    
    def newChunk(self, a, b):
        if self.chunk:
            slef.processChunk(chunk)
        self.chunk = { 'src': a, 'dest': b, 'lines': []}

    def setChunkProperties(props):
        for key in props.keys():
            if self.chunk.has_key(key) and props[key] != self.chunk[key]:
                error('Values for %s dont match: %s expected %s in chunk %s' %
                      (key, props[key], chunk[key], chunk['src']))
            self.chunk.update(props)

    def addChunkLine(line):
        self.chunk['lines'].append(line)

    
    def processLine(self, line):
        if not(len(line)):
            pass
        elif line.startswith('+++ '):
            dest = stripInput('b', line.split()[1])
            if mode(self.chunk) == 'remove' and dest == '/dev/null':
                pass
            else:
                self.setChunkProperties({ 'dest': dest })
        elif line.startswith('--- '):
            src = stripInput('a', line.split()[1])
            if mode(self.chunk) == 'add' and src == '/dev/null':
                pass
            else:
                self.setChunkProperties({ 'src': src })
        elif chunkLine(line):
            self.addChunkLine(line)
        else:
            command = line.split()
            if command[0] == 'diff':
                self.newChunk(stripInput('a', command[2]), stripInput('b', command[3]))
            elif command[0] == 'rename':
                if command[1] == 'from':
                    self.setChunkProperties({'src': command[2], 'mode': 'rename' })
                elif command[1] == 'to':
                    self.setChunkProperties({'dest': command[2], 'mode': 'rename' })
                else:
                    error('invalid rename: ' + string.join(command, ' '))
            elif command[0] == 'new':
                self.setChunkProperties({'mode': 'add' })
            elif command[0] == 'deleted':
                self.setChunkProperties({'mode': 'remove'})
            elif command[0] == 'index':
                # don't know what this does, but git diff emits it
                pass
            else:
                error('unknown command: ' + string.join(command, ' '))


# Create a parser
parser = Parser()
# parse the lines from stdin
for line in  sys.stdin.read().split('\n'):
    parser.processLine(line)

# Make sure we get the last chunk:
parser.newChunk()


