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
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.
david@0
     1
#!/usr/bin/python
david@0
     2
david@0
     3
# Apply a git diff to a subversion repository directly. Its possible
david@0
     4
# to do a similar thing using git-svn, but for someone who doesn't
david@0
     5
# want the pain or learning git, this is a lot easier.
david@0
     6
# This could easily be adapted to any version control system that
david@0
     7
# supports rename.
david@0
     8
#
david@0
     9
# This could probably be made much faster if it
david@0
    10
# used the svn library, instead of the command line - its really
david@0
    11
# very slow.
david@0
    12
david@0
    13
import sys
david@0
    14
import string
david@0
    15
from popen2 import popen3
david@0
    16
import os
david@0
    17
from os import path
david@0
    18
from optparse import OptionParser
david@0
    19
david@0
    20
optionsParser = OptionParser()
david@0
    21
optionsParser.add_option("-p", "--strip", dest="strip",
david@0
    22
                  help="""Like patch -p: Strip the smallest prefix
david@0
    23
                  containing num leading slashes  from  each
david@0
    24
                  file  name found in the patch file""",
david@0
    25
                  metavar="STRIP")
david@0
    26
david@0
    27
(options, args) = optionsParser.parse_args()
david@0
    28
david@0
    29
if options.has_key("strip"):
david@0
    30
    STRIP = int(options["strip"]) + 1
david@0
    31
else:
david@0
    32
    STRIP = 1
david@0
    33
david@0
    34
david@0
    35
david@0
    36
def error(msg):
david@0
    37
    print 'Error: %s' % msg
david@0
    38
david@0
    39
david@0
    40
def mode(chunk):
david@0
    41
    if chunk and chunk.has_key('mode'):
david@0
    42
        return chunk['mode']
david@0
    43
    else:
david@0
    44
        return "default"
david@0
    45
david@0
    46
###############################################################
david@0
    47
# Chunk processing
david@0
    48
david@0
    49
# How we handle the various types of chunk
david@0
    50
modes = {
david@0
    51
    "remove": { "pre"  : [ ],
david@0
    52
                "patch": [ "--- a/%(src)s",
david@0
    53
                           "+++ /dev/null" ],
david@0
    54
                "post" : [ "svn rm --non-interactive %(src)s" ] },
david@0
    55
    "add"   : { "pre"  : [ ],
david@0
    56
                "patch": [ "--- /dev/null",
david@0
    57
                           "+++ b/%(dest)s" ],
david@0
    58
                "post" : [ "svn add %(dest)s" ] },
david@0
    59
    "rename": { "pre"  : [ "svn rename --non-interactive %(src)s %(dest)s" ],
david@0
    60
                "patch": [  "--- a/%(dest)s",
david@0
    61
                            "+++ b/%(dest)s" ],
david@0
    62
                "post" : [ ] },
david@0
    63
    "default":{ "pre"  : [ ],
david@0
    64
                "patch": [  "--- a/%(src)s",
david@0
    65
                            "+++ b/%(src)s" ],
david@0
    66
                "post" : [ ] }
david@0
    67
    }
david@0
    68
david@0
    69
# Execute external commands
david@0
    70
def system(cmd):
david@0
    71
    # make sure any output happens in a consistent order
david@0
    72
    sys.stdout.write(cmd + '\n')
david@0
    73
    sys.stdout.flush()
david@0
    74
    sys.stderr.flush()
david@0
    75
    os.system(cmd)
david@0
    76
david@0
    77
# Svn expects all parent directories to exist before a
david@0
    78
# rename. Hg for example doesn't need this at all
david@0
    79
def svnmakedirs(dest):
david@0
    80
     destdir = path.dirname(dest)
david@0
    81
     if len(destdir) and not(path.exists(destdir)):
david@0
    82
         svnmakedirs(destdir)
david@0
    83
         os.mkdir(destdir)
david@0
    84
         system("svn add %s" % destdir)
david@0
    85
david@0
    86
# Process a chunk. A chunk is actually a dictionary of properties we
david@0
    87
# determined during parsing:
david@0
    88
#  mode: ( remove | add | rename | None ) None := default
david@0
    89
#  src: the src file (or '/dev/null')
david@0
    90
#  dest: the dest file (or '/dev/null')
david@0
    91
#  lines: these are the lines of the patch
david@0
    92
def processChunk(chunk):
david@0
    93
    svnmakedirs(chunk['dest'])
david@0
    94
david@0
    95
    for command in modes[mode(chunk)]["pre"]:
david@0
    96
        system(command % chunk)
david@0
    97
    
david@0
    98
    if len(chunk['lines']):
david@0
    99
        sys.stdout.write("patch -p1\n")
