##// END OF EJS Templates
patch: merge backend setmode() into writelines()...
Patrick Mezard -
r14390:ce77c275 default
parent child Browse files
Show More
@@ -1,1778 +1,1773 b''
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 import cStringIO, email.Parser, os, errno, re
10 10 import tempfile, zlib
11 11
12 12 from i18n import _
13 13 from node import hex, nullid, short
14 14 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding
15 15
16 16 gitre = re.compile('diff --git a/(.*) b/(.*)')
17 17
18 18 class PatchError(Exception):
19 19 pass
20 20
21 21
22 22 # public functions
23 23
24 24 def split(stream):
25 25 '''return an iterator of individual patches from a stream'''
26 26 def isheader(line, inheader):
27 27 if inheader and line[0] in (' ', '\t'):
28 28 # continuation
29 29 return True
30 30 if line[0] in (' ', '-', '+'):
31 31 # diff line - don't check for header pattern in there
32 32 return False
33 33 l = line.split(': ', 1)
34 34 return len(l) == 2 and ' ' not in l[0]
35 35
36 36 def chunk(lines):
37 37 return cStringIO.StringIO(''.join(lines))
38 38
39 39 def hgsplit(stream, cur):
40 40 inheader = True
41 41
42 42 for line in stream:
43 43 if not line.strip():
44 44 inheader = False
45 45 if not inheader and line.startswith('# HG changeset patch'):
46 46 yield chunk(cur)
47 47 cur = []
48 48 inheader = True
49 49
50 50 cur.append(line)
51 51
52 52 if cur:
53 53 yield chunk(cur)
54 54
55 55 def mboxsplit(stream, cur):
56 56 for line in stream:
57 57 if line.startswith('From '):
58 58 for c in split(chunk(cur[1:])):
59 59 yield c
60 60 cur = []
61 61
62 62 cur.append(line)
63 63
64 64 if cur:
65 65 for c in split(chunk(cur[1:])):
66 66 yield c
67 67
68 68 def mimesplit(stream, cur):
69 69 def msgfp(m):
70 70 fp = cStringIO.StringIO()
71 71 g = email.Generator.Generator(fp, mangle_from_=False)
72 72 g.flatten(m)
73 73 fp.seek(0)
74 74 return fp
75 75
76 76 for line in stream:
77 77 cur.append(line)
78 78 c = chunk(cur)
79 79
80 80 m = email.Parser.Parser().parse(c)
81 81 if not m.is_multipart():
82 82 yield msgfp(m)
83 83 else:
84 84 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
85 85 for part in m.walk():
86 86 ct = part.get_content_type()
87 87 if ct not in ok_types:
88 88 continue
89 89 yield msgfp(part)
90 90
91 91 def headersplit(stream, cur):
92 92 inheader = False
93 93
94 94 for line in stream:
95 95 if not inheader and isheader(line, inheader):
96 96 yield chunk(cur)
97 97 cur = []
98 98 inheader = True
99 99 if inheader and not isheader(line, inheader):
100 100 inheader = False
101 101
102 102 cur.append(line)
103 103
104 104 if cur:
105 105 yield chunk(cur)
106 106
107 107 def remainder(cur):
108 108 yield chunk(cur)
109 109
110 110 class fiter(object):
111 111 def __init__(self, fp):
112 112 self.fp = fp
113 113
114 114 def __iter__(self):
115 115 return self
116 116
117 117 def next(self):
118 118 l = self.fp.readline()
119 119 if not l:
120 120 raise StopIteration
121 121 return l
122 122
123 123 inheader = False
124 124 cur = []
125 125
126 126 mimeheaders = ['content-type']
127 127
128 128 if not hasattr(stream, 'next'):
129 129 # http responses, for example, have readline but not next
130 130 stream = fiter(stream)
131 131
132 132 for line in stream:
133 133 cur.append(line)
134 134 if line.startswith('# HG changeset patch'):
135 135 return hgsplit(stream, cur)
136 136 elif line.startswith('From '):
137 137 return mboxsplit(stream, cur)
138 138 elif isheader(line, inheader):
139 139 inheader = True
140 140 if line.split(':', 1)[0].lower() in mimeheaders:
141 141 # let email parser handle this
142 142 return mimesplit(stream, cur)
143 143 elif line.startswith('--- ') and inheader:
144 144 # No evil headers seen by diff start, split by hand
145 145 return headersplit(stream, cur)
146 146 # Not enough info, keep reading
147 147
148 148 # if we are here, we have a very plain patch
149 149 return remainder(cur)
150 150
151 151 def extract(ui, fileobj):
152 152 '''extract patch from data read from fileobj.
153 153
154 154 patch can be a normal patch or contained in an email message.
155 155
156 156 return tuple (filename, message, user, date, branch, node, p1, p2).
157 157 Any item in the returned tuple can be None. If filename is None,
158 158 fileobj did not contain a patch. Caller must unlink filename when done.'''
159 159
160 160 # attempt to detect the start of a patch
161 161 # (this heuristic is borrowed from quilt)
162 162 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
163 163 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
164 164 r'---[ \t].*?^\+\+\+[ \t]|'
165 165 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
166 166
167 167 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
168 168 tmpfp = os.fdopen(fd, 'w')
169 169 try:
170 170 msg = email.Parser.Parser().parse(fileobj)
171 171
172 172 subject = msg['Subject']
173 173 user = msg['From']
174 174 if not subject and not user:
175 175 # Not an email, restore parsed headers if any
176 176 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
177 177
178 178 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
179 179 # should try to parse msg['Date']
180 180 date = None
181 181 nodeid = None
182 182 branch = None
183 183 parents = []
184 184
185 185 if subject:
186 186 if subject.startswith('[PATCH'):
187 187 pend = subject.find(']')
188 188 if pend >= 0:
189 189 subject = subject[pend + 1:].lstrip()
190 190 subject = subject.replace('\n\t', ' ')
191 191 ui.debug('Subject: %s\n' % subject)
192 192 if user:
193 193 ui.debug('From: %s\n' % user)
194 194 diffs_seen = 0
195 195 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
196 196 message = ''
197 197 for part in msg.walk():
198 198 content_type = part.get_content_type()
199 199 ui.debug('Content-Type: %s\n' % content_type)
200 200 if content_type not in ok_types:
201 201 continue
202 202 payload = part.get_payload(decode=True)
203 203 m = diffre.search(payload)
204 204 if m:
205 205 hgpatch = False
206 206 hgpatchheader = False
207 207 ignoretext = False
208 208
209 209 ui.debug('found patch at byte %d\n' % m.start(0))
210 210 diffs_seen += 1
211 211 cfp = cStringIO.StringIO()
212 212 for line in payload[:m.start(0)].splitlines():
213 213 if line.startswith('# HG changeset patch') and not hgpatch:
214 214 ui.debug('patch generated by hg export\n')
215 215 hgpatch = True
216 216 hgpatchheader = True
217 217 # drop earlier commit message content
218 218 cfp.seek(0)
219 219 cfp.truncate()
220 220 subject = None
221 221 elif hgpatchheader:
222 222 if line.startswith('# User '):
223 223 user = line[7:]
224 224 ui.debug('From: %s\n' % user)
225 225 elif line.startswith("# Date "):
226 226 date = line[7:]
227 227 elif line.startswith("# Branch "):
228 228 branch = line[9:]
229 229 elif line.startswith("# Node ID "):
230 230 nodeid = line[10:]
231 231 elif line.startswith("# Parent "):
232 232 parents.append(line[10:])
233 233 elif not line.startswith("# "):
234 234 hgpatchheader = False
235 235 elif line == '---' and gitsendmail:
236 236 ignoretext = True
237 237 if not hgpatchheader and not ignoretext:
238 238 cfp.write(line)
239 239 cfp.write('\n')
240 240 message = cfp.getvalue()
241 241 if tmpfp:
242 242 tmpfp.write(payload)
243 243 if not payload.endswith('\n'):
244 244 tmpfp.write('\n')
245 245 elif not diffs_seen and message and content_type == 'text/plain':
246 246 message += '\n' + payload
247 247 except:
248 248 tmpfp.close()
249 249 os.unlink(tmpname)
250 250 raise
251 251
252 252 if subject and not message.startswith(subject):
253 253 message = '%s\n%s' % (subject, message)
254 254 tmpfp.close()
255 255 if not diffs_seen:
256 256 os.unlink(tmpname)
257 257 return None, message, user, date, branch, None, None, None
258 258 p1 = parents and parents.pop(0) or None
259 259 p2 = parents and parents.pop(0) or None
260 260 return tmpname, message, user, date, branch, nodeid, p1, p2
261 261
262 262 class patchmeta(object):
263 263 """Patched file metadata
264 264
265 265 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
266 266 or COPY. 'path' is patched file path. 'oldpath' is set to the
267 267 origin file when 'op' is either COPY or RENAME, None otherwise. If
268 268 file mode is changed, 'mode' is a tuple (islink, isexec) where
269 269 'islink' is True if the file is a symlink and 'isexec' is True if
270 270 the file is executable. Otherwise, 'mode' is None.
271 271 """
272 272 def __init__(self, path):
273 273 self.path = path
274 274 self.oldpath = None
275 275 self.mode = None
276 276 self.op = 'MODIFY'
277 277 self.binary = False
278 278
279 279 def setmode(self, mode):
280 280 islink = mode & 020000
281 281 isexec = mode & 0100
282 282 self.mode = (islink, isexec)
283 283
284 284 def __repr__(self):
285 285 return "<patchmeta %s %r>" % (self.op, self.path)
286 286
287 287 def readgitpatch(lr):
288 288 """extract git-style metadata about patches from <patchname>"""
289 289
290 290 # Filter patch for git information
291 291 gp = None
292 292 gitpatches = []
293 293 for line in lr:
294 294 line = line.rstrip(' \r\n')
295 295 if line.startswith('diff --git'):
296 296 m = gitre.match(line)
297 297 if m:
298 298 if gp:
299 299 gitpatches.append(gp)
300 300 dst = m.group(2)
301 301 gp = patchmeta(dst)
302 302 elif gp:
303 303 if line.startswith('--- '):
304 304 gitpatches.append(gp)
305 305 gp = None
306 306 continue
307 307 if line.startswith('rename from '):
308 308 gp.op = 'RENAME'
309 309 gp.oldpath = line[12:]
310 310 elif line.startswith('rename to '):
311 311 gp.path = line[10:]
312 312 elif line.startswith('copy from '):
313 313 gp.op = 'COPY'
314 314 gp.oldpath = line[10:]
315 315 elif line.startswith('copy to '):
316 316 gp.path = line[8:]
317 317 elif line.startswith('deleted file'):
318 318 gp.op = 'DELETE'
319 319 elif line.startswith('new file mode '):
320 320 gp.op = 'ADD'
321 321 gp.setmode(int(line[-6:], 8))
322 322 elif line.startswith('new mode '):
323 323 gp.setmode(int(line[-6:], 8))
324 324 elif line.startswith('GIT binary patch'):
325 325 gp.binary = True
326 326 if gp:
327 327 gitpatches.append(gp)
328 328
329 329 return gitpatches
330 330
331 331 class linereader(object):
332 332 # simple class to allow pushing lines back into the input stream
333 333 def __init__(self, fp, textmode=False):
334 334 self.fp = fp
335 335 self.buf = []
336 336 self.textmode = textmode
337 337 self.eol = None
338 338
339 339 def push(self, line):
340 340 if line is not None:
341 341 self.buf.append(line)
342 342
343 343 def readline(self):
344 344 if self.buf:
345 345 l = self.buf[0]
346 346 del self.buf[0]
347 347 return l
348 348 l = self.fp.readline()
349 349 if not self.eol:
350 350 if l.endswith('\r\n'):
351 351 self.eol = '\r\n'
352 352 elif l.endswith('\n'):
353 353 self.eol = '\n'
354 354 if self.textmode and l.endswith('\r\n'):
355 355 l = l[:-2] + '\n'
356 356 return l
357 357
358 358 def __iter__(self):
359 359 while 1:
360 360 l = self.readline()
361 361 if not l:
362 362 break
363 363 yield l
364 364
365 365 class abstractbackend(object):
366 366 def __init__(self, ui):
367 367 self.ui = ui
368 368
369 369 def readlines(self, fname):
370 370 """Return target file lines, or its content as a single line
371 371 for symlinks.
372 372 """
373 373 raise NotImplementedError
374 374
375 def writelines(self, fname, lines, mode):
375 def setfile(self, fname, lines, mode):
376 376 """Write lines to target file. mode is a (islink, isexec)
377 tuple, or None if there is no mode information.
377 tuple, or None if there is no mode information. If lines is None,
378 the file must exists and its content is left unchanged.
378 379 """
379 380 raise NotImplementedError
380 381
381 382 def unlink(self, fname):
382 383 """Unlink target file."""
383 384 raise NotImplementedError
384 385
385 386 def writerej(self, fname, failed, total, lines):
386 387 """Write rejected lines for fname. total is the number of hunks
387 388 which failed to apply and total the total number of hunks for this
388 389 files.
389 390 """
390 391 pass
391 392
392 393 def copy(self, src, dst):
393 394 """Copy src file into dst file. Create intermediate directories if
394 395 necessary. Files are specified relatively to the patching base
395 396 directory.
396 397 """
397 398 raise NotImplementedError
398 399
399 400 def exists(self, fname):
400 401 raise NotImplementedError
401 402
402 def setmode(self, fname, islink, isexec):
403 """Change target file mode."""
404 raise NotImplementedError
405
406 403 class fsbackend(abstractbackend):
407 404 def __init__(self, ui, basedir):
408 405 super(fsbackend, self).__init__(ui)
409 406 self.opener = scmutil.opener(basedir)
410 407
411 408 def _join(self, f):
412 409 return os.path.join(self.opener.base, f)
413 410
414 411 def readlines(self, fname):
415 412 if os.path.islink(self._join(fname)):
416 413 return [os.readlink(self._join(fname))]
417 414 fp = self.opener(fname, 'r')
418 415 try:
419 416 return list(fp)
420 417 finally:
421 418 fp.close()
422 419
423 def writelines(self, fname, lines, mode):
420 def setfile(self, fname, lines, mode):
421 if lines is None:
422 if mode:
423 util.setflags(self._join(fname), mode[0], mode[1])
424 return
424 425 if not mode:
425 426 # Preserve mode information
426 427 isexec, islink = False, False
427 428 try:
428 429 isexec = os.lstat(self._join(fname)).st_mode & 0100 != 0
429 430 islink = os.path.islink(self._join(fname))
430 431 except OSError, e:
431 432 if e.errno != errno.ENOENT:
432 433 raise
433 434 else:
434 435 islink, isexec = mode
435 436 if islink:
436 437 self.opener.symlink(''.join(lines), fname)
437 438 else:
438 439 self.opener(fname, 'w').writelines(lines)
439 440 if isexec:
440 441 util.setflags(self._join(fname), False, True)
441 442
442 443 def unlink(self, fname):
443 444 try:
444 445 util.unlinkpath(self._join(fname))
445 446 except OSError, inst:
446 447 if inst.errno != errno.ENOENT:
447 448 raise
448 449
449 450 def writerej(self, fname, failed, total, lines):
450 451 fname = fname + ".rej"
451 452 self.ui.warn(
452 453 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
453 454 (failed, total, fname))
454 455 fp = self.opener(fname, 'w')
455 456 fp.writelines(lines)
456 457 fp.close()
457 458
458 459 def copy(self, src, dst):
459 460 basedir = self.opener.base
460 461 abssrc, absdst = [scmutil.canonpath(basedir, basedir, x)
461 462 for x in [src, dst]]
462 463 if os.path.lexists(absdst):
463 464 raise util.Abort(_("cannot create %s: destination already exists")
464 465 % dst)
465 466 dstdir = os.path.dirname(absdst)
466 467 if dstdir and not os.path.isdir(dstdir):
467 468 try:
468 469 os.makedirs(dstdir)
469 470 except IOError:
470 471 raise util.Abort(
471 472 _("cannot create %s: unable to create destination directory")
472 473 % dst)
473 474 util.copyfile(abssrc, absdst)
474 475
475 476 def exists(self, fname):
476 477 return os.path.lexists(self._join(fname))
477 478
478 def setmode(self, fname, islink, isexec):
479 util.setflags(self._join(fname), islink, isexec)
480
481 479 class workingbackend(fsbackend):
482 480 def __init__(self, ui, repo, similarity):
483 481 super(workingbackend, self).__init__(ui, repo.root)
484 482 self.repo = repo
485 483 self.similarity = similarity
486 484 self.removed = set()
487 485 self.changed = set()
488 486 self.copied = []
489 487
490 def writelines(self, fname, lines, mode):
491 super(workingbackend, self).writelines(fname, lines, mode)
488 def setfile(self, fname, lines, mode):
489 super(workingbackend, self).setfile(fname, lines, mode)
492 490 self.changed.add(fname)
493 491
494 492 def unlink(self, fname):
495 493 super(workingbackend, self).unlink(fname)
496 494 self.removed.add(fname)
497 495 self.changed.add(fname)
498 496
499 497 def copy(self, src, dst):
500 498 super(workingbackend, self).copy(src, dst)
501 499 self.copied.append((src, dst))
502 500 self.changed.add(dst)
503 501
504 def setmode(self, fname, islink, isexec):
505 super(workingbackend, self).setmode(fname, islink, isexec)
506 self.changed.add(fname)
507
508 502 def close(self):
509 503 wctx = self.repo[None]
510 504 addremoved = set(self.changed)
511 505 for src, dst in self.copied:
512 506 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
513 507 addremoved.discard(src)
514 508 if (not self.similarity) and self.removed:
515 509 wctx.remove(sorted(self.removed))
516 510 if addremoved:
517 511 cwd = self.repo.getcwd()
518 512 if cwd:
519 513 addremoved = [util.pathto(self.repo.root, cwd, f)
520 514 for f in addremoved]
521 515 scmutil.addremove(self.repo, addremoved, similarity=self.similarity)
522 516 return sorted(self.changed)
523 517
524 518 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
525 519 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
526 520 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
527 521 eolmodes = ['strict', 'crlf', 'lf', 'auto']
528 522
529 523 class patchfile(object):
530 524 def __init__(self, ui, fname, backend, mode, missing=False,
531 525 eolmode='strict'):
532 526 self.fname = fname
533 527 self.eolmode = eolmode
534 528 self.eol = None
535 529 self.backend = backend
536 530 self.ui = ui
537 531 self.lines = []
538 532 self.exists = False
539 533 self.missing = missing
540 534 self.mode = mode
541 535 if not missing:
542 536 try:
543 537 self.lines = self.backend.readlines(fname)
544 538 if self.lines:
545 539 # Normalize line endings
546 540 if self.lines[0].endswith('\r\n'):
547 541 self.eol = '\r\n'
548 542 elif self.lines[0].endswith('\n'):
549 543 self.eol = '\n'
550 544 if eolmode != 'strict':
551 545 nlines = []
552 546 for l in self.lines:
553 547 if l.endswith('\r\n'):
554 548 l = l[:-2] + '\n'
555 549 nlines.append(l)
556 550 self.lines = nlines
557 551 self.exists = True
558 552 except IOError:
559 553 pass
560 554 else:
561 555 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
562 556
563 557 self.hash = {}
564 558 self.dirty = 0
565 559 self.offset = 0
566 560 self.skew = 0
567 561 self.rej = []
568 562 self.fileprinted = False
569 563 self.printfile(False)
570 564 self.hunks = 0
571 565
572 566 def writelines(self, fname, lines, mode):
573 567 if self.eolmode == 'auto':
574 568 eol = self.eol
575 569 elif self.eolmode == 'crlf':
576 570 eol = '\r\n'
577 571 else:
578 572 eol = '\n'
579 573
580 574 if self.eolmode != 'strict' and eol and eol != '\n':
581 575 rawlines = []
582 576 for l in lines:
583 577 if l and l[-1] == '\n':
584 578 l = l[:-1] + eol
585 579 rawlines.append(l)
586 580 lines = rawlines
587 581
588 self.backend.writelines(fname, lines, mode)
582 self.backend.setfile(fname, lines, mode)
589 583
590 584 def printfile(self, warn):
591 585 if self.fileprinted:
592 586 return
593 587 if warn or self.ui.verbose:
594 588 self.fileprinted = True
595 589 s = _("patching file %s\n") % self.fname
596 590 if warn:
597 591 self.ui.warn(s)
598 592 else:
599 593 self.ui.note(s)
600 594
601 595
602 596 def findlines(self, l, linenum):
603 597 # looks through the hash and finds candidate lines. The
604 598 # result is a list of line numbers sorted based on distance
605 599 # from linenum
606 600
607 601 cand = self.hash.get(l, [])
608 602 if len(cand) > 1:
609 603 # resort our list of potentials forward then back.
610 604 cand.sort(key=lambda x: abs(x - linenum))
611 605 return cand
612 606
613 607 def write_rej(self):
614 608 # our rejects are a little different from patch(1). This always
615 609 # creates rejects in the same form as the original patch. A file
616 610 # header is inserted so that you can run the reject through patch again
617 611 # without having to type the filename.
618 612 if not self.rej:
619 613 return
620 614 base = os.path.basename(self.fname)
621 615 lines = ["--- %s\n+++ %s\n" % (base, base)]
622 616 for x in self.rej:
623 617 for l in x.hunk:
624 618 lines.append(l)
625 619 if l[-1] != '\n':
626 620 lines.append("\n\ No newline at end of file\n")
627 621 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
628 622
629 623 def apply(self, h):
630 624 if not h.complete():
631 625 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
632 626 (h.number, h.desc, len(h.a), h.lena, len(h.b),
633 627 h.lenb))
634 628
635 629 self.hunks += 1
636 630
637 631 if self.missing:
638 632 self.rej.append(h)
639 633 return -1
640 634
641 635 if self.exists and h.createfile():
642 636 self.ui.warn(_("file %s already exists\n") % self.fname)
643 637 self.rej.append(h)
644 638 return -1
645 639
646 640 if isinstance(h, binhunk):
647 641 if h.rmfile():
648 642 self.backend.unlink(self.fname)
649 643 else:
650 644 self.lines[:] = h.new()
651 645 self.offset += len(h.new())
652 646 self.dirty = True
653 647 return 0
654 648
655 649 horig = h
656 650 if (self.eolmode in ('crlf', 'lf')
657 651 or self.eolmode == 'auto' and self.eol):
658 652 # If new eols are going to be normalized, then normalize
659 653 # hunk data before patching. Otherwise, preserve input
660 654 # line-endings.
661 655 h = h.getnormalized()
662 656
663 657 # fast case first, no offsets, no fuzz
664 658 old = h.old()
665 659 # patch starts counting at 1 unless we are adding the file
666 660 if h.starta == 0:
667 661 start = 0
668 662 else:
669 663 start = h.starta + self.offset - 1
670 664 orig_start = start
671 665 # if there's skew we want to emit the "(offset %d lines)" even
672 666 # when the hunk cleanly applies at start + skew, so skip the
673 667 # fast case code
674 668 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
675 669 if h.rmfile():
676 670 self.backend.unlink(self.fname)
677 671 else:
678 672 self.lines[start : start + h.lena] = h.new()
679 673 self.offset += h.lenb - h.lena
680 674 self.dirty = True
681 675 return 0
682 676
683 677 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
684 678 self.hash = {}
685 679 for x, s in enumerate(self.lines):
686 680 self.hash.setdefault(s, []).append(x)
687 681 if h.hunk[-1][0] != ' ':
688 682 # if the hunk tried to put something at the bottom of the file
689 683 # override the start line and use eof here
690 684 search_start = len(self.lines)
691 685 else:
692 686 search_start = orig_start + self.skew
693 687
694 688 for fuzzlen in xrange(3):
695 689 for toponly in [True, False]:
696 690 old = h.old(fuzzlen, toponly)
697 691
698 692 cand = self.findlines(old[0][1:], search_start)
699 693 for l in cand:
700 694 if diffhelpers.testhunk(old, self.lines, l) == 0:
701 695 newlines = h.new(fuzzlen, toponly)
702 696 self.lines[l : l + len(old)] = newlines
703 697 self.offset += len(newlines) - len(old)
704 698 self.skew = l - orig_start
705 699 self.dirty = True
706 700 offset = l - orig_start - fuzzlen
707 701 if fuzzlen:
708 702 msg = _("Hunk #%d succeeded at %d "
709 703 "with fuzz %d "
710 704 "(offset %d lines).\n")
711 705 self.printfile(True)
712 706 self.ui.warn(msg %
713 707 (h.number, l + 1, fuzzlen, offset))
714 708 else:
715 709 msg = _("Hunk #%d succeeded at %d "
716 710 "(offset %d lines).\n")
717 711 self.ui.note(msg % (h.number, l + 1, offset))
718 712 return fuzzlen
719 713 self.printfile(True)
720 714 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
721 715 self.rej.append(horig)
722 716 return -1
723 717
724 718 def close(self):
725 719 if self.dirty:
726 720 self.writelines(self.fname, self.lines, self.mode)
727 721 self.write_rej()
728 722 return len(self.rej)
729 723
730 724 class hunk(object):
731 725 def __init__(self, desc, num, lr, context, create=False, remove=False):
732 726 self.number = num
733 727 self.desc = desc
734 728 self.hunk = [desc]
735 729 self.a = []
736 730 self.b = []
737 731 self.starta = self.lena = None
738 732 self.startb = self.lenb = None
739 733 if lr is not None:
740 734 if context:
741 735 self.read_context_hunk(lr)
742 736 else:
743 737 self.read_unified_hunk(lr)
744 738 self.create = create
745 739 self.remove = remove and not create
746 740
747 741 def getnormalized(self):
748 742 """Return a copy with line endings normalized to LF."""
749 743
750 744 def normalize(lines):
751 745 nlines = []
752 746 for line in lines:
753 747 if line.endswith('\r\n'):
754 748 line = line[:-2] + '\n'
755 749 nlines.append(line)
756 750 return nlines
757 751
758 752 # Dummy object, it is rebuilt manually
759 753 nh = hunk(self.desc, self.number, None, None, False, False)
760 754 nh.number = self.number
761 755 nh.desc = self.desc
762 756 nh.hunk = self.hunk
763 757 nh.a = normalize(self.a)
764 758 nh.b = normalize(self.b)
765 759 nh.starta = self.starta
766 760 nh.startb = self.startb
767 761 nh.lena = self.lena
768 762 nh.lenb = self.lenb
769 763 nh.create = self.create
770 764 nh.remove = self.remove
771 765 return nh
772 766
773 767 def read_unified_hunk(self, lr):
774 768 m = unidesc.match(self.desc)
775 769 if not m:
776 770 raise PatchError(_("bad hunk #%d") % self.number)
777 771 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
778 772 if self.lena is None:
779 773 self.lena = 1
780 774 else:
781 775 self.lena = int(self.lena)
782 776 if self.lenb is None:
783 777 self.lenb = 1
784 778 else:
785 779 self.lenb = int(self.lenb)
786 780 self.starta = int(self.starta)
787 781 self.startb = int(self.startb)
788 782 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
789 783 # if we hit eof before finishing out the hunk, the last line will
790 784 # be zero length. Lets try to fix it up.
791 785 while len(self.hunk[-1]) == 0:
792 786 del self.hunk[-1]
793 787 del self.a[-1]
794 788 del self.b[-1]
795 789 self.lena -= 1
796 790 self.lenb -= 1
797 791 self._fixnewline(lr)
798 792
799 793 def read_context_hunk(self, lr):
800 794 self.desc = lr.readline()
801 795 m = contextdesc.match(self.desc)
802 796 if not m:
803 797 raise PatchError(_("bad hunk #%d") % self.number)
804 798 foo, self.starta, foo2, aend, foo3 = m.groups()
805 799 self.starta = int(self.starta)
806 800 if aend is None:
807 801 aend = self.starta
808 802 self.lena = int(aend) - self.starta
809 803 if self.starta:
810 804 self.lena += 1
811 805 for x in xrange(self.lena):
812 806 l = lr.readline()
813 807 if l.startswith('---'):
814 808 # lines addition, old block is empty
815 809 lr.push(l)
816 810 break
817 811 s = l[2:]
818 812 if l.startswith('- ') or l.startswith('! '):
819 813 u = '-' + s
820 814 elif l.startswith(' '):
821 815 u = ' ' + s
822 816 else:
823 817 raise PatchError(_("bad hunk #%d old text line %d") %
824 818 (self.number, x))
825 819 self.a.append(u)
826 820 self.hunk.append(u)
827 821
828 822 l = lr.readline()
829 823 if l.startswith('\ '):
830 824 s = self.a[-1][:-1]
831 825 self.a[-1] = s
832 826 self.hunk[-1] = s
833 827 l = lr.readline()
834 828 m = contextdesc.match(l)
835 829 if not m:
836 830 raise PatchError(_("bad hunk #%d") % self.number)
837 831 foo, self.startb, foo2, bend, foo3 = m.groups()
838 832 self.startb = int(self.startb)
839 833 if bend is None:
840 834 bend = self.startb
841 835 self.lenb = int(bend) - self.startb
842 836 if self.startb:
843 837 self.lenb += 1
844 838 hunki = 1
845 839 for x in xrange(self.lenb):
846 840 l = lr.readline()
847 841 if l.startswith('\ '):
848 842 # XXX: the only way to hit this is with an invalid line range.
849 843 # The no-eol marker is not counted in the line range, but I
850 844 # guess there are diff(1) out there which behave differently.
851 845 s = self.b[-1][:-1]
852 846 self.b[-1] = s
853 847 self.hunk[hunki - 1] = s
854 848 continue
855 849 if not l:
856 850 # line deletions, new block is empty and we hit EOF
857 851 lr.push(l)
858 852 break
859 853 s = l[2:]
860 854 if l.startswith('+ ') or l.startswith('! '):
861 855 u = '+' + s
862 856 elif l.startswith(' '):
863 857 u = ' ' + s
864 858 elif len(self.b) == 0:
865 859 # line deletions, new block is empty
866 860 lr.push(l)
867 861 break
868 862 else:
869 863 raise PatchError(_("bad hunk #%d old text line %d") %
870 864 (self.number, x))
871 865 self.b.append(s)
872 866 while True:
873 867 if hunki >= len(self.hunk):
874 868 h = ""
875 869 else:
876 870 h = self.hunk[hunki]
877 871 hunki += 1
878 872 if h == u:
879 873 break
880 874 elif h.startswith('-'):
881 875 continue
882 876 else:
883 877 self.hunk.insert(hunki - 1, u)
884 878 break
885 879
886 880 if not self.a:
887 881 # this happens when lines were only added to the hunk
888 882 for x in self.hunk:
889 883 if x.startswith('-') or x.startswith(' '):
890 884 self.a.append(x)
891 885 if not self.b:
892 886 # this happens when lines were only deleted from the hunk
893 887 for x in self.hunk:
894 888 if x.startswith('+') or x.startswith(' '):
895 889 self.b.append(x[1:])
896 890 # @@ -start,len +start,len @@
897 891 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
898 892 self.startb, self.lenb)
899 893 self.hunk[0] = self.desc
900 894 self._fixnewline(lr)
901 895
902 896 def _fixnewline(self, lr):
903 897 l = lr.readline()
904 898 if l.startswith('\ '):
905 899 diffhelpers.fix_newline(self.hunk, self.a, self.b)
906 900 else:
907 901 lr.push(l)
908 902
909 903 def complete(self):
910 904 return len(self.a) == self.lena and len(self.b) == self.lenb
911 905
912 906 def createfile(self):
913 907 return self.starta == 0 and self.lena == 0 and self.create
914 908
915 909 def rmfile(self):
916 910 return self.startb == 0 and self.lenb == 0 and self.remove
917 911
918 912 def fuzzit(self, l, fuzz, toponly):
919 913 # this removes context lines from the top and bottom of list 'l'. It
920 914 # checks the hunk to make sure only context lines are removed, and then
921 915 # returns a new shortened list of lines.
922 916 fuzz = min(fuzz, len(l)-1)
923 917 if fuzz:
924 918 top = 0
925 919 bot = 0
926 920 hlen = len(self.hunk)
927 921 for x in xrange(hlen - 1):
928 922 # the hunk starts with the @@ line, so use x+1
929 923 if self.hunk[x + 1][0] == ' ':
930 924 top += 1
931 925 else:
932 926 break
933 927 if not toponly:
934 928 for x in xrange(hlen - 1):
935 929 if self.hunk[hlen - bot - 1][0] == ' ':
936 930 bot += 1
937 931 else:
938 932 break
939 933
940 934 # top and bot now count context in the hunk
941 935 # adjust them if either one is short
942 936 context = max(top, bot, 3)
943 937 if bot < context:
944 938 bot = max(0, fuzz - (context - bot))
945 939 else:
946 940 bot = min(fuzz, bot)
947 941 if top < context:
948 942 top = max(0, fuzz - (context - top))
949 943 else:
950 944 top = min(fuzz, top)
951 945
952 946 return l[top:len(l)-bot]
953 947 return l
954 948
955 949 def old(self, fuzz=0, toponly=False):
956 950 return self.fuzzit(self.a, fuzz, toponly)
957 951
958 952 def new(self, fuzz=0, toponly=False):
959 953 return self.fuzzit(self.b, fuzz, toponly)
960 954
961 955 class binhunk:
962 956 'A binary patch file. Only understands literals so far.'
963 957 def __init__(self, gitpatch, lr):
964 958 self.gitpatch = gitpatch
965 959 self.text = None
966 960 self.hunk = ['GIT binary patch\n']
967 961 self._read(lr)
968 962
969 963 def createfile(self):
970 964 return self.gitpatch.op == 'ADD'
971 965
972 966 def rmfile(self):
973 967 return self.gitpatch.op == 'DELETE'
974 968
975 969 def complete(self):
976 970 return self.text is not None
977 971
978 972 def new(self):
979 973 return [self.text]
980 974
981 975 def _read(self, lr):
982 976 line = lr.readline()
983 977 self.hunk.append(line)
984 978 while line and not line.startswith('literal '):
985 979 line = lr.readline()
986 980 self.hunk.append(line)
987 981 if not line:
988 982 raise PatchError(_('could not extract binary patch'))
989 983 size = int(line[8:].rstrip())
990 984 dec = []
991 985 line = lr.readline()
992 986 self.hunk.append(line)
993 987 while len(line) > 1:
994 988 l = line[0]
995 989 if l <= 'Z' and l >= 'A':
996 990 l = ord(l) - ord('A') + 1
997 991 else:
998 992 l = ord(l) - ord('a') + 27
999 993 dec.append(base85.b85decode(line[1:-1])[:l])
1000 994 line = lr.readline()
1001 995 self.hunk.append(line)
1002 996 text = zlib.decompress(''.join(dec))
1003 997 if len(text) != size:
1004 998 raise PatchError(_('binary patch is %d bytes, not %d') %
1005 999 len(text), size)
1006 1000 self.text = text
1007 1001
1008 1002 def parsefilename(str):
1009 1003 # --- filename \t|space stuff
1010 1004 s = str[4:].rstrip('\r\n')
1011 1005 i = s.find('\t')
1012 1006 if i < 0:
1013 1007 i = s.find(' ')
1014 1008 if i < 0:
1015 1009 return s
1016 1010 return s[:i]
1017 1011
1018 1012 def pathstrip(path, strip):
1019 1013 pathlen = len(path)
1020 1014 i = 0
1021 1015 if strip == 0:
1022 1016 return '', path.rstrip()
1023 1017 count = strip
1024 1018 while count > 0:
1025 1019 i = path.find('/', i)
1026 1020 if i == -1:
1027 1021 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1028 1022 (count, strip, path))
1029 1023 i += 1
1030 1024 # consume '//' in the path
1031 1025 while i < pathlen - 1 and path[i] == '/':
1032 1026 i += 1
1033 1027 count -= 1
1034 1028 return path[:i].lstrip(), path[i:].rstrip()
1035 1029
1036 1030 def selectfile(backend, afile_orig, bfile_orig, hunk, strip):
1037 1031 nulla = afile_orig == "/dev/null"
1038 1032 nullb = bfile_orig == "/dev/null"
1039 1033 abase, afile = pathstrip(afile_orig, strip)
1040 1034 gooda = not nulla and backend.exists(afile)
1041 1035 bbase, bfile = pathstrip(bfile_orig, strip)
1042 1036 if afile == bfile:
1043 1037 goodb = gooda
1044 1038 else:
1045 1039 goodb = not nullb and backend.exists(bfile)
1046 1040 createfunc = hunk.createfile
1047 1041 missing = not goodb and not gooda and not createfunc()
1048 1042
1049 1043 # some diff programs apparently produce patches where the afile is
1050 1044 # not /dev/null, but afile starts with bfile
1051 1045 abasedir = afile[:afile.rfind('/') + 1]
1052 1046 bbasedir = bfile[:bfile.rfind('/') + 1]
1053 1047 if missing and abasedir == bbasedir and afile.startswith(bfile):
1054 1048 # this isn't very pretty
1055 1049 hunk.create = True
1056 1050 if createfunc():
1057 1051 missing = False
1058 1052 else:
1059 1053 hunk.create = False
1060 1054
1061 1055 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1062 1056 # diff is between a file and its backup. In this case, the original
1063 1057 # file should be patched (see original mpatch code).
1064 1058 isbackup = (abase == bbase and bfile.startswith(afile))
1065 1059 fname = None
1066 1060 if not missing:
1067 1061 if gooda and goodb:
1068 1062 fname = isbackup and afile or bfile
1069 1063 elif gooda:
1070 1064 fname = afile
1071 1065
1072 1066 if not fname:
1073 1067 if not nullb:
1074 1068 fname = isbackup and afile or bfile
1075 1069 elif not nulla:
1076 1070 fname = afile
1077 1071 else:
1078 1072 raise PatchError(_("undefined source and destination files"))
1079 1073
1080 1074 return fname, missing
1081 1075
1082 1076 def scangitpatch(lr, firstline):
1083 1077 """
1084 1078 Git patches can emit:
1085 1079 - rename a to b
1086 1080 - change b
1087 1081 - copy a to c
1088 1082 - change c
1089 1083
1090 1084 We cannot apply this sequence as-is, the renamed 'a' could not be
1091 1085 found for it would have been renamed already. And we cannot copy
1092 1086 from 'b' instead because 'b' would have been changed already. So
1093 1087 we scan the git patch for copy and rename commands so we can
1094 1088 perform the copies ahead of time.
1095 1089 """
1096 1090 pos = 0
1097 1091 try:
1098 1092 pos = lr.fp.tell()
1099 1093 fp = lr.fp
1100 1094 except IOError:
1101 1095 fp = cStringIO.StringIO(lr.fp.read())
1102 1096 gitlr = linereader(fp, lr.textmode)
1103 1097 gitlr.push(firstline)
1104 1098 gitpatches = readgitpatch(gitlr)
1105 1099 fp.seek(pos)
1106 1100 return gitpatches
1107 1101
1108 1102 def iterhunks(fp):
1109 1103 """Read a patch and yield the following events:
1110 1104 - ("file", afile, bfile, firsthunk): select a new target file.
1111 1105 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1112 1106 "file" event.
1113 1107 - ("git", gitchanges): current diff is in git format, gitchanges
1114 1108 maps filenames to gitpatch records. Unique event.
1115 1109 """
1116 1110 afile = ""
1117 1111 bfile = ""
1118 1112 state = None
1119 1113 hunknum = 0
1120 1114 emitfile = newfile = False
1121 1115 gitpatches = None
1122 1116
1123 1117 # our states
1124 1118 BFILE = 1
1125 1119 context = None
1126 1120 lr = linereader(fp)
1127 1121
1128 1122 while True:
1129 1123 x = lr.readline()
1130 1124 if not x:
1131 1125 break
1132 1126 if state == BFILE and (
1133 1127 (not context and x[0] == '@')
1134 1128 or (context is not False and x.startswith('***************'))
1135 1129 or x.startswith('GIT binary patch')):
1136 1130 gp = None
1137 1131 if gitpatches and gitpatches[-1][0] == bfile:
1138 1132 gp = gitpatches.pop()[1]
1139 1133 if x.startswith('GIT binary patch'):
1140 1134 h = binhunk(gp, lr)
1141 1135 else:
1142 1136 if context is None and x.startswith('***************'):
1143 1137 context = True
1144 1138 create = afile == '/dev/null' or gp and gp.op == 'ADD'
1145 1139 remove = bfile == '/dev/null' or gp and gp.op == 'DELETE'
1146 1140 h = hunk(x, hunknum + 1, lr, context, create, remove)
1147 1141 hunknum += 1
1148 1142 if emitfile:
1149 1143 emitfile = False
1150 1144 yield 'file', (afile, bfile, h, gp)
1151 1145 yield 'hunk', h
1152 1146 elif x.startswith('diff --git'):
1153 1147 m = gitre.match(x)
1154 1148 if not m:
1155 1149 continue
1156 1150 if gitpatches is None:
1157 1151 # scan whole input for git metadata
1158 1152 gitpatches = [('b/' + gp.path, gp) for gp
1159 1153 in scangitpatch(lr, x)]
1160 1154 yield 'git', [g[1] for g in gitpatches
1161 1155 if g[1].op in ('COPY', 'RENAME')]
1162 1156 gitpatches.reverse()
1163 1157 afile = 'a/' + m.group(1)
1164 1158 bfile = 'b/' + m.group(2)
1165 1159 while bfile != gitpatches[-1][0]:
1166 1160 gp = gitpatches.pop()[1]
1167 1161 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp)
1168 1162 gp = gitpatches[-1][1]
1169 1163 # copy/rename + modify should modify target, not source
1170 1164 if gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD') or gp.mode:
1171 1165 afile = bfile
1172 1166 newfile = True
1173 1167 elif x.startswith('---'):
1174 1168 # check for a unified diff
1175 1169 l2 = lr.readline()
1176 1170 if not l2.startswith('+++'):
1177 1171 lr.push(l2)
1178 1172 continue
1179 1173 newfile = True
1180 1174 context = False
1181 1175 afile = parsefilename(x)
1182 1176 bfile = parsefilename(l2)
1183 1177 elif x.startswith('***'):
1184 1178 # check for a context diff
1185 1179 l2 = lr.readline()
1186 1180 if not l2.startswith('---'):
1187 1181 lr.push(l2)
1188 1182 continue
1189 1183 l3 = lr.readline()
1190 1184 lr.push(l3)
1191 1185 if not l3.startswith("***************"):
1192 1186 lr.push(l2)
1193 1187 continue
1194 1188 newfile = True
1195 1189 context = True
1196 1190 afile = parsefilename(x)
1197 1191 bfile = parsefilename(l2)
1198 1192
1199 1193 if newfile:
1200 1194 newfile = False
1201 1195 emitfile = True
1202 1196 state = BFILE
1203 1197 hunknum = 0
1204 1198
1205 1199 while gitpatches:
1206 1200 gp = gitpatches.pop()[1]
1207 1201 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp)
1208 1202
1209 1203 def applydiff(ui, fp, changed, backend, strip=1, eolmode='strict'):
1210 1204 """Reads a patch from fp and tries to apply it.
1211 1205
1212 1206 The dict 'changed' is filled in with all of the filenames changed
1213 1207 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1214 1208 found and 1 if there was any fuzz.
1215 1209
1216 1210 If 'eolmode' is 'strict', the patch content and patched file are
1217 1211 read in binary mode. Otherwise, line endings are ignored when
1218 1212 patching then normalized according to 'eolmode'.
1219 1213 """
1220 1214 return _applydiff(ui, fp, patchfile, backend, changed, strip=strip,
1221 1215 eolmode=eolmode)
1222 1216
1223 1217 def _applydiff(ui, fp, patcher, backend, changed, strip=1, eolmode='strict'):
1224 1218
1225 1219 def pstrip(p):
1226 1220 return pathstrip(p, strip - 1)[1]
1227 1221
1228 1222 rejects = 0
1229 1223 err = 0
1230 1224 current_file = None
1231 1225
1232 1226 for state, values in iterhunks(fp):
1233 1227 if state == 'hunk':
1234 1228 if not current_file:
1235 1229 continue
1236 1230 ret = current_file.apply(values)
1237 1231 if ret >= 0:
1238 1232 changed.setdefault(current_file.fname, None)
1239 1233 if ret > 0:
1240 1234 err = 1
1241 1235 elif state == 'file':
1242 1236 if current_file:
1243 1237 rejects += current_file.close()
1244 1238 current_file = None
1245 1239 afile, bfile, first_hunk, gp = values
1246 1240 if gp:
1247 1241 path = pstrip(gp.path)
1248 1242 changed[path] = gp
1249 1243 if gp.op == 'DELETE':
1250 1244 backend.unlink(path)
1251 1245 continue
1252 1246 if gp.op == 'RENAME':
1253 1247 backend.unlink(pstrip(gp.oldpath))
1254 1248 if gp.mode and not first_hunk:
1249 data = None
1255 1250 if gp.op == 'ADD':
1256 # Added files without content have no hunk and must be created
1257 backend.writelines(path, [], gp.mode)
1258 else:
1259 backend.setmode(path, gp.mode[0], gp.mode[1])
1251 # Added files without content have no hunk and
1252 # must be created
1253 data = []
1254 backend.setfile(path, data, gp.mode)
1260 1255 if not first_hunk:
1261 1256 continue
1262 1257 try:
1263 1258 mode = gp and gp.mode or None
1264 1259 current_file, missing = selectfile(backend, afile, bfile,
1265 1260 first_hunk, strip)
1266 1261 current_file = patcher(ui, current_file, backend, mode,
1267 1262 missing=missing, eolmode=eolmode)
1268 1263 except PatchError, inst:
1269 1264 ui.warn(str(inst) + '\n')
1270 1265 current_file = None
1271 1266 rejects += 1
1272 1267 continue
1273 1268 elif state == 'git':
1274 1269 for gp in values:
1275 1270 backend.copy(pstrip(gp.oldpath), pstrip(gp.path))
1276 1271 else:
1277 1272 raise util.Abort(_('unsupported parser state: %s') % state)
1278 1273
1279 1274 if current_file:
1280 1275 rejects += current_file.close()
1281 1276
1282 1277 if rejects:
1283 1278 return -1
1284 1279 return err
1285 1280
1286 1281 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1287 1282 similarity):
1288 1283 """use <patcher> to apply <patchname> to the working directory.
1289 1284 returns whether patch was applied with fuzz factor."""
1290 1285
1291 1286 fuzz = False
1292 1287 args = []
1293 1288 cwd = repo.root
1294 1289 if cwd:
1295 1290 args.append('-d %s' % util.shellquote(cwd))
1296 1291 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1297 1292 util.shellquote(patchname)))
1298 1293 try:
1299 1294 for line in fp:
1300 1295 line = line.rstrip()
1301 1296 ui.note(line + '\n')
1302 1297 if line.startswith('patching file '):
1303 1298 pf = util.parsepatchoutput(line)
1304 1299 printed_file = False
1305 1300 files.setdefault(pf, None)
1306 1301 elif line.find('with fuzz') >= 0:
1307 1302 fuzz = True
1308 1303 if not printed_file:
1309 1304 ui.warn(pf + '\n')
1310 1305 printed_file = True
1311 1306 ui.warn(line + '\n')
1312 1307 elif line.find('saving rejects to file') >= 0:
1313 1308 ui.warn(line + '\n')
1314 1309 elif line.find('FAILED') >= 0:
1315 1310 if not printed_file:
1316 1311 ui.warn(pf + '\n')
1317 1312 printed_file = True
1318 1313 ui.warn(line + '\n')
1319 1314 finally:
1320 1315 if files:
1321 1316 cfiles = list(files)
1322 1317 cwd = repo.getcwd()
1323 1318 if cwd:
1324 1319 cfiles = [util.pathto(repo.root, cwd, f)
1325 1320 for f in cfile]
1326 1321 scmutil.addremove(repo, cfiles, similarity=similarity)
1327 1322 code = fp.close()
1328 1323 if code:
1329 1324 raise PatchError(_("patch command failed: %s") %
1330 1325 util.explainexit(code)[0])
1331 1326 return fuzz
1332 1327
1333 1328 def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
1334 1329 similarity=0):
1335 1330 """use builtin patch to apply <patchobj> to the working directory.
1336 1331 returns whether patch was applied with fuzz factor."""
1337 1332
1338 1333 if files is None:
1339 1334 files = {}
1340 1335 if eolmode is None:
1341 1336 eolmode = ui.config('patch', 'eol', 'strict')
1342 1337 if eolmode.lower() not in eolmodes:
1343 1338 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1344 1339 eolmode = eolmode.lower()
1345 1340
1346 1341 backend = workingbackend(ui, repo, similarity)
1347 1342 try:
1348 1343 fp = open(patchobj, 'rb')
1349 1344 except TypeError:
1350 1345 fp = patchobj
1351 1346 try:
1352 1347 ret = applydiff(ui, fp, files, backend, strip=strip, eolmode=eolmode)
1353 1348 finally:
1354 1349 if fp != patchobj:
1355 1350 fp.close()
1356 1351 files.update(dict.fromkeys(backend.close()))
1357 1352 if ret < 0:
1358 1353 raise PatchError(_('patch failed to apply'))
1359 1354 return ret > 0
1360 1355
1361 1356 def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
1362 1357 similarity=0):
1363 1358 """Apply <patchname> to the working directory.
1364 1359
1365 1360 'eolmode' specifies how end of lines should be handled. It can be:
1366 1361 - 'strict': inputs are read in binary mode, EOLs are preserved
1367 1362 - 'crlf': EOLs are ignored when patching and reset to CRLF
1368 1363 - 'lf': EOLs are ignored when patching and reset to LF
1369 1364 - None: get it from user settings, default to 'strict'
1370 1365 'eolmode' is ignored when using an external patcher program.
1371 1366
1372 1367 Returns whether patch was applied with fuzz factor.
1373 1368 """
1374 1369 patcher = ui.config('ui', 'patch')
1375 1370 if files is None:
1376 1371 files = {}
1377 1372 try:
1378 1373 if patcher:
1379 1374 return _externalpatch(ui, repo, patcher, patchname, strip,
1380 1375 files, similarity)
1381 1376 return internalpatch(ui, repo, patchname, strip, files, eolmode,
1382 1377 similarity)
1383 1378 except PatchError, err:
1384 1379 raise util.Abort(str(err))
1385 1380
1386 1381 def changedfiles(ui, repo, patchpath, strip=1):
1387 1382 backend = fsbackend(ui, repo.root)
1388 1383 fp = open(patchpath, 'rb')
1389 1384 try:
1390 1385 changed = set()
1391 1386 for state, values in iterhunks(fp):
1392 1387 if state == 'file':
1393 1388 afile, bfile, first_hunk, gp = values
1394 1389 if gp:
1395 1390 changed.add(pathstrip(gp.path, strip - 1)[1])
1396 1391 if gp.op == 'RENAME':
1397 1392 changed.add(pathstrip(gp.oldpath, strip - 1)[1])
1398 1393 if not first_hunk:
1399 1394 continue
1400 1395 current_file, missing = selectfile(backend, afile, bfile,
1401 1396 first_hunk, strip)
1402 1397 changed.add(current_file)
1403 1398 elif state not in ('hunk', 'git'):
1404 1399 raise util.Abort(_('unsupported parser state: %s') % state)
1405 1400 return changed
1406 1401 finally:
1407 1402 fp.close()
1408 1403
1409 1404 def b85diff(to, tn):
1410 1405 '''print base85-encoded binary diff'''
1411 1406 def gitindex(text):
1412 1407 if not text:
1413 1408 return hex(nullid)
1414 1409 l = len(text)
1415 1410 s = util.sha1('blob %d\0' % l)
1416 1411 s.update(text)
1417 1412 return s.hexdigest()
1418 1413
1419 1414 def fmtline(line):
1420 1415 l = len(line)
1421 1416 if l <= 26:
1422 1417 l = chr(ord('A') + l - 1)
1423 1418 else:
1424 1419 l = chr(l - 26 + ord('a') - 1)
1425 1420 return '%c%s\n' % (l, base85.b85encode(line, True))
1426 1421
1427 1422 def chunk(text, csize=52):
1428 1423 l = len(text)
1429 1424 i = 0
1430 1425 while i < l:
1431 1426 yield text[i:i + csize]
1432 1427 i += csize
1433 1428
1434 1429 tohash = gitindex(to)
1435 1430 tnhash = gitindex(tn)
1436 1431 if tohash == tnhash:
1437 1432 return ""
1438 1433
1439 1434 # TODO: deltas
1440 1435 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1441 1436 (tohash, tnhash, len(tn))]
1442 1437 for l in chunk(zlib.compress(tn)):
1443 1438 ret.append(fmtline(l))
1444 1439 ret.append('\n')
1445 1440 return ''.join(ret)
1446 1441
1447 1442 class GitDiffRequired(Exception):
1448 1443 pass
1449 1444
1450 1445 def diffopts(ui, opts=None, untrusted=False):
1451 1446 def get(key, name=None, getter=ui.configbool):
1452 1447 return ((opts and opts.get(key)) or
1453 1448 getter('diff', name or key, None, untrusted=untrusted))
1454 1449 return mdiff.diffopts(
1455 1450 text=opts and opts.get('text'),
1456 1451 git=get('git'),
1457 1452 nodates=get('nodates'),
1458 1453 showfunc=get('show_function', 'showfunc'),
1459 1454 ignorews=get('ignore_all_space', 'ignorews'),
1460 1455 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1461 1456 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1462 1457 context=get('unified', getter=ui.config))
1463 1458
1464 1459 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1465 1460 losedatafn=None, prefix=''):
1466 1461 '''yields diff of changes to files between two nodes, or node and
1467 1462 working directory.
1468 1463
1469 1464 if node1 is None, use first dirstate parent instead.
1470 1465 if node2 is None, compare node1 with working directory.
1471 1466
1472 1467 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1473 1468 every time some change cannot be represented with the current
1474 1469 patch format. Return False to upgrade to git patch format, True to
1475 1470 accept the loss or raise an exception to abort the diff. It is
1476 1471 called with the name of current file being diffed as 'fn'. If set
1477 1472 to None, patches will always be upgraded to git format when
1478 1473 necessary.
1479 1474
1480 1475 prefix is a filename prefix that is prepended to all filenames on
1481 1476 display (used for subrepos).
1482 1477 '''
1483 1478
1484 1479 if opts is None:
1485 1480 opts = mdiff.defaultopts
1486 1481
1487 1482 if not node1 and not node2:
1488 1483 node1 = repo.dirstate.p1()
1489 1484
1490 1485 def lrugetfilectx():
1491 1486 cache = {}
1492 1487 order = []
1493 1488 def getfilectx(f, ctx):
1494 1489 fctx = ctx.filectx(f, filelog=cache.get(f))
1495 1490 if f not in cache:
1496 1491 if len(cache) > 20:
1497 1492 del cache[order.pop(0)]
1498 1493 cache[f] = fctx.filelog()
1499 1494 else:
1500 1495 order.remove(f)
1501 1496 order.append(f)
1502 1497 return fctx
1503 1498 return getfilectx
1504 1499 getfilectx = lrugetfilectx()
1505 1500
1506 1501 ctx1 = repo[node1]
1507 1502 ctx2 = repo[node2]
1508 1503
1509 1504 if not changes:
1510 1505 changes = repo.status(ctx1, ctx2, match=match)
1511 1506 modified, added, removed = changes[:3]
1512 1507
1513 1508 if not modified and not added and not removed:
1514 1509 return []
1515 1510
1516 1511 revs = None
1517 1512 if not repo.ui.quiet:
1518 1513 hexfunc = repo.ui.debugflag and hex or short
1519 1514 revs = [hexfunc(node) for node in [node1, node2] if node]
1520 1515
1521 1516 copy = {}
1522 1517 if opts.git or opts.upgrade:
1523 1518 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1524 1519
1525 1520 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1526 1521 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1527 1522 if opts.upgrade and not opts.git:
1528 1523 try:
1529 1524 def losedata(fn):
1530 1525 if not losedatafn or not losedatafn(fn=fn):
1531 1526 raise GitDiffRequired()
1532 1527 # Buffer the whole output until we are sure it can be generated
1533 1528 return list(difffn(opts.copy(git=False), losedata))
1534 1529 except GitDiffRequired:
1535 1530 return difffn(opts.copy(git=True), None)
1536 1531 else:
1537 1532 return difffn(opts, None)
1538 1533
1539 1534 def difflabel(func, *args, **kw):
1540 1535 '''yields 2-tuples of (output, label) based on the output of func()'''
1541 1536 prefixes = [('diff', 'diff.diffline'),
1542 1537 ('copy', 'diff.extended'),
1543 1538 ('rename', 'diff.extended'),
1544 1539 ('old', 'diff.extended'),
1545 1540 ('new', 'diff.extended'),
1546 1541 ('deleted', 'diff.extended'),
1547 1542 ('---', 'diff.file_a'),
1548 1543 ('+++', 'diff.file_b'),
1549 1544 ('@@', 'diff.hunk'),
1550 1545 ('-', 'diff.deleted'),
1551 1546 ('+', 'diff.inserted')]
1552 1547
1553 1548 for chunk in func(*args, **kw):
1554 1549 lines = chunk.split('\n')
1555 1550 for i, line in enumerate(lines):
1556 1551 if i != 0:
1557 1552 yield ('\n', '')
1558 1553 stripline = line
1559 1554 if line and line[0] in '+-':
1560 1555 # highlight trailing whitespace, but only in changed lines
1561 1556 stripline = line.rstrip()
1562 1557 for prefix, label in prefixes:
1563 1558 if stripline.startswith(prefix):
1564 1559 yield (stripline, label)
1565 1560 break
1566 1561 else:
1567 1562 yield (line, '')
1568 1563 if line != stripline:
1569 1564 yield (line[len(stripline):], 'diff.trailingwhitespace')
1570 1565
1571 1566 def diffui(*args, **kw):
1572 1567 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1573 1568 return difflabel(diff, *args, **kw)
1574 1569
1575 1570
1576 1571 def _addmodehdr(header, omode, nmode):
1577 1572 if omode != nmode:
1578 1573 header.append('old mode %s\n' % omode)
1579 1574 header.append('new mode %s\n' % nmode)
1580 1575
1581 1576 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1582 1577 copy, getfilectx, opts, losedatafn, prefix):
1583 1578
1584 1579 def join(f):
1585 1580 return os.path.join(prefix, f)
1586 1581
1587 1582 date1 = util.datestr(ctx1.date())
1588 1583 man1 = ctx1.manifest()
1589 1584
1590 1585 gone = set()
1591 1586 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1592 1587
1593 1588 copyto = dict([(v, k) for k, v in copy.items()])
1594 1589
1595 1590 if opts.git:
1596 1591 revs = None
1597 1592
1598 1593 for f in sorted(modified + added + removed):
1599 1594 to = None
1600 1595 tn = None
1601 1596 dodiff = True
1602 1597 header = []
1603 1598 if f in man1:
1604 1599 to = getfilectx(f, ctx1).data()
1605 1600 if f not in removed:
1606 1601 tn = getfilectx(f, ctx2).data()
1607 1602 a, b = f, f
1608 1603 if opts.git or losedatafn:
1609 1604 if f in added:
1610 1605 mode = gitmode[ctx2.flags(f)]
1611 1606 if f in copy or f in copyto:
1612 1607 if opts.git:
1613 1608 if f in copy:
1614 1609 a = copy[f]
1615 1610 else:
1616 1611 a = copyto[f]
1617 1612 omode = gitmode[man1.flags(a)]
1618 1613 _addmodehdr(header, omode, mode)
1619 1614 if a in removed and a not in gone:
1620 1615 op = 'rename'
1621 1616 gone.add(a)
1622 1617 else:
1623 1618 op = 'copy'
1624 1619 header.append('%s from %s\n' % (op, join(a)))
1625 1620 header.append('%s to %s\n' % (op, join(f)))
1626 1621 to = getfilectx(a, ctx1).data()
1627 1622 else:
1628 1623 losedatafn(f)
1629 1624 else:
1630 1625 if opts.git:
1631 1626 header.append('new file mode %s\n' % mode)
1632 1627 elif ctx2.flags(f):
1633 1628 losedatafn(f)
1634 1629 # In theory, if tn was copied or renamed we should check
1635 1630 # if the source is binary too but the copy record already
1636 1631 # forces git mode.
1637 1632 if util.binary(tn):
1638 1633 if opts.git:
1639 1634 dodiff = 'binary'
1640 1635 else:
1641 1636 losedatafn(f)
1642 1637 if not opts.git and not tn:
1643 1638 # regular diffs cannot represent new empty file
1644 1639 losedatafn(f)
1645 1640 elif f in removed:
1646 1641 if opts.git:
1647 1642 # have we already reported a copy above?
1648 1643 if ((f in copy and copy[f] in added
1649 1644 and copyto[copy[f]] == f) or
1650 1645 (f in copyto and copyto[f] in added
1651 1646 and copy[copyto[f]] == f)):
1652 1647 dodiff = False
1653 1648 else:
1654 1649 header.append('deleted file mode %s\n' %
1655 1650 gitmode[man1.flags(f)])
1656 1651 elif not to or util.binary(to):
1657 1652 # regular diffs cannot represent empty file deletion
1658 1653 losedatafn(f)
1659 1654 else:
1660 1655 oflag = man1.flags(f)
1661 1656 nflag = ctx2.flags(f)
1662 1657 binary = util.binary(to) or util.binary(tn)
1663 1658 if opts.git:
1664 1659 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1665 1660 if binary:
1666 1661 dodiff = 'binary'
1667 1662 elif binary or nflag != oflag:
1668 1663 losedatafn(f)
1669 1664 if opts.git:
1670 1665 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1671 1666
1672 1667 if dodiff:
1673 1668 if dodiff == 'binary':
1674 1669 text = b85diff(to, tn)
1675 1670 else:
1676 1671 text = mdiff.unidiff(to, date1,
1677 1672 # ctx2 date may be dynamic
1678 1673 tn, util.datestr(ctx2.date()),
1679 1674 join(a), join(b), revs, opts=opts)
1680 1675 if header and (text or len(header) > 1):
1681 1676 yield ''.join(header)
1682 1677 if text:
1683 1678 yield text
1684 1679
1685 1680 def diffstatdata(lines):
1686 1681 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1687 1682
1688 1683 filename, adds, removes = None, 0, 0
1689 1684 for line in lines:
1690 1685 if line.startswith('diff'):
1691 1686 if filename:
1692 1687 isbinary = adds == 0 and removes == 0
1693 1688 yield (filename, adds, removes, isbinary)
1694 1689 # set numbers to 0 anyway when starting new file
1695 1690 adds, removes = 0, 0
1696 1691 if line.startswith('diff --git'):
1697 1692 filename = gitre.search(line).group(1)
1698 1693 elif line.startswith('diff -r'):
1699 1694 # format: "diff -r ... -r ... filename"
1700 1695 filename = diffre.search(line).group(1)
1701 1696 elif line.startswith('+') and not line.startswith('+++'):
1702 1697 adds += 1
1703 1698 elif line.startswith('-') and not line.startswith('---'):
1704 1699 removes += 1
1705 1700 if filename:
1706 1701 isbinary = adds == 0 and removes == 0
1707 1702 yield (filename, adds, removes, isbinary)
1708 1703
1709 1704 def diffstat(lines, width=80, git=False):
1710 1705 output = []
1711 1706 stats = list(diffstatdata(lines))
1712 1707
1713 1708 maxtotal, maxname = 0, 0
1714 1709 totaladds, totalremoves = 0, 0
1715 1710 hasbinary = False
1716 1711
1717 1712 sized = [(filename, adds, removes, isbinary, encoding.colwidth(filename))
1718 1713 for filename, adds, removes, isbinary in stats]
1719 1714
1720 1715 for filename, adds, removes, isbinary, namewidth in sized:
1721 1716 totaladds += adds
1722 1717 totalremoves += removes
1723 1718 maxname = max(maxname, namewidth)
1724 1719 maxtotal = max(maxtotal, adds + removes)
1725 1720 if isbinary:
1726 1721 hasbinary = True
1727 1722
1728 1723 countwidth = len(str(maxtotal))
1729 1724 if hasbinary and countwidth < 3:
1730 1725 countwidth = 3
1731 1726 graphwidth = width - countwidth - maxname - 6
1732 1727 if graphwidth < 10:
1733 1728 graphwidth = 10
1734 1729
1735 1730 def scale(i):
1736 1731 if maxtotal <= graphwidth:
1737 1732 return i
1738 1733 # If diffstat runs out of room it doesn't print anything,
1739 1734 # which isn't very useful, so always print at least one + or -
1740 1735 # if there were at least some changes.
1741 1736 return max(i * graphwidth // maxtotal, int(bool(i)))
1742 1737
1743 1738 for filename, adds, removes, isbinary, namewidth in sized:
1744 1739 if git and isbinary:
1745 1740 count = 'Bin'
1746 1741 else:
1747 1742 count = adds + removes
1748 1743 pluses = '+' * scale(adds)
1749 1744 minuses = '-' * scale(removes)
1750 1745 output.append(' %s%s | %*s %s%s\n' %
1751 1746 (filename, ' ' * (maxname - namewidth),
1752 1747 countwidth, count,
1753 1748 pluses, minuses))
1754 1749
1755 1750 if stats:
1756 1751 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1757 1752 % (len(stats), totaladds, totalremoves))
1758 1753
1759 1754 return ''.join(output)
1760 1755
1761 1756 def diffstatui(*args, **kw):
1762 1757 '''like diffstat(), but yields 2-tuples of (output, label) for
1763 1758 ui.write()
1764 1759 '''
1765 1760
1766 1761 for line in diffstat(*args, **kw).splitlines():
1767 1762 if line and line[-1] in '+-':
1768 1763 name, graph = line.rsplit(' ', 1)
1769 1764 yield (name + ' ', '')
1770 1765 m = re.search(r'\++', graph)
1771 1766 if m:
1772 1767 yield (m.group(0), 'diffstat.inserted')
1773 1768 m = re.search(r'-+', graph)
1774 1769 if m:
1775 1770 yield (m.group(0), 'diffstat.deleted')
1776 1771 else:
1777 1772 yield (line, '')
1778 1773 yield ('\n', '')
General Comments 0
You need to be logged in to leave comments. Login now