##// END OF EJS Templates
Add Chris Mason's mpatch library....
Bryan O'Sullivan -
r4897:4574925d default
parent child Browse files
Show More
@@ -0,0 +1,150
1 /*
2 * diffhelpers.c - helper routines for mpatch
3 *
4 * Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 *
6 * This software may be used and distributed according to the terms
7 * of the GNU General Public License v2, incorporated herein by reference.
8 */
9
10 #include <Python.h>
11 #include <stdlib.h>
12 #include <string.h>
13
14 static char diffhelpers_doc[] = "Efficient diff parsing";
15 static PyObject *diffhelpers_Error;
16
17
18 /* fixup the last lines of a and b when the patch has no newline at eof */
19 static void _fix_newline(PyObject *hunk, PyObject *a, PyObject *b)
20 {
21 int hunksz = PyList_Size(hunk);
22 PyObject *s = PyList_GET_ITEM(hunk, hunksz-1);
23 char *l = PyString_AS_STRING(s);
24 int sz = PyString_GET_SIZE(s);
25 int alen = PyList_Size(a);
26 int blen = PyList_Size(b);
27 char c = l[0];
28
29 PyObject *hline = PyString_FromStringAndSize(l, sz-1);
30 if (c == ' ' || c == '+') {
31 PyObject *rline = PyString_FromStringAndSize(l+1, sz-2);
32 PyList_SetItem(b, blen-1, rline);
33 }
34 if (c == ' ' || c == '-') {
35 Py_INCREF(hline);
36 PyList_SetItem(a, alen-1, hline);
37 }
38 PyList_SetItem(hunk, hunksz-1, hline);
39 }
40
41 /* python callable form of _fix_newline */
42 static PyObject *
43 fix_newline(PyObject *self, PyObject *args)
44 {
45 PyObject *hunk, *a, *b;
46 if (!PyArg_ParseTuple(args, "OOO", &hunk, &a, &b))
47 return NULL;
48 _fix_newline(hunk, a, b);
49 return Py_BuildValue("l", 0);
50 }
51
52 /*
53 * read lines from fp into the hunk. The hunk is parsed into two arrays
54 * a and b. a gets the old state of the text, b gets the new state
55 * The control char from the hunk is saved when inserting into a, but not b
56 * (for performance while deleting files)
57 */
58 static PyObject *
59 addlines(PyObject *self, PyObject *args)
60 {
61
62 PyObject *fp, *hunk, *a, *b, *x;
63 int i;
64 int lena, lenb;
65 int num;
66 int todoa, todob;
67 char *s, c;
68 PyObject *l;
69 if (!PyArg_ParseTuple(args, "OOiiOO", &fp, &hunk, &lena, &lenb, &a, &b))
70 return NULL;
71
72 while(1) {
73 todoa = lena - PyList_Size(a);
74 todob = lenb - PyList_Size(b);
75 num = todoa > todob ? todoa : todob;
76 if (num == 0)
77 break;
78 for (i = 0 ; i < num ; i++) {
79 x = PyFile_GetLine(fp, 0);
80 s = PyString_AS_STRING(x);
81 c = *s;
82 if (strcmp(s, "\\ No newline at end of file\n") == 0) {
83 _fix_newline(hunk, a, b);
84 continue;
85 }
86 PyList_Append(hunk, x);
87 if (c == '+') {
88 l = PyString_FromString(s + 1);
89 PyList_Append(b, l);
90 Py_DECREF(l);
91 } else if (c == '-') {
92 PyList_Append(a, x);
93 } else {
94 l = PyString_FromString(s + 1);
95 PyList_Append(b, l);
96 Py_DECREF(l);
97 PyList_Append(a, x);
98 }
99 Py_DECREF(x);
100 }
101 }
102 return Py_BuildValue("l", 0);
103 }
104
105 /*
106 * compare the lines in a with the lines in b. a is assumed to have
107 * a control char at the start of each line, this char is ignored in the
108 * compare
109 */
110 static PyObject *
111 testhunk(PyObject *self, PyObject *args)
112 {
113
114 PyObject *a, *b;
115 long bstart;
116 int alen, blen;
117 int i;
118 char *sa, *sb;
119
120 if (!PyArg_ParseTuple(args, "OOl", &a, &b, &bstart))
121 return NULL;
122 alen = PyList_Size(a);
123 blen = PyList_Size(b);
124 if (alen > blen - bstart) {
125 return Py_BuildValue("l", -1);
126 }
127 for (i = 0 ; i < alen ; i++) {
128 sa = PyString_AS_STRING(PyList_GET_ITEM(a, i));
129 sb = PyString_AS_STRING(PyList_GET_ITEM(b, i + bstart));
130 if (strcmp(sa+1, sb) != 0)
131 return Py_BuildValue("l", -1);
132 }
133 return Py_BuildValue("l", 0);
134 }
135
136 static PyMethodDef methods[] = {
137 {"addlines", addlines, METH_VARARGS, "add lines to a hunk\n"},
138 {"fix_newline", fix_newline, METH_VARARGS, "fixup newline counters\n"},
139 {"testhunk", testhunk, METH_VARARGS, "test lines in a hunk\n"},
140 {NULL, NULL}
141 };
142
143 PyMODINIT_FUNC
144 initdiffhelpers(void)
145 {
146 Py_InitModule3("diffhelpers", methods, diffhelpers_doc);
147 diffhelpers_Error = PyErr_NewException("diffhelpers.diffhelpersError",
148 NULL, NULL);
149 }
150
This diff has been collapsed as it changes many lines, (848 lines changed) Show them Hide them
@@ -1,675 +1,1251
1 # patch.py - patch file parsing routines
1 # patch.py - patch file parsing routines
2 #
2 #
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
4 #
5 #
5 # This software may be used and distributed according to the terms
6 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
7
8
8 from i18n import _
9 from i18n import _
9 from node import *
10 from node import *
10 import base85, cmdutil, mdiff, util, context, revlog
11 import base85, cmdutil, mdiff, util, context, revlog, diffhelpers
11 import cStringIO, email.Parser, os, popen2, re, sha
12 import cStringIO, email.Parser, os, popen2, re, sha
12 import sys, tempfile, zlib
13 import sys, tempfile, zlib
13
14
15 class PatchError(Exception):
16 pass
17
14 # helper functions
18 # helper functions
15
19
16 def copyfile(src, dst, basedir=None):
20 def copyfile(src, dst, basedir=None):
17 if not basedir:
21 if not basedir:
18 basedir = os.getcwd()
22 basedir = os.getcwd()
19
23
20 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
24 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
21 if os.path.exists(absdst):
25 if os.path.exists(absdst):
22 raise util.Abort(_("cannot create %s: destination already exists") %
26 raise util.Abort(_("cannot create %s: destination already exists") %
23 dst)
27 dst)
24
28
25 targetdir = os.path.dirname(absdst)
29 targetdir = os.path.dirname(absdst)
26 if not os.path.isdir(targetdir):
30 if not os.path.isdir(targetdir):
27 os.makedirs(targetdir)
31 os.makedirs(targetdir)
28
32
29 util.copyfile(abssrc, absdst)
33 util.copyfile(abssrc, absdst)
30
34
31 # public functions
35 # public functions
32
36
33 def extract(ui, fileobj):
37 def extract(ui, fileobj):
34 '''extract patch from data read from fileobj.
38 '''extract patch from data read from fileobj.
35
39
36 patch can be a normal patch or contained in an email message.
40 patch can be a normal patch or contained in an email message.
37
41
38 return tuple (filename, message, user, date, node, p1, p2).
42 return tuple (filename, message, user, date, node, p1, p2).
39 Any item in the returned tuple can be None. If filename is None,
43 Any item in the returned tuple can be None. If filename is None,
40 fileobj did not contain a patch. Caller must unlink filename when done.'''
44 fileobj did not contain a patch. Caller must unlink filename when done.'''
41
45
42 # attempt to detect the start of a patch
46 # attempt to detect the start of a patch
43 # (this heuristic is borrowed from quilt)
47 # (this heuristic is borrowed from quilt)
44 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
48 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
45 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
49 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
46 '(---|\*\*\*)[ \t])', re.MULTILINE)
50 '(---|\*\*\*)[ \t])', re.MULTILINE)
47
51
48 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
52 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
49 tmpfp = os.fdopen(fd, 'w')
53 tmpfp = os.fdopen(fd, 'w')
50 try:
54 try:
51 msg = email.Parser.Parser().parse(fileobj)
55 msg = email.Parser.Parser().parse(fileobj)
52
56
53 subject = msg['Subject']
57 subject = msg['Subject']
54 user = msg['From']
58 user = msg['From']
55 # should try to parse msg['Date']
59 # should try to parse msg['Date']
56 date = None
60 date = None
57 nodeid = None
61 nodeid = None
58 branch = None
62 branch = None
59 parents = []
63 parents = []
60
64
61 if subject:
65 if subject:
62 if subject.startswith('[PATCH'):
66 if subject.startswith('[PATCH'):
63 pend = subject.find(']')
67 pend = subject.find(']')
64 if pend >= 0:
68 if pend >= 0:
65 subject = subject[pend+1:].lstrip()
69 subject = subject[pend+1:].lstrip()
66 subject = subject.replace('\n\t', ' ')
70 subject = subject.replace('\n\t', ' ')
67 ui.debug('Subject: %s\n' % subject)
71 ui.debug('Subject: %s\n' % subject)
68 if user:
72 if user:
69 ui.debug('From: %s\n' % user)
73 ui.debug('From: %s\n' % user)
70 diffs_seen = 0
74 diffs_seen = 0
71 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
75 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
72
76
73 for part in msg.walk():
77 for part in msg.walk():
74 content_type = part.get_content_type()
78 content_type = part.get_content_type()
75 ui.debug('Content-Type: %s\n' % content_type)
79 ui.debug('Content-Type: %s\n' % content_type)
76 if content_type not in ok_types:
80 if content_type not in ok_types:
77 continue
81 continue
78 payload = part.get_payload(decode=True)
82 payload = part.get_payload(decode=True)
79 m = diffre.search(payload)
83 m = diffre.search(payload)
80 if m:
84 if m:
81 hgpatch = False
85 hgpatch = False
82 ignoretext = False
86 ignoretext = False
83
87
84 ui.debug(_('found patch at byte %d\n') % m.start(0))
88 ui.debug(_('found patch at byte %d\n') % m.start(0))
85 diffs_seen += 1
89 diffs_seen += 1
86 cfp = cStringIO.StringIO()
90 cfp = cStringIO.StringIO()
87 for line in payload[:m.start(0)].splitlines():
91 for line in payload[:m.start(0)].splitlines():
88 if line.startswith('# HG changeset patch'):
92 if line.startswith('# HG changeset patch'):
89 ui.debug(_('patch generated by hg export\n'))
93 ui.debug(_('patch generated by hg export\n'))
90 hgpatch = True
94 hgpatch = True
91 # drop earlier commit message content
95 # drop earlier commit message content
92 cfp.seek(0)
96 cfp.seek(0)
93 cfp.truncate()
97 cfp.truncate()
94 subject = None
98 subject = None
95 elif hgpatch:
99 elif hgpatch:
96 if line.startswith('# User '):
100 if line.startswith('# User '):
97 user = line[7:]
101 user = line[7:]
98 ui.debug('From: %s\n' % user)
102 ui.debug('From: %s\n' % user)
99 elif line.startswith("# Date "):
103 elif line.startswith("# Date "):
100 date = line[7:]
104 date = line[7:]
101 elif line.startswith("# Branch "):
105 elif line.startswith("# Branch "):
102 branch = line[9:]
106 branch = line[9:]
103 elif line.startswith("# Node ID "):
107 elif line.startswith("# Node ID "):
104 nodeid = line[10:]
108 nodeid = line[10:]
105 elif line.startswith("# Parent "):
109 elif line.startswith("# Parent "):
106 parents.append(line[10:])
110 parents.append(line[10:])
107 elif line == '---' and 'git-send-email' in msg['X-Mailer']:
111 elif line == '---' and 'git-send-email' in msg['X-Mailer']:
108 ignoretext = True
112 ignoretext = True
109 if not line.startswith('# ') and not ignoretext:
113 if not line.startswith('# ') and not ignoretext:
110 cfp.write(line)
114 cfp.write(line)
111 cfp.write('\n')
115 cfp.write('\n')
112 message = cfp.getvalue()
116 message = cfp.getvalue()
113 if tmpfp:
117 if tmpfp:
114 tmpfp.write(payload)
118 tmpfp.write(payload)
115 if not payload.endswith('\n'):
119 if not payload.endswith('\n'):
116 tmpfp.write('\n')
120 tmpfp.write('\n')
117 elif not diffs_seen and message and content_type == 'text/plain':
121 elif not diffs_seen and message and content_type == 'text/plain':
118 message += '\n' + payload
122 message += '\n' + payload
119 except:
123 except:
120 tmpfp.close()
124 tmpfp.close()
121 os.unlink(tmpname)
125 os.unlink(tmpname)
122 raise
126 raise
123
127
124 if subject and not message.startswith(subject):
128 if subject and not message.startswith(subject):
125 message = '%s\n%s' % (subject, message)
129 message = '%s\n%s' % (subject, message)
126 tmpfp.close()
130 tmpfp.close()
127 if not diffs_seen:
131 if not diffs_seen:
128 os.unlink(tmpname)
132 os.unlink(tmpname)
129 return None, message, user, date, branch, None, None, None
133 return None, message, user, date, branch, None, None, None
130 p1 = parents and parents.pop(0) or None
134 p1 = parents and parents.pop(0) or None
131 p2 = parents and parents.pop(0) or None
135 p2 = parents and parents.pop(0) or None
132 return tmpname, message, user, date, branch, nodeid, p1, p2
136 return tmpname, message, user, date, branch, nodeid, p1, p2
133
137
134 GP_PATCH = 1 << 0 # we have to run patch
138 GP_PATCH = 1 << 0 # we have to run patch
135 GP_FILTER = 1 << 1 # there's some copy/rename operation
139 GP_FILTER = 1 << 1 # there's some copy/rename operation
136 GP_BINARY = 1 << 2 # there's a binary patch
140 GP_BINARY = 1 << 2 # there's a binary patch
137
141
138 def readgitpatch(patchname):
142 def readgitpatch(fp, firstline):
139 """extract git-style metadata about patches from <patchname>"""
143 """extract git-style metadata about patches from <patchname>"""
140 class gitpatch:
144 class gitpatch:
141 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
145 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
142 def __init__(self, path):
146 def __init__(self, path):
143 self.path = path
147 self.path = path
144 self.oldpath = None
148 self.oldpath = None
145 self.mode = None
149 self.mode = None
146 self.op = 'MODIFY'
150 self.op = 'MODIFY'
147 self.copymod = False
151 self.copymod = False
148 self.lineno = 0
152 self.lineno = 0
149 self.binary = False
153 self.binary = False
150
154
155 def reader(fp, firstline):
156 yield firstline
157 for line in fp:
158 yield line
159
151 # Filter patch for git information
160 # Filter patch for git information
152 gitre = re.compile('diff --git a/(.*) b/(.*)')
161 gitre = re.compile('diff --git a/(.*) b/(.*)')
153 pf = file(patchname)
154 gp = None
162 gp = None
155 gitpatches = []
163 gitpatches = []
156 # Can have a git patch with only metadata, causing patch to complain
164 # Can have a git patch with only metadata, causing patch to complain
157 dopatch = 0
165 dopatch = 0
158
166
159 lineno = 0
167 lineno = 0
160 for line in pf:
168 for line in reader(fp, firstline):
161 lineno += 1
169 lineno += 1
162 if line.startswith('diff --git'):
170 if line.startswith('diff --git'):
163 m = gitre.match(line)
171 m = gitre.match(line)
164 if m:
172 if m:
165 if gp:
173 if gp:
166 gitpatches.append(gp)
174 gitpatches.append(gp)
167 src, dst = m.group(1, 2)
175 src, dst = m.group(1, 2)
168 gp = gitpatch(dst)
176 gp = gitpatch(dst)
169 gp.lineno = lineno
177 gp.lineno = lineno
170 elif gp:
178 elif gp:
171 if line.startswith('--- '):
179 if line.startswith('--- '):
172 if gp.op in ('COPY', 'RENAME'):
180 if gp.op in ('COPY', 'RENAME'):
173 gp.copymod = True
181 gp.copymod = True
174 dopatch |= GP_FILTER
182 dopatch |= GP_FILTER
175 gitpatches.append(gp)
183 gitpatches.append(gp)
176 gp = None
184 gp = None
177 dopatch |= GP_PATCH
185 dopatch |= GP_PATCH
178 continue
186 continue
179 if line.startswith('rename from '):
187 if line.startswith('rename from '):
180 gp.op = 'RENAME'
188 gp.op = 'RENAME'
181 gp.oldpath = line[12:].rstrip()
189 gp.oldpath = line[12:].rstrip()
182 elif line.startswith('rename to '):
190 elif line.startswith('rename to '):
183 gp.path = line[10:].rstrip()
191 gp.path = line[10:].rstrip()
184 elif line.startswith('copy from '):
192 elif line.startswith('copy from '):
185 gp.op = 'COPY'
193 gp.op = 'COPY'
186 gp.oldpath = line[10:].rstrip()
194 gp.oldpath = line[10:].rstrip()
187 elif line.startswith('copy to '):
195 elif line.startswith('copy to '):
188 gp.path = line[8:].rstrip()
196 gp.path = line[8:].rstrip()
189 elif line.startswith('deleted file'):
197 elif line.startswith('deleted file'):
190 gp.op = 'DELETE'
198 gp.op = 'DELETE'
191 elif line.startswith('new file mode '):
199 elif line.startswith('new file mode '):
192 gp.op = 'ADD'
200 gp.op = 'ADD'
193 gp.mode = int(line.rstrip()[-3:], 8)
201 gp.mode = int(line.rstrip()[-3:], 8)
194 elif line.startswith('new mode '):
202 elif line.startswith('new mode '):
195 gp.mode = int(line.rstrip()[-3:], 8)
203 gp.mode = int(line.rstrip()[-3:], 8)
196 elif line.startswith('GIT binary patch'):
204 elif line.startswith('GIT binary patch'):
197 dopatch |= GP_BINARY
205 dopatch |= GP_BINARY
198 gp.binary = True
206 gp.binary = True
199 if gp:
207 if gp:
200 gitpatches.append(gp)
208 gitpatches.append(gp)
201
209
202 if not gitpatches:
210 if not gitpatches:
203 dopatch = GP_PATCH
211 dopatch = GP_PATCH
204
212
205 return (dopatch, gitpatches)
213 return (dopatch, gitpatches)
206
214
207 def dogitpatch(patchname, gitpatches, cwd=None):
215 def patch(patchname, ui, strip=1, cwd=None, files={}):
208 """Preprocess git patch so that vanilla patch can handle it"""
216 """apply the patch <patchname> to the working directory.
209 def extractbin(fp):
217 a list of patched files is returned"""
210 i = [0] # yuck
218 fp = file(patchname)
211 def readline():
219 fuzz = False
212 i[0] += 1
220 if cwd:
213 return fp.readline().rstrip()
221 curdir = os.getcwd()
214 line = readline()
222 os.chdir(cwd)
223 try:
224 ret = applydiff(ui, fp, files, strip=strip)
225 except PatchError:
226 raise util.Abort(_("patch failed to apply"))
227 if cwd:
228 os.chdir(curdir)
229 if ret < 0:
230 raise util.Abort(_("patch failed to apply"))
231 if ret > 0:
232 fuzz = True
233 return fuzz
234
235 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
236 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
237 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
238
239 class patchfile:
240 def __init__(self, ui, fname):
241 self.fname = fname
242 self.ui = ui
243 try:
244 fp = file(fname, 'r')
245 self.lines = fp.readlines()
246 self.exists = True
247 except IOError:
248 dirname = os.path.dirname(fname)
249 if dirname and not os.path.isdir(dirname):
250 dirs = dirname.split(os.path.sep)
251 d = ""
252 for x in dirs:
253 d = os.path.join(d, x)
254 if not os.path.isdir(d):
255 os.mkdir(d)
256 self.lines = []
257 self.exists = False
258
259 self.hash = {}
260 self.dirty = 0
261 self.offset = 0
262 self.rej = []
263 self.fileprinted = False
264 self.printfile(False)
265 self.hunks = 0
266
267 def printfile(self, warn):
268 if self.fileprinted:
269 return
270 if warn or self.ui.verbose:
271 self.fileprinted = True
272 s = _("patching file %s\n" % self.fname)
273 if warn:
274 self.ui.warn(s)
275 else:
276 self.ui.note(s)
277
278
279 def findlines(self, l, linenum):
280 # looks through the hash and finds candidate lines. The
281 # result is a list of line numbers sorted based on distance
282 # from linenum
283 def sorter(a, b):
284 vala = abs(a - linenum)
285 valb = abs(b - linenum)
286 return cmp(vala, valb)
287
288 try:
289 cand = self.hash[l]
290 except:
291 return []
292
293 if len(cand) > 1:
294 # resort our list of potentials forward then back.
295 cand.sort(cmp=sorter)
296 return cand
297
298 def hashlines(self):
299 self.hash = {}
300 for x in xrange(len(self.lines)):
301 s = self.lines[x]
302 self.hash.setdefault(s, []).append(x)
303
304 def write_rej(self):
305 # our rejects are a little different from patch(1). This always
306 # creates rejects in the same form as the original patch. A file
307 # header is inserted so that you can run the reject through patch again
308 # without having to type the filename.
309
310 if not self.rej:
311 return
312 if self.hunks != 1:
313 hunkstr = "s"
314 else:
315 hunkstr = ""
316
317 fname = self.fname + ".rej"
318 self.ui.warn(
319 _("%d out of %d hunk%s FAILED -- saving rejects to file %s\n" %
320 (len(self.rej), self.hunks, hunkstr, fname)))
321 try: os.unlink(fname)
322 except:
323 pass
324 fp = file(fname, 'w')
325 base = os.path.basename(self.fname)
326 fp.write("--- %s\n+++ %s\n" % (base, base))
327 for x in self.rej:
328 for l in x.hunk:
329 fp.write(l)
330 if l[-1] != '\n':
331 fp.write("\n\ No newline at end of file\n")
332
333 def write(self, dest=None):
334 if self.dirty:
335 if not dest:
336 dest = self.fname
337 st = None
338 try:
339 st = os.lstat(dest)
340 if st.st_nlink > 1:
341 os.unlink(dest)
342 except: pass
343 fp = file(dest, 'w')
344 if st:
345 os.chmod(dest, st.st_mode)
346 fp.writelines(self.lines)
347 fp.close()
348
349 def close(self):
350 self.write()
351 self.write_rej()
352
353 def apply(self, h, reverse):
354 if not h.complete():
355 raise PatchError("bad hunk #%d %s (%d %d %d %d)" %
356 (h.number, h.desc, len(h.a), h.lena, len(h.b),
357 h.lenb))
358
359 self.hunks += 1
360 if reverse:
361 h.reverse()
362
363 if self.exists and h.createfile():
364 self.ui.warn(_("file %s already exists\n" % self.fname))
365 self.rej.append(h)
366 return -1
367
368 if isinstance(h, binhunk):
369 if h.rmfile():
370 os.unlink(self.fname)
371 else:
372 self.lines[:] = h.new()
373 self.offset += len(h.new())
374 self.dirty = 1
375 return 0
376
377 # fast case first, no offsets, no fuzz
378 old = h.old()
379 # patch starts counting at 1 unless we are adding the file
380 if h.starta == 0:
381 start = 0
382 else:
383 start = h.starta + self.offset - 1
384 orig_start = start
385 if diffhelpers.testhunk(old, self.lines, start) == 0:
386 if h.rmfile():
387 os.unlink(self.fname)
388 else:
389 self.lines[start : start + h.lena] = h.new()
390 self.offset += h.lenb - h.lena
391 self.dirty = 1
392 return 0
393
394 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
395 self.hashlines()
396 if h.hunk[-1][0] != ' ':
397 # if the hunk tried to put something at the bottom of the file
398 # override the start line and use eof here
399 search_start = len(self.lines)
400 else:
401 search_start = orig_start
402
403 for fuzzlen in xrange(3):
404 for toponly in [ True, False ]:
405 old = h.old(fuzzlen, toponly)
406
407 cand = self.findlines(old[0][1:], search_start)
408 for l in cand:
409 if diffhelpers.testhunk(old, self.lines, l) == 0:
410 newlines = h.new(fuzzlen, toponly)
411 self.lines[l : l + len(old)] = newlines
412 self.offset += len(newlines) - len(old)
413 self.dirty = 1
414 if fuzzlen:
415 fuzzstr = "with fuzz %d " % fuzzlen
416 f = self.ui.warn
417 self.printfile(True)
418 else:
419 fuzzstr = ""
420 f = self.ui.note
421 offset = l - orig_start - fuzzlen
422 if offset == 1:
423 linestr = "line"
424 else:
425 linestr = "lines"
426 f(_("Hunk #%d succeeded at %d %s(offset %d %s).\n" %
427 (h.number, l+1, fuzzstr, offset, linestr)))
428 return fuzzlen
429 self.printfile(True)
430 self.ui.warn(_("Hunk #%d FAILED at %d\n" % (h.number, orig_start)))
431 self.rej.append(h)
432 return -1
433
434 class hunk:
435 def __init__(self, desc, num, lr, context):
436 self.number = num
437 self.desc = desc
438 self.hunk = [ desc ]
439 self.a = []
440 self.b = []
441 if context:
442 self.read_context_hunk(lr)
443 else:
444 self.read_unified_hunk(lr)
445
446 def read_unified_hunk(self, lr):
447 m = unidesc.match(self.desc)
448 if not m:
449 raise PatchError("bad hunk #%d" % self.number)
450 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
451 if self.lena == None:
452 self.lena = 1
453 else:
454 self.lena = int(self.lena)
455 if self.lenb == None:
456 self.lenb = 1
457 else:
458 self.lenb = int(self.lenb)
459 self.starta = int(self.starta)
460 self.startb = int(self.startb)
461 diffhelpers.addlines(lr.fp, self.hunk, self.lena, self.lenb, self.a, self.b)
462 # if we hit eof before finishing out the hunk, the last line will
463 # be zero length. Lets try to fix it up.
464 while len(self.hunk[-1]) == 0:
465 del self.hunk[-1]
466 del self.a[-1]
467 del self.b[-1]
468 self.lena -= 1
469 self.lenb -= 1
470
471 def read_context_hunk(self, lr):
472 self.desc = lr.readline()
473 m = contextdesc.match(self.desc)
474 if not m:
475 raise PatchError("bad hunk #%d" % self.number)
476 foo, self.starta, foo2, aend, foo3 = m.groups()
477 self.starta = int(self.starta)
478 if aend == None:
479 aend = self.starta
480 self.lena = int(aend) - self.starta
481 if self.starta:
482 self.lena += 1
483 for x in xrange(self.lena):
484 l = lr.readline()
485 if l.startswith('---'):
486 lr.push(l)
487 break
488 s = l[2:]
489 if l.startswith('- ') or l.startswith('! '):
490 u = '-' + s
491 elif l.startswith(' '):
492 u = ' ' + s
493 else:
494 raise PatchError("bad hunk #%d old text line %d" % (self.number, x))
495 self.a.append(u)
496 self.hunk.append(u)
497
498 l = lr.readline()
499 if l.startswith('\ '):
500 s = self.a[-1][:-1]
501 self.a[-1] = s
502 self.hunk[-1] = s
503 l = lr.readline()
504 m = contextdesc.match(l)
505 if not m:
506 raise PatchError("bad hunk #%d" % self.number)
507 foo, self.startb, foo2, bend, foo3 = m.groups()
508 self.startb = int(self.startb)
509 if bend == None:
510 bend = self.startb
511 self.lenb = int(bend) - self.startb
512 if self.startb:
513 self.lenb += 1
514 hunki = 1
515 for x in xrange(self.lenb):
516 l = lr.readline()
517 if l.startswith('\ '):
518 s = self.b[-1][:-1]
519 self.b[-1] = s
520 self.hunk[hunki-1] = s
521 continue
522 if not l:
523 lr.push(l)
524 break
525 s = l[2:]
526 if l.startswith('+ ') or l.startswith('! '):
527 u = '+' + s
528 elif l.startswith(' '):
529 u = ' ' + s
530 elif len(self.b) == 0:
531 # this can happen when the hunk does not add any lines
532 lr.push(l)
533 break
534 else:
535 raise PatchError("bad hunk #%d old text line %d" % (self.number, x))
536 self.b.append(s)
537 while True:
538 if hunki >= len(self.hunk):
539 h = ""
540 else:
541 h = self.hunk[hunki]
542 hunki += 1
543 if h == u:
544 break
545 elif h.startswith('-'):
546 continue
547 else:
548 self.hunk.insert(hunki-1, u)
549 break
550
551 if not self.a:
552 # this happens when lines were only added to the hunk
553 for x in self.hunk:
554 if x.startswith('-') or x.startswith(' '):
555 self.a.append(x)
556 if not self.b:
557 # this happens when lines were only deleted from the hunk
558 for x in self.hunk:
559 if x.startswith('+') or x.startswith(' '):
560 self.b.append(x[1:])
561 # @@ -start,len +start,len @@
562 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
563 self.startb, self.lenb)
564 self.hunk[0] = self.desc
565
566 def reverse(self):
567 origlena = self.lena
568 origstarta = self.starta
569 self.lena = self.lenb
570 self.starta = self.startb
571 self.lenb = origlena
572 self.startb = origstarta
573 self.a = []
574 self.b = []
575 # self.hunk[0] is the @@ description
576 for x in xrange(1, len(self.hunk)):
577 o = self.hunk[x]
578 if o.startswith('-'):
579 n = '+' + o[1:]
580 self.b.append(o[1:])
581 elif o.startswith('+'):
582 n = '-' + o[1:]
583 self.a.append(n)
584 else:
585 n = o
586 self.b.append(o[1:])
587 self.a.append(o)
588 self.hunk[x] = o
589
590 def fix_newline(self):
591 diffhelpers.fix_newline(self.hunk, self.a, self.b)
592
593 def complete(self):
594 return len(self.a) == self.lena and len(self.b) == self.lenb
595
596 def createfile(self):
597 return self.starta == 0 and self.lena == 0
598
599 def rmfile(self):
600 return self.startb == 0 and self.lenb == 0
601
602 def fuzzit(self, l, fuzz, toponly):
603 # this removes context lines from the top and bottom of list 'l'. It
604 # checks the hunk to make sure only context lines are removed, and then
605 # returns a new shortened list of lines.
606 fuzz = min(fuzz, len(l)-1)
607 if fuzz:
608 top = 0
609 bot = 0
610 hlen = len(self.hunk)
611 for x in xrange(hlen-1):
612 # the hunk starts with the @@ line, so use x+1
613 if self.hunk[x+1][0] == ' ':
614 top += 1
615 else:
616 break
617 if not toponly:
618 for x in xrange(hlen-1):
619 if self.hunk[hlen-bot-1][0] == ' ':
620 bot += 1
621 else:
622 break
623
624 # top and bot now count context in the hunk
625 # adjust them if either one is short
626 context = max(top, bot, 3)
627 if bot < context:
628 bot = max(0, fuzz - (context - bot))
629 else:
630 bot = min(fuzz, bot)
631 if top < context:
632 top = max(0, fuzz - (context - top))
633 else:
634 top = min(fuzz, top)
635
636 return l[top:len(l)-bot]
637 return l
638
639 def old(self, fuzz=0, toponly=False):
640 return self.fuzzit(self.a, fuzz, toponly)
641
642 def newctrl(self):
643 res = []
644 for x in self.hunk:
645 c = x[0]
646 if c == ' ' or c == '+':
647 res.append(x)
648 return res
649
650 def new(self, fuzz=0, toponly=False):
651 return self.fuzzit(self.b, fuzz, toponly)
652
653 class binhunk:
654 'A binary patch file. Only understands literals so far.'
655 def __init__(self, gitpatch):
656 self.gitpatch = gitpatch
657 self.text = None
658 self.hunk = ['GIT binary patch\n']
659
660 def createfile(self):
661 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
662
663 def rmfile(self):
664 return self.gitpatch.op == 'DELETE'
665
666 def complete(self):
667 return self.text is not None
668
669 def new(self):
670 return [self.text]
671
672 def extract(self, fp):
673 line = fp.readline()
674 self.hunk.append(line)
215 while line and not line.startswith('literal '):
675 while line and not line.startswith('literal '):
216 line = readline()
676 line = fp.readline()
677 self.hunk.append(line)
217 if not line:
678 if not line:
218 return None, i[0]
679 raise PatchError('could not extract binary patch')
219 size = int(line[8:])
680 size = int(line[8:].rstrip())
220 dec = []
681 dec = []
221 line = readline()
682 line = fp.readline()
222 while line:
683 self.hunk.append(line)
684 while len(line) > 1:
223 l = line[0]
685 l = line[0]
224 if l <= 'Z' and l >= 'A':
686 if l <= 'Z' and l >= 'A':
225 l = ord(l) - ord('A') + 1
687 l = ord(l) - ord('A') + 1
226 else:
688 else:
227 l = ord(l) - ord('a') + 27
689 l = ord(l) - ord('a') + 27
228 dec.append(base85.b85decode(line[1:])[:l])
690 dec.append(base85.b85decode(line[1:-1])[:l])
229 line = readline()
691 line = fp.readline()
692 self.hunk.append(line)
230 text = zlib.decompress(''.join(dec))
693 text = zlib.decompress(''.join(dec))
231 if len(text) != size:
694 if len(text) != size:
232 raise util.Abort(_('binary patch is %d bytes, not %d') %
695 raise PatchError('binary patch is %d bytes, not %d' %
233 (len(text), size))
696 len(text), size)
234 return text, i[0]
697 self.text = text
235
698
236 pf = file(patchname)
699 def parsefilename(str):
237 pfline = 1
700 # --- filename \t|space stuff
238
701 s = str[4:]
239 fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
702 i = s.find('\t')
240 tmpfp = os.fdopen(fd, 'w')
703 if i < 0:
704 i = s.find(' ')
705 if i < 0:
706 return s
707 return s[:i]
241
708
242 try:
709 def selectfile(afile_orig, bfile_orig, hunk, strip, reverse):
243 for i in xrange(len(gitpatches)):
710 def pathstrip(path, count=1):
244 p = gitpatches[i]
711 pathlen = len(path)
245 if not p.copymod and not p.binary:
712 i = 0
246 continue
713 if count == 0:
247
714 return path.rstrip()
248 # rewrite patch hunk
715 while count > 0:
249 while pfline < p.lineno:
716 i = path.find(os.sep, i)
250 tmpfp.write(pf.readline())
717 if i == -1:
251 pfline += 1
718 raise PatchError("Unable to strip away %d dirs from %s" %
719 (count, path))
720 i += 1
721 # consume '//' in the path
722 while i < pathlen - 1 and path[i] == os.sep:
723 i += 1
724 count -= 1
725 return path[i:].rstrip()
252
726
253 if p.binary:
727 nulla = afile_orig == "/dev/null"
254 text, delta = extractbin(pf)
728 nullb = bfile_orig == "/dev/null"
255 if not text:
729 afile = pathstrip(afile_orig, strip)
256 raise util.Abort(_('binary patch extraction failed'))
730 gooda = os.path.exists(afile) and not nulla
257 pfline += delta
731 bfile = pathstrip(bfile_orig, strip)
258 if not cwd:
732 if afile == bfile:
259 cwd = os.getcwd()
733 goodb = gooda
260 absdst = os.path.join(cwd, p.path)
734 else:
261 basedir = os.path.dirname(absdst)
735 goodb = os.path.exists(bfile) and not nullb
262 if not os.path.isdir(basedir):
736 createfunc = hunk.createfile
263 os.makedirs(basedir)
737 if reverse:
264 out = file(absdst, 'wb')
738 createfunc = hunk.rmfile
265 out.write(text)
739 if not goodb and not gooda and not createfunc():
266 out.close()
740 raise PatchError(_("Unable to find %s or %s for patching\n" %
267 elif p.copymod:
741 (afile, bfile)))
268 copyfile(p.oldpath, p.path, basedir=cwd)
742 if gooda and goodb:
269 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
743 fname = bfile
270 line = pf.readline()
744 if afile in bfile:
271 pfline += 1
745 fname = afile
272 while not line.startswith('--- a/'):
746 elif gooda:
273 tmpfp.write(line)
747 fname = afile
274 line = pf.readline()
748 elif not nullb:
275 pfline += 1
749 fname = bfile
276 tmpfp.write('--- a/%s\n' % p.path)
750 if afile in bfile:
751 fname = afile
752 elif not nulla:
753 fname = afile
754 return fname
755
756 class linereader:
757 # simple class to allow pushing lines back into the input stream
758 def __init__(self, fp):
759 self.fp = fp
760 self.buf = []
761
762 def push(self, line):
763 self.buf.append(line)
277
764
278 line = pf.readline()
765 def readline(self):
279 while line:
766 if self.buf:
280 tmpfp.write(line)
767 l = self.buf[0]
281 line = pf.readline()
768 del self.buf[0]
282 except:
769 return l
283 tmpfp.close()
770 return self.fp.readline()
284 os.unlink(patchname)
771
285 raise
772 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False,
773 rejmerge=None, updatedir=None):
774 """reads a patch from fp and tries to apply it. The dict 'changed' is
775 filled in with all of the filenames changed by the patch. Returns 0
776 for a clean patch, -1 if any rejects were found and 1 if there was
777 any fuzz."""
778
779 def scangitpatch(fp, firstline, cwd=None):
780 '''git patches can modify a file, then copy that file to
781 a new file, but expect the source to be the unmodified form.
782 So we scan the patch looking for that case so we can do
783 the copies ahead of time.'''
286
784
287 tmpfp.close()
785 pos = 0
288 return patchname
786 try:
787 pos = fp.tell()
788 except IOError:
789 fp = cStringIO.StringIO(fp.read())
790
791 (dopatch, gitpatches) = readgitpatch(fp, firstline)
792 for gp in gitpatches:
793 if gp.copymod:
794 copyfile(gp.oldpath, gp.path, basedir=cwd)
795
796 fp.seek(pos)
289
797
290 def patch(patchname, ui, strip=1, cwd=None, files={}):
798 return fp, dopatch, gitpatches
291 """apply the patch <patchname> to the working directory.
799
292 a list of patched files is returned"""
800 current_hunk = None
801 current_file = None
802 afile = ""
803 bfile = ""
804 state = None
805 hunknum = 0
806 rejects = 0
807
808 git = False
809 gitre = re.compile('diff --git (a/.*) (b/.*)')
293
810
294 # helper function
811 # our states
295 def __patch(patchname):
812 BFILE = 1
296 """patch and updates the files and fuzz variables"""
813 err = 0
297 fuzz = False
814 context = None
298
815 lr = linereader(fp)
299 args = []
816 dopatch = True
300 patcher = ui.config('ui', 'patch')
817 gitworkdone = False
301 if not patcher:
302 patcher = util.find_exe('gpatch') or util.find_exe('patch')
303 # Try to be smart only if patch call was not supplied
304 if util.needbinarypatch():
305 args.append('--binary')
306
307 if not patcher:
308 raise util.Abort(_('no patch command found in hgrc or PATH'))
309
310 if cwd:
311 args.append('-d %s' % util.shellquote(cwd))
312 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
313 util.shellquote(patchname)))
314
818
315 for line in fp:
819 while True:
316 line = line.rstrip()
820 newfile = False
317 ui.note(line + '\n')
821 x = lr.readline()
318 if line.startswith('patching file '):
822 if not x:
319 pf = util.parse_patch_output(line)
823 break
320 printed_file = False
824 if current_hunk:
321 files.setdefault(pf, (None, None))
825 if x.startswith('\ '):
322 elif line.find('with fuzz') >= 0:
826 current_hunk.fix_newline()
323 fuzz = True
827 ret = current_file.apply(current_hunk, reverse)
324 if not printed_file:
828 if ret > 0:
325 ui.warn(pf + '\n')
829 err = 1
326 printed_file = True
830 current_hunk = None
327 ui.warn(line + '\n')
831 gitworkdone = False
328 elif line.find('saving rejects to file') >= 0:
832 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
329 ui.warn(line + '\n')
833 ((context or context == None) and x.startswith('***************')))):
330 elif line.find('FAILED') >= 0:
834 try:
331 if not printed_file:
835 if context == None and x.startswith('***************'):
332 ui.warn(pf + '\n')
836 context = True
333 printed_file = True
837 current_hunk = hunk(x, hunknum + 1, lr, context)
334 ui.warn(line + '\n')
838 except PatchError:
335 code = fp.close()
839 current_hunk = None
336 if code:
840 continue
337 raise util.Abort(_("patch command failed: %s") %
841 hunknum += 1
338 util.explain_exit(code)[0])
842 if not current_file:
339 return fuzz
843 if sourcefile:
844 current_file = patchfile(ui, sourcefile)
845 else:
846 current_file = selectfile(afile, bfile, current_hunk,
847 strip, reverse)
848 current_file = patchfile(ui, current_file)
849 changed.setdefault(current_file.fname, (None, None))
850 elif state == BFILE and x.startswith('GIT binary patch'):
851 current_hunk = binhunk(changed[bfile[2:]][1])
852 if not current_file:
853 if sourcefile:
854 current_file = patchfile(ui, sourcefile)
855 else:
856 current_file = selectfile(afile, bfile, current_hunk,
857 strip, reverse)
858 current_file = patchfile(ui, current_file)
859 hunknum += 1
860 current_hunk.extract(fp)
861 elif x.startswith('diff --git'):
862 # check for git diff, scanning the whole patch file if needed
863 m = gitre.match(x)
864 if m:
865 afile, bfile = m.group(1, 2)
866 if not git:
867 git = True
868 fp, dopatch, gitpatches = scangitpatch(fp, x)
869 for gp in gitpatches:
870 changed[gp.path] = (gp.op, gp)
871 # else error?
872 # copy/rename + modify should modify target, not source
873 if changed.get(bfile[2:], (None, None))[0] in ('COPY',
874 'RENAME'):
875 afile = bfile
876 gitworkdone = True
877 newfile = True
878 elif x.startswith('---'):
879 # check for a unified diff
880 l2 = lr.readline()
881 if not l2.startswith('+++'):
882 lr.push(l2)
883 continue
884 newfile = True
885 context = False
886 afile = parsefilename(x)
887 bfile = parsefilename(l2)
888 elif x.startswith('***'):
889 # check for a context diff
890 l2 = lr.readline()
891 if not l2.startswith('---'):
892 lr.push(l2)
893 continue
894 l3 = lr.readline()
895 lr.push(l3)
896 if not l3.startswith("***************"):
897 lr.push(l2)
898 continue
899 newfile = True
900 context = True
901 afile = parsefilename(x)
902 bfile = parsefilename(l2)
340
903
341 (dopatch, gitpatches) = readgitpatch(patchname)
904 if newfile:
342 for gp in gitpatches:
905 if current_file:
343 files[gp.path] = (gp.op, gp)
906 current_file.close()
344
907 if rejmerge:
345 fuzz = False
908 rejmerge(current_file)
346 if dopatch:
909 rejects += len(current_file.rej)
347 filterpatch = dopatch & (GP_FILTER | GP_BINARY)
910 state = BFILE
348 if filterpatch:
911 current_file = None
349 patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
912 hunknum = 0
350 try:
913 if current_hunk:
351 if dopatch & GP_PATCH:
914 if current_hunk.complete():
352 fuzz = __patch(patchname)
915 ret = current_file.apply(current_hunk, reverse)
353 finally:
916 if ret > 0:
354 if filterpatch:
917 err = 1
355 os.unlink(patchname)
918 else:
356
919 fname = current_file and current_file.fname or None
357 return fuzz
920 raise PatchError("malformed patch %s %s" % (fname,
921 current_hunk.desc))
922 if current_file:
923 current_file.close()
924 if rejmerge:
925 rejmerge(current_file)
926 rejects += len(current_file.rej)
927 if updatedir and git:
928 updatedir(gitpatches)
929 if rejects:
930 return -1
931 if hunknum == 0 and dopatch and not gitworkdone:
932 raise PatchError("No valid hunks found")
933 return err
358
934
359 def diffopts(ui, opts={}, untrusted=False):
935 def diffopts(ui, opts={}, untrusted=False):
360 def get(key, name=None):
936 def get(key, name=None):
361 return (opts.get(key) or
937 return (opts.get(key) or
362 ui.configbool('diff', name or key, None, untrusted=untrusted))
938 ui.configbool('diff', name or key, None, untrusted=untrusted))
363 return mdiff.diffopts(
939 return mdiff.diffopts(
364 text=opts.get('text'),
940 text=opts.get('text'),
365 git=get('git'),
941 git=get('git'),
366 nodates=get('nodates'),
942 nodates=get('nodates'),
367 showfunc=get('show_function', 'showfunc'),
943 showfunc=get('show_function', 'showfunc'),
368 ignorews=get('ignore_all_space', 'ignorews'),
944 ignorews=get('ignore_all_space', 'ignorews'),
369 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
945 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
370 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
946 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
371
947
372 def updatedir(ui, repo, patches, wlock=None):
948 def updatedir(ui, repo, patches, wlock=None):
373 '''Update dirstate after patch application according to metadata'''
949 '''Update dirstate after patch application according to metadata'''
374 if not patches:
950 if not patches:
375 return
951 return
376 copies = []
952 copies = []
377 removes = {}
953 removes = {}
378 cfiles = patches.keys()
954 cfiles = patches.keys()
379 cwd = repo.getcwd()
955 cwd = repo.getcwd()
380 if cwd:
956 if cwd:
381 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
957 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
382 for f in patches:
958 for f in patches:
383 ctype, gp = patches[f]
959 ctype, gp = patches[f]
384 if ctype == 'RENAME':
960 if ctype == 'RENAME':
385 copies.append((gp.oldpath, gp.path, gp.copymod))
961 copies.append((gp.oldpath, gp.path, gp.copymod))
386 removes[gp.oldpath] = 1
962 removes[gp.oldpath] = 1
387 elif ctype == 'COPY':
963 elif ctype == 'COPY':
388 copies.append((gp.oldpath, gp.path, gp.copymod))
964 copies.append((gp.oldpath, gp.path, gp.copymod))
389 elif ctype == 'DELETE':
965 elif ctype == 'DELETE':
390 removes[gp.path] = 1
966 removes[gp.path] = 1
391 for src, dst, after in copies:
967 for src, dst, after in copies:
392 if not after:
968 if not after:
393 copyfile(src, dst, repo.root)
969 copyfile(src, dst, repo.root)
394 repo.copy(src, dst, wlock=wlock)
970 repo.copy(src, dst, wlock=wlock)
395 removes = removes.keys()
971 removes = removes.keys()
396 if removes:
972 if removes:
397 removes.sort()
973 removes.sort()
398 repo.remove(removes, True, wlock=wlock)
974 repo.remove(removes, True, wlock=wlock)
399 for f in patches:
975 for f in patches:
400 ctype, gp = patches[f]
976 ctype, gp = patches[f]
401 if gp and gp.mode:
977 if gp and gp.mode:
402 x = gp.mode & 0100 != 0
978 x = gp.mode & 0100 != 0
403 dst = os.path.join(repo.root, gp.path)
979 dst = os.path.join(repo.root, gp.path)
404 # patch won't create empty files
980 # patch won't create empty files
405 if ctype == 'ADD' and not os.path.exists(dst):
981 if ctype == 'ADD' and not os.path.exists(dst):
406 repo.wwrite(gp.path, '', x and 'x' or '')
982 repo.wwrite(gp.path, '', x and 'x' or '')
407 else:
983 else:
408 util.set_exec(dst, x)
984 util.set_exec(dst, x)
409 cmdutil.addremove(repo, cfiles, wlock=wlock)
985 cmdutil.addremove(repo, cfiles, wlock=wlock)
410 files = patches.keys()
986 files = patches.keys()
411 files.extend([r for r in removes if r not in files])
987 files.extend([r for r in removes if r not in files])
412 files.sort()
988 files.sort()
413
989
414 return files
990 return files
415
991
416 def b85diff(fp, to, tn):
992 def b85diff(fp, to, tn):
417 '''print base85-encoded binary diff'''
993 '''print base85-encoded binary diff'''
418 def gitindex(text):
994 def gitindex(text):
419 if not text:
995 if not text:
420 return '0' * 40
996 return '0' * 40
421 l = len(text)
997 l = len(text)
422 s = sha.new('blob %d\0' % l)
998 s = sha.new('blob %d\0' % l)
423 s.update(text)
999 s.update(text)
424 return s.hexdigest()
1000 return s.hexdigest()
425
1001
426 def fmtline(line):
1002 def fmtline(line):
427 l = len(line)
1003 l = len(line)
428 if l <= 26:
1004 if l <= 26:
429 l = chr(ord('A') + l - 1)
1005 l = chr(ord('A') + l - 1)
430 else:
1006 else:
431 l = chr(l - 26 + ord('a') - 1)
1007 l = chr(l - 26 + ord('a') - 1)
432 return '%c%s\n' % (l, base85.b85encode(line, True))
1008 return '%c%s\n' % (l, base85.b85encode(line, True))
433
1009
434 def chunk(text, csize=52):
1010 def chunk(text, csize=52):
435 l = len(text)
1011 l = len(text)
436 i = 0
1012 i = 0
437 while i < l:
1013 while i < l:
438 yield text[i:i+csize]
1014 yield text[i:i+csize]
439 i += csize
1015 i += csize
440
1016
441 tohash = gitindex(to)
1017 tohash = gitindex(to)
442 tnhash = gitindex(tn)
1018 tnhash = gitindex(tn)
443 if tohash == tnhash:
1019 if tohash == tnhash:
444 return ""
1020 return ""
445
1021
446 # TODO: deltas
1022 # TODO: deltas
447 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1023 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
448 (tohash, tnhash, len(tn))]
1024 (tohash, tnhash, len(tn))]
449 for l in chunk(zlib.compress(tn)):
1025 for l in chunk(zlib.compress(tn)):
450 ret.append(fmtline(l))
1026 ret.append(fmtline(l))
451 ret.append('\n')
1027 ret.append('\n')
452 return ''.join(ret)
1028 return ''.join(ret)
453
1029
454 def diff(repo, node1=None, node2=None, files=None, match=util.always,
1030 def diff(repo, node1=None, node2=None, files=None, match=util.always,
455 fp=None, changes=None, opts=None):
1031 fp=None, changes=None, opts=None):
456 '''print diff of changes to files between two nodes, or node and
1032 '''print diff of changes to files between two nodes, or node and
457 working directory.
1033 working directory.
458
1034
459 if node1 is None, use first dirstate parent instead.
1035 if node1 is None, use first dirstate parent instead.
460 if node2 is None, compare node1 with working directory.'''
1036 if node2 is None, compare node1 with working directory.'''
461
1037
462 if opts is None:
1038 if opts is None:
463 opts = mdiff.defaultopts
1039 opts = mdiff.defaultopts
464 if fp is None:
1040 if fp is None:
465 fp = repo.ui
1041 fp = repo.ui
466
1042
467 if not node1:
1043 if not node1:
468 node1 = repo.dirstate.parents()[0]
1044 node1 = repo.dirstate.parents()[0]
469
1045
470 ccache = {}
1046 ccache = {}
471 def getctx(r):
1047 def getctx(r):
472 if r not in ccache:
1048 if r not in ccache:
473 ccache[r] = context.changectx(repo, r)
1049 ccache[r] = context.changectx(repo, r)
474 return ccache[r]
1050 return ccache[r]
475
1051
476 flcache = {}
1052 flcache = {}
477 def getfilectx(f, ctx):
1053 def getfilectx(f, ctx):
478 flctx = ctx.filectx(f, filelog=flcache.get(f))
1054 flctx = ctx.filectx(f, filelog=flcache.get(f))
479 if f not in flcache:
1055 if f not in flcache:
480 flcache[f] = flctx._filelog
1056 flcache[f] = flctx._filelog
481 return flctx
1057 return flctx
482
1058
483 # reading the data for node1 early allows it to play nicely
1059 # reading the data for node1 early allows it to play nicely
484 # with repo.status and the revlog cache.
1060 # with repo.status and the revlog cache.
485 ctx1 = context.changectx(repo, node1)
1061 ctx1 = context.changectx(repo, node1)
486 # force manifest reading
1062 # force manifest reading
487 man1 = ctx1.manifest()
1063 man1 = ctx1.manifest()
488 date1 = util.datestr(ctx1.date())
1064 date1 = util.datestr(ctx1.date())
489
1065
490 if not changes:
1066 if not changes:
491 changes = repo.status(node1, node2, files, match=match)[:5]
1067 changes = repo.status(node1, node2, files, match=match)[:5]
492 modified, added, removed, deleted, unknown = changes
1068 modified, added, removed, deleted, unknown = changes
493
1069
494 if not modified and not added and not removed:
1070 if not modified and not added and not removed:
495 return
1071 return
496
1072
497 if node2:
1073 if node2:
498 ctx2 = context.changectx(repo, node2)
1074 ctx2 = context.changectx(repo, node2)
499 execf2 = ctx2.manifest().execf
1075 execf2 = ctx2.manifest().execf
500 else:
1076 else:
501 ctx2 = context.workingctx(repo)
1077 ctx2 = context.workingctx(repo)
502 execf2 = util.execfunc(repo.root, None)
1078 execf2 = util.execfunc(repo.root, None)
503 if execf2 is None:
1079 if execf2 is None:
504 execf2 = ctx2.parents()[0].manifest().copy().execf
1080 execf2 = ctx2.parents()[0].manifest().copy().execf
505
1081
506 # returns False if there was no rename between ctx1 and ctx2
1082 # returns False if there was no rename between ctx1 and ctx2
507 # returns None if the file was created between ctx1 and ctx2
1083 # returns None if the file was created between ctx1 and ctx2
508 # returns the (file, node) present in ctx1 that was renamed to f in ctx2
1084 # returns the (file, node) present in ctx1 that was renamed to f in ctx2
509 def renamed(f):
1085 def renamed(f):
510 startrev = ctx1.rev()
1086 startrev = ctx1.rev()
511 c = ctx2
1087 c = ctx2
512 crev = c.rev()
1088 crev = c.rev()
513 if crev is None:
1089 if crev is None:
514 crev = repo.changelog.count()
1090 crev = repo.changelog.count()
515 orig = f
1091 orig = f
516 while crev > startrev:
1092 while crev > startrev:
517 if f in c.files():
1093 if f in c.files():
518 try:
1094 try:
519 src = getfilectx(f, c).renamed()
1095 src = getfilectx(f, c).renamed()
520 except revlog.LookupError:
1096 except revlog.LookupError:
521 return None
1097 return None
522 if src:
1098 if src:
523 f = src[0]
1099 f = src[0]
524 crev = c.parents()[0].rev()
1100 crev = c.parents()[0].rev()
525 # try to reuse
1101 # try to reuse
526 c = getctx(crev)
1102 c = getctx(crev)
527 if f not in man1:
1103 if f not in man1:
528 return None
1104 return None
529 if f == orig:
1105 if f == orig:
530 return False
1106 return False
531 return f
1107 return f
532
1108
533 if repo.ui.quiet:
1109 if repo.ui.quiet:
534 r = None
1110 r = None
535 else:
1111 else:
536 hexfunc = repo.ui.debugflag and hex or short
1112 hexfunc = repo.ui.debugflag and hex or short
537 r = [hexfunc(node) for node in [node1, node2] if node]
1113 r = [hexfunc(node) for node in [node1, node2] if node]
538
1114
539 if opts.git:
1115 if opts.git:
540 copied = {}
1116 copied = {}
541 for f in added:
1117 for f in added:
542 src = renamed(f)
1118 src = renamed(f)
543 if src:
1119 if src:
544 copied[f] = src
1120 copied[f] = src
545 srcs = [x[1] for x in copied.items()]
1121 srcs = [x[1] for x in copied.items()]
546
1122
547 all = modified + added + removed
1123 all = modified + added + removed
548 all.sort()
1124 all.sort()
549 gone = {}
1125 gone = {}
550
1126
551 for f in all:
1127 for f in all:
552 to = None
1128 to = None
553 tn = None
1129 tn = None
554 dodiff = True
1130 dodiff = True
555 header = []
1131 header = []
556 if f in man1:
1132 if f in man1:
557 to = getfilectx(f, ctx1).data()
1133 to = getfilectx(f, ctx1).data()
558 if f not in removed:
1134 if f not in removed:
559 tn = getfilectx(f, ctx2).data()
1135 tn = getfilectx(f, ctx2).data()
560 if opts.git:
1136 if opts.git:
561 def gitmode(x):
1137 def gitmode(x):
562 return x and '100755' or '100644'
1138 return x and '100755' or '100644'
563 def addmodehdr(header, omode, nmode):
1139 def addmodehdr(header, omode, nmode):
564 if omode != nmode:
1140 if omode != nmode:
565 header.append('old mode %s\n' % omode)
1141 header.append('old mode %s\n' % omode)
566 header.append('new mode %s\n' % nmode)
1142 header.append('new mode %s\n' % nmode)
567
1143
568 a, b = f, f
1144 a, b = f, f
569 if f in added:
1145 if f in added:
570 mode = gitmode(execf2(f))
1146 mode = gitmode(execf2(f))
571 if f in copied:
1147 if f in copied:
572 a = copied[f]
1148 a = copied[f]
573 omode = gitmode(man1.execf(a))
1149 omode = gitmode(man1.execf(a))
574 addmodehdr(header, omode, mode)
1150 addmodehdr(header, omode, mode)
575 if a in removed and a not in gone:
1151 if a in removed and a not in gone:
576 op = 'rename'
1152 op = 'rename'
577 gone[a] = 1
1153 gone[a] = 1
578 else:
1154 else:
579 op = 'copy'
1155 op = 'copy'
580 header.append('%s from %s\n' % (op, a))
1156 header.append('%s from %s\n' % (op, a))
581 header.append('%s to %s\n' % (op, f))
1157 header.append('%s to %s\n' % (op, f))
582 to = getfilectx(a, ctx1).data()
1158 to = getfilectx(a, ctx1).data()
583 else:
1159 else:
584 header.append('new file mode %s\n' % mode)
1160 header.append('new file mode %s\n' % mode)
585 if util.binary(tn):
1161 if util.binary(tn):
586 dodiff = 'binary'
1162 dodiff = 'binary'
587 elif f in removed:
1163 elif f in removed:
588 if f in srcs:
1164 if f in srcs:
589 dodiff = False
1165 dodiff = False
590 else:
1166 else:
591 mode = gitmode(man1.execf(f))
1167 mode = gitmode(man1.execf(f))
592 header.append('deleted file mode %s\n' % mode)
1168 header.append('deleted file mode %s\n' % mode)
593 else:
1169 else:
594 omode = gitmode(man1.execf(f))
1170 omode = gitmode(man1.execf(f))
595 nmode = gitmode(execf2(f))
1171 nmode = gitmode(execf2(f))
596 addmodehdr(header, omode, nmode)
1172 addmodehdr(header, omode, nmode)
597 if util.binary(to) or util.binary(tn):
1173 if util.binary(to) or util.binary(tn):
598 dodiff = 'binary'
1174 dodiff = 'binary'
599 r = None
1175 r = None
600 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
1176 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
601 if dodiff:
1177 if dodiff:
602 if dodiff == 'binary':
1178 if dodiff == 'binary':
603 text = b85diff(fp, to, tn)
1179 text = b85diff(fp, to, tn)
604 else:
1180 else:
605 text = mdiff.unidiff(to, date1,
1181 text = mdiff.unidiff(to, date1,
606 # ctx2 date may be dynamic
1182 # ctx2 date may be dynamic
607 tn, util.datestr(ctx2.date()),
1183 tn, util.datestr(ctx2.date()),
608 f, r, opts=opts)
1184 f, r, opts=opts)
609 if text or len(header) > 1:
1185 if text or len(header) > 1:
610 fp.write(''.join(header))
1186 fp.write(''.join(header))
611 fp.write(text)
1187 fp.write(text)
612
1188
613 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1189 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
614 opts=None):
1190 opts=None):
615 '''export changesets as hg patches.'''
1191 '''export changesets as hg patches.'''
616
1192
617 total = len(revs)
1193 total = len(revs)
618 revwidth = max([len(str(rev)) for rev in revs])
1194 revwidth = max([len(str(rev)) for rev in revs])
619
1195
620 def single(rev, seqno, fp):
1196 def single(rev, seqno, fp):
621 ctx = repo.changectx(rev)
1197 ctx = repo.changectx(rev)
622 node = ctx.node()
1198 node = ctx.node()
623 parents = [p.node() for p in ctx.parents() if p]
1199 parents = [p.node() for p in ctx.parents() if p]
624 branch = ctx.branch()
1200 branch = ctx.branch()
625 if switch_parent:
1201 if switch_parent:
626 parents.reverse()
1202 parents.reverse()
627 prev = (parents and parents[0]) or nullid
1203 prev = (parents and parents[0]) or nullid
628
1204
629 if not fp:
1205 if not fp:
630 fp = cmdutil.make_file(repo, template, node, total=total,
1206 fp = cmdutil.make_file(repo, template, node, total=total,
631 seqno=seqno, revwidth=revwidth)
1207 seqno=seqno, revwidth=revwidth)
632 if fp != sys.stdout and hasattr(fp, 'name'):
1208 if fp != sys.stdout and hasattr(fp, 'name'):
633 repo.ui.note("%s\n" % fp.name)
1209 repo.ui.note("%s\n" % fp.name)
634
1210
635 fp.write("# HG changeset patch\n")
1211 fp.write("# HG changeset patch\n")
636 fp.write("# User %s\n" % ctx.user())
1212 fp.write("# User %s\n" % ctx.user())
637 fp.write("# Date %d %d\n" % ctx.date())
1213 fp.write("# Date %d %d\n" % ctx.date())
638 if branch and (branch != 'default'):
1214 if branch and (branch != 'default'):
639 fp.write("# Branch %s\n" % branch)
1215 fp.write("# Branch %s\n" % branch)
640 fp.write("# Node ID %s\n" % hex(node))
1216 fp.write("# Node ID %s\n" % hex(node))
641 fp.write("# Parent %s\n" % hex(prev))
1217 fp.write("# Parent %s\n" % hex(prev))
642 if len(parents) > 1:
1218 if len(parents) > 1:
643 fp.write("# Parent %s\n" % hex(parents[1]))
1219 fp.write("# Parent %s\n" % hex(parents[1]))
644 fp.write(ctx.description().rstrip())
1220 fp.write(ctx.description().rstrip())
645 fp.write("\n\n")
1221 fp.write("\n\n")
646
1222
647 diff(repo, prev, node, fp=fp, opts=opts)
1223 diff(repo, prev, node, fp=fp, opts=opts)
648 if fp not in (sys.stdout, repo.ui):
1224 if fp not in (sys.stdout, repo.ui):
649 fp.close()
1225 fp.close()
650
1226
651 for seqno, rev in enumerate(revs):
1227 for seqno, rev in enumerate(revs):
652 single(rev, seqno+1, fp)
1228 single(rev, seqno+1, fp)
653
1229
654 def diffstat(patchlines):
1230 def diffstat(patchlines):
655 if not util.find_exe('diffstat'):
1231 if not util.find_exe('diffstat'):
656 return
1232 return
657 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
1233 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
658 try:
1234 try:
659 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
1235 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
660 try:
1236 try:
661 for line in patchlines: print >> p.tochild, line
1237 for line in patchlines: print >> p.tochild, line
662 p.tochild.close()
1238 p.tochild.close()
663 if p.wait(): return
1239 if p.wait(): return
664 fp = os.fdopen(fd, 'r')
1240 fp = os.fdopen(fd, 'r')
665 stat = []
1241 stat = []
666 for line in fp: stat.append(line.lstrip())
1242 for line in fp: stat.append(line.lstrip())
667 last = stat.pop()
1243 last = stat.pop()
668 stat.insert(0, last)
1244 stat.insert(0, last)
669 stat = ''.join(stat)
1245 stat = ''.join(stat)
670 if stat.startswith('0 files'): raise ValueError
1246 if stat.startswith('0 files'): raise ValueError
671 return stat
1247 return stat
672 except: raise
1248 except: raise
673 finally:
1249 finally:
674 try: os.unlink(name)
1250 try: os.unlink(name)
675 except: pass
1251 except: pass
@@ -1,78 +1,79
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # This is the mercurial setup script.
3 # This is the mercurial setup script.
4 #
4 #
5 # 'python setup.py install', or
5 # 'python setup.py install', or
6 # 'python setup.py --help' for more options
6 # 'python setup.py --help' for more options
7
7
8 import sys
8 import sys
9 if not hasattr(sys, 'version_info') or sys.version_info < (2, 3, 0, 'final'):
9 if not hasattr(sys, 'version_info') or sys.version_info < (2, 3, 0, 'final'):
10 raise SystemExit, "Mercurial requires python 2.3 or later."
10 raise SystemExit, "Mercurial requires python 2.3 or later."
11
11
12 import os
12 import os
13 from distutils.core import setup, Extension
13 from distutils.core import setup, Extension
14 from distutils.command.install_data import install_data
14 from distutils.command.install_data import install_data
15
15
16 import mercurial.version
16 import mercurial.version
17 import mercurial.demandimport
17 import mercurial.demandimport
18 mercurial.demandimport.enable = lambda: None
18 mercurial.demandimport.enable = lambda: None
19
19
20 extra = {}
20 extra = {}
21
21
22 # py2exe needs to be installed to work
22 # py2exe needs to be installed to work
23 try:
23 try:
24 import py2exe
24 import py2exe
25
25
26 # Help py2exe to find win32com.shell
26 # Help py2exe to find win32com.shell
27 try:
27 try:
28 import modulefinder
28 import modulefinder
29 import win32com
29 import win32com
30 for p in win32com.__path__[1:]: # Take the path to win32comext
30 for p in win32com.__path__[1:]: # Take the path to win32comext
31 modulefinder.AddPackagePath("win32com", p)
31 modulefinder.AddPackagePath("win32com", p)
32 pn = "win32com.shell"
32 pn = "win32com.shell"
33 __import__(pn)
33 __import__(pn)
34 m = sys.modules[pn]
34 m = sys.modules[pn]
35 for p in m.__path__[1:]:
35 for p in m.__path__[1:]:
36 modulefinder.AddPackagePath(pn, p)
36 modulefinder.AddPackagePath(pn, p)
37 except ImportError:
37 except ImportError:
38 pass
38 pass
39
39
40 extra['console'] = ['hg']
40 extra['console'] = ['hg']
41
41
42 except ImportError:
42 except ImportError:
43 pass
43 pass
44
44
45 # specify version string, otherwise 'hg identify' will be used:
45 # specify version string, otherwise 'hg identify' will be used:
46 version = ''
46 version = ''
47
47
48 class install_package_data(install_data):
48 class install_package_data(install_data):
49 def finalize_options(self):
49 def finalize_options(self):
50 self.set_undefined_options('install',
50 self.set_undefined_options('install',
51 ('install_lib', 'install_dir'))
51 ('install_lib', 'install_dir'))
52 install_data.finalize_options(self)
52 install_data.finalize_options(self)
53
53
54 mercurial.version.remember_version(version)
54 mercurial.version.remember_version(version)
55 cmdclass = {'install_data': install_package_data}
55 cmdclass = {'install_data': install_package_data}
56
56
57 setup(name='mercurial',
57 setup(name='mercurial',
58 version=mercurial.version.get_version(),
58 version=mercurial.version.get_version(),
59 author='Matt Mackall',
59 author='Matt Mackall',
60 author_email='mpm@selenic.com',
60 author_email='mpm@selenic.com',
61 url='http://selenic.com/mercurial',
61 url='http://selenic.com/mercurial',
62 description='Scalable distributed SCM',
62 description='Scalable distributed SCM',
63 license='GNU GPL',
63 license='GNU GPL',
64 packages=['mercurial', 'mercurial.hgweb', 'hgext', 'hgext.convert'],
64 packages=['mercurial', 'mercurial.hgweb', 'hgext', 'hgext.convert'],
65 ext_modules=[Extension('mercurial.mpatch', ['mercurial/mpatch.c']),
65 ext_modules=[Extension('mercurial.mpatch', ['mercurial/mpatch.c']),
66 Extension('mercurial.bdiff', ['mercurial/bdiff.c']),
66 Extension('mercurial.bdiff', ['mercurial/bdiff.c']),
67 Extension('mercurial.base85', ['mercurial/base85.c'])],
67 Extension('mercurial.base85', ['mercurial/base85.c']),
68 Extension('mercurial.diffhelpers', ['mercurial/diffhelpers.c'])],
68 data_files=[(os.path.join('mercurial', root),
69 data_files=[(os.path.join('mercurial', root),
69 [os.path.join(root, file_) for file_ in files])
70 [os.path.join(root, file_) for file_ in files])
70 for root, dirs, files in os.walk('templates')],
71 for root, dirs, files in os.walk('templates')],
71 cmdclass=cmdclass,
72 cmdclass=cmdclass,
72 scripts=['hg', 'hgmerge'],
73 scripts=['hg', 'hgmerge'],
73 options=dict(py2exe=dict(packages=['hgext']),
74 options=dict(py2exe=dict(packages=['hgext']),
74 bdist_mpkg=dict(zipdist=True,
75 bdist_mpkg=dict(zipdist=True,
75 license='COPYING',
76 license='COPYING',
76 readme='contrib/macosx/Readme.html',
77 readme='contrib/macosx/Readme.html',
77 welcome='contrib/macosx/Welcome.html')),
78 welcome='contrib/macosx/Welcome.html')),
78 **extra)
79 **extra)
General Comments 0
You need to be logged in to leave comments. Login now