comparison mercurial/patch.py @ 2860:0f08f2c042ec

Move patch-related code into its own module.
author Brendan Cully <brendan@kublai.com>
date Fri, 11 Aug 2006 15:50:16 -0700
parents
children f3c68e0ca37d
comparison
equal deleted inserted replaced
2859:b3d1145ed06c 2860:0f08f2c042ec
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