##// END OF EJS Templates
patch, i18n: avoid parameterized messages...
Wagner Bruna -
r10518:5fe51d34 stable
parent child Browse files
Show More
@@ -1,1663 +1,1660 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 line.startswith('--- ') and inheader:
161 161 # No evil headers seen by diff start, 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 offset = l - orig_start - fuzzlen
610 611 if fuzzlen:
611 fuzzstr = "with fuzz %d " % fuzzlen
612 f = self.ui.warn
612 msg = _("Hunk #%d succeeded at %d "
613 "with fuzz %d "
614 "(offset %d lines).\n")
613 615 self.printfile(True)
616 self.ui.warn(msg %
617 (h.number, l + 1, fuzzlen, offset))
614 618 else:
615 fuzzstr = ""
616 f = self.ui.note
617 offset = l - orig_start - fuzzlen
618 if offset == 1:
619 msg = _("Hunk #%d succeeded at %d %s"
620 "(offset %d line).\n")
621 else:
622 msg = _("Hunk #%d succeeded at %d %s"
619 msg = _("Hunk #%d succeeded at %d "
623 620 "(offset %d lines).\n")
624 f(msg % (h.number, l + 1, fuzzstr, offset))
621 self.ui.note(msg % (h.number, l + 1, offset))
625 622 return fuzzlen
626 623 self.printfile(True)
627 624 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
628 625 self.rej.append(horig)
629 626 return -1
630 627
631 628 class hunk(object):
632 629 def __init__(self, desc, num, lr, context, create=False, remove=False):
633 630 self.number = num
634 631 self.desc = desc
635 632 self.hunk = [desc]
636 633 self.a = []
637 634 self.b = []
638 635 self.starta = self.lena = None
639 636 self.startb = self.lenb = None
640 637 if lr is not None:
641 638 if context:
642 639 self.read_context_hunk(lr)
643 640 else:
644 641 self.read_unified_hunk(lr)
645 642 self.create = create
646 643 self.remove = remove and not create
647 644
648 645 def getnormalized(self):
649 646 """Return a copy with line endings normalized to LF."""
650 647
651 648 def normalize(lines):
652 649 nlines = []
653 650 for line in lines:
654 651 if line.endswith('\r\n'):
655 652 line = line[:-2] + '\n'
656 653 nlines.append(line)
657 654 return nlines
658 655
659 656 # Dummy object, it is rebuilt manually
660 657 nh = hunk(self.desc, self.number, None, None, False, False)
661 658 nh.number = self.number
662 659 nh.desc = self.desc
663 660 nh.a = normalize(self.a)
664 661 nh.b = normalize(self.b)
665 662 nh.starta = self.starta
666 663 nh.startb = self.startb
667 664 nh.lena = self.lena
668 665 nh.lenb = self.lenb
669 666 nh.create = self.create
670 667 nh.remove = self.remove
671 668 return nh
672 669
673 670 def read_unified_hunk(self, lr):
674 671 m = unidesc.match(self.desc)
675 672 if not m:
676 673 raise PatchError(_("bad hunk #%d") % self.number)
677 674 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
678 675 if self.lena is None:
679 676 self.lena = 1
680 677 else:
681 678 self.lena = int(self.lena)
682 679 if self.lenb is None:
683 680 self.lenb = 1
684 681 else:
685 682 self.lenb = int(self.lenb)
686 683 self.starta = int(self.starta)
687 684 self.startb = int(self.startb)
688 685 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
689 686 # if we hit eof before finishing out the hunk, the last line will
690 687 # be zero length. Lets try to fix it up.
691 688 while len(self.hunk[-1]) == 0:
692 689 del self.hunk[-1]
693 690 del self.a[-1]
694 691 del self.b[-1]
695 692 self.lena -= 1
696 693 self.lenb -= 1
697 694
698 695 def read_context_hunk(self, lr):
699 696 self.desc = lr.readline()
700 697 m = contextdesc.match(self.desc)
701 698 if not m:
702 699 raise PatchError(_("bad hunk #%d") % self.number)
703 700 foo, self.starta, foo2, aend, foo3 = m.groups()
704 701 self.starta = int(self.starta)
705 702 if aend is None:
706 703 aend = self.starta
707 704 self.lena = int(aend) - self.starta
708 705 if self.starta:
709 706 self.lena += 1
710 707 for x in xrange(self.lena):
711 708 l = lr.readline()
712 709 if l.startswith('---'):
713 710 lr.push(l)
714 711 break
715 712 s = l[2:]
716 713 if l.startswith('- ') or l.startswith('! '):
717 714 u = '-' + s
718 715 elif l.startswith(' '):
719 716 u = ' ' + s
720 717 else:
721 718 raise PatchError(_("bad hunk #%d old text line %d") %
722 719 (self.number, x))
723 720 self.a.append(u)
724 721 self.hunk.append(u)
725 722
726 723 l = lr.readline()
727 724 if l.startswith('\ '):
728 725 s = self.a[-1][:-1]
729 726 self.a[-1] = s
730 727 self.hunk[-1] = s
731 728 l = lr.readline()
732 729 m = contextdesc.match(l)
733 730 if not m:
734 731 raise PatchError(_("bad hunk #%d") % self.number)
735 732 foo, self.startb, foo2, bend, foo3 = m.groups()
736 733 self.startb = int(self.startb)
737 734 if bend is None:
738 735 bend = self.startb
739 736 self.lenb = int(bend) - self.startb
740 737 if self.startb:
741 738 self.lenb += 1
742 739 hunki = 1
743 740 for x in xrange(self.lenb):
744 741 l = lr.readline()
745 742 if l.startswith('\ '):
746 743 s = self.b[-1][:-1]
747 744 self.b[-1] = s
748 745 self.hunk[hunki - 1] = s
749 746 continue
750 747 if not l:
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 elif len(self.b) == 0:
759 756 # this can happen when the hunk does not add any lines
760 757 lr.push(l)
761 758 break
762 759 else:
763 760 raise PatchError(_("bad hunk #%d old text line %d") %
764 761 (self.number, x))
765 762 self.b.append(s)
766 763 while True:
767 764 if hunki >= len(self.hunk):
768 765 h = ""
769 766 else:
770 767 h = self.hunk[hunki]
771 768 hunki += 1
772 769 if h == u:
773 770 break
774 771 elif h.startswith('-'):
775 772 continue
776 773 else:
777 774 self.hunk.insert(hunki - 1, u)
778 775 break
779 776
780 777 if not self.a:
781 778 # this happens when lines were only added to the hunk
782 779 for x in self.hunk:
783 780 if x.startswith('-') or x.startswith(' '):
784 781 self.a.append(x)
785 782 if not self.b:
786 783 # this happens when lines were only deleted from the hunk
787 784 for x in self.hunk:
788 785 if x.startswith('+') or x.startswith(' '):
789 786 self.b.append(x[1:])
790 787 # @@ -start,len +start,len @@
791 788 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
792 789 self.startb, self.lenb)
793 790 self.hunk[0] = self.desc
794 791
795 792 def fix_newline(self):
796 793 diffhelpers.fix_newline(self.hunk, self.a, self.b)
797 794
798 795 def complete(self):
799 796 return len(self.a) == self.lena and len(self.b) == self.lenb
800 797
801 798 def createfile(self):
802 799 return self.starta == 0 and self.lena == 0 and self.create
803 800
804 801 def rmfile(self):
805 802 return self.startb == 0 and self.lenb == 0 and self.remove
806 803
807 804 def fuzzit(self, l, fuzz, toponly):
808 805 # this removes context lines from the top and bottom of list 'l'. It
809 806 # checks the hunk to make sure only context lines are removed, and then
810 807 # returns a new shortened list of lines.
811 808 fuzz = min(fuzz, len(l)-1)
812 809 if fuzz:
813 810 top = 0
814 811 bot = 0
815 812 hlen = len(self.hunk)
816 813 for x in xrange(hlen - 1):
817 814 # the hunk starts with the @@ line, so use x+1
818 815 if self.hunk[x + 1][0] == ' ':
819 816 top += 1
820 817 else:
821 818 break
822 819 if not toponly:
823 820 for x in xrange(hlen - 1):
824 821 if self.hunk[hlen - bot - 1][0] == ' ':
825 822 bot += 1
826 823 else:
827 824 break
828 825
829 826 # top and bot now count context in the hunk
830 827 # adjust them if either one is short
831 828 context = max(top, bot, 3)
832 829 if bot < context:
833 830 bot = max(0, fuzz - (context - bot))
834 831 else:
835 832 bot = min(fuzz, bot)
836 833 if top < context:
837 834 top = max(0, fuzz - (context - top))
838 835 else:
839 836 top = min(fuzz, top)
840 837
841 838 return l[top:len(l)-bot]
842 839 return l
843 840
844 841 def old(self, fuzz=0, toponly=False):
845 842 return self.fuzzit(self.a, fuzz, toponly)
846 843
847 844 def new(self, fuzz=0, toponly=False):
848 845 return self.fuzzit(self.b, fuzz, toponly)
849 846
850 847 class binhunk:
851 848 'A binary patch file. Only understands literals so far.'
852 849 def __init__(self, gitpatch):
853 850 self.gitpatch = gitpatch
854 851 self.text = None
855 852 self.hunk = ['GIT binary patch\n']
856 853
857 854 def createfile(self):
858 855 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
859 856
860 857 def rmfile(self):
861 858 return self.gitpatch.op == 'DELETE'
862 859
863 860 def complete(self):
864 861 return self.text is not None
865 862
866 863 def new(self):
867 864 return [self.text]
868 865
869 866 def extract(self, lr):
870 867 line = lr.readline()
871 868 self.hunk.append(line)
872 869 while line and not line.startswith('literal '):
873 870 line = lr.readline()
874 871 self.hunk.append(line)
875 872 if not line:
876 873 raise PatchError(_('could not extract binary patch'))
877 874 size = int(line[8:].rstrip())
878 875 dec = []
879 876 line = lr.readline()
880 877 self.hunk.append(line)
881 878 while len(line) > 1:
882 879 l = line[0]
883 880 if l <= 'Z' and l >= 'A':
884 881 l = ord(l) - ord('A') + 1
885 882 else:
886 883 l = ord(l) - ord('a') + 27
887 884 dec.append(base85.b85decode(line[1:-1])[:l])
888 885 line = lr.readline()
889 886 self.hunk.append(line)
890 887 text = zlib.decompress(''.join(dec))
891 888 if len(text) != size:
892 889 raise PatchError(_('binary patch is %d bytes, not %d') %
893 890 len(text), size)
894 891 self.text = text
895 892
896 893 def parsefilename(str):
897 894 # --- filename \t|space stuff
898 895 s = str[4:].rstrip('\r\n')
899 896 i = s.find('\t')
900 897 if i < 0:
901 898 i = s.find(' ')
902 899 if i < 0:
903 900 return s
904 901 return s[:i]
905 902
906 903 def selectfile(afile_orig, bfile_orig, hunk, strip):
907 904 def pathstrip(path, count=1):
908 905 pathlen = len(path)
909 906 i = 0
910 907 if count == 0:
911 908 return '', path.rstrip()
912 909 while count > 0:
913 910 i = path.find('/', i)
914 911 if i == -1:
915 912 raise PatchError(_("unable to strip away %d dirs from %s") %
916 913 (count, path))
917 914 i += 1
918 915 # consume '//' in the path
919 916 while i < pathlen - 1 and path[i] == '/':
920 917 i += 1
921 918 count -= 1
922 919 return path[:i].lstrip(), path[i:].rstrip()
923 920
924 921 nulla = afile_orig == "/dev/null"
925 922 nullb = bfile_orig == "/dev/null"
926 923 abase, afile = pathstrip(afile_orig, strip)
927 924 gooda = not nulla and util.lexists(afile)
928 925 bbase, bfile = pathstrip(bfile_orig, strip)
929 926 if afile == bfile:
930 927 goodb = gooda
931 928 else:
932 929 goodb = not nullb and os.path.exists(bfile)
933 930 createfunc = hunk.createfile
934 931 missing = not goodb and not gooda and not createfunc()
935 932
936 933 # some diff programs apparently produce create patches where the
937 934 # afile is not /dev/null, but rather the same name as the bfile
938 935 if missing and afile == bfile:
939 936 # this isn't very pretty
940 937 hunk.create = True
941 938 if createfunc():
942 939 missing = False
943 940 else:
944 941 hunk.create = False
945 942
946 943 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
947 944 # diff is between a file and its backup. In this case, the original
948 945 # file should be patched (see original mpatch code).
949 946 isbackup = (abase == bbase and bfile.startswith(afile))
950 947 fname = None
951 948 if not missing:
952 949 if gooda and goodb:
953 950 fname = isbackup and afile or bfile
954 951 elif gooda:
955 952 fname = afile
956 953
957 954 if not fname:
958 955 if not nullb:
959 956 fname = isbackup and afile or bfile
960 957 elif not nulla:
961 958 fname = afile
962 959 else:
963 960 raise PatchError(_("undefined source and destination files"))
964 961
965 962 return fname, missing
966 963
967 964 def scangitpatch(lr, firstline):
968 965 """
969 966 Git patches can emit:
970 967 - rename a to b
971 968 - change b
972 969 - copy a to c
973 970 - change c
974 971
975 972 We cannot apply this sequence as-is, the renamed 'a' could not be
976 973 found for it would have been renamed already. And we cannot copy
977 974 from 'b' instead because 'b' would have been changed already. So
978 975 we scan the git patch for copy and rename commands so we can
979 976 perform the copies ahead of time.
980 977 """
981 978 pos = 0
982 979 try:
983 980 pos = lr.fp.tell()
984 981 fp = lr.fp
985 982 except IOError:
986 983 fp = cStringIO.StringIO(lr.fp.read())
987 984 gitlr = linereader(fp, lr.textmode)
988 985 gitlr.push(firstline)
989 986 (dopatch, gitpatches) = readgitpatch(gitlr)
990 987 fp.seek(pos)
991 988 return dopatch, gitpatches
992 989
993 990 def iterhunks(ui, fp, sourcefile=None):
994 991 """Read a patch and yield the following events:
995 992 - ("file", afile, bfile, firsthunk): select a new target file.
996 993 - ("hunk", hunk): a new hunk is ready to be applied, follows a
997 994 "file" event.
998 995 - ("git", gitchanges): current diff is in git format, gitchanges
999 996 maps filenames to gitpatch records. Unique event.
1000 997 """
1001 998 changed = {}
1002 999 current_hunk = None
1003 1000 afile = ""
1004 1001 bfile = ""
1005 1002 state = None
1006 1003 hunknum = 0
1007 1004 emitfile = False
1008 1005 git = False
1009 1006
1010 1007 # our states
1011 1008 BFILE = 1
1012 1009 context = None
1013 1010 lr = linereader(fp)
1014 1011 dopatch = True
1015 1012 # gitworkdone is True if a git operation (copy, rename, ...) was
1016 1013 # performed already for the current file. Useful when the file
1017 1014 # section may have no hunk.
1018 1015 gitworkdone = False
1019 1016
1020 1017 while True:
1021 1018 newfile = False
1022 1019 x = lr.readline()
1023 1020 if not x:
1024 1021 break
1025 1022 if current_hunk:
1026 1023 if x.startswith('\ '):
1027 1024 current_hunk.fix_newline()
1028 1025 yield 'hunk', current_hunk
1029 1026 current_hunk = None
1030 1027 gitworkdone = False
1031 1028 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
1032 1029 ((context is not False) and x.startswith('***************')))):
1033 1030 try:
1034 1031 if context is None and x.startswith('***************'):
1035 1032 context = True
1036 1033 gpatch = changed.get(bfile)
1037 1034 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
1038 1035 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
1039 1036 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
1040 1037 except PatchError, err:
1041 1038 ui.debug(err)
1042 1039 current_hunk = None
1043 1040 continue
1044 1041 hunknum += 1
1045 1042 if emitfile:
1046 1043 emitfile = False
1047 1044 yield 'file', (afile, bfile, current_hunk)
1048 1045 elif state == BFILE and x.startswith('GIT binary patch'):
1049 1046 current_hunk = binhunk(changed[bfile])
1050 1047 hunknum += 1
1051 1048 if emitfile:
1052 1049 emitfile = False
1053 1050 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
1054 1051 current_hunk.extract(lr)
1055 1052 elif x.startswith('diff --git'):
1056 1053 # check for git diff, scanning the whole patch file if needed
1057 1054 m = gitre.match(x)
1058 1055 if m:
1059 1056 afile, bfile = m.group(1, 2)
1060 1057 if not git:
1061 1058 git = True
1062 1059 dopatch, gitpatches = scangitpatch(lr, x)
1063 1060 yield 'git', gitpatches
1064 1061 for gp in gitpatches:
1065 1062 changed[gp.path] = gp
1066 1063 # else error?
1067 1064 # copy/rename + modify should modify target, not source
1068 1065 gp = changed.get(bfile)
1069 1066 if gp and gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD'):
1070 1067 afile = bfile
1071 1068 gitworkdone = True
1072 1069 newfile = True
1073 1070 elif x.startswith('---'):
1074 1071 # check for a unified diff
1075 1072 l2 = lr.readline()
1076 1073 if not l2.startswith('+++'):
1077 1074 lr.push(l2)
1078 1075 continue
1079 1076 newfile = True
1080 1077 context = False
1081 1078 afile = parsefilename(x)
1082 1079 bfile = parsefilename(l2)
1083 1080 elif x.startswith('***'):
1084 1081 # check for a context diff
1085 1082 l2 = lr.readline()
1086 1083 if not l2.startswith('---'):
1087 1084 lr.push(l2)
1088 1085 continue
1089 1086 l3 = lr.readline()
1090 1087 lr.push(l3)
1091 1088 if not l3.startswith("***************"):
1092 1089 lr.push(l2)
1093 1090 continue
1094 1091 newfile = True
1095 1092 context = True
1096 1093 afile = parsefilename(x)
1097 1094 bfile = parsefilename(l2)
1098 1095
1099 1096 if newfile:
1100 1097 emitfile = True
1101 1098 state = BFILE
1102 1099 hunknum = 0
1103 1100 if current_hunk:
1104 1101 if current_hunk.complete():
1105 1102 yield 'hunk', current_hunk
1106 1103 else:
1107 1104 raise PatchError(_("malformed patch %s %s") % (afile,
1108 1105 current_hunk.desc))
1109 1106
1110 1107 if hunknum == 0 and dopatch and not gitworkdone:
1111 1108 raise NoHunks
1112 1109
1113 1110 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eolmode='strict'):
1114 1111 """
1115 1112 Reads a patch from fp and tries to apply it.
1116 1113
1117 1114 The dict 'changed' is filled in with all of the filenames changed
1118 1115 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1119 1116 found and 1 if there was any fuzz.
1120 1117
1121 1118 If 'eolmode' is 'strict', the patch content and patched file are
1122 1119 read in binary mode. Otherwise, line endings are ignored when
1123 1120 patching then normalized according to 'eolmode'.
1124 1121 """
1125 1122 rejects = 0
1126 1123 err = 0
1127 1124 current_file = None
1128 1125 gitpatches = None
1129 1126 opener = util.opener(os.getcwd())
1130 1127
1131 1128 def closefile():
1132 1129 if not current_file:
1133 1130 return 0
1134 1131 current_file.close()
1135 1132 return len(current_file.rej)
1136 1133
1137 1134 for state, values in iterhunks(ui, fp, sourcefile):
1138 1135 if state == 'hunk':
1139 1136 if not current_file:
1140 1137 continue
1141 1138 current_hunk = values
1142 1139 ret = current_file.apply(current_hunk)
1143 1140 if ret >= 0:
1144 1141 changed.setdefault(current_file.fname, None)
1145 1142 if ret > 0:
1146 1143 err = 1
1147 1144 elif state == 'file':
1148 1145 rejects += closefile()
1149 1146 afile, bfile, first_hunk = values
1150 1147 try:
1151 1148 if sourcefile:
1152 1149 current_file = patchfile(ui, sourcefile, opener,
1153 1150 eolmode=eolmode)
1154 1151 else:
1155 1152 current_file, missing = selectfile(afile, bfile,
1156 1153 first_hunk, strip)
1157 1154 current_file = patchfile(ui, current_file, opener,
1158 1155 missing, eolmode)
1159 1156 except PatchError, err:
1160 1157 ui.warn(str(err) + '\n')
1161 1158 current_file, current_hunk = None, None
1162 1159 rejects += 1
1163 1160 continue
1164 1161 elif state == 'git':
1165 1162 gitpatches = values
1166 1163 cwd = os.getcwd()
1167 1164 for gp in gitpatches:
1168 1165 if gp.op in ('COPY', 'RENAME'):
1169 1166 copyfile(gp.oldpath, gp.path, cwd)
1170 1167 changed[gp.path] = gp
1171 1168 else:
1172 1169 raise util.Abort(_('unsupported parser state: %s') % state)
1173 1170
1174 1171 rejects += closefile()
1175 1172
1176 1173 if rejects:
1177 1174 return -1
1178 1175 return err
1179 1176
1180 1177 def diffopts(ui, opts=None, untrusted=False):
1181 1178 def get(key, name=None, getter=ui.configbool):
1182 1179 return ((opts and opts.get(key)) or
1183 1180 getter('diff', name or key, None, untrusted=untrusted))
1184 1181 return mdiff.diffopts(
1185 1182 text=opts and opts.get('text'),
1186 1183 git=get('git'),
1187 1184 nodates=get('nodates'),
1188 1185 showfunc=get('show_function', 'showfunc'),
1189 1186 ignorews=get('ignore_all_space', 'ignorews'),
1190 1187 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1191 1188 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1192 1189 context=get('unified', getter=ui.config))
1193 1190
1194 1191 def updatedir(ui, repo, patches, similarity=0):
1195 1192 '''Update dirstate after patch application according to metadata'''
1196 1193 if not patches:
1197 1194 return
1198 1195 copies = []
1199 1196 removes = set()
1200 1197 cfiles = patches.keys()
1201 1198 cwd = repo.getcwd()
1202 1199 if cwd:
1203 1200 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1204 1201 for f in patches:
1205 1202 gp = patches[f]
1206 1203 if not gp:
1207 1204 continue
1208 1205 if gp.op == 'RENAME':
1209 1206 copies.append((gp.oldpath, gp.path))
1210 1207 removes.add(gp.oldpath)
1211 1208 elif gp.op == 'COPY':
1212 1209 copies.append((gp.oldpath, gp.path))
1213 1210 elif gp.op == 'DELETE':
1214 1211 removes.add(gp.path)
1215 1212 for src, dst in copies:
1216 1213 repo.copy(src, dst)
1217 1214 if (not similarity) and removes:
1218 1215 repo.remove(sorted(removes), True)
1219 1216 for f in patches:
1220 1217 gp = patches[f]
1221 1218 if gp and gp.mode:
1222 1219 islink, isexec = gp.mode
1223 1220 dst = repo.wjoin(gp.path)
1224 1221 # patch won't create empty files
1225 1222 if gp.op == 'ADD' and not os.path.exists(dst):
1226 1223 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1227 1224 repo.wwrite(gp.path, '', flags)
1228 1225 elif gp.op != 'DELETE':
1229 1226 util.set_flags(dst, islink, isexec)
1230 1227 cmdutil.addremove(repo, cfiles, similarity=similarity)
1231 1228 files = patches.keys()
1232 1229 files.extend([r for r in removes if r not in files])
1233 1230 return sorted(files)
1234 1231
1235 1232 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
1236 1233 """use <patcher> to apply <patchname> to the working directory.
1237 1234 returns whether patch was applied with fuzz factor."""
1238 1235
1239 1236 fuzz = False
1240 1237 if cwd:
1241 1238 args.append('-d %s' % util.shellquote(cwd))
1242 1239 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1243 1240 util.shellquote(patchname)))
1244 1241
1245 1242 for line in fp:
1246 1243 line = line.rstrip()
1247 1244 ui.note(line + '\n')
1248 1245 if line.startswith('patching file '):
1249 1246 pf = util.parse_patch_output(line)
1250 1247 printed_file = False
1251 1248 files.setdefault(pf, None)
1252 1249 elif line.find('with fuzz') >= 0:
1253 1250 fuzz = True
1254 1251 if not printed_file:
1255 1252 ui.warn(pf + '\n')
1256 1253 printed_file = True
1257 1254 ui.warn(line + '\n')
1258 1255 elif line.find('saving rejects to file') >= 0:
1259 1256 ui.warn(line + '\n')
1260 1257 elif line.find('FAILED') >= 0:
1261 1258 if not printed_file:
1262 1259 ui.warn(pf + '\n')
1263 1260 printed_file = True
1264 1261 ui.warn(line + '\n')
1265 1262 code = fp.close()
1266 1263 if code:
1267 1264 raise PatchError(_("patch command failed: %s") %
1268 1265 util.explain_exit(code)[0])
1269 1266 return fuzz
1270 1267
1271 1268 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1272 1269 """use builtin patch to apply <patchobj> to the working directory.
1273 1270 returns whether patch was applied with fuzz factor."""
1274 1271
1275 1272 if files is None:
1276 1273 files = {}
1277 1274 if eolmode is None:
1278 1275 eolmode = ui.config('patch', 'eol', 'strict')
1279 1276 if eolmode.lower() not in eolmodes:
1280 1277 raise util.Abort(_('Unsupported line endings type: %s') % eolmode)
1281 1278 eolmode = eolmode.lower()
1282 1279
1283 1280 try:
1284 1281 fp = open(patchobj, 'rb')
1285 1282 except TypeError:
1286 1283 fp = patchobj
1287 1284 if cwd:
1288 1285 curdir = os.getcwd()
1289 1286 os.chdir(cwd)
1290 1287 try:
1291 1288 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1292 1289 finally:
1293 1290 if cwd:
1294 1291 os.chdir(curdir)
1295 1292 if fp != patchobj:
1296 1293 fp.close()
1297 1294 if ret < 0:
1298 1295 raise PatchError
1299 1296 return ret > 0
1300 1297
1301 1298 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1302 1299 """Apply <patchname> to the working directory.
1303 1300
1304 1301 'eolmode' specifies how end of lines should be handled. It can be:
1305 1302 - 'strict': inputs are read in binary mode, EOLs are preserved
1306 1303 - 'crlf': EOLs are ignored when patching and reset to CRLF
1307 1304 - 'lf': EOLs are ignored when patching and reset to LF
1308 1305 - None: get it from user settings, default to 'strict'
1309 1306 'eolmode' is ignored when using an external patcher program.
1310 1307
1311 1308 Returns whether patch was applied with fuzz factor.
1312 1309 """
1313 1310 patcher = ui.config('ui', 'patch')
1314 1311 args = []
1315 1312 if files is None:
1316 1313 files = {}
1317 1314 try:
1318 1315 if patcher:
1319 1316 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1320 1317 files)
1321 1318 else:
1322 1319 try:
1323 1320 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1324 1321 except NoHunks:
1325 1322 patcher = (util.find_exe('gpatch') or util.find_exe('patch')
1326 1323 or 'patch')
1327 1324 ui.debug('no valid hunks found; trying with %r instead\n' %
1328 1325 patcher)
1329 1326 if util.needbinarypatch():
1330 1327 args.append('--binary')
1331 1328 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1332 1329 files)
1333 1330 except PatchError, err:
1334 1331 s = str(err)
1335 1332 if s:
1336 1333 raise util.Abort(s)
1337 1334 else:
1338 1335 raise util.Abort(_('patch failed to apply'))
1339 1336
1340 1337 def b85diff(to, tn):
1341 1338 '''print base85-encoded binary diff'''
1342 1339 def gitindex(text):
1343 1340 if not text:
1344 1341 return '0' * 40
1345 1342 l = len(text)
1346 1343 s = util.sha1('blob %d\0' % l)
1347 1344 s.update(text)
1348 1345 return s.hexdigest()
1349 1346
1350 1347 def fmtline(line):
1351 1348 l = len(line)
1352 1349 if l <= 26:
1353 1350 l = chr(ord('A') + l - 1)
1354 1351 else:
1355 1352 l = chr(l - 26 + ord('a') - 1)
1356 1353 return '%c%s\n' % (l, base85.b85encode(line, True))
1357 1354
1358 1355 def chunk(text, csize=52):
1359 1356 l = len(text)
1360 1357 i = 0
1361 1358 while i < l:
1362 1359 yield text[i:i + csize]
1363 1360 i += csize
1364 1361
1365 1362 tohash = gitindex(to)
1366 1363 tnhash = gitindex(tn)
1367 1364 if tohash == tnhash:
1368 1365 return ""
1369 1366
1370 1367 # TODO: deltas
1371 1368 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1372 1369 (tohash, tnhash, len(tn))]
1373 1370 for l in chunk(zlib.compress(tn)):
1374 1371 ret.append(fmtline(l))
1375 1372 ret.append('\n')
1376 1373 return ''.join(ret)
1377 1374
1378 1375 class GitDiffRequired(Exception):
1379 1376 pass
1380 1377
1381 1378 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1382 1379 losedatafn=None):
1383 1380 '''yields diff of changes to files between two nodes, or node and
1384 1381 working directory.
1385 1382
1386 1383 if node1 is None, use first dirstate parent instead.
1387 1384 if node2 is None, compare node1 with working directory.
1388 1385
1389 1386 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1390 1387 every time some change cannot be represented with the current
1391 1388 patch format. Return False to upgrade to git patch format, True to
1392 1389 accept the loss or raise an exception to abort the diff. It is
1393 1390 called with the name of current file being diffed as 'fn'. If set
1394 1391 to None, patches will always be upgraded to git format when
1395 1392 necessary.
1396 1393 '''
1397 1394
1398 1395 if opts is None:
1399 1396 opts = mdiff.defaultopts
1400 1397
1401 1398 if not node1 and not node2:
1402 1399 node1 = repo.dirstate.parents()[0]
1403 1400
1404 1401 def lrugetfilectx():
1405 1402 cache = {}
1406 1403 order = []
1407 1404 def getfilectx(f, ctx):
1408 1405 fctx = ctx.filectx(f, filelog=cache.get(f))
1409 1406 if f not in cache:
1410 1407 if len(cache) > 20:
1411 1408 del cache[order.pop(0)]
1412 1409 cache[f] = fctx.filelog()
1413 1410 else:
1414 1411 order.remove(f)
1415 1412 order.append(f)
1416 1413 return fctx
1417 1414 return getfilectx
1418 1415 getfilectx = lrugetfilectx()
1419 1416
1420 1417 ctx1 = repo[node1]
1421 1418 ctx2 = repo[node2]
1422 1419
1423 1420 if not changes:
1424 1421 changes = repo.status(ctx1, ctx2, match=match)
1425 1422 modified, added, removed = changes[:3]
1426 1423
1427 1424 if not modified and not added and not removed:
1428 1425 return []
1429 1426
1430 1427 revs = None
1431 1428 if not repo.ui.quiet:
1432 1429 hexfunc = repo.ui.debugflag and hex or short
1433 1430 revs = [hexfunc(node) for node in [node1, node2] if node]
1434 1431
1435 1432 copy = {}
1436 1433 if opts.git or opts.upgrade:
1437 1434 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1438 1435
1439 1436 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1440 1437 modified, added, removed, copy, getfilectx, opts, losedata)
1441 1438 if opts.upgrade and not opts.git:
1442 1439 try:
1443 1440 def losedata(fn):
1444 1441 if not losedatafn or not losedatafn(fn=fn):
1445 1442 raise GitDiffRequired()
1446 1443 # Buffer the whole output until we are sure it can be generated
1447 1444 return list(difffn(opts.copy(git=False), losedata))
1448 1445 except GitDiffRequired:
1449 1446 return difffn(opts.copy(git=True), None)
1450 1447 else:
1451 1448 return difffn(opts, None)
1452 1449
1453 1450 def _addmodehdr(header, omode, nmode):
1454 1451 if omode != nmode:
1455 1452 header.append('old mode %s\n' % omode)
1456 1453 header.append('new mode %s\n' % nmode)
1457 1454
1458 1455 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1459 1456 copy, getfilectx, opts, losedatafn):
1460 1457
1461 1458 date1 = util.datestr(ctx1.date())
1462 1459 man1 = ctx1.manifest()
1463 1460
1464 1461 gone = set()
1465 1462 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1466 1463
1467 1464 copyto = dict([(v, k) for k, v in copy.items()])
1468 1465
1469 1466 if opts.git:
1470 1467 revs = None
1471 1468
1472 1469 for f in sorted(modified + added + removed):
1473 1470 to = None
1474 1471 tn = None
1475 1472 dodiff = True
1476 1473 header = []
1477 1474 if f in man1:
1478 1475 to = getfilectx(f, ctx1).data()
1479 1476 if f not in removed:
1480 1477 tn = getfilectx(f, ctx2).data()
1481 1478 a, b = f, f
1482 1479 if opts.git or losedatafn:
1483 1480 if f in added:
1484 1481 mode = gitmode[ctx2.flags(f)]
1485 1482 if f in copy or f in copyto:
1486 1483 if opts.git:
1487 1484 if f in copy:
1488 1485 a = copy[f]
1489 1486 else:
1490 1487 a = copyto[f]
1491 1488 omode = gitmode[man1.flags(a)]
1492 1489 _addmodehdr(header, omode, mode)
1493 1490 if a in removed and a not in gone:
1494 1491 op = 'rename'
1495 1492 gone.add(a)
1496 1493 else:
1497 1494 op = 'copy'
1498 1495 header.append('%s from %s\n' % (op, a))
1499 1496 header.append('%s to %s\n' % (op, f))
1500 1497 to = getfilectx(a, ctx1).data()
1501 1498 else:
1502 1499 losedatafn(f)
1503 1500 else:
1504 1501 if opts.git:
1505 1502 header.append('new file mode %s\n' % mode)
1506 1503 elif ctx2.flags(f):
1507 1504 losedatafn(f)
1508 1505 if util.binary(tn):
1509 1506 if opts.git:
1510 1507 dodiff = 'binary'
1511 1508 else:
1512 1509 losedatafn(f)
1513 1510 if not opts.git and not tn:
1514 1511 # regular diffs cannot represent new empty file
1515 1512 losedatafn(f)
1516 1513 elif f in removed:
1517 1514 if opts.git:
1518 1515 # have we already reported a copy above?
1519 1516 if ((f in copy and copy[f] in added
1520 1517 and copyto[copy[f]] == f) or
1521 1518 (f in copyto and copyto[f] in added
1522 1519 and copy[copyto[f]] == f)):
1523 1520 dodiff = False
1524 1521 else:
1525 1522 header.append('deleted file mode %s\n' %
1526 1523 gitmode[man1.flags(f)])
1527 1524 elif not to:
1528 1525 # regular diffs cannot represent empty file deletion
1529 1526 losedatafn(f)
1530 1527 else:
1531 1528 oflag = man1.flags(f)
1532 1529 nflag = ctx2.flags(f)
1533 1530 binary = util.binary(to) or util.binary(tn)
1534 1531 if opts.git:
1535 1532 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1536 1533 if binary:
1537 1534 dodiff = 'binary'
1538 1535 elif binary or nflag != oflag:
1539 1536 losedatafn(f)
1540 1537 if opts.git:
1541 1538 header.insert(0, mdiff.diffline(revs, a, b, opts))
1542 1539
1543 1540 if dodiff:
1544 1541 if dodiff == 'binary':
1545 1542 text = b85diff(to, tn)
1546 1543 else:
1547 1544 text = mdiff.unidiff(to, date1,
1548 1545 # ctx2 date may be dynamic
1549 1546 tn, util.datestr(ctx2.date()),
1550 1547 a, b, revs, opts=opts)
1551 1548 if header and (text or len(header) > 1):
1552 1549 yield ''.join(header)
1553 1550 if text:
1554 1551 yield text
1555 1552
1556 1553 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1557 1554 opts=None):
1558 1555 '''export changesets as hg patches.'''
1559 1556
1560 1557 total = len(revs)
1561 1558 revwidth = max([len(str(rev)) for rev in revs])
1562 1559
1563 1560 def single(rev, seqno, fp):
1564 1561 ctx = repo[rev]
1565 1562 node = ctx.node()
1566 1563 parents = [p.node() for p in ctx.parents() if p]
1567 1564 branch = ctx.branch()
1568 1565 if switch_parent:
1569 1566 parents.reverse()
1570 1567 prev = (parents and parents[0]) or nullid
1571 1568
1572 1569 if not fp:
1573 1570 fp = cmdutil.make_file(repo, template, node, total=total,
1574 1571 seqno=seqno, revwidth=revwidth,
1575 1572 mode='ab')
1576 1573 if fp != sys.stdout and hasattr(fp, 'name'):
1577 1574 repo.ui.note("%s\n" % fp.name)
1578 1575
1579 1576 fp.write("# HG changeset patch\n")
1580 1577 fp.write("# User %s\n" % ctx.user())
1581 1578 fp.write("# Date %d %d\n" % ctx.date())
1582 1579 if branch and (branch != 'default'):
1583 1580 fp.write("# Branch %s\n" % branch)
1584 1581 fp.write("# Node ID %s\n" % hex(node))
1585 1582 fp.write("# Parent %s\n" % hex(prev))
1586 1583 if len(parents) > 1:
1587 1584 fp.write("# Parent %s\n" % hex(parents[1]))
1588 1585 fp.write(ctx.description().rstrip())
1589 1586 fp.write("\n\n")
1590 1587
1591 1588 for chunk in diff(repo, prev, node, opts=opts):
1592 1589 fp.write(chunk)
1593 1590
1594 1591 for seqno, rev in enumerate(revs):
1595 1592 single(rev, seqno + 1, fp)
1596 1593
1597 1594 def diffstatdata(lines):
1598 1595 filename, adds, removes = None, 0, 0
1599 1596 for line in lines:
1600 1597 if line.startswith('diff'):
1601 1598 if filename:
1602 1599 isbinary = adds == 0 and removes == 0
1603 1600 yield (filename, adds, removes, isbinary)
1604 1601 # set numbers to 0 anyway when starting new file
1605 1602 adds, removes = 0, 0
1606 1603 if line.startswith('diff --git'):
1607 1604 filename = gitre.search(line).group(1)
1608 1605 else:
1609 1606 # format: "diff -r ... -r ... filename"
1610 1607 filename = line.split(None, 5)[-1]
1611 1608 elif line.startswith('+') and not line.startswith('+++'):
1612 1609 adds += 1
1613 1610 elif line.startswith('-') and not line.startswith('---'):
1614 1611 removes += 1
1615 1612 if filename:
1616 1613 isbinary = adds == 0 and removes == 0
1617 1614 yield (filename, adds, removes, isbinary)
1618 1615
1619 1616 def diffstat(lines, width=80, git=False):
1620 1617 output = []
1621 1618 stats = list(diffstatdata(lines))
1622 1619
1623 1620 maxtotal, maxname = 0, 0
1624 1621 totaladds, totalremoves = 0, 0
1625 1622 hasbinary = False
1626 1623 for filename, adds, removes, isbinary in stats:
1627 1624 totaladds += adds
1628 1625 totalremoves += removes
1629 1626 maxname = max(maxname, len(filename))
1630 1627 maxtotal = max(maxtotal, adds + removes)
1631 1628 if isbinary:
1632 1629 hasbinary = True
1633 1630
1634 1631 countwidth = len(str(maxtotal))
1635 1632 if hasbinary and countwidth < 3:
1636 1633 countwidth = 3
1637 1634 graphwidth = width - countwidth - maxname - 6
1638 1635 if graphwidth < 10:
1639 1636 graphwidth = 10
1640 1637
1641 1638 def scale(i):
1642 1639 if maxtotal <= graphwidth:
1643 1640 return i
1644 1641 # If diffstat runs out of room it doesn't print anything,
1645 1642 # which isn't very useful, so always print at least one + or -
1646 1643 # if there were at least some changes.
1647 1644 return max(i * graphwidth // maxtotal, int(bool(i)))
1648 1645
1649 1646 for filename, adds, removes, isbinary in stats:
1650 1647 if git and isbinary:
1651 1648 count = 'Bin'
1652 1649 else:
1653 1650 count = adds + removes
1654 1651 pluses = '+' * scale(adds)
1655 1652 minuses = '-' * scale(removes)
1656 1653 output.append(' %-*s | %*s %s%s\n' % (maxname, filename, countwidth,
1657 1654 count, pluses, minuses))
1658 1655
1659 1656 if stats:
1660 1657 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1661 1658 % (len(stats), totaladds, totalremoves))
1662 1659
1663 1660 return ''.join(output)
General Comments 0
You need to be logged in to leave comments. Login now