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