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