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 +