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