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