src/svn-apply-git-diff.py
changeset 0 99d5c48f3951
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/src/svn-apply-git-diff.py	Wed Jan 21 11:37:42 2009 +0000
     1.3 @@ -0,0 +1,193 @@
     1.4 +#!/usr/bin/python
     1.5 +
     1.6 +# Apply a git diff to a subversion repository directly. Its possible
     1.7 +# to do a similar thing using git-svn, but for someone who doesn't
     1.8 +# want the pain or learning git, this is a lot easier.
     1.9 +# This could easily be adapted to any version control system that
    1.10 +# supports rename.
    1.11 +#
    1.12 +# This could probably be made much faster if it
    1.13 +# used the svn library, instead of the command line - its really
    1.14 +# very slow.
    1.15 +
    1.16 +import sys
    1.17 +import string
    1.18 +from popen2 import popen3
    1.19 +import os
    1.20 +from os import path
    1.21 +from optparse import OptionParser
    1.22 +
    1.23 +optionsParser = OptionParser()
    1.24 +optionsParser.add_option("-p", "--strip", dest="strip",
    1.25 +                  help="""Like patch -p: Strip the smallest prefix
    1.26 +                  containing num leading slashes  from  each
    1.27 +                  file  name found in the patch file""",
    1.28 +                  metavar="STRIP")
    1.29 +
    1.30 +(options, args) = optionsParser.parse_args()
    1.31 +
    1.32 +if options.has_key("strip"):
    1.33 +    STRIP = int(options["strip"]) + 1
    1.34 +else:
    1.35 +    STRIP = 1
    1.36 +
    1.37 +
    1.38 +
    1.39 +def error(msg):
    1.40 +    print 'Error: %s' % msg
    1.41 +
    1.42 +
    1.43 +def mode(chunk):
    1.44 +    if chunk and chunk.has_key('mode'):
    1.45 +        return chunk['mode']
    1.46 +    else:
    1.47 +        return "default"
    1.48 +
    1.49 +###############################################################
    1.50 +# Chunk processing
    1.51 +
    1.52 +# How we handle the various types of chunk
    1.53 +modes = {
    1.54 +    "remove": { "pre"  : [ ],
    1.55 +                "patch": [ "--- a/%(src)s",
    1.56 +                           "+++ /dev/null" ],
    1.57 +                "post" : [ "svn rm --non-interactive %(src)s" ] },
    1.58 +    "add"   : { "pre"  : [ ],
    1.59 +                "patch": [ "--- /dev/null",
    1.60 +                           "+++ b/%(dest)s" ],
    1.61 +                "post" : [ "svn add %(dest)s" ] },
    1.62 +    "rename": { "pre"  : [ "svn rename --non-interactive %(src)s %(dest)s" ],
    1.63 +                "patch": [  "--- a/%(dest)s",
    1.64 +                            "+++ b/%(dest)s" ],
    1.65 +                "post" : [ ] },
    1.66 +    "default":{ "pre"  : [ ],
    1.67 +                "patch": [  "--- a/%(src)s",
    1.68 +                            "+++ b/%(src)s" ],
    1.69 +                "post" : [ ] }
    1.70 +    }
    1.71 +
    1.72 +# Execute external commands
    1.73 +def system(cmd):
    1.74 +    # make sure any output happens in a consistent order
    1.75 +    sys.stdout.write(cmd + '\n')
    1.76 +    sys.stdout.flush()
    1.77 +    sys.stderr.flush()
    1.78 +    os.system(cmd)
    1.79 +
    1.80 +# Svn expects all parent directories to exist before a
    1.81 +# rename. Hg for example doesn't need this at all
    1.82 +def svnmakedirs(dest):
    1.83 +     destdir = path.dirname(dest)
    1.84 +     if len(destdir) and not(path.exists(destdir)):
    1.85 +         svnmakedirs(destdir)
    1.86 +         os.mkdir(destdir)
    1.87 +         system("svn add %s" % destdir)
    1.88 +
    1.89 +# Process a chunk. A chunk is actually a dictionary of properties we
    1.90 +# determined during parsing:
    1.91 +#  mode: ( remove | add | rename | None ) None := default
    1.92 +#  src: the src file (or '/dev/null')
    1.93 +#  dest: the dest file (or '/dev/null')
    1.94 +#  lines: these are the lines of the patch
    1.95 +def processChunk(chunk):
    1.96 +    svnmakedirs(chunk['dest'])
    1.97 +
    1.98 +    for command in modes[mode(chunk)]["pre"]:
    1.99 +        system(command % chunk)
   1.100 +    
   1.101 +    if len(chunk['lines']):
   1.102 +        sys.stdout.write("patch -p1\n")
   1.103 +        sys.stdout.writelines((line % chunk) + '\n' for line in modes[mode(chunk)]["patch"])
   1.104 +        (stdout, stdin, stderr) = popen3("patch -p1")
   1.105 +        stdin.writelines((line % chunk) + '\n' for line in modes[mode(chunk)]["patch"])
   1.106 +        stdin.writelines(line + '\n' for line in chunk['lines'])
   1.107 +        stdin.close()
   1.108 +        sys.stdout.write(stdout.read())
   1.109 +        sys.stderr.write(stderr.read())
   1.110 +        stdout.close()
   1.111 +        stderr.close()
   1.112 +
   1.113 +    for command in modes[mode(chunk)]["post"]:
   1.114 +        system(command % chunk)
   1.115 +
   1.116 +def stripInput(pre, path):
   1.117 +    if path.startswith('/'):
   1.118 +        return path
   1.119 +    elif len(path.split('/')) > STRIP:
   1.120 +        return string.join(path.split('/')[STRIP:], '/')
   1.121 +    else:
   1.122 +        return path
   1.123 +
   1.124 +def chunkLine(line):
   1.125 +    return [ '\\', '@', '+', '-', ' ' ].count(line[0])
   1.126 +
   1.127 +# The parser, which processes lines, and when it has a chunk, calling
   1.128 +# processChunk above
   1.129 +class Parser:
   1.130 +    def __init__(self):
   1.131 +        self.chunk = None
   1.132 +    
   1.133 +    def newChunk(self, a, b):
   1.134 +        if self.chunk:
   1.135 +            slef.processChunk(chunk)
   1.136 +        self.chunk = { 'src': a, 'dest': b, 'lines': []}
   1.137 +
   1.138 +    def setChunkProperties(props):
   1.139 +        for key in props.keys():
   1.140 +            if self.chunk.has_key(key) and props[key] != self.chunk[key]:
   1.141 +                error('Values for %s dont match: %s expected %s in chunk %s' %
   1.142 +                      (key, props[key], chunk[key], chunk['src']))
   1.143 +            self.chunk.update(props)
   1.144 +
   1.145 +    def addChunkLine(line):
   1.146 +        self.chunk['lines'].append(line)
   1.147 +
   1.148 +    
   1.149 +    def processLine(self, line):
   1.150 +        if not(len(line)):
   1.151 +            pass
   1.152 +        elif line.startswith('+++ '):
   1.153 +            dest = stripInput('b', line.split()[1])
   1.154 +            if mode(self.chunk) == 'remove' and dest == '/dev/null':
   1.155 +                pass
   1.156 +            else:
   1.157 +                self.setChunkProperties({ 'dest': dest })
   1.158 +        elif line.startswith('--- '):
   1.159 +            src = stripInput('a', line.split()[1])
   1.160 +            if mode(self.chunk) == 'add' and src == '/dev/null':
   1.161 +                pass
   1.162 +            else:
   1.163 +                self.setChunkProperties({ 'src': src })
   1.164 +        elif chunkLine(line):
   1.165 +            self.addChunkLine(line)
   1.166 +        else:
   1.167 +            command = line.split()
   1.168 +            if command[0] == 'diff':
   1.169 +                self.newChunk(stripInput('a', command[2]), stripInput('b', command[3]))
   1.170 +            elif command[0] == 'rename':
   1.171 +                if command[1] == 'from':
   1.172 +                    self.setChunkProperties({'src': command[2], 'mode': 'rename' })
   1.173 +                elif command[1] == 'to':
   1.174 +                    self.setChunkProperties({'dest': command[2], 'mode': 'rename' })
   1.175 +                else:
   1.176 +                    error('invalid rename: ' + string.join(command, ' '))
   1.177 +            elif command[0] == 'new':
   1.178 +                self.setChunkProperties({'mode': 'add' })
   1.179 +            elif command[0] == 'deleted':
   1.180 +                self.setChunkProperties({'mode': 'remove'})
   1.181 +            elif command[0] == 'index':
   1.182 +                # don't know what this does, but git diff emits it
   1.183 +                pass
   1.184 +            else:
   1.185 +                error('unknown command: ' + string.join(command, ' '))
   1.186 +
   1.187 +
   1.188 +# Create a parser
   1.189 +parser = Parser()
   1.190 +# parse the lines from stdin
   1.191 +for line in  sys.stdin.read().split('\n'):
   1.192 +    parser.processLine(line)
   1.193 +
   1.194 +# Make sure we get the last chunk:
   1.195 +parser.newChunk()
   1.196 +