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