##// 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 b''
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 b''
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
4 5 #
5 6 # This software may be used and distributed according to the terms
6 7 # of the GNU General Public License, incorporated herein by reference.
7 8
8 9 from i18n import _
9 10 from node import *
10 import base85, cmdutil, mdiff, util, context, revlog
11 import base85, cmdutil, mdiff, util, context, revlog, diffhelpers
11 12 import cStringIO, email.Parser, os, popen2, re, sha
12 13 import sys, tempfile, zlib
13 14
15 class PatchError(Exception):
16 pass
17
14 18 # helper functions
15 19
16 20 def copyfile(src, dst, basedir=None):
17 21 if not basedir:
18 22 basedir = os.getcwd()
19 23
20 24 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
21 25 if os.path.exists(absdst):
22 26 raise util.Abort(_("cannot create %s: destination already exists") %
23 27 dst)
24 28
25 29 targetdir = os.path.dirname(absdst)
26 30 if not os.path.isdir(targetdir):
27 31 os.makedirs(targetdir)
28 32
29 33 util.copyfile(abssrc, absdst)
30 34
31 35 # public functions
32 36
33 37 def extract(ui, fileobj):
34 38 '''extract patch from data read from fileobj.
35 39
36 40 patch can be a normal patch or contained in an email message.
37 41
38 42 return tuple (filename, message, user, date, node, p1, p2).
39 43 Any item in the returned tuple can be None. If filename is None,
40 44 fileobj did not contain a patch. Caller must unlink filename when done.'''
41 45
42 46 # attempt to detect the start of a patch
43 47 # (this heuristic is borrowed from quilt)
44 48 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
45 49 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
46 50 '(---|\*\*\*)[ \t])', re.MULTILINE)
47 51
48 52 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
49 53 tmpfp = os.fdopen(fd, 'w')
50 54 try:
51 55 msg = email.Parser.Parser().parse(fileobj)
52 56
53 57 subject = msg['Subject']
54 58 user = msg['From']
55 59 # should try to parse msg['Date']
56 60 date = None
57 61 nodeid = None
58 62 branch = None
59 63 parents = []
60 64
61 65 if subject:
62 66 if subject.startswith('[PATCH'):
63 67 pend = subject.find(']')
64 68 if pend >= 0:
65 69 subject = subject[pend+1:].lstrip()
66 70 subject = subject.replace('\n\t', ' ')
67 71 ui.debug('Subject: %s\n' % subject)
68 72 if user:
69 73 ui.debug('From: %s\n' % user)
70 74 diffs_seen = 0
71 75 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
72 76
73 77 for part in msg.walk():
74 78 content_type = part.get_content_type()
75 79 ui.debug('Content-Type: %s\n' % content_type)
76 80 if content_type not in ok_types:
77 81 continue
78 82 payload = part.get_payload(decode=True)
79 83 m = diffre.search(payload)
80 84 if m:
81 85 hgpatch = False
82 86 ignoretext = False
83 87
84 88 ui.debug(_('found patch at byte %d\n') % m.start(0))
85 89 diffs_seen += 1
86 90 cfp = cStringIO.StringIO()
87 91 for line in payload[:m.start(0)].splitlines():
88 92 if line.startswith('# HG changeset patch'):
89 93 ui.debug(_('patch generated by hg export\n'))
90 94 hgpatch = True
91 95 # drop earlier commit message content
92 96 cfp.seek(0)
93 97 cfp.truncate()
94 98 subject = None
95 99 elif hgpatch:
96 100 if line.startswith('# User '):
97 101 user = line[7:]
98 102 ui.debug('From: %s\n' % user)
99 103 elif line.startswith("# Date "):
100 104 date = line[7:]
101 105 elif line.startswith("# Branch "):
102 106 branch = line[9:]
103 107 elif line.startswith("# Node ID "):
104 108 nodeid = line[10:]
105 109 elif line.startswith("# Parent "):
106 110 parents.append(line[10:])
107 111 elif line == '---' and 'git-send-email' in msg['X-Mailer']:
108 112 ignoretext = True
109 113 if not line.startswith('# ') and not ignoretext:
110 114 cfp.write(line)
111 115 cfp.write('\n')
112 116 message = cfp.getvalue()
113 117 if tmpfp:
114 118 tmpfp.write(payload)
115 119 if not payload.endswith('\n'):
116 120 tmpfp.write('\n')
117 121 elif not diffs_seen and message and content_type == 'text/plain':
118 122 message += '\n' + payload
119 123 except:
120 124 tmpfp.close()
121 125 os.unlink(tmpname)
122 126 raise
123 127
124 128 if subject and not message.startswith(subject):
125 129 message = '%s\n%s' % (subject, message)
126 130 tmpfp.close()
127 131 if not diffs_seen:
128 132 os.unlink(tmpname)
129 133 return None, message, user, date, branch, None, None, None
130 134 p1 = parents and parents.pop(0) or None
131 135 p2 = parents and parents.pop(0) or None
132 136 return tmpname, message, user, date, branch, nodeid, p1, p2
133 137
134 138 GP_PATCH = 1 << 0 # we have to run patch
135 139 GP_FILTER = 1 << 1 # there's some copy/rename operation
136 140 GP_BINARY = 1 << 2 # there's a binary patch
137 141
138 def readgitpatch(patchname):
142 def readgitpatch(fp, firstline):
139 143 """extract git-style metadata about patches from <patchname>"""
140 144 class gitpatch:
141 145 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
142 146 def __init__(self, path):
143 147 self.path = path
144 148 self.oldpath = None
145 149 self.mode = None
146 150 self.op = 'MODIFY'
147 151 self.copymod = False
148 152 self.lineno = 0
149 153 self.binary = False
150 154
155 def reader(fp, firstline):
156 yield firstline
157 for line in fp:
158 yield line
159
151 160 # Filter patch for git information
152 161 gitre = re.compile('diff --git a/(.*) b/(.*)')
153 pf = file(patchname)
154 162 gp = None
155 163 gitpatches = []
156 164 # Can have a git patch with only metadata, causing patch to complain
157 165 dopatch = 0
158 166
159 167 lineno = 0
160 for line in pf:
168 for line in reader(fp, firstline):
161 169 lineno += 1
162 170 if line.startswith('diff --git'):
163 171 m = gitre.match(line)
164 172 if m:
165 173 if gp:
166 174 gitpatches.append(gp)
167 175 src, dst = m.group(1, 2)
168 176 gp = gitpatch(dst)
169 177 gp.lineno = lineno
170 178 elif gp:
171 179 if line.startswith('--- '):
172 180 if gp.op in ('COPY', 'RENAME'):
173 181 gp.copymod = True
174 182 dopatch |= GP_FILTER
175 183 gitpatches.append(gp)
176 184 gp = None
177 185 dopatch |= GP_PATCH
178 186 continue
179 187 if line.startswith('rename from '):
180 188 gp.op = 'RENAME'
181 189 gp.oldpath = line[12:].rstrip()
182 190 elif line.startswith('rename to '):
183 191 gp.path = line[10:].rstrip()
184 192 elif line.startswith('copy from '):
185 193 gp.op = 'COPY'
186 194 gp.oldpath = line[10:].rstrip()
187 195 elif line.startswith('copy to '):
188 196 gp.path = line[8:].rstrip()
189 197 elif line.startswith('deleted file'):
190 198 gp.op = 'DELETE'
191 199 elif line.startswith('new file mode '):
192 200 gp.op = 'ADD'
193 201 gp.mode = int(line.rstrip()[-3:], 8)
194 202 elif line.startswith('new mode '):
195 203 gp.mode = int(line.rstrip()[-3:], 8)
196 204 elif line.startswith('GIT binary patch'):
197 205 dopatch |= GP_BINARY
198 206 gp.binary = True
199 207 if gp:
200 208 gitpatches.append(gp)
201 209
202 210 if not gitpatches:
203 211 dopatch = GP_PATCH
204 212
205 213 return (dopatch, gitpatches)
206 214
207 def dogitpatch(patchname, gitpatches, cwd=None):
208 """Preprocess git patch so that vanilla patch can handle it"""
209 def extractbin(fp):
210 i = [0] # yuck
211 def readline():
212 i[0] += 1
213 return fp.readline().rstrip()
214 line = readline()
215 def patch(patchname, ui, strip=1, cwd=None, files={}):
216 """apply the patch <patchname> to the working directory.
217 a list of patched files is returned"""
218 fp = file(patchname)
219 fuzz = False
220 if cwd:
221 curdir = os.getcwd()
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 675 while line and not line.startswith('literal '):
216 line = readline()
676 line = fp.readline()
677 self.hunk.append(line)
217 678 if not line:
218 return None, i[0]
219 size = int(line[8:])
679 raise PatchError('could not extract binary patch')
680 size = int(line[8:].rstrip())
220 681 dec = []
221 line = readline()
222 while line:
682 line = fp.readline()
683 self.hunk.append(line)
684 while len(line) > 1:
223 685 l = line[0]
224 686 if l <= 'Z' and l >= 'A':
225 687 l = ord(l) - ord('A') + 1
226 688 else:
227 689 l = ord(l) - ord('a') + 27
228 dec.append(base85.b85decode(line[1:])[:l])
229 line = readline()
690 dec.append(base85.b85decode(line[1:-1])[:l])
691 line = fp.readline()
692 self.hunk.append(line)
230 693 text = zlib.decompress(''.join(dec))
231 694 if len(text) != size:
232 raise util.Abort(_('binary patch is %d bytes, not %d') %
233 (len(text), size))
234 return text, i[0]
695 raise PatchError('binary patch is %d bytes, not %d' %
696 len(text), size)
697 self.text = text
235 698
236 pf = file(patchname)
237 pfline = 1
238
239 fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
240 tmpfp = os.fdopen(fd, 'w')
699 def parsefilename(str):
700 # --- filename \t|space stuff
701 s = str[4:]
702 i = s.find('\t')
703 if i < 0:
704 i = s.find(' ')
705 if i < 0:
706 return s
707 return s[:i]
241 708
242 try:
243 for i in xrange(len(gitpatches)):
244 p = gitpatches[i]
245 if not p.copymod and not p.binary:
246 continue
247
248 # rewrite patch hunk
249 while pfline < p.lineno:
250 tmpfp.write(pf.readline())
251 pfline += 1
709 def selectfile(afile_orig, bfile_orig, hunk, strip, reverse):
710 def pathstrip(path, count=1):
711 pathlen = len(path)
712 i = 0
713 if count == 0:
714 return path.rstrip()
715 while count > 0:
716 i = path.find(os.sep, i)
717 if i == -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:
254 text, delta = extractbin(pf)
255 if not text:
256 raise util.Abort(_('binary patch extraction failed'))
257 pfline += delta
258 if not cwd:
259 cwd = os.getcwd()
260 absdst = os.path.join(cwd, p.path)
261 basedir = os.path.dirname(absdst)
262 if not os.path.isdir(basedir):
263 os.makedirs(basedir)
264 out = file(absdst, 'wb')
265 out.write(text)
266 out.close()
267 elif p.copymod:
268 copyfile(p.oldpath, p.path, basedir=cwd)
269 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
270 line = pf.readline()
271 pfline += 1
272 while not line.startswith('--- a/'):
273 tmpfp.write(line)
274 line = pf.readline()
275 pfline += 1
276 tmpfp.write('--- a/%s\n' % p.path)
727 nulla = afile_orig == "/dev/null"
728 nullb = bfile_orig == "/dev/null"
729 afile = pathstrip(afile_orig, strip)
730 gooda = os.path.exists(afile) and not nulla
731 bfile = pathstrip(bfile_orig, strip)
732 if afile == bfile:
733 goodb = gooda
734 else:
735 goodb = os.path.exists(bfile) and not nullb
736 createfunc = hunk.createfile
737 if reverse:
738 createfunc = hunk.rmfile
739 if not goodb and not gooda and not createfunc():
740 raise PatchError(_("Unable to find %s or %s for patching\n" %
741 (afile, bfile)))
742 if gooda and goodb:
743 fname = bfile
744 if afile in bfile:
745 fname = afile
746 elif gooda:
747 fname = afile
748 elif not nullb:
749 fname = bfile
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()
279 while line:
280 tmpfp.write(line)
281 line = pf.readline()
282 except:
283 tmpfp.close()
284 os.unlink(patchname)
285 raise
765 def readline(self):
766 if self.buf:
767 l = self.buf[0]
768 del self.buf[0]
769 return l
770 return self.fp.readline()
771
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()
288 return patchname
785 pos = 0
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={}):
291 """apply the patch <patchname> to the working directory.
292 a list of patched files is returned"""
798 return fp, dopatch, gitpatches
799
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
295 def __patch(patchname):
296 """patch and updates the files and fuzz variables"""
297 fuzz = False
298
299 args = []
300 patcher = ui.config('ui', 'patch')
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)))
811 # our states
812 BFILE = 1
813 err = 0
814 context = None
815 lr = linereader(fp)
816 dopatch = True
817 gitworkdone = False
314 818
315 for line in fp:
316 line = line.rstrip()
317 ui.note(line + '\n')
318 if line.startswith('patching file '):
319 pf = util.parse_patch_output(line)
320 printed_file = False
321 files.setdefault(pf, (None, None))
322 elif line.find('with fuzz') >= 0:
323 fuzz = True
324 if not printed_file:
325 ui.warn(pf + '\n')
326 printed_file = True
327 ui.warn(line + '\n')
328 elif line.find('saving rejects to file') >= 0:
329 ui.warn(line + '\n')
330 elif line.find('FAILED') >= 0:
331 if not printed_file:
332 ui.warn(pf + '\n')
333 printed_file = True
334 ui.warn(line + '\n')
335 code = fp.close()
336 if code:
337 raise util.Abort(_("patch command failed: %s") %
338 util.explain_exit(code)[0])
339 return fuzz
819 while True:
820 newfile = False
821 x = lr.readline()
822 if not x:
823 break
824 if current_hunk:
825 if x.startswith('\ '):
826 current_hunk.fix_newline()
827 ret = current_file.apply(current_hunk, reverse)
828 if ret > 0:
829 err = 1
830 current_hunk = None
831 gitworkdone = False
832 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
833 ((context or context == None) and x.startswith('***************')))):
834 try:
835 if context == None and x.startswith('***************'):
836 context = True
837 current_hunk = hunk(x, hunknum + 1, lr, context)
838 except PatchError:
839 current_hunk = None
840 continue
841 hunknum += 1
842 if not current_file:
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)
342 for gp in gitpatches:
343 files[gp.path] = (gp.op, gp)
344
345 fuzz = False
346 if dopatch:
347 filterpatch = dopatch & (GP_FILTER | GP_BINARY)
348 if filterpatch:
349 patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
350 try:
351 if dopatch & GP_PATCH:
352 fuzz = __patch(patchname)
353 finally:
354 if filterpatch:
355 os.unlink(patchname)
356
357 return fuzz
904 if newfile:
905 if current_file:
906 current_file.close()
907 if rejmerge:
908 rejmerge(current_file)
909 rejects += len(current_file.rej)
910 state = BFILE
911 current_file = None
912 hunknum = 0
913 if current_hunk:
914 if current_hunk.complete():
915 ret = current_file.apply(current_hunk, reverse)
916 if ret > 0:
917 err = 1
918 else:
919 fname = current_file and current_file.fname or None
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 935 def diffopts(ui, opts={}, untrusted=False):
360 936 def get(key, name=None):
361 937 return (opts.get(key) or
362 938 ui.configbool('diff', name or key, None, untrusted=untrusted))
363 939 return mdiff.diffopts(
364 940 text=opts.get('text'),
365 941 git=get('git'),
366 942 nodates=get('nodates'),
367 943 showfunc=get('show_function', 'showfunc'),
368 944 ignorews=get('ignore_all_space', 'ignorews'),
369 945 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
370 946 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
371 947
372 948 def updatedir(ui, repo, patches, wlock=None):
373 949 '''Update dirstate after patch application according to metadata'''
374 950 if not patches:
375 951 return
376 952 copies = []
377 953 removes = {}
378 954 cfiles = patches.keys()
379 955 cwd = repo.getcwd()
380 956 if cwd:
381 957 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
382 958 for f in patches:
383 959 ctype, gp = patches[f]
384 960 if ctype == 'RENAME':
385 961 copies.append((gp.oldpath, gp.path, gp.copymod))
386 962 removes[gp.oldpath] = 1
387 963 elif ctype == 'COPY':
388 964 copies.append((gp.oldpath, gp.path, gp.copymod))
389 965 elif ctype == 'DELETE':
390 966 removes[gp.path] = 1
391 967 for src, dst, after in copies:
392 968 if not after:
393 969 copyfile(src, dst, repo.root)
394 970 repo.copy(src, dst, wlock=wlock)
395 971 removes = removes.keys()
396 972 if removes:
397 973 removes.sort()
398 974 repo.remove(removes, True, wlock=wlock)
399 975 for f in patches:
400 976 ctype, gp = patches[f]
401 977 if gp and gp.mode:
402 978 x = gp.mode & 0100 != 0
403 979 dst = os.path.join(repo.root, gp.path)
404 980 # patch won't create empty files
405 981 if ctype == 'ADD' and not os.path.exists(dst):
406 982 repo.wwrite(gp.path, '', x and 'x' or '')
407 983 else:
408 984 util.set_exec(dst, x)
409 985 cmdutil.addremove(repo, cfiles, wlock=wlock)
410 986 files = patches.keys()
411 987 files.extend([r for r in removes if r not in files])
412 988 files.sort()
413 989
414 990 return files
415 991
416 992 def b85diff(fp, to, tn):
417 993 '''print base85-encoded binary diff'''
418 994 def gitindex(text):
419 995 if not text:
420 996 return '0' * 40
421 997 l = len(text)
422 998 s = sha.new('blob %d\0' % l)
423 999 s.update(text)
424 1000 return s.hexdigest()
425 1001
426 1002 def fmtline(line):
427 1003 l = len(line)
428 1004 if l <= 26:
429 1005 l = chr(ord('A') + l - 1)
430 1006 else:
431 1007 l = chr(l - 26 + ord('a') - 1)
432 1008 return '%c%s\n' % (l, base85.b85encode(line, True))
433 1009
434 1010 def chunk(text, csize=52):
435 1011 l = len(text)
436 1012 i = 0
437 1013 while i < l:
438 1014 yield text[i:i+csize]
439 1015 i += csize
440 1016
441 1017 tohash = gitindex(to)
442 1018 tnhash = gitindex(tn)
443 1019 if tohash == tnhash:
444 1020 return ""
445 1021
446 1022 # TODO: deltas
447 1023 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
448 1024 (tohash, tnhash, len(tn))]
449 1025 for l in chunk(zlib.compress(tn)):
450 1026 ret.append(fmtline(l))
451 1027 ret.append('\n')
452 1028 return ''.join(ret)
453 1029
454 1030 def diff(repo, node1=None, node2=None, files=None, match=util.always,
455 1031 fp=None, changes=None, opts=None):
456 1032 '''print diff of changes to files between two nodes, or node and
457 1033 working directory.
458 1034
459 1035 if node1 is None, use first dirstate parent instead.
460 1036 if node2 is None, compare node1 with working directory.'''
461 1037
462 1038 if opts is None:
463 1039 opts = mdiff.defaultopts
464 1040 if fp is None:
465 1041 fp = repo.ui
466 1042
467 1043 if not node1:
468 1044 node1 = repo.dirstate.parents()[0]
469 1045
470 1046 ccache = {}
471 1047 def getctx(r):
472 1048 if r not in ccache:
473 1049 ccache[r] = context.changectx(repo, r)
474 1050 return ccache[r]
475 1051
476 1052 flcache = {}
477 1053 def getfilectx(f, ctx):
478 1054 flctx = ctx.filectx(f, filelog=flcache.get(f))
479 1055 if f not in flcache:
480 1056 flcache[f] = flctx._filelog
481 1057 return flctx
482 1058
483 1059 # reading the data for node1 early allows it to play nicely
484 1060 # with repo.status and the revlog cache.
485 1061 ctx1 = context.changectx(repo, node1)
486 1062 # force manifest reading
487 1063 man1 = ctx1.manifest()
488 1064 date1 = util.datestr(ctx1.date())
489 1065
490 1066 if not changes:
491 1067 changes = repo.status(node1, node2, files, match=match)[:5]
492 1068 modified, added, removed, deleted, unknown = changes
493 1069
494 1070 if not modified and not added and not removed:
495 1071 return
496 1072
497 1073 if node2:
498 1074 ctx2 = context.changectx(repo, node2)
499 1075 execf2 = ctx2.manifest().execf
500 1076 else:
501 1077 ctx2 = context.workingctx(repo)
502 1078 execf2 = util.execfunc(repo.root, None)
503 1079 if execf2 is None:
504 1080 execf2 = ctx2.parents()[0].manifest().copy().execf
505 1081
506 1082 # returns False if there was no rename between ctx1 and ctx2
507 1083 # returns None if the file was created between ctx1 and ctx2
508 1084 # returns the (file, node) present in ctx1 that was renamed to f in ctx2
509 1085 def renamed(f):
510 1086 startrev = ctx1.rev()
511 1087 c = ctx2
512 1088 crev = c.rev()
513 1089 if crev is None:
514 1090 crev = repo.changelog.count()
515 1091 orig = f
516 1092 while crev > startrev:
517 1093 if f in c.files():
518 1094 try:
519 1095 src = getfilectx(f, c).renamed()
520 1096 except revlog.LookupError:
521 1097 return None
522 1098 if src:
523 1099 f = src[0]
524 1100 crev = c.parents()[0].rev()
525 1101 # try to reuse
526 1102 c = getctx(crev)
527 1103 if f not in man1:
528 1104 return None
529 1105 if f == orig:
530 1106 return False
531 1107 return f
532 1108
533 1109 if repo.ui.quiet:
534 1110 r = None
535 1111 else:
536 1112 hexfunc = repo.ui.debugflag and hex or short
537 1113 r = [hexfunc(node) for node in [node1, node2] if node]
538 1114
539 1115 if opts.git:
540 1116 copied = {}
541 1117 for f in added:
542 1118 src = renamed(f)
543 1119 if src:
544 1120 copied[f] = src
545 1121 srcs = [x[1] for x in copied.items()]
546 1122
547 1123 all = modified + added + removed
548 1124 all.sort()
549 1125 gone = {}
550 1126
551 1127 for f in all:
552 1128 to = None
553 1129 tn = None
554 1130 dodiff = True
555 1131 header = []
556 1132 if f in man1:
557 1133 to = getfilectx(f, ctx1).data()
558 1134 if f not in removed:
559 1135 tn = getfilectx(f, ctx2).data()
560 1136 if opts.git:
561 1137 def gitmode(x):
562 1138 return x and '100755' or '100644'
563 1139 def addmodehdr(header, omode, nmode):
564 1140 if omode != nmode:
565 1141 header.append('old mode %s\n' % omode)
566 1142 header.append('new mode %s\n' % nmode)
567 1143
568 1144 a, b = f, f
569 1145 if f in added:
570 1146 mode = gitmode(execf2(f))
571 1147 if f in copied:
572 1148 a = copied[f]
573 1149 omode = gitmode(man1.execf(a))
574 1150 addmodehdr(header, omode, mode)
575 1151 if a in removed and a not in gone:
576 1152 op = 'rename'
577 1153 gone[a] = 1
578 1154 else:
579 1155 op = 'copy'
580 1156 header.append('%s from %s\n' % (op, a))
581 1157 header.append('%s to %s\n' % (op, f))
582 1158 to = getfilectx(a, ctx1).data()
583 1159 else:
584 1160 header.append('new file mode %s\n' % mode)
585 1161 if util.binary(tn):
586 1162 dodiff = 'binary'
587 1163 elif f in removed:
588 1164 if f in srcs:
589 1165 dodiff = False
590 1166 else:
591 1167 mode = gitmode(man1.execf(f))
592 1168 header.append('deleted file mode %s\n' % mode)
593 1169 else:
594 1170 omode = gitmode(man1.execf(f))
595 1171 nmode = gitmode(execf2(f))
596 1172 addmodehdr(header, omode, nmode)
597 1173 if util.binary(to) or util.binary(tn):
598 1174 dodiff = 'binary'
599 1175 r = None
600 1176 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
601 1177 if dodiff:
602 1178 if dodiff == 'binary':
603 1179 text = b85diff(fp, to, tn)
604 1180 else:
605 1181 text = mdiff.unidiff(to, date1,
606 1182 # ctx2 date may be dynamic
607 1183 tn, util.datestr(ctx2.date()),
608 1184 f, r, opts=opts)
609 1185 if text or len(header) > 1:
610 1186 fp.write(''.join(header))
611 1187 fp.write(text)
612 1188
613 1189 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
614 1190 opts=None):
615 1191 '''export changesets as hg patches.'''
616 1192
617 1193 total = len(revs)
618 1194 revwidth = max([len(str(rev)) for rev in revs])
619 1195
620 1196 def single(rev, seqno, fp):
621 1197 ctx = repo.changectx(rev)
622 1198 node = ctx.node()
623 1199 parents = [p.node() for p in ctx.parents() if p]
624 1200 branch = ctx.branch()
625 1201 if switch_parent:
626 1202 parents.reverse()
627 1203 prev = (parents and parents[0]) or nullid
628 1204
629 1205 if not fp:
630 1206 fp = cmdutil.make_file(repo, template, node, total=total,
631 1207 seqno=seqno, revwidth=revwidth)
632 1208 if fp != sys.stdout and hasattr(fp, 'name'):
633 1209 repo.ui.note("%s\n" % fp.name)
634 1210
635 1211 fp.write("# HG changeset patch\n")
636 1212 fp.write("# User %s\n" % ctx.user())
637 1213 fp.write("# Date %d %d\n" % ctx.date())
638 1214 if branch and (branch != 'default'):
639 1215 fp.write("# Branch %s\n" % branch)
640 1216 fp.write("# Node ID %s\n" % hex(node))
641 1217 fp.write("# Parent %s\n" % hex(prev))
642 1218 if len(parents) > 1:
643 1219 fp.write("# Parent %s\n" % hex(parents[1]))
644 1220 fp.write(ctx.description().rstrip())
645 1221 fp.write("\n\n")
646 1222
647 1223 diff(repo, prev, node, fp=fp, opts=opts)
648 1224 if fp not in (sys.stdout, repo.ui):
649 1225 fp.close()
650 1226
651 1227 for seqno, rev in enumerate(revs):
652 1228 single(rev, seqno+1, fp)
653 1229
654 1230 def diffstat(patchlines):
655 1231 if not util.find_exe('diffstat'):
656 1232 return
657 1233 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
658 1234 try:
659 1235 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
660 1236 try:
661 1237 for line in patchlines: print >> p.tochild, line
662 1238 p.tochild.close()
663 1239 if p.wait(): return
664 1240 fp = os.fdopen(fd, 'r')
665 1241 stat = []
666 1242 for line in fp: stat.append(line.lstrip())
667 1243 last = stat.pop()
668 1244 stat.insert(0, last)
669 1245 stat = ''.join(stat)
670 1246 if stat.startswith('0 files'): raise ValueError
671 1247 return stat
672 1248 except: raise
673 1249 finally:
674 1250 try: os.unlink(name)
675 1251 except: pass
@@ -1,78 +1,79 b''
1 1 #!/usr/bin/env python
2 2 #
3 3 # This is the mercurial setup script.
4 4 #
5 5 # 'python setup.py install', or
6 6 # 'python setup.py --help' for more options
7 7
8 8 import sys
9 9 if not hasattr(sys, 'version_info') or sys.version_info < (2, 3, 0, 'final'):
10 10 raise SystemExit, "Mercurial requires python 2.3 or later."
11 11
12 12 import os
13 13 from distutils.core import setup, Extension
14 14 from distutils.command.install_data import install_data
15 15
16 16 import mercurial.version
17 17 import mercurial.demandimport
18 18 mercurial.demandimport.enable = lambda: None
19 19
20 20 extra = {}
21 21
22 22 # py2exe needs to be installed to work
23 23 try:
24 24 import py2exe
25 25
26 26 # Help py2exe to find win32com.shell
27 27 try:
28 28 import modulefinder
29 29 import win32com
30 30 for p in win32com.__path__[1:]: # Take the path to win32comext
31 31 modulefinder.AddPackagePath("win32com", p)
32 32 pn = "win32com.shell"
33 33 __import__(pn)
34 34 m = sys.modules[pn]
35 35 for p in m.__path__[1:]:
36 36 modulefinder.AddPackagePath(pn, p)
37 37 except ImportError:
38 38 pass
39 39
40 40 extra['console'] = ['hg']
41 41
42 42 except ImportError:
43 43 pass
44 44
45 45 # specify version string, otherwise 'hg identify' will be used:
46 46 version = ''
47 47
48 48 class install_package_data(install_data):
49 49 def finalize_options(self):
50 50 self.set_undefined_options('install',
51 51 ('install_lib', 'install_dir'))
52 52 install_data.finalize_options(self)
53 53
54 54 mercurial.version.remember_version(version)
55 55 cmdclass = {'install_data': install_package_data}
56 56
57 57 setup(name='mercurial',
58 58 version=mercurial.version.get_version(),
59 59 author='Matt Mackall',
60 60 author_email='mpm@selenic.com',
61 61 url='http://selenic.com/mercurial',
62 62 description='Scalable distributed SCM',
63 63 license='GNU GPL',
64 64 packages=['mercurial', 'mercurial.hgweb', 'hgext', 'hgext.convert'],
65 65 ext_modules=[Extension('mercurial.mpatch', ['mercurial/mpatch.c']),
66 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 69 data_files=[(os.path.join('mercurial', root),
69 70 [os.path.join(root, file_) for file_ in files])
70 71 for root, dirs, files in os.walk('templates')],
71 72 cmdclass=cmdclass,
72 73 scripts=['hg', 'hgmerge'],
73 74 options=dict(py2exe=dict(packages=['hgext']),
74 75 bdist_mpkg=dict(zipdist=True,
75 76 license='COPYING',
76 77 readme='contrib/macosx/Readme.html',
77 78 welcome='contrib/macosx/Welcome.html')),
78 79 **extra)
General Comments 0
You need to be logged in to leave comments. Login now