comparison mercurial/patch.py @ 2864:71e78f2ca5ae

merge git patch code.
author Vadim Gelfer <vadim.gelfer@gmail.com>
date Sat, 12 Aug 2006 12:47:18 -0700
parents f3c68e0ca37d
children 2893e51407a4
comparison
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