##// 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,16 +1,20 b''
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):
@@ -135,7 +139,7 b' GP_PATCH = 1 << 0 # we have to run pat'
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"
@@ -148,16 +152,20 b' def readgitpatch(patchname):'
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)
@@ -204,157 +212,725 b' def readgitpatch(patchname):'
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):
@@ -64,7 +64,8 b" setup(name='mercurial',"
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')],
General Comments 0
You need to be logged in to leave comments. Login now