3 # Apply a git diff to a subversion repository directly. Its possible
4 # to do a similar thing using git-svn, but for someone who doesn't
5 # want the pain or learning git, this is a lot easier.
6 # This could easily be adapted to any version control system that
9 # This could probably be made much faster if it
10 # used the svn library, instead of the command line - its really
15 from popen2 import popen3
18 from optparse import OptionParser
20 optionsParser = OptionParser()
21 optionsParser.add_option("-p", "--strip", dest="strip",
22 help="""Like patch -p: Strip the smallest prefix
23 containing num leading slashes from each
24 file name found in the patch file""",
27 (options, args) = optionsParser.parse_args()
29 if options.has_key("strip"):
30 STRIP = int(options["strip"]) + 1
37 print 'Error: %s' % msg
41 if chunk and chunk.has_key('mode'):
46 ###############################################################
49 # How we handle the various types of chunk
51 "remove": { "pre" : [ ],
52 "patch": [ "--- a/%(src)s",
54 "post" : [ "svn rm --non-interactive %(src)s" ] },
55 "add" : { "pre" : [ ],
56 "patch": [ "--- /dev/null",
58 "post" : [ "svn add %(dest)s" ] },
59 "rename": { "pre" : [ "svn rename --non-interactive %(src)s %(dest)s" ],
60 "patch": [ "--- a/%(dest)s",
63 "default":{ "pre" : [ ],
64 "patch": [ "--- a/%(src)s",
69 # Execute external commands
71 # make sure any output happens in a consistent order
72 sys.stdout.write(cmd + '\n')
77 # Svn expects all parent directories to exist before a
78 # rename. Hg for example doesn't need this at all
79 def svnmakedirs(dest):
80 destdir = path.dirname(dest)
81 if len(destdir) and not(path.exists(destdir)):
84 system("svn add %s" % destdir)
86 # Process a chunk. A chunk is actually a dictionary of properties we
87 # determined during parsing:
88 # mode: ( remove | add | rename | None ) None := default
89 # src: the src file (or '/dev/null')
90 # dest: the dest file (or '/dev/null')
91 # lines: these are the lines of the patch
92 def processChunk(chunk):
93 svnmakedirs(chunk['dest'])
95 for command in modes[mode(chunk)]["pre"]:
96 system(command % chunk)
98 if len(chunk['lines']):
99 sys.stdout.write("patch -p1\n")
100 sys.stdout.writelines((line % chunk) + '\n' for line in modes[mode(chunk)]["patch"])
101 (stdout, stdin, stderr) = popen3("patch -p1")
102 stdin.writelines((line % chunk) + '\n' for line in modes[mode(chunk)]["patch"])
103 stdin.writelines(line + '\n' for line in chunk['lines'])
105 sys.stdout.write(stdout.read())
106 sys.stderr.write(stderr.read())
110 for command in modes[mode(chunk)]["post"]:
111 system(command % chunk)
113 def stripInput(pre, path):
114 if path.startswith('/'):
116 elif len(path.split('/')) > STRIP:
117 return string.join(path.split('/')[STRIP:], '/')
122 return [ '\\', '@', '+', '-', ' ' ].count(line[0])
124 # The parser, which processes lines, and when it has a chunk, calling
130 def newChunk(self, a, b):
132 slef.processChunk(chunk)
133 self.chunk = { 'src': a, 'dest': b, 'lines': []}
135 def setChunkProperties(props):
136 for key in props.keys():
137 if self.chunk.has_key(key) and props[key] != self.chunk[key]:
138 error('Values for %s dont match: %s expected %s in chunk %s' %
139 (key, props[key], chunk[key], chunk['src']))
140 self.chunk.update(props)
142 def addChunkLine(line):
143 self.chunk['lines'].append(line)
146 def processLine(self, line):
149 elif line.startswith('+++ '):
150 dest = stripInput('b', line.split()[1])
151 if mode(self.chunk) == 'remove' and dest == '/dev/null':
154 self.setChunkProperties({ 'dest': dest })
155 elif line.startswith('--- '):
156 src = stripInput('a', line.split()[1])
157 if mode(self.chunk) == 'add' and src == '/dev/null':
160 self.setChunkProperties({ 'src': src })
161 elif chunkLine(line):
162 self.addChunkLine(line)
164 command = line.split()
165 if command[0] == 'diff':
166 self.newChunk(stripInput('a', command[2]), stripInput('b', command[3]))
167 elif command[0] == 'rename':
168 if command[1] == 'from':
169 self.setChunkProperties({'src': command[2], 'mode': 'rename' })
170 elif command[1] == 'to':
171 self.setChunkProperties({'dest': command[2], 'mode': 'rename' })
173 error('invalid rename: ' + string.join(command, ' '))
174 elif command[0] == 'new':
175 self.setChunkProperties({'mode': 'add' })
176 elif command[0] == 'deleted':
177 self.setChunkProperties({'mode': 'remove'})
178 elif command[0] == 'index':
179 # don't know what this does, but git diff emits it
182 error('unknown command: ' + string.join(command, ' '))
187 # parse the lines from stdin
188 for line in sys.stdin.read().split('\n'):
189 parser.processLine(line)
191 # Make sure we get the last chunk: