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