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