david@0
   100
        sys.stdout.writelines((line % chunk) + '\n' for line in modes[mode(chunk)]["patch"])
david@0
   101
        (stdout, stdin, stderr) = popen3("patch -p1")
david@0
   102
        stdin.writelines((line % chunk) + '\n' for line in modes[mode(chunk)]["patch"])
david@0
   103
        stdin.writelines(line + '\n' for line in chunk['lines'])
david@0
   104
        stdin.close()
david@0
   105
        sys.stdout.write(stdout.read())
david@0
   106
        sys.stderr.write(stderr.read())
david@0
   107
        stdout.close()
david@0
   108
        stderr.close()
david@0
   109
david@0
   110
    for command in modes[mode(chunk)]["post"]:
david@0
   111
        system(command % chunk)
david@0
   112
david@0
   113
def stripInput(pre, path):
david@0
   114
    if path.startswith('/'):
david@0
   115
        return path
david@0
   116
    elif len(path.split('/')) > STRIP:
david@0
   117
        return string.join(path.split('/')[STRIP:], '/')
david@0
   118
    else:
david@0
   119
        return path
david@0
   120
david@0
   121
def chunkLine(line):
david@0
   122
    return [ '\\', '@', '+', '-', ' ' ].count(line[0])
david@0
   123
david@0
   124
# The parser, which processes lines, and when it has a chunk, calling
david@0
   125
# processChunk above
david@0
   126
class Parser:
david@0
   127
    def __init__(self):
david@0
   128
        self.chunk = None
david@0
   129
    
david@0
   130
    def newChunk(self, a, b):
david@0
   131
        if self.chunk:
david@0
   132
            slef.processChunk(chunk)
david@0
   133
        self.chunk = { 'src': a, 'dest': b, 'lines': []}
david@0
   134
david@0
   135
    def setChunkProperties(props):
david@0
   136
        for key in props.keys():
david@0
   137
            if self.chunk.has_key(key) and props[key] != self.chunk[key]:
david@0
   138
                error('Values for %s dont match: %s expected %s in chunk %s' %
david@0
   139
                      (key, props[key], chunk[key], chunk['src']))
david@0
   140
            self.chunk.update(props)
david@0
   141
david@0
   142
    def addChunkLine(line):
david@0
   143
        self.chunk['lines'].append(line)
david@0
   144
david@0
   145
    
david@0
   146
    def processLine(self, line):
david@0
   147
        if not(len(line)):
david@0
   148
            pass
david@0
   149
        elif line.startswith('+++ '):
david@0
   150
            dest = stripInput('b', line.split()[1])
david@0
   151
            if mode(self.chunk) == 'remove' and dest == '/dev/null':
david@0
   152
                pass
david@0
   153
            else:
david@0
   154
                self.setChunkProperties({ 'dest': dest })
david@0
   155
        elif line.startswith('--- '):
david@0
   156
            src = stripInput('a', line.split()[1])
david@0
   157
            if mode(self.chunk) == 'add' and src == '/dev/null':
david@0
   158
                pass
david@0
   159
            else:
david@0
   160
                self.setChunkProperties({ 'src': src })
david@0
   161
        elif chunkLine(line):
david@0
   162
            self.addChunkLine(line)
david@0
   163
        else:
david@0
   164
            command = line.split()
david@0
   165
            if command[0] == 'diff':
david@0
   166
                self.newChunk(stripInput('a', command[2]), stripInput('b', command[3]))
david@0
   167
            elif command[0] == 'rename':
david@0
   168
                if command[1] == 'from':
david@0
   169
                    self.setChunkProperties({'src': command[2], 'mode': 'rename' })
david@0
   170
                elif command[1] == 'to':
david@0
   171
                    self.setChunkProperties({'dest': command[2], 'mode': 'rename' })
david@0
   172
                else:
david@0
   173
                    error('invalid rename: ' + string.join(command, ' '))
david@0
   174
            elif command[0] == 'new':
david@0
   175
                self.setChunkProperties({'mode': 'add' })
david@0
   176
            elif command[0] == 'deleted':
david@0
   177
                self.setChunkProperties({'mode': 'remove'})
david@0
   178
            elif command[0] == 'index':
david@0
   179
                # don't know what this does, but git diff emits it
david@0
   180
                pass
david@0
   181
            else:
david@0
   182
                error('unknown command: ' + string.join(command, ' '))
david@0
   183
david@0
   184
david@0
   185
# Create a parser
david@0
   186
parser = Parser()
david@0
   187
# parse the lines from stdin
david@0
   188
for line in  sys.stdin.read().split('\n'):
david@0
   189
    parser.processLine(line)
david@0
   190
david@0
   191
# Make sure we get the last chunk:
david@0
   192
parser.newChunk()
david@0
   193