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