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