src/svn-apply-git-diff.py
author David Ireland <david@lshift.net>
Wed Jan 21 11:37:42 2009 +0000 (2009-01-21)
changeset 0 99d5c48f3951
child 1 ebdb5a7ecb40
permissions -rwxr-xr-x
A script which applies a git diff to an svn working copy, adding,
moving and deleting files using svn commands, as needed.
     1 #!/usr/bin/python
     2 
     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
     7 # supports rename.
     8 #
     9 # This could probably be made much faster if it
    10 # used the svn library, instead of the command line - its really
    11 # very slow.
    12 
    13 import sys
    14 import string
    15 from popen2 import popen3
    16 import os
    17 from os import path
    18 from optparse import OptionParser
    19 
    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""",
    25                   metavar="STRIP")
    26 
    27 (options, args) = optionsParser.parse_args()
    28 
    29 if options.has_key("strip"):
    30     STRIP = int(options["strip"]) + 1
    31 else:
    32     STRIP = 1
    33 
    34 
    35 
    36 def error(msg):
    37     print 'Error: %s' % msg
    38 
    39 
    40 def mode(chunk):
    41     if chunk and chunk.has_key('mode'):
    42         return chunk['mode']
    43     else:
    44         return "default"
    45 
    46 ###############################################################
    47 # Chunk processing
    48 
    49 # How we handle the various types of chunk
    50 modes = {
    51     "remove": { "pre"  : [ ],
    52                 "patch": [ "--- a/%(src)s",
    53                            "+++ /dev/null" ],
    54                 "post" : [ "svn rm --non-interactive %(src)s" ] },
    55     "add"   : { "pre"  : [ ],
    56                 "patch": [ "--- /dev/null",
    57                            "+++ b/%(dest)s" ],
    58                 "post" : [ "svn add %(dest)s" ] },
    59     "rename": { "pre"  : [ "svn rename --non-interactive %(src)s %(dest)s" ],
    60                 "patch": [  "--- a/%(dest)s",
    61                             "+++ b/%(dest)s" ],
    62                 "post" : [ ] },
    63     "default":{ "pre"  : [ ],
    64                 "patch": [  "--- a/%(src)s",
    65                             "+++ b/%(src)s" ],
    66                 "post" : [ ] }
    67     }
    68 
    69 # Execute external commands
    70 def system(cmd):
    71     # make sure any output happens in a consistent order
    72     sys.stdout.write(cmd + '\n')
    73     sys.stdout.flush()
    74     sys.stderr.flush()
    75     os.system(cmd)
    76 
    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)):
    82          svnmakedirs(destdir)
    83          os.mkdir(destdir)
    84          system("svn add %s" % destdir)
    85 
    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'])
    94 
    95     for command in modes[mode(chunk)]["pre"]:
    96         system(command % chunk)
    97     
    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'])
   104         stdin.close()
   105         sys.stdout.write(stdout.read())
   106         sys.stderr.write(stderr.read())
   107         stdout.close()
   108         stderr.close()
   109 
   110     for command in modes[mode(chunk)]["post"]:
   111         system(command % chunk)
   112 
   113 def stripInput(pre, path):
   114     if path.startswith('/'):
   115         return path
   116     elif len(path.split('/')) > STRIP:
   117         return string.join(path.split('/')[STRIP:], '/')
   118     else:
   119         return path
   120 
   121 def chunkLine(line):
   122     return [ '\\', '@', '+', '-', ' ' ].count(line[0])
   123 
   124 # The parser, which processes lines, and when it has a chunk, calling
   125 # processChunk above
   126 class Parser:
   127     def __init__(self):
   128         self.chunk = None
   129     
   130     def newChunk(self, a, b):
   131         if self.chunk:
   132             slef.processChunk(chunk)
   133         self.chunk = { 'src': a, 'dest': b, 'lines': []}
   134 
   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)
   141 
   142     def addChunkLine(line):
   143         self.chunk['lines'].append(line)
   144 
   145     
   146     def processLine(self, line):
   147         if not(len(line)):
   148             pass
   149         elif line.startswith('+++ '):
   150             dest = stripInput('b', line.split()[1])
   151             if mode(self.chunk) == 'remove' and dest == '/dev/null':
   152                 pass
   153             else:
   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':
   158                 pass
   159             else:
   160                 self.setChunkProperties({ 'src': src })
   161         elif chunkLine(line):
   162             self.addChunkLine(line)
   163         else:
   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' })
   172                 else:
   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
   180                 pass
   181             else:
   182                 error('unknown command: ' + string.join(command, ' '))
   183 
   184 
   185 # Create a parser
   186 parser = Parser()
   187 # parse the lines from stdin
   188 for line in  sys.stdin.read().split('\n'):
   189     parser.processLine(line)
   190 
   191 # Make sure we get the last chunk:
   192 parser.newChunk()
   193