mercurial/patch.py
changeset 2864 71e78f2ca5ae
parent 2862 f3c68e0ca37d
child 2865 2893e51407a4
equal deleted inserted replaced
2858:345bac2bc4ec 2864:71e78f2ca5ae
       
     1 # patch.py - patch file parsing routines
       
     2 #
       
     3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
       
     4 #
       
     5 # This software may be used and distributed according to the terms
       
     6 # of the GNU General Public License, incorporated herein by reference.
       
     7 
       
     8 from demandload import demandload
       
     9 demandload(globals(), "util")
       
    10 demandload(globals(), "os re shutil tempfile")
       
    11 
       
    12 def readgitpatch(patchname):
       
    13     """extract git-style metadata about patches from <patchname>"""
       
    14     class gitpatch:
       
    15         "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
       
    16         def __init__(self, path):
       
    17             self.path = path
       
    18             self.oldpath = None
       
    19             self.mode = None
       
    20             self.op = 'MODIFY'
       
    21             self.copymod = False
       
    22             self.lineno = 0
       
    23     
       
    24     # Filter patch for git information
       
    25     gitre = re.compile('diff --git a/(.*) b/(.*)')
       
    26     pf = file(patchname)
       
    27     gp = None
       
    28     gitpatches = []
       
    29     # Can have a git patch with only metadata, causing patch to complain
       
    30     dopatch = False
       
    31 
       
    32     lineno = 0
       
    33     for line in pf:
       
    34         lineno += 1
       
    35         if line.startswith('diff --git'):
       
    36             m = gitre.match(line)
       
    37             if m:
       
    38                 if gp:
       
    39                     gitpatches.append(gp)
       
    40                 src, dst = m.group(1,2)
       
    41                 gp = gitpatch(dst)
       
    42                 gp.lineno = lineno
       
    43         elif gp:
       
    44             if line.startswith('--- '):
       
    45                 if gp.op in ('COPY', 'RENAME'):
       
    46                     gp.copymod = True
       
    47                     dopatch = 'filter'
       
    48                 gitpatches.append(gp)
       
    49                 gp = None
       
    50                 if not dopatch:
       
    51                     dopatch = True
       
    52                 continue
       
    53             if line.startswith('rename from '):
       
    54                 gp.op = 'RENAME'
       
    55                 gp.oldpath = line[12:].rstrip()
       
    56             elif line.startswith('rename to '):
       
    57                 gp.path = line[10:].rstrip()
       
    58             elif line.startswith('copy from '):
       
    59                 gp.op = 'COPY'
       
    60                 gp.oldpath = line[10:].rstrip()
       
    61             elif line.startswith('copy to '):
       
    62                 gp.path = line[8:].rstrip()
       
    63             elif line.startswith('deleted file'):
       
    64                 gp.op = 'DELETE'
       
    65             elif line.startswith('new file mode '):
       
    66                 gp.op = 'ADD'
       
    67                 gp.mode = int(line.rstrip()[-3:], 8)
       
    68             elif line.startswith('new mode '):
       
    69                 gp.mode = int(line.rstrip()[-3:], 8)
       
    70     if gp:
       
    71         gitpatches.append(gp)
       
    72 
       
    73     if not gitpatches:
       
    74         dopatch = True
       
    75 
       
    76     return (dopatch, gitpatches)
       
    77 
       
    78 def dogitpatch(patchname, gitpatches):
       
    79     """Preprocess git patch so that vanilla patch can handle it"""
       
    80     pf = file(patchname)
       
    81     pfline = 1
       
    82 
       
    83     fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
       
    84     tmpfp = os.fdopen(fd, 'w')
       
    85 
       
    86     try:
       
    87         for i in range(len(gitpatches)):
       
    88             p = gitpatches[i]
       
    89             if not p.copymod:
       
    90                 continue
       
    91 
       
    92             if os.path.exists(p.path):
       
    93                 raise util.Abort(_("cannot create %s: destination already exists") %
       
    94                             p.path)
       
    95 
       
    96             (src, dst) = [os.path.join(os.getcwd(), n)
       
    97                           for n in (p.oldpath, p.path)]
       
    98 
       
    99             targetdir = os.path.dirname(dst)
       
   100             if not os.path.isdir(targetdir):
       
   101                 os.makedirs(targetdir)
       
   102             try:
       
   103                 shutil.copyfile(src, dst)
       
   104                 shutil.copymode(src, dst)
       
   105             except shutil.Error, inst:
       
   106                 raise util.Abort(str(inst))
       
   107 
       
   108             # rewrite patch hunk
       
   109             while pfline < p.lineno:
       
   110                 tmpfp.write(pf.readline())
       
   111                 pfline += 1
       
   112             tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
       
   113             line = pf.readline()
       
   114             pfline += 1
       
   115             while not line.startswith('--- a/'):
       
   116                 tmpfp.write(line)
       
   117                 line = pf.readline()
       
   118                 pfline += 1
       
   119             tmpfp.write('--- a/%s\n' % p.path)
       
   120 
       
   121         line = pf.readline()
       
   122         while line:
       
   123             tmpfp.write(line)
       
   124             line = pf.readline()
       
   125     except:
       
   126         tmpfp.close()
       
   127         os.unlink(patchname)
       
   128         raise
       
   129 
       
   130     tmpfp.close()
       
   131     return patchname
       
   132 
       
   133 def patch(strip, patchname, ui, cwd=None):
       
   134     """apply the patch <patchname> to the working directory.
       
   135     a list of patched files is returned"""
       
   136 
       
   137     (dopatch, gitpatches) = readgitpatch(patchname)
       
   138 
       
   139     files = {}
       
   140     if dopatch:
       
   141         if dopatch == 'filter':
       
   142             patchname = dogitpatch(patchname, gitpatches)
       
   143         patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''), 'patch')
       
   144         args = []
       
   145         if cwd:
       
   146             args.append('-d %s' % util.shellquote(cwd))
       
   147         fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
       
   148                                            util.shellquote(patchname)))
       
   149 
       
   150         if dopatch == 'filter':
       
   151             False and os.unlink(patchname)
       
   152 
       
   153         for line in fp:
       
   154             line = line.rstrip()
       
   155             ui.status("%s\n" % line)
       
   156             if line.startswith('patching file '):
       
   157                 pf = util.parse_patch_output(line)
       
   158                 files.setdefault(pf, (None, None))
       
   159         code = fp.close()
       
   160         if code:
       
   161             raise util.Abort(_("patch command failed: %s") % explain_exit(code)[0])
       
   162 
       
   163     for gp in gitpatches:
       
   164         files[gp.path] = (gp.op, gp)
       
   165 
       
   166     return files