##// END OF EJS Templates
import: abort instead of crashing when copy source does not exist (issue5375)...
Ryan McElroy -
r30078:173bdb50 default
parent child Browse files
Show More
@@ -1,2606 +1,2608 b''
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import collections
12 12 import copy
13 13 import email
14 14 import errno
15 15 import hashlib
16 16 import os
17 17 import posixpath
18 18 import re
19 19 import shutil
20 20 import tempfile
21 21 import zlib
22 22
23 23 from .i18n import _
24 24 from .node import (
25 25 hex,
26 26 short,
27 27 )
28 28 from . import (
29 29 base85,
30 30 copies,
31 31 diffhelpers,
32 32 encoding,
33 33 error,
34 34 mail,
35 35 mdiff,
36 36 pathutil,
37 37 scmutil,
38 38 util,
39 39 )
40 40 stringio = util.stringio
41 41
42 42 gitre = re.compile('diff --git a/(.*) b/(.*)')
43 43 tabsplitter = re.compile(r'(\t+|[^\t]+)')
44 44
45 45 class PatchError(Exception):
46 46 pass
47 47
48 48
49 49 # public functions
50 50
51 51 def split(stream):
52 52 '''return an iterator of individual patches from a stream'''
53 53 def isheader(line, inheader):
54 54 if inheader and line[0] in (' ', '\t'):
55 55 # continuation
56 56 return True
57 57 if line[0] in (' ', '-', '+'):
58 58 # diff line - don't check for header pattern in there
59 59 return False
60 60 l = line.split(': ', 1)
61 61 return len(l) == 2 and ' ' not in l[0]
62 62
63 63 def chunk(lines):
64 64 return stringio(''.join(lines))
65 65
66 66 def hgsplit(stream, cur):
67 67 inheader = True
68 68
69 69 for line in stream:
70 70 if not line.strip():
71 71 inheader = False
72 72 if not inheader and line.startswith('# HG changeset patch'):
73 73 yield chunk(cur)
74 74 cur = []
75 75 inheader = True
76 76
77 77 cur.append(line)
78 78
79 79 if cur:
80 80 yield chunk(cur)
81 81
82 82 def mboxsplit(stream, cur):
83 83 for line in stream:
84 84 if line.startswith('From '):
85 85 for c in split(chunk(cur[1:])):
86 86 yield c
87 87 cur = []
88 88
89 89 cur.append(line)
90 90
91 91 if cur:
92 92 for c in split(chunk(cur[1:])):
93 93 yield c
94 94
95 95 def mimesplit(stream, cur):
96 96 def msgfp(m):
97 97 fp = stringio()
98 98 g = email.Generator.Generator(fp, mangle_from_=False)
99 99 g.flatten(m)
100 100 fp.seek(0)
101 101 return fp
102 102
103 103 for line in stream:
104 104 cur.append(line)
105 105 c = chunk(cur)
106 106
107 107 m = email.Parser.Parser().parse(c)
108 108 if not m.is_multipart():
109 109 yield msgfp(m)
110 110 else:
111 111 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
112 112 for part in m.walk():
113 113 ct = part.get_content_type()
114 114 if ct not in ok_types:
115 115 continue
116 116 yield msgfp(part)
117 117
118 118 def headersplit(stream, cur):
119 119 inheader = False
120 120
121 121 for line in stream:
122 122 if not inheader and isheader(line, inheader):
123 123 yield chunk(cur)
124 124 cur = []
125 125 inheader = True
126 126 if inheader and not isheader(line, inheader):
127 127 inheader = False
128 128
129 129 cur.append(line)
130 130
131 131 if cur:
132 132 yield chunk(cur)
133 133
134 134 def remainder(cur):
135 135 yield chunk(cur)
136 136
137 137 class fiter(object):
138 138 def __init__(self, fp):
139 139 self.fp = fp
140 140
141 141 def __iter__(self):
142 142 return self
143 143
144 144 def next(self):
145 145 l = self.fp.readline()
146 146 if not l:
147 147 raise StopIteration
148 148 return l
149 149
150 150 inheader = False
151 151 cur = []
152 152
153 153 mimeheaders = ['content-type']
154 154
155 155 if not util.safehasattr(stream, 'next'):
156 156 # http responses, for example, have readline but not next
157 157 stream = fiter(stream)
158 158
159 159 for line in stream:
160 160 cur.append(line)
161 161 if line.startswith('# HG changeset patch'):
162 162 return hgsplit(stream, cur)
163 163 elif line.startswith('From '):
164 164 return mboxsplit(stream, cur)
165 165 elif isheader(line, inheader):
166 166 inheader = True
167 167 if line.split(':', 1)[0].lower() in mimeheaders:
168 168 # let email parser handle this
169 169 return mimesplit(stream, cur)
170 170 elif line.startswith('--- ') and inheader:
171 171 # No evil headers seen by diff start, split by hand
172 172 return headersplit(stream, cur)
173 173 # Not enough info, keep reading
174 174
175 175 # if we are here, we have a very plain patch
176 176 return remainder(cur)
177 177
178 178 ## Some facility for extensible patch parsing:
179 179 # list of pairs ("header to match", "data key")
180 180 patchheadermap = [('Date', 'date'),
181 181 ('Branch', 'branch'),
182 182 ('Node ID', 'nodeid'),
183 183 ]
184 184
185 185 def extract(ui, fileobj):
186 186 '''extract patch from data read from fileobj.
187 187
188 188 patch can be a normal patch or contained in an email message.
189 189
190 190 return a dictionary. Standard keys are:
191 191 - filename,
192 192 - message,
193 193 - user,
194 194 - date,
195 195 - branch,
196 196 - node,
197 197 - p1,
198 198 - p2.
199 199 Any item can be missing from the dictionary. If filename is missing,
200 200 fileobj did not contain a patch. Caller must unlink filename when done.'''
201 201
202 202 # attempt to detect the start of a patch
203 203 # (this heuristic is borrowed from quilt)
204 204 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
205 205 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
206 206 r'---[ \t].*?^\+\+\+[ \t]|'
207 207 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
208 208
209 209 data = {}
210 210 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
211 211 tmpfp = os.fdopen(fd, 'w')
212 212 try:
213 213 msg = email.Parser.Parser().parse(fileobj)
214 214
215 215 subject = msg['Subject'] and mail.headdecode(msg['Subject'])
216 216 data['user'] = msg['From'] and mail.headdecode(msg['From'])
217 217 if not subject and not data['user']:
218 218 # Not an email, restore parsed headers if any
219 219 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
220 220
221 221 # should try to parse msg['Date']
222 222 parents = []
223 223
224 224 if subject:
225 225 if subject.startswith('[PATCH'):
226 226 pend = subject.find(']')
227 227 if pend >= 0:
228 228 subject = subject[pend + 1:].lstrip()
229 229 subject = re.sub(r'\n[ \t]+', ' ', subject)
230 230 ui.debug('Subject: %s\n' % subject)
231 231 if data['user']:
232 232 ui.debug('From: %s\n' % data['user'])
233 233 diffs_seen = 0
234 234 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
235 235 message = ''
236 236 for part in msg.walk():
237 237 content_type = part.get_content_type()
238 238 ui.debug('Content-Type: %s\n' % content_type)
239 239 if content_type not in ok_types:
240 240 continue
241 241 payload = part.get_payload(decode=True)
242 242 m = diffre.search(payload)
243 243 if m:
244 244 hgpatch = False
245 245 hgpatchheader = False
246 246 ignoretext = False
247 247
248 248 ui.debug('found patch at byte %d\n' % m.start(0))
249 249 diffs_seen += 1
250 250 cfp = stringio()
251 251 for line in payload[:m.start(0)].splitlines():
252 252 if line.startswith('# HG changeset patch') and not hgpatch:
253 253 ui.debug('patch generated by hg export\n')
254 254 hgpatch = True
255 255 hgpatchheader = True
256 256 # drop earlier commit message content
257 257 cfp.seek(0)
258 258 cfp.truncate()
259 259 subject = None
260 260 elif hgpatchheader:
261 261 if line.startswith('# User '):
262 262 data['user'] = line[7:]
263 263 ui.debug('From: %s\n' % data['user'])
264 264 elif line.startswith("# Parent "):
265 265 parents.append(line[9:].lstrip())
266 266 elif line.startswith("# "):
267 267 for header, key in patchheadermap:
268 268 prefix = '# %s ' % header
269 269 if line.startswith(prefix):
270 270 data[key] = line[len(prefix):]
271 271 else:
272 272 hgpatchheader = False
273 273 elif line == '---':
274 274 ignoretext = True
275 275 if not hgpatchheader and not ignoretext:
276 276 cfp.write(line)
277 277 cfp.write('\n')
278 278 message = cfp.getvalue()
279 279 if tmpfp:
280 280 tmpfp.write(payload)
281 281 if not payload.endswith('\n'):
282 282 tmpfp.write('\n')
283 283 elif not diffs_seen and message and content_type == 'text/plain':
284 284 message += '\n' + payload
285 285 except: # re-raises
286 286 tmpfp.close()
287 287 os.unlink(tmpname)
288 288 raise
289 289
290 290 if subject and not message.startswith(subject):
291 291 message = '%s\n%s' % (subject, message)
292 292 data['message'] = message
293 293 tmpfp.close()
294 294 if parents:
295 295 data['p1'] = parents.pop(0)
296 296 if parents:
297 297 data['p2'] = parents.pop(0)
298 298
299 299 if diffs_seen:
300 300 data['filename'] = tmpname
301 301 else:
302 302 os.unlink(tmpname)
303 303 return data
304 304
305 305 class patchmeta(object):
306 306 """Patched file metadata
307 307
308 308 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
309 309 or COPY. 'path' is patched file path. 'oldpath' is set to the
310 310 origin file when 'op' is either COPY or RENAME, None otherwise. If
311 311 file mode is changed, 'mode' is a tuple (islink, isexec) where
312 312 'islink' is True if the file is a symlink and 'isexec' is True if
313 313 the file is executable. Otherwise, 'mode' is None.
314 314 """
315 315 def __init__(self, path):
316 316 self.path = path
317 317 self.oldpath = None
318 318 self.mode = None
319 319 self.op = 'MODIFY'
320 320 self.binary = False
321 321
322 322 def setmode(self, mode):
323 323 islink = mode & 0o20000
324 324 isexec = mode & 0o100
325 325 self.mode = (islink, isexec)
326 326
327 327 def copy(self):
328 328 other = patchmeta(self.path)
329 329 other.oldpath = self.oldpath
330 330 other.mode = self.mode
331 331 other.op = self.op
332 332 other.binary = self.binary
333 333 return other
334 334
335 335 def _ispatchinga(self, afile):
336 336 if afile == '/dev/null':
337 337 return self.op == 'ADD'
338 338 return afile == 'a/' + (self.oldpath or self.path)
339 339
340 340 def _ispatchingb(self, bfile):
341 341 if bfile == '/dev/null':
342 342 return self.op == 'DELETE'
343 343 return bfile == 'b/' + self.path
344 344
345 345 def ispatching(self, afile, bfile):
346 346 return self._ispatchinga(afile) and self._ispatchingb(bfile)
347 347
348 348 def __repr__(self):
349 349 return "<patchmeta %s %r>" % (self.op, self.path)
350 350
351 351 def readgitpatch(lr):
352 352 """extract git-style metadata about patches from <patchname>"""
353 353
354 354 # Filter patch for git information
355 355 gp = None
356 356 gitpatches = []
357 357 for line in lr:
358 358 line = line.rstrip(' \r\n')
359 359 if line.startswith('diff --git a/'):
360 360 m = gitre.match(line)
361 361 if m:
362 362 if gp:
363 363 gitpatches.append(gp)
364 364 dst = m.group(2)
365 365 gp = patchmeta(dst)
366 366 elif gp:
367 367 if line.startswith('--- '):
368 368 gitpatches.append(gp)
369 369 gp = None
370 370 continue
371 371 if line.startswith('rename from '):
372 372 gp.op = 'RENAME'
373 373 gp.oldpath = line[12:]
374 374 elif line.startswith('rename to '):
375 375 gp.path = line[10:]
376 376 elif line.startswith('copy from '):
377 377 gp.op = 'COPY'
378 378 gp.oldpath = line[10:]
379 379 elif line.startswith('copy to '):
380 380 gp.path = line[8:]
381 381 elif line.startswith('deleted file'):
382 382 gp.op = 'DELETE'
383 383 elif line.startswith('new file mode '):
384 384 gp.op = 'ADD'
385 385 gp.setmode(int(line[-6:], 8))
386 386 elif line.startswith('new mode '):
387 387 gp.setmode(int(line[-6:], 8))
388 388 elif line.startswith('GIT binary patch'):
389 389 gp.binary = True
390 390 if gp:
391 391 gitpatches.append(gp)
392 392
393 393 return gitpatches
394 394
395 395 class linereader(object):
396 396 # simple class to allow pushing lines back into the input stream
397 397 def __init__(self, fp):
398 398 self.fp = fp
399 399 self.buf = []
400 400
401 401 def push(self, line):
402 402 if line is not None:
403 403 self.buf.append(line)
404 404
405 405 def readline(self):
406 406 if self.buf:
407 407 l = self.buf[0]
408 408 del self.buf[0]
409 409 return l
410 410 return self.fp.readline()
411 411
412 412 def __iter__(self):
413 413 return iter(self.readline, '')
414 414
415 415 class abstractbackend(object):
416 416 def __init__(self, ui):
417 417 self.ui = ui
418 418
419 419 def getfile(self, fname):
420 420 """Return target file data and flags as a (data, (islink,
421 421 isexec)) tuple. Data is None if file is missing/deleted.
422 422 """
423 423 raise NotImplementedError
424 424
425 425 def setfile(self, fname, data, mode, copysource):
426 426 """Write data to target file fname and set its mode. mode is a
427 427 (islink, isexec) tuple. If data is None, the file content should
428 428 be left unchanged. If the file is modified after being copied,
429 429 copysource is set to the original file name.
430 430 """
431 431 raise NotImplementedError
432 432
433 433 def unlink(self, fname):
434 434 """Unlink target file."""
435 435 raise NotImplementedError
436 436
437 437 def writerej(self, fname, failed, total, lines):
438 438 """Write rejected lines for fname. total is the number of hunks
439 439 which failed to apply and total the total number of hunks for this
440 440 files.
441 441 """
442 442 pass
443 443
444 444 def exists(self, fname):
445 445 raise NotImplementedError
446 446
447 447 class fsbackend(abstractbackend):
448 448 def __init__(self, ui, basedir):
449 449 super(fsbackend, self).__init__(ui)
450 450 self.opener = scmutil.opener(basedir)
451 451
452 452 def _join(self, f):
453 453 return os.path.join(self.opener.base, f)
454 454
455 455 def getfile(self, fname):
456 456 if self.opener.islink(fname):
457 457 return (self.opener.readlink(fname), (True, False))
458 458
459 459 isexec = False
460 460 try:
461 461 isexec = self.opener.lstat(fname).st_mode & 0o100 != 0
462 462 except OSError as e:
463 463 if e.errno != errno.ENOENT:
464 464 raise
465 465 try:
466 466 return (self.opener.read(fname), (False, isexec))
467 467 except IOError as e:
468 468 if e.errno != errno.ENOENT:
469 469 raise
470 470 return None, None
471 471
472 472 def setfile(self, fname, data, mode, copysource):
473 473 islink, isexec = mode
474 474 if data is None:
475 475 self.opener.setflags(fname, islink, isexec)
476 476 return
477 477 if islink:
478 478 self.opener.symlink(data, fname)
479 479 else:
480 480 self.opener.write(fname, data)
481 481 if isexec:
482 482 self.opener.setflags(fname, False, True)
483 483
484 484 def unlink(self, fname):
485 485 self.opener.unlinkpath(fname, ignoremissing=True)
486 486
487 487 def writerej(self, fname, failed, total, lines):
488 488 fname = fname + ".rej"
489 489 self.ui.warn(
490 490 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
491 491 (failed, total, fname))
492 492 fp = self.opener(fname, 'w')
493 493 fp.writelines(lines)
494 494 fp.close()
495 495
496 496 def exists(self, fname):
497 497 return self.opener.lexists(fname)
498 498
499 499 class workingbackend(fsbackend):
500 500 def __init__(self, ui, repo, similarity):
501 501 super(workingbackend, self).__init__(ui, repo.root)
502 502 self.repo = repo
503 503 self.similarity = similarity
504 504 self.removed = set()
505 505 self.changed = set()
506 506 self.copied = []
507 507
508 508 def _checkknown(self, fname):
509 509 if self.repo.dirstate[fname] == '?' and self.exists(fname):
510 510 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
511 511
512 512 def setfile(self, fname, data, mode, copysource):
513 513 self._checkknown(fname)
514 514 super(workingbackend, self).setfile(fname, data, mode, copysource)
515 515 if copysource is not None:
516 516 self.copied.append((copysource, fname))
517 517 self.changed.add(fname)
518 518
519 519 def unlink(self, fname):
520 520 self._checkknown(fname)
521 521 super(workingbackend, self).unlink(fname)
522 522 self.removed.add(fname)
523 523 self.changed.add(fname)
524 524
525 525 def close(self):
526 526 wctx = self.repo[None]
527 527 changed = set(self.changed)
528 528 for src, dst in self.copied:
529 529 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
530 530 if self.removed:
531 531 wctx.forget(sorted(self.removed))
532 532 for f in self.removed:
533 533 if f not in self.repo.dirstate:
534 534 # File was deleted and no longer belongs to the
535 535 # dirstate, it was probably marked added then
536 536 # deleted, and should not be considered by
537 537 # marktouched().
538 538 changed.discard(f)
539 539 if changed:
540 540 scmutil.marktouched(self.repo, changed, self.similarity)
541 541 return sorted(self.changed)
542 542
543 543 class filestore(object):
544 544 def __init__(self, maxsize=None):
545 545 self.opener = None
546 546 self.files = {}
547 547 self.created = 0
548 548 self.maxsize = maxsize
549 549 if self.maxsize is None:
550 550 self.maxsize = 4*(2**20)
551 551 self.size = 0
552 552 self.data = {}
553 553
554 554 def setfile(self, fname, data, mode, copied=None):
555 555 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
556 556 self.data[fname] = (data, mode, copied)
557 557 self.size += len(data)
558 558 else:
559 559 if self.opener is None:
560 560 root = tempfile.mkdtemp(prefix='hg-patch-')
561 561 self.opener = scmutil.opener(root)
562 562 # Avoid filename issues with these simple names
563 563 fn = str(self.created)
564 564 self.opener.write(fn, data)
565 565 self.created += 1
566 566 self.files[fname] = (fn, mode, copied)
567 567
568 568 def getfile(self, fname):
569 569 if fname in self.data:
570 570 return self.data[fname]
571 571 if not self.opener or fname not in self.files:
572 572 return None, None, None
573 573 fn, mode, copied = self.files[fname]
574 574 return self.opener.read(fn), mode, copied
575 575
576 576 def close(self):
577 577 if self.opener:
578 578 shutil.rmtree(self.opener.base)
579 579
580 580 class repobackend(abstractbackend):
581 581 def __init__(self, ui, repo, ctx, store):
582 582 super(repobackend, self).__init__(ui)
583 583 self.repo = repo
584 584 self.ctx = ctx
585 585 self.store = store
586 586 self.changed = set()
587 587 self.removed = set()
588 588 self.copied = {}
589 589
590 590 def _checkknown(self, fname):
591 591 if fname not in self.ctx:
592 592 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
593 593
594 594 def getfile(self, fname):
595 595 try:
596 596 fctx = self.ctx[fname]
597 597 except error.LookupError:
598 598 return None, None
599 599 flags = fctx.flags()
600 600 return fctx.data(), ('l' in flags, 'x' in flags)
601 601
602 602 def setfile(self, fname, data, mode, copysource):
603 603 if copysource:
604 604 self._checkknown(copysource)
605 605 if data is None:
606 606 data = self.ctx[fname].data()
607 607 self.store.setfile(fname, data, mode, copysource)
608 608 self.changed.add(fname)
609 609 if copysource:
610 610 self.copied[fname] = copysource
611 611
612 612 def unlink(self, fname):
613 613 self._checkknown(fname)
614 614 self.removed.add(fname)
615 615
616 616 def exists(self, fname):
617 617 return fname in self.ctx
618 618
619 619 def close(self):
620 620 return self.changed | self.removed
621 621
622 622 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
623 623 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
624 624 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
625 625 eolmodes = ['strict', 'crlf', 'lf', 'auto']
626 626
627 627 class patchfile(object):
628 628 def __init__(self, ui, gp, backend, store, eolmode='strict'):
629 629 self.fname = gp.path
630 630 self.eolmode = eolmode
631 631 self.eol = None
632 632 self.backend = backend
633 633 self.ui = ui
634 634 self.lines = []
635 635 self.exists = False
636 636 self.missing = True
637 637 self.mode = gp.mode
638 638 self.copysource = gp.oldpath
639 639 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
640 640 self.remove = gp.op == 'DELETE'
641 641 if self.copysource is None:
642 642 data, mode = backend.getfile(self.fname)
643 643 else:
644 644 data, mode = store.getfile(self.copysource)[:2]
645 645 if data is not None:
646 646 self.exists = self.copysource is None or backend.exists(self.fname)
647 647 self.missing = False
648 648 if data:
649 649 self.lines = mdiff.splitnewlines(data)
650 650 if self.mode is None:
651 651 self.mode = mode
652 652 if self.lines:
653 653 # Normalize line endings
654 654 if self.lines[0].endswith('\r\n'):
655 655 self.eol = '\r\n'
656 656 elif self.lines[0].endswith('\n'):
657 657 self.eol = '\n'
658 658 if eolmode != 'strict':
659 659 nlines = []
660 660 for l in self.lines:
661 661 if l.endswith('\r\n'):
662 662 l = l[:-2] + '\n'
663 663 nlines.append(l)
664 664 self.lines = nlines
665 665 else:
666 666 if self.create:
667 667 self.missing = False
668 668 if self.mode is None:
669 669 self.mode = (False, False)
670 670 if self.missing:
671 671 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
672 672 self.ui.warn(_("(use '--prefix' to apply patch relative to the "
673 673 "current directory)\n"))
674 674
675 675 self.hash = {}
676 676 self.dirty = 0
677 677 self.offset = 0
678 678 self.skew = 0
679 679 self.rej = []
680 680 self.fileprinted = False
681 681 self.printfile(False)
682 682 self.hunks = 0
683 683
684 684 def writelines(self, fname, lines, mode):
685 685 if self.eolmode == 'auto':
686 686 eol = self.eol
687 687 elif self.eolmode == 'crlf':
688 688 eol = '\r\n'
689 689 else:
690 690 eol = '\n'
691 691
692 692 if self.eolmode != 'strict' and eol and eol != '\n':
693 693 rawlines = []
694 694 for l in lines:
695 695 if l and l[-1] == '\n':
696 696 l = l[:-1] + eol
697 697 rawlines.append(l)
698 698 lines = rawlines
699 699
700 700 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
701 701
702 702 def printfile(self, warn):
703 703 if self.fileprinted:
704 704 return
705 705 if warn or self.ui.verbose:
706 706 self.fileprinted = True
707 707 s = _("patching file %s\n") % self.fname
708 708 if warn:
709 709 self.ui.warn(s)
710 710 else:
711 711 self.ui.note(s)
712 712
713 713
714 714 def findlines(self, l, linenum):
715 715 # looks through the hash and finds candidate lines. The
716 716 # result is a list of line numbers sorted based on distance
717 717 # from linenum
718 718
719 719 cand = self.hash.get(l, [])
720 720 if len(cand) > 1:
721 721 # resort our list of potentials forward then back.
722 722 cand.sort(key=lambda x: abs(x - linenum))
723 723 return cand
724 724
725 725 def write_rej(self):
726 726 # our rejects are a little different from patch(1). This always
727 727 # creates rejects in the same form as the original patch. A file
728 728 # header is inserted so that you can run the reject through patch again
729 729 # without having to type the filename.
730 730 if not self.rej:
731 731 return
732 732 base = os.path.basename(self.fname)
733 733 lines = ["--- %s\n+++ %s\n" % (base, base)]
734 734 for x in self.rej:
735 735 for l in x.hunk:
736 736 lines.append(l)
737 737 if l[-1] != '\n':
738 738 lines.append("\n\ No newline at end of file\n")
739 739 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
740 740
741 741 def apply(self, h):
742 742 if not h.complete():
743 743 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
744 744 (h.number, h.desc, len(h.a), h.lena, len(h.b),
745 745 h.lenb))
746 746
747 747 self.hunks += 1
748 748
749 749 if self.missing:
750 750 self.rej.append(h)
751 751 return -1
752 752
753 753 if self.exists and self.create:
754 754 if self.copysource:
755 755 self.ui.warn(_("cannot create %s: destination already "
756 756 "exists\n") % self.fname)
757 757 else:
758 758 self.ui.warn(_("file %s already exists\n") % self.fname)
759 759 self.rej.append(h)
760 760 return -1
761 761
762 762 if isinstance(h, binhunk):
763 763 if self.remove:
764 764 self.backend.unlink(self.fname)
765 765 else:
766 766 l = h.new(self.lines)
767 767 self.lines[:] = l
768 768 self.offset += len(l)
769 769 self.dirty = True
770 770 return 0
771 771
772 772 horig = h
773 773 if (self.eolmode in ('crlf', 'lf')
774 774 or self.eolmode == 'auto' and self.eol):
775 775 # If new eols are going to be normalized, then normalize
776 776 # hunk data before patching. Otherwise, preserve input
777 777 # line-endings.
778 778 h = h.getnormalized()
779 779
780 780 # fast case first, no offsets, no fuzz
781 781 old, oldstart, new, newstart = h.fuzzit(0, False)
782 782 oldstart += self.offset
783 783 orig_start = oldstart
784 784 # if there's skew we want to emit the "(offset %d lines)" even
785 785 # when the hunk cleanly applies at start + skew, so skip the
786 786 # fast case code
787 787 if (self.skew == 0 and
788 788 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
789 789 if self.remove:
790 790 self.backend.unlink(self.fname)
791 791 else:
792 792 self.lines[oldstart:oldstart + len(old)] = new
793 793 self.offset += len(new) - len(old)
794 794 self.dirty = True
795 795 return 0
796 796
797 797 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
798 798 self.hash = {}
799 799 for x, s in enumerate(self.lines):
800 800 self.hash.setdefault(s, []).append(x)
801 801
802 802 for fuzzlen in xrange(self.ui.configint("patch", "fuzz", 2) + 1):
803 803 for toponly in [True, False]:
804 804 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
805 805 oldstart = oldstart + self.offset + self.skew
806 806 oldstart = min(oldstart, len(self.lines))
807 807 if old:
808 808 cand = self.findlines(old[0][1:], oldstart)
809 809 else:
810 810 # Only adding lines with no or fuzzed context, just
811 811 # take the skew in account
812 812 cand = [oldstart]
813 813
814 814 for l in cand:
815 815 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
816 816 self.lines[l : l + len(old)] = new
817 817 self.offset += len(new) - len(old)
818 818 self.skew = l - orig_start
819 819 self.dirty = True
820 820 offset = l - orig_start - fuzzlen
821 821 if fuzzlen:
822 822 msg = _("Hunk #%d succeeded at %d "
823 823 "with fuzz %d "
824 824 "(offset %d lines).\n")
825 825 self.printfile(True)
826 826 self.ui.warn(msg %
827 827 (h.number, l + 1, fuzzlen, offset))
828 828 else:
829 829 msg = _("Hunk #%d succeeded at %d "
830 830 "(offset %d lines).\n")
831 831 self.ui.note(msg % (h.number, l + 1, offset))
832 832 return fuzzlen
833 833 self.printfile(True)
834 834 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
835 835 self.rej.append(horig)
836 836 return -1
837 837
838 838 def close(self):
839 839 if self.dirty:
840 840 self.writelines(self.fname, self.lines, self.mode)
841 841 self.write_rej()
842 842 return len(self.rej)
843 843
844 844 class header(object):
845 845 """patch header
846 846 """
847 847 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
848 848 diff_re = re.compile('diff -r .* (.*)$')
849 849 allhunks_re = re.compile('(?:index|deleted file) ')
850 850 pretty_re = re.compile('(?:new file|deleted file) ')
851 851 special_re = re.compile('(?:index|deleted|copy|rename) ')
852 852 newfile_re = re.compile('(?:new file)')
853 853
854 854 def __init__(self, header):
855 855 self.header = header
856 856 self.hunks = []
857 857
858 858 def binary(self):
859 859 return any(h.startswith('index ') for h in self.header)
860 860
861 861 def pretty(self, fp):
862 862 for h in self.header:
863 863 if h.startswith('index '):
864 864 fp.write(_('this modifies a binary file (all or nothing)\n'))
865 865 break
866 866 if self.pretty_re.match(h):
867 867 fp.write(h)
868 868 if self.binary():
869 869 fp.write(_('this is a binary file\n'))
870 870 break
871 871 if h.startswith('---'):
872 872 fp.write(_('%d hunks, %d lines changed\n') %
873 873 (len(self.hunks),
874 874 sum([max(h.added, h.removed) for h in self.hunks])))
875 875 break
876 876 fp.write(h)
877 877
878 878 def write(self, fp):
879 879 fp.write(''.join(self.header))
880 880
881 881 def allhunks(self):
882 882 return any(self.allhunks_re.match(h) for h in self.header)
883 883
884 884 def files(self):
885 885 match = self.diffgit_re.match(self.header[0])
886 886 if match:
887 887 fromfile, tofile = match.groups()
888 888 if fromfile == tofile:
889 889 return [fromfile]
890 890 return [fromfile, tofile]
891 891 else:
892 892 return self.diff_re.match(self.header[0]).groups()
893 893
894 894 def filename(self):
895 895 return self.files()[-1]
896 896
897 897 def __repr__(self):
898 898 return '<header %s>' % (' '.join(map(repr, self.files())))
899 899
900 900 def isnewfile(self):
901 901 return any(self.newfile_re.match(h) for h in self.header)
902 902
903 903 def special(self):
904 904 # Special files are shown only at the header level and not at the hunk
905 905 # level for example a file that has been deleted is a special file.
906 906 # The user cannot change the content of the operation, in the case of
907 907 # the deleted file he has to take the deletion or not take it, he
908 908 # cannot take some of it.
909 909 # Newly added files are special if they are empty, they are not special
910 910 # if they have some content as we want to be able to change it
911 911 nocontent = len(self.header) == 2
912 912 emptynewfile = self.isnewfile() and nocontent
913 913 return emptynewfile or \
914 914 any(self.special_re.match(h) for h in self.header)
915 915
916 916 class recordhunk(object):
917 917 """patch hunk
918 918
919 919 XXX shouldn't we merge this with the other hunk class?
920 920 """
921 921 maxcontext = 3
922 922
923 923 def __init__(self, header, fromline, toline, proc, before, hunk, after):
924 924 def trimcontext(number, lines):
925 925 delta = len(lines) - self.maxcontext
926 926 if False and delta > 0:
927 927 return number + delta, lines[:self.maxcontext]
928 928 return number, lines
929 929
930 930 self.header = header
931 931 self.fromline, self.before = trimcontext(fromline, before)
932 932 self.toline, self.after = trimcontext(toline, after)
933 933 self.proc = proc
934 934 self.hunk = hunk
935 935 self.added, self.removed = self.countchanges(self.hunk)
936 936
937 937 def __eq__(self, v):
938 938 if not isinstance(v, recordhunk):
939 939 return False
940 940
941 941 return ((v.hunk == self.hunk) and
942 942 (v.proc == self.proc) and
943 943 (self.fromline == v.fromline) and
944 944 (self.header.files() == v.header.files()))
945 945
946 946 def __hash__(self):
947 947 return hash((tuple(self.hunk),
948 948 tuple(self.header.files()),
949 949 self.fromline,
950 950 self.proc))
951 951
952 952 def countchanges(self, hunk):
953 953 """hunk -> (n+,n-)"""
954 954 add = len([h for h in hunk if h[0] == '+'])
955 955 rem = len([h for h in hunk if h[0] == '-'])
956 956 return add, rem
957 957
958 958 def write(self, fp):
959 959 delta = len(self.before) + len(self.after)
960 960 if self.after and self.after[-1] == '\\ No newline at end of file\n':
961 961 delta -= 1
962 962 fromlen = delta + self.removed
963 963 tolen = delta + self.added
964 964 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
965 965 (self.fromline, fromlen, self.toline, tolen,
966 966 self.proc and (' ' + self.proc)))
967 967 fp.write(''.join(self.before + self.hunk + self.after))
968 968
969 969 pretty = write
970 970
971 971 def filename(self):
972 972 return self.header.filename()
973 973
974 974 def __repr__(self):
975 975 return '<hunk %r@%d>' % (self.filename(), self.fromline)
976 976
977 977 def filterpatch(ui, headers, operation=None):
978 978 """Interactively filter patch chunks into applied-only chunks"""
979 979 if operation is None:
980 980 operation = 'record'
981 981 messages = {
982 982 'multiple': {
983 983 'discard': _("discard change %d/%d to '%s'?"),
984 984 'record': _("record change %d/%d to '%s'?"),
985 985 'revert': _("revert change %d/%d to '%s'?"),
986 986 }[operation],
987 987 'single': {
988 988 'discard': _("discard this change to '%s'?"),
989 989 'record': _("record this change to '%s'?"),
990 990 'revert': _("revert this change to '%s'?"),
991 991 }[operation],
992 992 }
993 993
994 994 def prompt(skipfile, skipall, query, chunk):
995 995 """prompt query, and process base inputs
996 996
997 997 - y/n for the rest of file
998 998 - y/n for the rest
999 999 - ? (help)
1000 1000 - q (quit)
1001 1001
1002 1002 Return True/False and possibly updated skipfile and skipall.
1003 1003 """
1004 1004 newpatches = None
1005 1005 if skipall is not None:
1006 1006 return skipall, skipfile, skipall, newpatches
1007 1007 if skipfile is not None:
1008 1008 return skipfile, skipfile, skipall, newpatches
1009 1009 while True:
1010 1010 resps = _('[Ynesfdaq?]'
1011 1011 '$$ &Yes, record this change'
1012 1012 '$$ &No, skip this change'
1013 1013 '$$ &Edit this change manually'
1014 1014 '$$ &Skip remaining changes to this file'
1015 1015 '$$ Record remaining changes to this &file'
1016 1016 '$$ &Done, skip remaining changes and files'
1017 1017 '$$ Record &all changes to all remaining files'
1018 1018 '$$ &Quit, recording no changes'
1019 1019 '$$ &? (display help)')
1020 1020 r = ui.promptchoice("%s %s" % (query, resps))
1021 1021 ui.write("\n")
1022 1022 if r == 8: # ?
1023 1023 for c, t in ui.extractchoices(resps)[1]:
1024 1024 ui.write('%s - %s\n' % (c, encoding.lower(t)))
1025 1025 continue
1026 1026 elif r == 0: # yes
1027 1027 ret = True
1028 1028 elif r == 1: # no
1029 1029 ret = False
1030 1030 elif r == 2: # Edit patch
1031 1031 if chunk is None:
1032 1032 ui.write(_('cannot edit patch for whole file'))
1033 1033 ui.write("\n")
1034 1034 continue
1035 1035 if chunk.header.binary():
1036 1036 ui.write(_('cannot edit patch for binary file'))
1037 1037 ui.write("\n")
1038 1038 continue
1039 1039 # Patch comment based on the Git one (based on comment at end of
1040 1040 # https://mercurial-scm.org/wiki/RecordExtension)
1041 1041 phelp = '---' + _("""
1042 1042 To remove '-' lines, make them ' ' lines (context).
1043 1043 To remove '+' lines, delete them.
1044 1044 Lines starting with # will be removed from the patch.
1045 1045
1046 1046 If the patch applies cleanly, the edited hunk will immediately be
1047 1047 added to the record list. If it does not apply cleanly, a rejects
1048 1048 file will be generated: you can use that when you try again. If
1049 1049 all lines of the hunk are removed, then the edit is aborted and
1050 1050 the hunk is left unchanged.
1051 1051 """)
1052 1052 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
1053 1053 suffix=".diff", text=True)
1054 1054 ncpatchfp = None
1055 1055 try:
1056 1056 # Write the initial patch
1057 1057 f = os.fdopen(patchfd, "w")
1058 1058 chunk.header.write(f)
1059 1059 chunk.write(f)
1060 1060 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
1061 1061 f.close()
1062 1062 # Start the editor and wait for it to complete
1063 1063 editor = ui.geteditor()
1064 1064 ret = ui.system("%s \"%s\"" % (editor, patchfn),
1065 1065 environ={'HGUSER': ui.username()})
1066 1066 if ret != 0:
1067 1067 ui.warn(_("editor exited with exit code %d\n") % ret)
1068 1068 continue
1069 1069 # Remove comment lines
1070 1070 patchfp = open(patchfn)
1071 1071 ncpatchfp = stringio()
1072 1072 for line in patchfp:
1073 1073 if not line.startswith('#'):
1074 1074 ncpatchfp.write(line)
1075 1075 patchfp.close()
1076 1076 ncpatchfp.seek(0)
1077 1077 newpatches = parsepatch(ncpatchfp)
1078 1078 finally:
1079 1079 os.unlink(patchfn)
1080 1080 del ncpatchfp
1081 1081 # Signal that the chunk shouldn't be applied as-is, but
1082 1082 # provide the new patch to be used instead.
1083 1083 ret = False
1084 1084 elif r == 3: # Skip
1085 1085 ret = skipfile = False
1086 1086 elif r == 4: # file (Record remaining)
1087 1087 ret = skipfile = True
1088 1088 elif r == 5: # done, skip remaining
1089 1089 ret = skipall = False
1090 1090 elif r == 6: # all
1091 1091 ret = skipall = True
1092 1092 elif r == 7: # quit
1093 1093 raise error.Abort(_('user quit'))
1094 1094 return ret, skipfile, skipall, newpatches
1095 1095
1096 1096 seen = set()
1097 1097 applied = {} # 'filename' -> [] of chunks
1098 1098 skipfile, skipall = None, None
1099 1099 pos, total = 1, sum(len(h.hunks) for h in headers)
1100 1100 for h in headers:
1101 1101 pos += len(h.hunks)
1102 1102 skipfile = None
1103 1103 fixoffset = 0
1104 1104 hdr = ''.join(h.header)
1105 1105 if hdr in seen:
1106 1106 continue
1107 1107 seen.add(hdr)
1108 1108 if skipall is None:
1109 1109 h.pretty(ui)
1110 1110 msg = (_('examine changes to %s?') %
1111 1111 _(' and ').join("'%s'" % f for f in h.files()))
1112 1112 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1113 1113 if not r:
1114 1114 continue
1115 1115 applied[h.filename()] = [h]
1116 1116 if h.allhunks():
1117 1117 applied[h.filename()] += h.hunks
1118 1118 continue
1119 1119 for i, chunk in enumerate(h.hunks):
1120 1120 if skipfile is None and skipall is None:
1121 1121 chunk.pretty(ui)
1122 1122 if total == 1:
1123 1123 msg = messages['single'] % chunk.filename()
1124 1124 else:
1125 1125 idx = pos - len(h.hunks) + i
1126 1126 msg = messages['multiple'] % (idx, total, chunk.filename())
1127 1127 r, skipfile, skipall, newpatches = prompt(skipfile,
1128 1128 skipall, msg, chunk)
1129 1129 if r:
1130 1130 if fixoffset:
1131 1131 chunk = copy.copy(chunk)
1132 1132 chunk.toline += fixoffset
1133 1133 applied[chunk.filename()].append(chunk)
1134 1134 elif newpatches is not None:
1135 1135 for newpatch in newpatches:
1136 1136 for newhunk in newpatch.hunks:
1137 1137 if fixoffset:
1138 1138 newhunk.toline += fixoffset
1139 1139 applied[newhunk.filename()].append(newhunk)
1140 1140 else:
1141 1141 fixoffset += chunk.removed - chunk.added
1142 1142 return (sum([h for h in applied.itervalues()
1143 1143 if h[0].special() or len(h) > 1], []), {})
1144 1144 class hunk(object):
1145 1145 def __init__(self, desc, num, lr, context):
1146 1146 self.number = num
1147 1147 self.desc = desc
1148 1148 self.hunk = [desc]
1149 1149 self.a = []
1150 1150 self.b = []
1151 1151 self.starta = self.lena = None
1152 1152 self.startb = self.lenb = None
1153 1153 if lr is not None:
1154 1154 if context:
1155 1155 self.read_context_hunk(lr)
1156 1156 else:
1157 1157 self.read_unified_hunk(lr)
1158 1158
1159 1159 def getnormalized(self):
1160 1160 """Return a copy with line endings normalized to LF."""
1161 1161
1162 1162 def normalize(lines):
1163 1163 nlines = []
1164 1164 for line in lines:
1165 1165 if line.endswith('\r\n'):
1166 1166 line = line[:-2] + '\n'
1167 1167 nlines.append(line)
1168 1168 return nlines
1169 1169
1170 1170 # Dummy object, it is rebuilt manually
1171 1171 nh = hunk(self.desc, self.number, None, None)
1172 1172 nh.number = self.number
1173 1173 nh.desc = self.desc
1174 1174 nh.hunk = self.hunk
1175 1175 nh.a = normalize(self.a)
1176 1176 nh.b = normalize(self.b)
1177 1177 nh.starta = self.starta
1178 1178 nh.startb = self.startb
1179 1179 nh.lena = self.lena
1180 1180 nh.lenb = self.lenb
1181 1181 return nh
1182 1182
1183 1183 def read_unified_hunk(self, lr):
1184 1184 m = unidesc.match(self.desc)
1185 1185 if not m:
1186 1186 raise PatchError(_("bad hunk #%d") % self.number)
1187 1187 self.starta, self.lena, self.startb, self.lenb = m.groups()
1188 1188 if self.lena is None:
1189 1189 self.lena = 1
1190 1190 else:
1191 1191 self.lena = int(self.lena)
1192 1192 if self.lenb is None:
1193 1193 self.lenb = 1
1194 1194 else:
1195 1195 self.lenb = int(self.lenb)
1196 1196 self.starta = int(self.starta)
1197 1197 self.startb = int(self.startb)
1198 1198 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
1199 1199 self.b)
1200 1200 # if we hit eof before finishing out the hunk, the last line will
1201 1201 # be zero length. Lets try to fix it up.
1202 1202 while len(self.hunk[-1]) == 0:
1203 1203 del self.hunk[-1]
1204 1204 del self.a[-1]
1205 1205 del self.b[-1]
1206 1206 self.lena -= 1
1207 1207 self.lenb -= 1
1208 1208 self._fixnewline(lr)
1209 1209
1210 1210 def read_context_hunk(self, lr):
1211 1211 self.desc = lr.readline()
1212 1212 m = contextdesc.match(self.desc)
1213 1213 if not m:
1214 1214 raise PatchError(_("bad hunk #%d") % self.number)
1215 1215 self.starta, aend = m.groups()
1216 1216 self.starta = int(self.starta)
1217 1217 if aend is None:
1218 1218 aend = self.starta
1219 1219 self.lena = int(aend) - self.starta
1220 1220 if self.starta:
1221 1221 self.lena += 1
1222 1222 for x in xrange(self.lena):
1223 1223 l = lr.readline()
1224 1224 if l.startswith('---'):
1225 1225 # lines addition, old block is empty
1226 1226 lr.push(l)
1227 1227 break
1228 1228 s = l[2:]
1229 1229 if l.startswith('- ') or l.startswith('! '):
1230 1230 u = '-' + s
1231 1231 elif l.startswith(' '):
1232 1232 u = ' ' + s
1233 1233 else:
1234 1234 raise PatchError(_("bad hunk #%d old text line %d") %
1235 1235 (self.number, x))
1236 1236 self.a.append(u)
1237 1237 self.hunk.append(u)
1238 1238
1239 1239 l = lr.readline()
1240 1240 if l.startswith('\ '):
1241 1241 s = self.a[-1][:-1]
1242 1242 self.a[-1] = s
1243 1243 self.hunk[-1] = s
1244 1244 l = lr.readline()
1245 1245 m = contextdesc.match(l)
1246 1246 if not m:
1247 1247 raise PatchError(_("bad hunk #%d") % self.number)
1248 1248 self.startb, bend = m.groups()
1249 1249 self.startb = int(self.startb)
1250 1250 if bend is None:
1251 1251 bend = self.startb
1252 1252 self.lenb = int(bend) - self.startb
1253 1253 if self.startb:
1254 1254 self.lenb += 1
1255 1255 hunki = 1
1256 1256 for x in xrange(self.lenb):
1257 1257 l = lr.readline()
1258 1258 if l.startswith('\ '):
1259 1259 # XXX: the only way to hit this is with an invalid line range.
1260 1260 # The no-eol marker is not counted in the line range, but I
1261 1261 # guess there are diff(1) out there which behave differently.
1262 1262 s = self.b[-1][:-1]
1263 1263 self.b[-1] = s
1264 1264 self.hunk[hunki - 1] = s
1265 1265 continue
1266 1266 if not l:
1267 1267 # line deletions, new block is empty and we hit EOF
1268 1268 lr.push(l)
1269 1269 break
1270 1270 s = l[2:]
1271 1271 if l.startswith('+ ') or l.startswith('! '):
1272 1272 u = '+' + s
1273 1273 elif l.startswith(' '):
1274 1274 u = ' ' + s
1275 1275 elif len(self.b) == 0:
1276 1276 # line deletions, new block is empty
1277 1277 lr.push(l)
1278 1278 break
1279 1279 else:
1280 1280 raise PatchError(_("bad hunk #%d old text line %d") %
1281 1281 (self.number, x))
1282 1282 self.b.append(s)
1283 1283 while True:
1284 1284 if hunki >= len(self.hunk):
1285 1285 h = ""
1286 1286 else:
1287 1287 h = self.hunk[hunki]
1288 1288 hunki += 1
1289 1289 if h == u:
1290 1290 break
1291 1291 elif h.startswith('-'):
1292 1292 continue
1293 1293 else:
1294 1294 self.hunk.insert(hunki - 1, u)
1295 1295 break
1296 1296
1297 1297 if not self.a:
1298 1298 # this happens when lines were only added to the hunk
1299 1299 for x in self.hunk:
1300 1300 if x.startswith('-') or x.startswith(' '):
1301 1301 self.a.append(x)
1302 1302 if not self.b:
1303 1303 # this happens when lines were only deleted from the hunk
1304 1304 for x in self.hunk:
1305 1305 if x.startswith('+') or x.startswith(' '):
1306 1306 self.b.append(x[1:])
1307 1307 # @@ -start,len +start,len @@
1308 1308 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
1309 1309 self.startb, self.lenb)
1310 1310 self.hunk[0] = self.desc
1311 1311 self._fixnewline(lr)
1312 1312
1313 1313 def _fixnewline(self, lr):
1314 1314 l = lr.readline()
1315 1315 if l.startswith('\ '):
1316 1316 diffhelpers.fix_newline(self.hunk, self.a, self.b)
1317 1317 else:
1318 1318 lr.push(l)
1319 1319
1320 1320 def complete(self):
1321 1321 return len(self.a) == self.lena and len(self.b) == self.lenb
1322 1322
1323 1323 def _fuzzit(self, old, new, fuzz, toponly):
1324 1324 # this removes context lines from the top and bottom of list 'l'. It
1325 1325 # checks the hunk to make sure only context lines are removed, and then
1326 1326 # returns a new shortened list of lines.
1327 1327 fuzz = min(fuzz, len(old))
1328 1328 if fuzz:
1329 1329 top = 0
1330 1330 bot = 0
1331 1331 hlen = len(self.hunk)
1332 1332 for x in xrange(hlen - 1):
1333 1333 # the hunk starts with the @@ line, so use x+1
1334 1334 if self.hunk[x + 1][0] == ' ':
1335 1335 top += 1
1336 1336 else:
1337 1337 break
1338 1338 if not toponly:
1339 1339 for x in xrange(hlen - 1):
1340 1340 if self.hunk[hlen - bot - 1][0] == ' ':
1341 1341 bot += 1
1342 1342 else:
1343 1343 break
1344 1344
1345 1345 bot = min(fuzz, bot)
1346 1346 top = min(fuzz, top)
1347 1347 return old[top:len(old) - bot], new[top:len(new) - bot], top
1348 1348 return old, new, 0
1349 1349
1350 1350 def fuzzit(self, fuzz, toponly):
1351 1351 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1352 1352 oldstart = self.starta + top
1353 1353 newstart = self.startb + top
1354 1354 # zero length hunk ranges already have their start decremented
1355 1355 if self.lena and oldstart > 0:
1356 1356 oldstart -= 1
1357 1357 if self.lenb and newstart > 0:
1358 1358 newstart -= 1
1359 1359 return old, oldstart, new, newstart
1360 1360
1361 1361 class binhunk(object):
1362 1362 'A binary patch file.'
1363 1363 def __init__(self, lr, fname):
1364 1364 self.text = None
1365 1365 self.delta = False
1366 1366 self.hunk = ['GIT binary patch\n']
1367 1367 self._fname = fname
1368 1368 self._read(lr)
1369 1369
1370 1370 def complete(self):
1371 1371 return self.text is not None
1372 1372
1373 1373 def new(self, lines):
1374 1374 if self.delta:
1375 1375 return [applybindelta(self.text, ''.join(lines))]
1376 1376 return [self.text]
1377 1377
1378 1378 def _read(self, lr):
1379 1379 def getline(lr, hunk):
1380 1380 l = lr.readline()
1381 1381 hunk.append(l)
1382 1382 return l.rstrip('\r\n')
1383 1383
1384 1384 size = 0
1385 1385 while True:
1386 1386 line = getline(lr, self.hunk)
1387 1387 if not line:
1388 1388 raise PatchError(_('could not extract "%s" binary data')
1389 1389 % self._fname)
1390 1390 if line.startswith('literal '):
1391 1391 size = int(line[8:].rstrip())
1392 1392 break
1393 1393 if line.startswith('delta '):
1394 1394 size = int(line[6:].rstrip())
1395 1395 self.delta = True
1396 1396 break
1397 1397 dec = []
1398 1398 line = getline(lr, self.hunk)
1399 1399 while len(line) > 1:
1400 1400 l = line[0]
1401 1401 if l <= 'Z' and l >= 'A':
1402 1402 l = ord(l) - ord('A') + 1
1403 1403 else:
1404 1404 l = ord(l) - ord('a') + 27
1405 1405 try:
1406 1406 dec.append(base85.b85decode(line[1:])[:l])
1407 1407 except ValueError as e:
1408 1408 raise PatchError(_('could not decode "%s" binary patch: %s')
1409 1409 % (self._fname, str(e)))
1410 1410 line = getline(lr, self.hunk)
1411 1411 text = zlib.decompress(''.join(dec))
1412 1412 if len(text) != size:
1413 1413 raise PatchError(_('"%s" length is %d bytes, should be %d')
1414 1414 % (self._fname, len(text), size))
1415 1415 self.text = text
1416 1416
1417 1417 def parsefilename(str):
1418 1418 # --- filename \t|space stuff
1419 1419 s = str[4:].rstrip('\r\n')
1420 1420 i = s.find('\t')
1421 1421 if i < 0:
1422 1422 i = s.find(' ')
1423 1423 if i < 0:
1424 1424 return s
1425 1425 return s[:i]
1426 1426
1427 1427 def reversehunks(hunks):
1428 1428 '''reverse the signs in the hunks given as argument
1429 1429
1430 1430 This function operates on hunks coming out of patch.filterpatch, that is
1431 1431 a list of the form: [header1, hunk1, hunk2, header2...]. Example usage:
1432 1432
1433 1433 >>> rawpatch = """diff --git a/folder1/g b/folder1/g
1434 1434 ... --- a/folder1/g
1435 1435 ... +++ b/folder1/g
1436 1436 ... @@ -1,7 +1,7 @@
1437 1437 ... +firstline
1438 1438 ... c
1439 1439 ... 1
1440 1440 ... 2
1441 1441 ... + 3
1442 1442 ... -4
1443 1443 ... 5
1444 1444 ... d
1445 1445 ... +lastline"""
1446 1446 >>> hunks = parsepatch(rawpatch)
1447 1447 >>> hunkscomingfromfilterpatch = []
1448 1448 >>> for h in hunks:
1449 1449 ... hunkscomingfromfilterpatch.append(h)
1450 1450 ... hunkscomingfromfilterpatch.extend(h.hunks)
1451 1451
1452 1452 >>> reversedhunks = reversehunks(hunkscomingfromfilterpatch)
1453 1453 >>> from . import util
1454 1454 >>> fp = util.stringio()
1455 1455 >>> for c in reversedhunks:
1456 1456 ... c.write(fp)
1457 1457 >>> fp.seek(0)
1458 1458 >>> reversedpatch = fp.read()
1459 1459 >>> print reversedpatch
1460 1460 diff --git a/folder1/g b/folder1/g
1461 1461 --- a/folder1/g
1462 1462 +++ b/folder1/g
1463 1463 @@ -1,4 +1,3 @@
1464 1464 -firstline
1465 1465 c
1466 1466 1
1467 1467 2
1468 1468 @@ -1,6 +2,6 @@
1469 1469 c
1470 1470 1
1471 1471 2
1472 1472 - 3
1473 1473 +4
1474 1474 5
1475 1475 d
1476 1476 @@ -5,3 +6,2 @@
1477 1477 5
1478 1478 d
1479 1479 -lastline
1480 1480
1481 1481 '''
1482 1482
1483 1483 from . import crecord as crecordmod
1484 1484 newhunks = []
1485 1485 for c in hunks:
1486 1486 if isinstance(c, crecordmod.uihunk):
1487 1487 # curses hunks encapsulate the record hunk in _hunk
1488 1488 c = c._hunk
1489 1489 if isinstance(c, recordhunk):
1490 1490 for j, line in enumerate(c.hunk):
1491 1491 if line.startswith("-"):
1492 1492 c.hunk[j] = "+" + c.hunk[j][1:]
1493 1493 elif line.startswith("+"):
1494 1494 c.hunk[j] = "-" + c.hunk[j][1:]
1495 1495 c.added, c.removed = c.removed, c.added
1496 1496 newhunks.append(c)
1497 1497 return newhunks
1498 1498
1499 1499 def parsepatch(originalchunks):
1500 1500 """patch -> [] of headers -> [] of hunks """
1501 1501 class parser(object):
1502 1502 """patch parsing state machine"""
1503 1503 def __init__(self):
1504 1504 self.fromline = 0
1505 1505 self.toline = 0
1506 1506 self.proc = ''
1507 1507 self.header = None
1508 1508 self.context = []
1509 1509 self.before = []
1510 1510 self.hunk = []
1511 1511 self.headers = []
1512 1512
1513 1513 def addrange(self, limits):
1514 1514 fromstart, fromend, tostart, toend, proc = limits
1515 1515 self.fromline = int(fromstart)
1516 1516 self.toline = int(tostart)
1517 1517 self.proc = proc
1518 1518
1519 1519 def addcontext(self, context):
1520 1520 if self.hunk:
1521 1521 h = recordhunk(self.header, self.fromline, self.toline,
1522 1522 self.proc, self.before, self.hunk, context)
1523 1523 self.header.hunks.append(h)
1524 1524 self.fromline += len(self.before) + h.removed
1525 1525 self.toline += len(self.before) + h.added
1526 1526 self.before = []
1527 1527 self.hunk = []
1528 1528 self.context = context
1529 1529
1530 1530 def addhunk(self, hunk):
1531 1531 if self.context:
1532 1532 self.before = self.context
1533 1533 self.context = []
1534 1534 self.hunk = hunk
1535 1535
1536 1536 def newfile(self, hdr):
1537 1537 self.addcontext([])
1538 1538 h = header(hdr)
1539 1539 self.headers.append(h)
1540 1540 self.header = h
1541 1541
1542 1542 def addother(self, line):
1543 1543 pass # 'other' lines are ignored
1544 1544
1545 1545 def finished(self):
1546 1546 self.addcontext([])
1547 1547 return self.headers
1548 1548
1549 1549 transitions = {
1550 1550 'file': {'context': addcontext,
1551 1551 'file': newfile,
1552 1552 'hunk': addhunk,
1553 1553 'range': addrange},
1554 1554 'context': {'file': newfile,
1555 1555 'hunk': addhunk,
1556 1556 'range': addrange,
1557 1557 'other': addother},
1558 1558 'hunk': {'context': addcontext,
1559 1559 'file': newfile,
1560 1560 'range': addrange},
1561 1561 'range': {'context': addcontext,
1562 1562 'hunk': addhunk},
1563 1563 'other': {'other': addother},
1564 1564 }
1565 1565
1566 1566 p = parser()
1567 1567 fp = stringio()
1568 1568 fp.write(''.join(originalchunks))
1569 1569 fp.seek(0)
1570 1570
1571 1571 state = 'context'
1572 1572 for newstate, data in scanpatch(fp):
1573 1573 try:
1574 1574 p.transitions[state][newstate](p, data)
1575 1575 except KeyError:
1576 1576 raise PatchError('unhandled transition: %s -> %s' %
1577 1577 (state, newstate))
1578 1578 state = newstate
1579 1579 del fp
1580 1580 return p.finished()
1581 1581
1582 1582 def pathtransform(path, strip, prefix):
1583 1583 '''turn a path from a patch into a path suitable for the repository
1584 1584
1585 1585 prefix, if not empty, is expected to be normalized with a / at the end.
1586 1586
1587 1587 Returns (stripped components, path in repository).
1588 1588
1589 1589 >>> pathtransform('a/b/c', 0, '')
1590 1590 ('', 'a/b/c')
1591 1591 >>> pathtransform(' a/b/c ', 0, '')
1592 1592 ('', ' a/b/c')
1593 1593 >>> pathtransform(' a/b/c ', 2, '')
1594 1594 ('a/b/', 'c')
1595 1595 >>> pathtransform('a/b/c', 0, 'd/e/')
1596 1596 ('', 'd/e/a/b/c')
1597 1597 >>> pathtransform(' a//b/c ', 2, 'd/e/')
1598 1598 ('a//b/', 'd/e/c')
1599 1599 >>> pathtransform('a/b/c', 3, '')
1600 1600 Traceback (most recent call last):
1601 1601 PatchError: unable to strip away 1 of 3 dirs from a/b/c
1602 1602 '''
1603 1603 pathlen = len(path)
1604 1604 i = 0
1605 1605 if strip == 0:
1606 1606 return '', prefix + path.rstrip()
1607 1607 count = strip
1608 1608 while count > 0:
1609 1609 i = path.find('/', i)
1610 1610 if i == -1:
1611 1611 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1612 1612 (count, strip, path))
1613 1613 i += 1
1614 1614 # consume '//' in the path
1615 1615 while i < pathlen - 1 and path[i] == '/':
1616 1616 i += 1
1617 1617 count -= 1
1618 1618 return path[:i].lstrip(), prefix + path[i:].rstrip()
1619 1619
1620 1620 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1621 1621 nulla = afile_orig == "/dev/null"
1622 1622 nullb = bfile_orig == "/dev/null"
1623 1623 create = nulla and hunk.starta == 0 and hunk.lena == 0
1624 1624 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1625 1625 abase, afile = pathtransform(afile_orig, strip, prefix)
1626 1626 gooda = not nulla and backend.exists(afile)
1627 1627 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1628 1628 if afile == bfile:
1629 1629 goodb = gooda
1630 1630 else:
1631 1631 goodb = not nullb and backend.exists(bfile)
1632 1632 missing = not goodb and not gooda and not create
1633 1633
1634 1634 # some diff programs apparently produce patches where the afile is
1635 1635 # not /dev/null, but afile starts with bfile
1636 1636 abasedir = afile[:afile.rfind('/') + 1]
1637 1637 bbasedir = bfile[:bfile.rfind('/') + 1]
1638 1638 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1639 1639 and hunk.starta == 0 and hunk.lena == 0):
1640 1640 create = True
1641 1641 missing = False
1642 1642
1643 1643 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1644 1644 # diff is between a file and its backup. In this case, the original
1645 1645 # file should be patched (see original mpatch code).
1646 1646 isbackup = (abase == bbase and bfile.startswith(afile))
1647 1647 fname = None
1648 1648 if not missing:
1649 1649 if gooda and goodb:
1650 1650 if isbackup:
1651 1651 fname = afile
1652 1652 else:
1653 1653 fname = bfile
1654 1654 elif gooda:
1655 1655 fname = afile
1656 1656
1657 1657 if not fname:
1658 1658 if not nullb:
1659 1659 if isbackup:
1660 1660 fname = afile
1661 1661 else:
1662 1662 fname = bfile
1663 1663 elif not nulla:
1664 1664 fname = afile
1665 1665 else:
1666 1666 raise PatchError(_("undefined source and destination files"))
1667 1667
1668 1668 gp = patchmeta(fname)
1669 1669 if create:
1670 1670 gp.op = 'ADD'
1671 1671 elif remove:
1672 1672 gp.op = 'DELETE'
1673 1673 return gp
1674 1674
1675 1675 def scanpatch(fp):
1676 1676 """like patch.iterhunks, but yield different events
1677 1677
1678 1678 - ('file', [header_lines + fromfile + tofile])
1679 1679 - ('context', [context_lines])
1680 1680 - ('hunk', [hunk_lines])
1681 1681 - ('range', (-start,len, +start,len, proc))
1682 1682 """
1683 1683 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1684 1684 lr = linereader(fp)
1685 1685
1686 1686 def scanwhile(first, p):
1687 1687 """scan lr while predicate holds"""
1688 1688 lines = [first]
1689 1689 for line in iter(lr.readline, ''):
1690 1690 if p(line):
1691 1691 lines.append(line)
1692 1692 else:
1693 1693 lr.push(line)
1694 1694 break
1695 1695 return lines
1696 1696
1697 1697 for line in iter(lr.readline, ''):
1698 1698 if line.startswith('diff --git a/') or line.startswith('diff -r '):
1699 1699 def notheader(line):
1700 1700 s = line.split(None, 1)
1701 1701 return not s or s[0] not in ('---', 'diff')
1702 1702 header = scanwhile(line, notheader)
1703 1703 fromfile = lr.readline()
1704 1704 if fromfile.startswith('---'):
1705 1705 tofile = lr.readline()
1706 1706 header += [fromfile, tofile]
1707 1707 else:
1708 1708 lr.push(fromfile)
1709 1709 yield 'file', header
1710 1710 elif line[0] == ' ':
1711 1711 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
1712 1712 elif line[0] in '-+':
1713 1713 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
1714 1714 else:
1715 1715 m = lines_re.match(line)
1716 1716 if m:
1717 1717 yield 'range', m.groups()
1718 1718 else:
1719 1719 yield 'other', line
1720 1720
1721 1721 def scangitpatch(lr, firstline):
1722 1722 """
1723 1723 Git patches can emit:
1724 1724 - rename a to b
1725 1725 - change b
1726 1726 - copy a to c
1727 1727 - change c
1728 1728
1729 1729 We cannot apply this sequence as-is, the renamed 'a' could not be
1730 1730 found for it would have been renamed already. And we cannot copy
1731 1731 from 'b' instead because 'b' would have been changed already. So
1732 1732 we scan the git patch for copy and rename commands so we can
1733 1733 perform the copies ahead of time.
1734 1734 """
1735 1735 pos = 0
1736 1736 try:
1737 1737 pos = lr.fp.tell()
1738 1738 fp = lr.fp
1739 1739 except IOError:
1740 1740 fp = stringio(lr.fp.read())
1741 1741 gitlr = linereader(fp)
1742 1742 gitlr.push(firstline)
1743 1743 gitpatches = readgitpatch(gitlr)
1744 1744 fp.seek(pos)
1745 1745 return gitpatches
1746 1746
1747 1747 def iterhunks(fp):
1748 1748 """Read a patch and yield the following events:
1749 1749 - ("file", afile, bfile, firsthunk): select a new target file.
1750 1750 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1751 1751 "file" event.
1752 1752 - ("git", gitchanges): current diff is in git format, gitchanges
1753 1753 maps filenames to gitpatch records. Unique event.
1754 1754 """
1755 1755 afile = ""
1756 1756 bfile = ""
1757 1757 state = None
1758 1758 hunknum = 0
1759 1759 emitfile = newfile = False
1760 1760 gitpatches = None
1761 1761
1762 1762 # our states
1763 1763 BFILE = 1
1764 1764 context = None
1765 1765 lr = linereader(fp)
1766 1766
1767 1767 for x in iter(lr.readline, ''):
1768 1768 if state == BFILE and (
1769 1769 (not context and x[0] == '@')
1770 1770 or (context is not False and x.startswith('***************'))
1771 1771 or x.startswith('GIT binary patch')):
1772 1772 gp = None
1773 1773 if (gitpatches and
1774 1774 gitpatches[-1].ispatching(afile, bfile)):
1775 1775 gp = gitpatches.pop()
1776 1776 if x.startswith('GIT binary patch'):
1777 1777 h = binhunk(lr, gp.path)
1778 1778 else:
1779 1779 if context is None and x.startswith('***************'):
1780 1780 context = True
1781 1781 h = hunk(x, hunknum + 1, lr, context)
1782 1782 hunknum += 1
1783 1783 if emitfile:
1784 1784 emitfile = False
1785 1785 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1786 1786 yield 'hunk', h
1787 1787 elif x.startswith('diff --git a/'):
1788 1788 m = gitre.match(x.rstrip(' \r\n'))
1789 1789 if not m:
1790 1790 continue
1791 1791 if gitpatches is None:
1792 1792 # scan whole input for git metadata
1793 1793 gitpatches = scangitpatch(lr, x)
1794 1794 yield 'git', [g.copy() for g in gitpatches
1795 1795 if g.op in ('COPY', 'RENAME')]
1796 1796 gitpatches.reverse()
1797 1797 afile = 'a/' + m.group(1)
1798 1798 bfile = 'b/' + m.group(2)
1799 1799 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1800 1800 gp = gitpatches.pop()
1801 1801 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1802 1802 if not gitpatches:
1803 1803 raise PatchError(_('failed to synchronize metadata for "%s"')
1804 1804 % afile[2:])
1805 1805 gp = gitpatches[-1]
1806 1806 newfile = True
1807 1807 elif x.startswith('---'):
1808 1808 # check for a unified diff
1809 1809 l2 = lr.readline()
1810 1810 if not l2.startswith('+++'):
1811 1811 lr.push(l2)
1812 1812 continue
1813 1813 newfile = True
1814 1814 context = False
1815 1815 afile = parsefilename(x)
1816 1816 bfile = parsefilename(l2)
1817 1817 elif x.startswith('***'):
1818 1818 # check for a context diff
1819 1819 l2 = lr.readline()
1820 1820 if not l2.startswith('---'):
1821 1821 lr.push(l2)
1822 1822 continue
1823 1823 l3 = lr.readline()
1824 1824 lr.push(l3)
1825 1825 if not l3.startswith("***************"):
1826 1826 lr.push(l2)
1827 1827 continue
1828 1828 newfile = True
1829 1829 context = True
1830 1830 afile = parsefilename(x)
1831 1831 bfile = parsefilename(l2)
1832 1832
1833 1833 if newfile:
1834 1834 newfile = False
1835 1835 emitfile = True
1836 1836 state = BFILE
1837 1837 hunknum = 0
1838 1838
1839 1839 while gitpatches:
1840 1840 gp = gitpatches.pop()
1841 1841 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1842 1842
1843 1843 def applybindelta(binchunk, data):
1844 1844 """Apply a binary delta hunk
1845 1845 The algorithm used is the algorithm from git's patch-delta.c
1846 1846 """
1847 1847 def deltahead(binchunk):
1848 1848 i = 0
1849 1849 for c in binchunk:
1850 1850 i += 1
1851 1851 if not (ord(c) & 0x80):
1852 1852 return i
1853 1853 return i
1854 1854 out = ""
1855 1855 s = deltahead(binchunk)
1856 1856 binchunk = binchunk[s:]
1857 1857 s = deltahead(binchunk)
1858 1858 binchunk = binchunk[s:]
1859 1859 i = 0
1860 1860 while i < len(binchunk):
1861 1861 cmd = ord(binchunk[i])
1862 1862 i += 1
1863 1863 if (cmd & 0x80):
1864 1864 offset = 0
1865 1865 size = 0
1866 1866 if (cmd & 0x01):
1867 1867 offset = ord(binchunk[i])
1868 1868 i += 1
1869 1869 if (cmd & 0x02):
1870 1870 offset |= ord(binchunk[i]) << 8
1871 1871 i += 1
1872 1872 if (cmd & 0x04):
1873 1873 offset |= ord(binchunk[i]) << 16
1874 1874 i += 1
1875 1875 if (cmd & 0x08):
1876 1876 offset |= ord(binchunk[i]) << 24
1877 1877 i += 1
1878 1878 if (cmd & 0x10):
1879 1879 size = ord(binchunk[i])
1880 1880 i += 1
1881 1881 if (cmd & 0x20):
1882 1882 size |= ord(binchunk[i]) << 8
1883 1883 i += 1
1884 1884 if (cmd & 0x40):
1885 1885 size |= ord(binchunk[i]) << 16
1886 1886 i += 1
1887 1887 if size == 0:
1888 1888 size = 0x10000
1889 1889 offset_end = offset + size
1890 1890 out += data[offset:offset_end]
1891 1891 elif cmd != 0:
1892 1892 offset_end = i + cmd
1893 1893 out += binchunk[i:offset_end]
1894 1894 i += cmd
1895 1895 else:
1896 1896 raise PatchError(_('unexpected delta opcode 0'))
1897 1897 return out
1898 1898
1899 1899 def applydiff(ui, fp, backend, store, strip=1, prefix='', eolmode='strict'):
1900 1900 """Reads a patch from fp and tries to apply it.
1901 1901
1902 1902 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1903 1903 there was any fuzz.
1904 1904
1905 1905 If 'eolmode' is 'strict', the patch content and patched file are
1906 1906 read in binary mode. Otherwise, line endings are ignored when
1907 1907 patching then normalized according to 'eolmode'.
1908 1908 """
1909 1909 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1910 1910 prefix=prefix, eolmode=eolmode)
1911 1911
1912 1912 def _applydiff(ui, fp, patcher, backend, store, strip=1, prefix='',
1913 1913 eolmode='strict'):
1914 1914
1915 1915 if prefix:
1916 1916 prefix = pathutil.canonpath(backend.repo.root, backend.repo.getcwd(),
1917 1917 prefix)
1918 1918 if prefix != '':
1919 1919 prefix += '/'
1920 1920 def pstrip(p):
1921 1921 return pathtransform(p, strip - 1, prefix)[1]
1922 1922
1923 1923 rejects = 0
1924 1924 err = 0
1925 1925 current_file = None
1926 1926
1927 1927 for state, values in iterhunks(fp):
1928 1928 if state == 'hunk':
1929 1929 if not current_file:
1930 1930 continue
1931 1931 ret = current_file.apply(values)
1932 1932 if ret > 0:
1933 1933 err = 1
1934 1934 elif state == 'file':
1935 1935 if current_file:
1936 1936 rejects += current_file.close()
1937 1937 current_file = None
1938 1938 afile, bfile, first_hunk, gp = values
1939 1939 if gp:
1940 1940 gp.path = pstrip(gp.path)
1941 1941 if gp.oldpath:
1942 1942 gp.oldpath = pstrip(gp.oldpath)
1943 1943 else:
1944 1944 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
1945 1945 prefix)
1946 1946 if gp.op == 'RENAME':
1947 1947 backend.unlink(gp.oldpath)
1948 1948 if not first_hunk:
1949 1949 if gp.op == 'DELETE':
1950 1950 backend.unlink(gp.path)
1951 1951 continue
1952 1952 data, mode = None, None
1953 1953 if gp.op in ('RENAME', 'COPY'):
1954 1954 data, mode = store.getfile(gp.oldpath)[:2]
1955 # FIXME: failing getfile has never been handled here
1956 assert data is not None
1955 if data is None:
1956 # This means that the old path does not exist
1957 raise PatchError(_("source file '%s' does not exist")
1958 % gp.oldpath)
1957 1959 if gp.mode:
1958 1960 mode = gp.mode
1959 1961 if gp.op == 'ADD':
1960 1962 # Added files without content have no hunk and
1961 1963 # must be created
1962 1964 data = ''
1963 1965 if data or mode:
1964 1966 if (gp.op in ('ADD', 'RENAME', 'COPY')
1965 1967 and backend.exists(gp.path)):
1966 1968 raise PatchError(_("cannot create %s: destination "
1967 1969 "already exists") % gp.path)
1968 1970 backend.setfile(gp.path, data, mode, gp.oldpath)
1969 1971 continue
1970 1972 try:
1971 1973 current_file = patcher(ui, gp, backend, store,
1972 1974 eolmode=eolmode)
1973 1975 except PatchError as inst:
1974 1976 ui.warn(str(inst) + '\n')
1975 1977 current_file = None
1976 1978 rejects += 1
1977 1979 continue
1978 1980 elif state == 'git':
1979 1981 for gp in values:
1980 1982 path = pstrip(gp.oldpath)
1981 1983 data, mode = backend.getfile(path)
1982 1984 if data is None:
1983 1985 # The error ignored here will trigger a getfile()
1984 1986 # error in a place more appropriate for error
1985 1987 # handling, and will not interrupt the patching
1986 1988 # process.
1987 1989 pass
1988 1990 else:
1989 1991 store.setfile(path, data, mode)
1990 1992 else:
1991 1993 raise error.Abort(_('unsupported parser state: %s') % state)
1992 1994
1993 1995 if current_file:
1994 1996 rejects += current_file.close()
1995 1997
1996 1998 if rejects:
1997 1999 return -1
1998 2000 return err
1999 2001
2000 2002 def _externalpatch(ui, repo, patcher, patchname, strip, files,
2001 2003 similarity):
2002 2004 """use <patcher> to apply <patchname> to the working directory.
2003 2005 returns whether patch was applied with fuzz factor."""
2004 2006
2005 2007 fuzz = False
2006 2008 args = []
2007 2009 cwd = repo.root
2008 2010 if cwd:
2009 2011 args.append('-d %s' % util.shellquote(cwd))
2010 2012 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
2011 2013 util.shellquote(patchname)))
2012 2014 try:
2013 2015 for line in fp:
2014 2016 line = line.rstrip()
2015 2017 ui.note(line + '\n')
2016 2018 if line.startswith('patching file '):
2017 2019 pf = util.parsepatchoutput(line)
2018 2020 printed_file = False
2019 2021 files.add(pf)
2020 2022 elif line.find('with fuzz') >= 0:
2021 2023 fuzz = True
2022 2024 if not printed_file:
2023 2025 ui.warn(pf + '\n')
2024 2026 printed_file = True
2025 2027 ui.warn(line + '\n')
2026 2028 elif line.find('saving rejects to file') >= 0:
2027 2029 ui.warn(line + '\n')
2028 2030 elif line.find('FAILED') >= 0:
2029 2031 if not printed_file:
2030 2032 ui.warn(pf + '\n')
2031 2033 printed_file = True
2032 2034 ui.warn(line + '\n')
2033 2035 finally:
2034 2036 if files:
2035 2037 scmutil.marktouched(repo, files, similarity)
2036 2038 code = fp.close()
2037 2039 if code:
2038 2040 raise PatchError(_("patch command failed: %s") %
2039 2041 util.explainexit(code)[0])
2040 2042 return fuzz
2041 2043
2042 2044 def patchbackend(ui, backend, patchobj, strip, prefix, files=None,
2043 2045 eolmode='strict'):
2044 2046 if files is None:
2045 2047 files = set()
2046 2048 if eolmode is None:
2047 2049 eolmode = ui.config('patch', 'eol', 'strict')
2048 2050 if eolmode.lower() not in eolmodes:
2049 2051 raise error.Abort(_('unsupported line endings type: %s') % eolmode)
2050 2052 eolmode = eolmode.lower()
2051 2053
2052 2054 store = filestore()
2053 2055 try:
2054 2056 fp = open(patchobj, 'rb')
2055 2057 except TypeError:
2056 2058 fp = patchobj
2057 2059 try:
2058 2060 ret = applydiff(ui, fp, backend, store, strip=strip, prefix=prefix,
2059 2061 eolmode=eolmode)
2060 2062 finally:
2061 2063 if fp != patchobj:
2062 2064 fp.close()
2063 2065 files.update(backend.close())
2064 2066 store.close()
2065 2067 if ret < 0:
2066 2068 raise PatchError(_('patch failed to apply'))
2067 2069 return ret > 0
2068 2070
2069 2071 def internalpatch(ui, repo, patchobj, strip, prefix='', files=None,
2070 2072 eolmode='strict', similarity=0):
2071 2073 """use builtin patch to apply <patchobj> to the working directory.
2072 2074 returns whether patch was applied with fuzz factor."""
2073 2075 backend = workingbackend(ui, repo, similarity)
2074 2076 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2075 2077
2076 2078 def patchrepo(ui, repo, ctx, store, patchobj, strip, prefix, files=None,
2077 2079 eolmode='strict'):
2078 2080 backend = repobackend(ui, repo, ctx, store)
2079 2081 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2080 2082
2081 2083 def patch(ui, repo, patchname, strip=1, prefix='', files=None, eolmode='strict',
2082 2084 similarity=0):
2083 2085 """Apply <patchname> to the working directory.
2084 2086
2085 2087 'eolmode' specifies how end of lines should be handled. It can be:
2086 2088 - 'strict': inputs are read in binary mode, EOLs are preserved
2087 2089 - 'crlf': EOLs are ignored when patching and reset to CRLF
2088 2090 - 'lf': EOLs are ignored when patching and reset to LF
2089 2091 - None: get it from user settings, default to 'strict'
2090 2092 'eolmode' is ignored when using an external patcher program.
2091 2093
2092 2094 Returns whether patch was applied with fuzz factor.
2093 2095 """
2094 2096 patcher = ui.config('ui', 'patch')
2095 2097 if files is None:
2096 2098 files = set()
2097 2099 if patcher:
2098 2100 return _externalpatch(ui, repo, patcher, patchname, strip,
2099 2101 files, similarity)
2100 2102 return internalpatch(ui, repo, patchname, strip, prefix, files, eolmode,
2101 2103 similarity)
2102 2104
2103 2105 def changedfiles(ui, repo, patchpath, strip=1):
2104 2106 backend = fsbackend(ui, repo.root)
2105 2107 with open(patchpath, 'rb') as fp:
2106 2108 changed = set()
2107 2109 for state, values in iterhunks(fp):
2108 2110 if state == 'file':
2109 2111 afile, bfile, first_hunk, gp = values
2110 2112 if gp:
2111 2113 gp.path = pathtransform(gp.path, strip - 1, '')[1]
2112 2114 if gp.oldpath:
2113 2115 gp.oldpath = pathtransform(gp.oldpath, strip - 1, '')[1]
2114 2116 else:
2115 2117 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
2116 2118 '')
2117 2119 changed.add(gp.path)
2118 2120 if gp.op == 'RENAME':
2119 2121 changed.add(gp.oldpath)
2120 2122 elif state not in ('hunk', 'git'):
2121 2123 raise error.Abort(_('unsupported parser state: %s') % state)
2122 2124 return changed
2123 2125
2124 2126 class GitDiffRequired(Exception):
2125 2127 pass
2126 2128
2127 2129 def diffallopts(ui, opts=None, untrusted=False, section='diff'):
2128 2130 '''return diffopts with all features supported and parsed'''
2129 2131 return difffeatureopts(ui, opts=opts, untrusted=untrusted, section=section,
2130 2132 git=True, whitespace=True, formatchanging=True)
2131 2133
2132 2134 diffopts = diffallopts
2133 2135
2134 2136 def difffeatureopts(ui, opts=None, untrusted=False, section='diff', git=False,
2135 2137 whitespace=False, formatchanging=False):
2136 2138 '''return diffopts with only opted-in features parsed
2137 2139
2138 2140 Features:
2139 2141 - git: git-style diffs
2140 2142 - whitespace: whitespace options like ignoreblanklines and ignorews
2141 2143 - formatchanging: options that will likely break or cause correctness issues
2142 2144 with most diff parsers
2143 2145 '''
2144 2146 def get(key, name=None, getter=ui.configbool, forceplain=None):
2145 2147 if opts:
2146 2148 v = opts.get(key)
2147 2149 # diffopts flags are either None-default (which is passed
2148 2150 # through unchanged, so we can identify unset values), or
2149 2151 # some other falsey default (eg --unified, which defaults
2150 2152 # to an empty string). We only want to override the config
2151 2153 # entries from hgrc with command line values if they
2152 2154 # appear to have been set, which is any truthy value,
2153 2155 # True, or False.
2154 2156 if v or isinstance(v, bool):
2155 2157 return v
2156 2158 if forceplain is not None and ui.plain():
2157 2159 return forceplain
2158 2160 return getter(section, name or key, None, untrusted=untrusted)
2159 2161
2160 2162 # core options, expected to be understood by every diff parser
2161 2163 buildopts = {
2162 2164 'nodates': get('nodates'),
2163 2165 'showfunc': get('show_function', 'showfunc'),
2164 2166 'context': get('unified', getter=ui.config),
2165 2167 }
2166 2168
2167 2169 if git:
2168 2170 buildopts['git'] = get('git')
2169 2171 if whitespace:
2170 2172 buildopts['ignorews'] = get('ignore_all_space', 'ignorews')
2171 2173 buildopts['ignorewsamount'] = get('ignore_space_change',
2172 2174 'ignorewsamount')
2173 2175 buildopts['ignoreblanklines'] = get('ignore_blank_lines',
2174 2176 'ignoreblanklines')
2175 2177 if formatchanging:
2176 2178 buildopts['text'] = opts and opts.get('text')
2177 2179 buildopts['nobinary'] = get('nobinary', forceplain=False)
2178 2180 buildopts['noprefix'] = get('noprefix', forceplain=False)
2179 2181
2180 2182 return mdiff.diffopts(**buildopts)
2181 2183
2182 2184 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
2183 2185 losedatafn=None, prefix='', relroot='', copy=None):
2184 2186 '''yields diff of changes to files between two nodes, or node and
2185 2187 working directory.
2186 2188
2187 2189 if node1 is None, use first dirstate parent instead.
2188 2190 if node2 is None, compare node1 with working directory.
2189 2191
2190 2192 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2191 2193 every time some change cannot be represented with the current
2192 2194 patch format. Return False to upgrade to git patch format, True to
2193 2195 accept the loss or raise an exception to abort the diff. It is
2194 2196 called with the name of current file being diffed as 'fn'. If set
2195 2197 to None, patches will always be upgraded to git format when
2196 2198 necessary.
2197 2199
2198 2200 prefix is a filename prefix that is prepended to all filenames on
2199 2201 display (used for subrepos).
2200 2202
2201 2203 relroot, if not empty, must be normalized with a trailing /. Any match
2202 2204 patterns that fall outside it will be ignored.
2203 2205
2204 2206 copy, if not empty, should contain mappings {dst@y: src@x} of copy
2205 2207 information.'''
2206 2208
2207 2209 if opts is None:
2208 2210 opts = mdiff.defaultopts
2209 2211
2210 2212 if not node1 and not node2:
2211 2213 node1 = repo.dirstate.p1()
2212 2214
2213 2215 def lrugetfilectx():
2214 2216 cache = {}
2215 2217 order = collections.deque()
2216 2218 def getfilectx(f, ctx):
2217 2219 fctx = ctx.filectx(f, filelog=cache.get(f))
2218 2220 if f not in cache:
2219 2221 if len(cache) > 20:
2220 2222 del cache[order.popleft()]
2221 2223 cache[f] = fctx.filelog()
2222 2224 else:
2223 2225 order.remove(f)
2224 2226 order.append(f)
2225 2227 return fctx
2226 2228 return getfilectx
2227 2229 getfilectx = lrugetfilectx()
2228 2230
2229 2231 ctx1 = repo[node1]
2230 2232 ctx2 = repo[node2]
2231 2233
2232 2234 relfiltered = False
2233 2235 if relroot != '' and match.always():
2234 2236 # as a special case, create a new matcher with just the relroot
2235 2237 pats = [relroot]
2236 2238 match = scmutil.match(ctx2, pats, default='path')
2237 2239 relfiltered = True
2238 2240
2239 2241 if not changes:
2240 2242 changes = repo.status(ctx1, ctx2, match=match)
2241 2243 modified, added, removed = changes[:3]
2242 2244
2243 2245 if not modified and not added and not removed:
2244 2246 return []
2245 2247
2246 2248 if repo.ui.debugflag:
2247 2249 hexfunc = hex
2248 2250 else:
2249 2251 hexfunc = short
2250 2252 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2251 2253
2252 2254 if copy is None:
2253 2255 copy = {}
2254 2256 if opts.git or opts.upgrade:
2255 2257 copy = copies.pathcopies(ctx1, ctx2, match=match)
2256 2258
2257 2259 if relroot is not None:
2258 2260 if not relfiltered:
2259 2261 # XXX this would ideally be done in the matcher, but that is
2260 2262 # generally meant to 'or' patterns, not 'and' them. In this case we
2261 2263 # need to 'and' all the patterns from the matcher with relroot.
2262 2264 def filterrel(l):
2263 2265 return [f for f in l if f.startswith(relroot)]
2264 2266 modified = filterrel(modified)
2265 2267 added = filterrel(added)
2266 2268 removed = filterrel(removed)
2267 2269 relfiltered = True
2268 2270 # filter out copies where either side isn't inside the relative root
2269 2271 copy = dict(((dst, src) for (dst, src) in copy.iteritems()
2270 2272 if dst.startswith(relroot)
2271 2273 and src.startswith(relroot)))
2272 2274
2273 2275 modifiedset = set(modified)
2274 2276 addedset = set(added)
2275 2277 removedset = set(removed)
2276 2278 for f in modified:
2277 2279 if f not in ctx1:
2278 2280 # Fix up added, since merged-in additions appear as
2279 2281 # modifications during merges
2280 2282 modifiedset.remove(f)
2281 2283 addedset.add(f)
2282 2284 for f in removed:
2283 2285 if f not in ctx1:
2284 2286 # Merged-in additions that are then removed are reported as removed.
2285 2287 # They are not in ctx1, so We don't want to show them in the diff.
2286 2288 removedset.remove(f)
2287 2289 modified = sorted(modifiedset)
2288 2290 added = sorted(addedset)
2289 2291 removed = sorted(removedset)
2290 2292 for dst, src in copy.items():
2291 2293 if src not in ctx1:
2292 2294 # Files merged in during a merge and then copied/renamed are
2293 2295 # reported as copies. We want to show them in the diff as additions.
2294 2296 del copy[dst]
2295 2297
2296 2298 def difffn(opts, losedata):
2297 2299 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2298 2300 copy, getfilectx, opts, losedata, prefix, relroot)
2299 2301 if opts.upgrade and not opts.git:
2300 2302 try:
2301 2303 def losedata(fn):
2302 2304 if not losedatafn or not losedatafn(fn=fn):
2303 2305 raise GitDiffRequired
2304 2306 # Buffer the whole output until we are sure it can be generated
2305 2307 return list(difffn(opts.copy(git=False), losedata))
2306 2308 except GitDiffRequired:
2307 2309 return difffn(opts.copy(git=True), None)
2308 2310 else:
2309 2311 return difffn(opts, None)
2310 2312
2311 2313 def difflabel(func, *args, **kw):
2312 2314 '''yields 2-tuples of (output, label) based on the output of func()'''
2313 2315 headprefixes = [('diff', 'diff.diffline'),
2314 2316 ('copy', 'diff.extended'),
2315 2317 ('rename', 'diff.extended'),
2316 2318 ('old', 'diff.extended'),
2317 2319 ('new', 'diff.extended'),
2318 2320 ('deleted', 'diff.extended'),
2319 2321 ('---', 'diff.file_a'),
2320 2322 ('+++', 'diff.file_b')]
2321 2323 textprefixes = [('@', 'diff.hunk'),
2322 2324 ('-', 'diff.deleted'),
2323 2325 ('+', 'diff.inserted')]
2324 2326 head = False
2325 2327 for chunk in func(*args, **kw):
2326 2328 lines = chunk.split('\n')
2327 2329 for i, line in enumerate(lines):
2328 2330 if i != 0:
2329 2331 yield ('\n', '')
2330 2332 if head:
2331 2333 if line.startswith('@'):
2332 2334 head = False
2333 2335 else:
2334 2336 if line and line[0] not in ' +-@\\':
2335 2337 head = True
2336 2338 stripline = line
2337 2339 diffline = False
2338 2340 if not head and line and line[0] in '+-':
2339 2341 # highlight tabs and trailing whitespace, but only in
2340 2342 # changed lines
2341 2343 stripline = line.rstrip()
2342 2344 diffline = True
2343 2345
2344 2346 prefixes = textprefixes
2345 2347 if head:
2346 2348 prefixes = headprefixes
2347 2349 for prefix, label in prefixes:
2348 2350 if stripline.startswith(prefix):
2349 2351 if diffline:
2350 2352 for token in tabsplitter.findall(stripline):
2351 2353 if '\t' == token[0]:
2352 2354 yield (token, 'diff.tab')
2353 2355 else:
2354 2356 yield (token, label)
2355 2357 else:
2356 2358 yield (stripline, label)
2357 2359 break
2358 2360 else:
2359 2361 yield (line, '')
2360 2362 if line != stripline:
2361 2363 yield (line[len(stripline):], 'diff.trailingwhitespace')
2362 2364
2363 2365 def diffui(*args, **kw):
2364 2366 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2365 2367 return difflabel(diff, *args, **kw)
2366 2368
2367 2369 def _filepairs(modified, added, removed, copy, opts):
2368 2370 '''generates tuples (f1, f2, copyop), where f1 is the name of the file
2369 2371 before and f2 is the the name after. For added files, f1 will be None,
2370 2372 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2371 2373 or 'rename' (the latter two only if opts.git is set).'''
2372 2374 gone = set()
2373 2375
2374 2376 copyto = dict([(v, k) for k, v in copy.items()])
2375 2377
2376 2378 addedset, removedset = set(added), set(removed)
2377 2379
2378 2380 for f in sorted(modified + added + removed):
2379 2381 copyop = None
2380 2382 f1, f2 = f, f
2381 2383 if f in addedset:
2382 2384 f1 = None
2383 2385 if f in copy:
2384 2386 if opts.git:
2385 2387 f1 = copy[f]
2386 2388 if f1 in removedset and f1 not in gone:
2387 2389 copyop = 'rename'
2388 2390 gone.add(f1)
2389 2391 else:
2390 2392 copyop = 'copy'
2391 2393 elif f in removedset:
2392 2394 f2 = None
2393 2395 if opts.git:
2394 2396 # have we already reported a copy above?
2395 2397 if (f in copyto and copyto[f] in addedset
2396 2398 and copy[copyto[f]] == f):
2397 2399 continue
2398 2400 yield f1, f2, copyop
2399 2401
2400 2402 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2401 2403 copy, getfilectx, opts, losedatafn, prefix, relroot):
2402 2404 '''given input data, generate a diff and yield it in blocks
2403 2405
2404 2406 If generating a diff would lose data like flags or binary data and
2405 2407 losedatafn is not None, it will be called.
2406 2408
2407 2409 relroot is removed and prefix is added to every path in the diff output.
2408 2410
2409 2411 If relroot is not empty, this function expects every path in modified,
2410 2412 added, removed and copy to start with it.'''
2411 2413
2412 2414 def gitindex(text):
2413 2415 if not text:
2414 2416 text = ""
2415 2417 l = len(text)
2416 2418 s = hashlib.sha1('blob %d\0' % l)
2417 2419 s.update(text)
2418 2420 return s.hexdigest()
2419 2421
2420 2422 if opts.noprefix:
2421 2423 aprefix = bprefix = ''
2422 2424 else:
2423 2425 aprefix = 'a/'
2424 2426 bprefix = 'b/'
2425 2427
2426 2428 def diffline(f, revs):
2427 2429 revinfo = ' '.join(["-r %s" % rev for rev in revs])
2428 2430 return 'diff %s %s' % (revinfo, f)
2429 2431
2430 2432 date1 = util.datestr(ctx1.date())
2431 2433 date2 = util.datestr(ctx2.date())
2432 2434
2433 2435 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
2434 2436
2435 2437 if relroot != '' and (repo.ui.configbool('devel', 'all')
2436 2438 or repo.ui.configbool('devel', 'check-relroot')):
2437 2439 for f in modified + added + removed + copy.keys() + copy.values():
2438 2440 if f is not None and not f.startswith(relroot):
2439 2441 raise AssertionError(
2440 2442 "file %s doesn't start with relroot %s" % (f, relroot))
2441 2443
2442 2444 for f1, f2, copyop in _filepairs(modified, added, removed, copy, opts):
2443 2445 content1 = None
2444 2446 content2 = None
2445 2447 flag1 = None
2446 2448 flag2 = None
2447 2449 if f1:
2448 2450 content1 = getfilectx(f1, ctx1).data()
2449 2451 if opts.git or losedatafn:
2450 2452 flag1 = ctx1.flags(f1)
2451 2453 if f2:
2452 2454 content2 = getfilectx(f2, ctx2).data()
2453 2455 if opts.git or losedatafn:
2454 2456 flag2 = ctx2.flags(f2)
2455 2457 binary = False
2456 2458 if opts.git or losedatafn:
2457 2459 binary = util.binary(content1) or util.binary(content2)
2458 2460
2459 2461 if losedatafn and not opts.git:
2460 2462 if (binary or
2461 2463 # copy/rename
2462 2464 f2 in copy or
2463 2465 # empty file creation
2464 2466 (not f1 and not content2) or
2465 2467 # empty file deletion
2466 2468 (not content1 and not f2) or
2467 2469 # create with flags
2468 2470 (not f1 and flag2) or
2469 2471 # change flags
2470 2472 (f1 and f2 and flag1 != flag2)):
2471 2473 losedatafn(f2 or f1)
2472 2474
2473 2475 path1 = f1 or f2
2474 2476 path2 = f2 or f1
2475 2477 path1 = posixpath.join(prefix, path1[len(relroot):])
2476 2478 path2 = posixpath.join(prefix, path2[len(relroot):])
2477 2479 header = []
2478 2480 if opts.git:
2479 2481 header.append('diff --git %s%s %s%s' %
2480 2482 (aprefix, path1, bprefix, path2))
2481 2483 if not f1: # added
2482 2484 header.append('new file mode %s' % gitmode[flag2])
2483 2485 elif not f2: # removed
2484 2486 header.append('deleted file mode %s' % gitmode[flag1])
2485 2487 else: # modified/copied/renamed
2486 2488 mode1, mode2 = gitmode[flag1], gitmode[flag2]
2487 2489 if mode1 != mode2:
2488 2490 header.append('old mode %s' % mode1)
2489 2491 header.append('new mode %s' % mode2)
2490 2492 if copyop is not None:
2491 2493 header.append('%s from %s' % (copyop, path1))
2492 2494 header.append('%s to %s' % (copyop, path2))
2493 2495 elif revs and not repo.ui.quiet:
2494 2496 header.append(diffline(path1, revs))
2495 2497
2496 2498 if binary and opts.git and not opts.nobinary:
2497 2499 text = mdiff.b85diff(content1, content2)
2498 2500 if text:
2499 2501 header.append('index %s..%s' %
2500 2502 (gitindex(content1), gitindex(content2)))
2501 2503 else:
2502 2504 text = mdiff.unidiff(content1, date1,
2503 2505 content2, date2,
2504 2506 path1, path2, opts=opts)
2505 2507 if header and (text or len(header) > 1):
2506 2508 yield '\n'.join(header) + '\n'
2507 2509 if text:
2508 2510 yield text
2509 2511
2510 2512 def diffstatsum(stats):
2511 2513 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
2512 2514 for f, a, r, b in stats:
2513 2515 maxfile = max(maxfile, encoding.colwidth(f))
2514 2516 maxtotal = max(maxtotal, a + r)
2515 2517 addtotal += a
2516 2518 removetotal += r
2517 2519 binary = binary or b
2518 2520
2519 2521 return maxfile, maxtotal, addtotal, removetotal, binary
2520 2522
2521 2523 def diffstatdata(lines):
2522 2524 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
2523 2525
2524 2526 results = []
2525 2527 filename, adds, removes, isbinary = None, 0, 0, False
2526 2528
2527 2529 def addresult():
2528 2530 if filename:
2529 2531 results.append((filename, adds, removes, isbinary))
2530 2532
2531 2533 for line in lines:
2532 2534 if line.startswith('diff'):
2533 2535 addresult()
2534 2536 # set numbers to 0 anyway when starting new file
2535 2537 adds, removes, isbinary = 0, 0, False
2536 2538 if line.startswith('diff --git a/'):
2537 2539 filename = gitre.search(line).group(2)
2538 2540 elif line.startswith('diff -r'):
2539 2541 # format: "diff -r ... -r ... filename"
2540 2542 filename = diffre.search(line).group(1)
2541 2543 elif line.startswith('+') and not line.startswith('+++ '):
2542 2544 adds += 1
2543 2545 elif line.startswith('-') and not line.startswith('--- '):
2544 2546 removes += 1
2545 2547 elif (line.startswith('GIT binary patch') or
2546 2548 line.startswith('Binary file')):
2547 2549 isbinary = True
2548 2550 addresult()
2549 2551 return results
2550 2552
2551 2553 def diffstat(lines, width=80, git=False):
2552 2554 output = []
2553 2555 stats = diffstatdata(lines)
2554 2556 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
2555 2557
2556 2558 countwidth = len(str(maxtotal))
2557 2559 if hasbinary and countwidth < 3:
2558 2560 countwidth = 3
2559 2561 graphwidth = width - countwidth - maxname - 6
2560 2562 if graphwidth < 10:
2561 2563 graphwidth = 10
2562 2564
2563 2565 def scale(i):
2564 2566 if maxtotal <= graphwidth:
2565 2567 return i
2566 2568 # If diffstat runs out of room it doesn't print anything,
2567 2569 # which isn't very useful, so always print at least one + or -
2568 2570 # if there were at least some changes.
2569 2571 return max(i * graphwidth // maxtotal, int(bool(i)))
2570 2572
2571 2573 for filename, adds, removes, isbinary in stats:
2572 2574 if isbinary:
2573 2575 count = 'Bin'
2574 2576 else:
2575 2577 count = adds + removes
2576 2578 pluses = '+' * scale(adds)
2577 2579 minuses = '-' * scale(removes)
2578 2580 output.append(' %s%s | %*s %s%s\n' %
2579 2581 (filename, ' ' * (maxname - encoding.colwidth(filename)),
2580 2582 countwidth, count, pluses, minuses))
2581 2583
2582 2584 if stats:
2583 2585 output.append(_(' %d files changed, %d insertions(+), '
2584 2586 '%d deletions(-)\n')
2585 2587 % (len(stats), totaladds, totalremoves))
2586 2588
2587 2589 return ''.join(output)
2588 2590
2589 2591 def diffstatui(*args, **kw):
2590 2592 '''like diffstat(), but yields 2-tuples of (output, label) for
2591 2593 ui.write()
2592 2594 '''
2593 2595
2594 2596 for line in diffstat(*args, **kw).splitlines():
2595 2597 if line and line[-1] in '+-':
2596 2598 name, graph = line.rsplit(' ', 1)
2597 2599 yield (name + ' ', '')
2598 2600 m = re.search(r'\++', graph)
2599 2601 if m:
2600 2602 yield (m.group(0), 'diffstat.inserted')
2601 2603 m = re.search(r'-+', graph)
2602 2604 if m:
2603 2605 yield (m.group(0), 'diffstat.deleted')
2604 2606 else:
2605 2607 yield (line, '')
2606 2608 yield ('\n', '')
@@ -1,1795 +1,1805 b''
1 1 $ hg init a
2 2 $ mkdir a/d1
3 3 $ mkdir a/d1/d2
4 4 $ echo line 1 > a/a
5 5 $ echo line 1 > a/d1/d2/a
6 6 $ hg --cwd a ci -Ama
7 7 adding a
8 8 adding d1/d2/a
9 9
10 10 $ echo line 2 >> a/a
11 11 $ hg --cwd a ci -u someone -d '1 0' -m'second change'
12 12
13 13 import with no args:
14 14
15 15 $ hg --cwd a import
16 16 abort: need at least one patch to import
17 17 [255]
18 18
19 19 generate patches for the test
20 20
21 21 $ hg --cwd a export tip > exported-tip.patch
22 22 $ hg --cwd a diff -r0:1 > diffed-tip.patch
23 23
24 24
25 25 import exported patch
26 26 (this also tests that editor is not invoked, if the patch contains the
27 27 commit message and '--edit' is not specified)
28 28
29 29 $ hg clone -r0 a b
30 30 adding changesets
31 31 adding manifests
32 32 adding file changes
33 33 added 1 changesets with 2 changes to 2 files
34 34 updating to branch default
35 35 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
36 36 $ HGEDITOR=cat hg --cwd b import ../exported-tip.patch
37 37 applying ../exported-tip.patch
38 38
39 39 message and committer and date should be same
40 40
41 41 $ hg --cwd b tip
42 42 changeset: 1:1d4bd90af0e4
43 43 tag: tip
44 44 user: someone
45 45 date: Thu Jan 01 00:00:01 1970 +0000
46 46 summary: second change
47 47
48 48 $ rm -r b
49 49
50 50
51 51 import exported patch with external patcher
52 52 (this also tests that editor is invoked, if the '--edit' is specified,
53 53 regardless of the commit message in the patch)
54 54
55 55 $ cat > dummypatch.py <<EOF
56 56 > from __future__ import print_function
57 57 > print('patching file a')
58 58 > file('a', 'wb').write('line2\n')
59 59 > EOF
60 60 $ hg clone -r0 a b
61 61 adding changesets
62 62 adding manifests
63 63 adding file changes
64 64 added 1 changesets with 2 changes to 2 files
65 65 updating to branch default
66 66 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
67 67 $ HGEDITOR=cat hg --config ui.patch='python ../dummypatch.py' --cwd b import --edit ../exported-tip.patch
68 68 applying ../exported-tip.patch
69 69 second change
70 70
71 71
72 72 HG: Enter commit message. Lines beginning with 'HG:' are removed.
73 73 HG: Leave message empty to abort commit.
74 74 HG: --
75 75 HG: user: someone
76 76 HG: branch 'default'
77 77 HG: changed a
78 78 $ cat b/a
79 79 line2
80 80 $ rm -r b
81 81
82 82
83 83 import of plain diff should fail without message
84 84 (this also tests that editor is invoked, if the patch doesn't contain
85 85 the commit message, regardless of '--edit')
86 86
87 87 $ hg clone -r0 a b
88 88 adding changesets
89 89 adding manifests
90 90 adding file changes
91 91 added 1 changesets with 2 changes to 2 files
92 92 updating to branch default
93 93 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
94 94 $ cat > $TESTTMP/editor.sh <<EOF
95 95 > env | grep HGEDITFORM
96 96 > cat \$1
97 97 > EOF
98 98 $ HGEDITOR="sh $TESTTMP/editor.sh" hg --cwd b import ../diffed-tip.patch
99 99 applying ../diffed-tip.patch
100 100 HGEDITFORM=import.normal.normal
101 101
102 102
103 103 HG: Enter commit message. Lines beginning with 'HG:' are removed.
104 104 HG: Leave message empty to abort commit.
105 105 HG: --
106 106 HG: user: test
107 107 HG: branch 'default'
108 108 HG: changed a
109 109 abort: empty commit message
110 110 [255]
111 111
112 112 Test avoiding editor invocation at applying the patch with --exact,
113 113 even if commit message is empty
114 114
115 115 $ echo a >> b/a
116 116 $ hg --cwd b commit -m ' '
117 117 $ hg --cwd b tip -T "{node}\n"
118 118 d8804f3f5396d800812f579c8452796a5993bdb2
119 119 $ hg --cwd b export -o ../empty-log.diff .
120 120 $ hg --cwd b update -q -C ".^1"
121 121 $ hg --cwd b --config extensions.strip= strip -q tip
122 122 $ HGEDITOR=cat hg --cwd b import --exact ../empty-log.diff
123 123 applying ../empty-log.diff
124 124 $ hg --cwd b tip -T "{node}\n"
125 125 d8804f3f5396d800812f579c8452796a5993bdb2
126 126
127 127 $ rm -r b
128 128
129 129
130 130 import of plain diff should be ok with message
131 131
132 132 $ hg clone -r0 a b
133 133 adding changesets
134 134 adding manifests
135 135 adding file changes
136 136 added 1 changesets with 2 changes to 2 files
137 137 updating to branch default
138 138 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
139 139 $ hg --cwd b import -mpatch ../diffed-tip.patch
140 140 applying ../diffed-tip.patch
141 141 $ rm -r b
142 142
143 143
144 144 import of plain diff with specific date and user
145 145 (this also tests that editor is not invoked, if
146 146 '--message'/'--logfile' is specified and '--edit' is not)
147 147
148 148 $ hg clone -r0 a b
149 149 adding changesets
150 150 adding manifests
151 151 adding file changes
152 152 added 1 changesets with 2 changes to 2 files
153 153 updating to branch default
154 154 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
155 155 $ hg --cwd b import -mpatch -d '1 0' -u 'user@nowhere.net' ../diffed-tip.patch
156 156 applying ../diffed-tip.patch
157 157 $ hg -R b tip -pv
158 158 changeset: 1:ca68f19f3a40
159 159 tag: tip
160 160 user: user@nowhere.net
161 161 date: Thu Jan 01 00:00:01 1970 +0000
162 162 files: a
163 163 description:
164 164 patch
165 165
166 166
167 167 diff -r 80971e65b431 -r ca68f19f3a40 a
168 168 --- a/a Thu Jan 01 00:00:00 1970 +0000
169 169 +++ b/a Thu Jan 01 00:00:01 1970 +0000
170 170 @@ -1,1 +1,2 @@
171 171 line 1
172 172 +line 2
173 173
174 174 $ rm -r b
175 175
176 176
177 177 import of plain diff should be ok with --no-commit
178 178 (this also tests that editor is not invoked, if '--no-commit' is
179 179 specified, regardless of '--edit')
180 180
181 181 $ hg clone -r0 a b
182 182 adding changesets
183 183 adding manifests
184 184 adding file changes
185 185 added 1 changesets with 2 changes to 2 files
186 186 updating to branch default
187 187 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
188 188 $ HGEDITOR=cat hg --cwd b import --no-commit --edit ../diffed-tip.patch
189 189 applying ../diffed-tip.patch
190 190 $ hg --cwd b diff --nodates
191 191 diff -r 80971e65b431 a
192 192 --- a/a
193 193 +++ b/a
194 194 @@ -1,1 +1,2 @@
195 195 line 1
196 196 +line 2
197 197 $ rm -r b
198 198
199 199
200 200 import of malformed plain diff should fail
201 201
202 202 $ hg clone -r0 a b
203 203 adding changesets
204 204 adding manifests
205 205 adding file changes
206 206 added 1 changesets with 2 changes to 2 files
207 207 updating to branch default
208 208 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
209 209 $ sed 's/1,1/foo/' < diffed-tip.patch > broken.patch
210 210 $ hg --cwd b import -mpatch ../broken.patch
211 211 applying ../broken.patch
212 212 abort: bad hunk #1
213 213 [255]
214 214 $ rm -r b
215 215
216 216
217 217 hg -R repo import
218 218 put the clone in a subdir - having a directory named "a"
219 219 used to hide a bug.
220 220
221 221 $ mkdir dir
222 222 $ hg clone -r0 a dir/b
223 223 adding changesets
224 224 adding manifests
225 225 adding file changes
226 226 added 1 changesets with 2 changes to 2 files
227 227 updating to branch default
228 228 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
229 229 $ cd dir
230 230 $ hg -R b import ../exported-tip.patch
231 231 applying ../exported-tip.patch
232 232 $ cd ..
233 233 $ rm -r dir
234 234
235 235
236 236 import from stdin
237 237
238 238 $ hg clone -r0 a b
239 239 adding changesets
240 240 adding manifests
241 241 adding file changes
242 242 added 1 changesets with 2 changes to 2 files
243 243 updating to branch default
244 244 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
245 245 $ hg --cwd b import - < exported-tip.patch
246 246 applying patch from stdin
247 247 $ rm -r b
248 248
249 249
250 250 import two patches in one stream
251 251
252 252 $ hg init b
253 253 $ hg --cwd a export 0:tip | hg --cwd b import -
254 254 applying patch from stdin
255 255 $ hg --cwd a id
256 256 1d4bd90af0e4 tip
257 257 $ hg --cwd b id
258 258 1d4bd90af0e4 tip
259 259 $ rm -r b
260 260
261 261
262 262 override commit message
263 263
264 264 $ hg clone -r0 a b
265 265 adding changesets
266 266 adding manifests
267 267 adding file changes
268 268 added 1 changesets with 2 changes to 2 files
269 269 updating to branch default
270 270 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
271 271 $ hg --cwd b import -m 'override' - < exported-tip.patch
272 272 applying patch from stdin
273 273 $ hg --cwd b tip | grep override
274 274 summary: override
275 275 $ rm -r b
276 276
277 277 $ cat > mkmsg.py <<EOF
278 278 > import email.Message, sys
279 279 > msg = email.Message.Message()
280 280 > patch = open(sys.argv[1], 'rb').read()
281 281 > msg.set_payload('email commit message\n' + patch)
282 282 > msg['Subject'] = 'email patch'
283 283 > msg['From'] = 'email patcher'
284 284 > file(sys.argv[2], 'wb').write(msg.as_string())
285 285 > EOF
286 286
287 287
288 288 plain diff in email, subject, message body
289 289
290 290 $ hg clone -r0 a b
291 291 adding changesets
292 292 adding manifests
293 293 adding file changes
294 294 added 1 changesets with 2 changes to 2 files
295 295 updating to branch default
296 296 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
297 297 $ python mkmsg.py diffed-tip.patch msg.patch
298 298 $ hg --cwd b import ../msg.patch
299 299 applying ../msg.patch
300 300 $ hg --cwd b tip | grep email
301 301 user: email patcher
302 302 summary: email patch
303 303 $ rm -r b
304 304
305 305
306 306 plain diff in email, no subject, message body
307 307
308 308 $ hg clone -r0 a b
309 309 adding changesets
310 310 adding manifests
311 311 adding file changes
312 312 added 1 changesets with 2 changes to 2 files
313 313 updating to branch default
314 314 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
315 315 $ grep -v '^Subject:' msg.patch | hg --cwd b import -
316 316 applying patch from stdin
317 317 $ rm -r b
318 318
319 319
320 320 plain diff in email, subject, no message body
321 321
322 322 $ hg clone -r0 a b
323 323 adding changesets
324 324 adding manifests
325 325 adding file changes
326 326 added 1 changesets with 2 changes to 2 files
327 327 updating to branch default
328 328 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
329 329 $ grep -v '^email ' msg.patch | hg --cwd b import -
330 330 applying patch from stdin
331 331 $ rm -r b
332 332
333 333
334 334 plain diff in email, no subject, no message body, should fail
335 335
336 336 $ hg clone -r0 a b
337 337 adding changesets
338 338 adding manifests
339 339 adding file changes
340 340 added 1 changesets with 2 changes to 2 files
341 341 updating to branch default
342 342 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
343 343 $ egrep -v '^(Subject|email)' msg.patch | hg --cwd b import -
344 344 applying patch from stdin
345 345 abort: empty commit message
346 346 [255]
347 347 $ rm -r b
348 348
349 349
350 350 hg export in email, should use patch header
351 351
352 352 $ hg clone -r0 a b
353 353 adding changesets
354 354 adding manifests
355 355 adding file changes
356 356 added 1 changesets with 2 changes to 2 files
357 357 updating to branch default
358 358 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
359 359 $ python mkmsg.py exported-tip.patch msg.patch
360 360 $ cat msg.patch | hg --cwd b import -
361 361 applying patch from stdin
362 362 $ hg --cwd b tip | grep second
363 363 summary: second change
364 364 $ rm -r b
365 365
366 366
367 367 subject: duplicate detection, removal of [PATCH]
368 368 The '---' tests the gitsendmail handling without proper mail headers
369 369
370 370 $ cat > mkmsg2.py <<EOF
371 371 > import email.Message, sys
372 372 > msg = email.Message.Message()
373 373 > patch = open(sys.argv[1], 'rb').read()
374 374 > msg.set_payload('email patch\n\nnext line\n---\n' + patch)
375 375 > msg['Subject'] = '[PATCH] email patch'
376 376 > msg['From'] = 'email patcher'
377 377 > file(sys.argv[2], 'wb').write(msg.as_string())
378 378 > EOF
379 379
380 380
381 381 plain diff in email, [PATCH] subject, message body with subject
382 382
383 383 $ hg clone -r0 a b
384 384 adding changesets
385 385 adding manifests
386 386 adding file changes
387 387 added 1 changesets with 2 changes to 2 files
388 388 updating to branch default
389 389 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
390 390 $ python mkmsg2.py diffed-tip.patch msg.patch
391 391 $ cat msg.patch | hg --cwd b import -
392 392 applying patch from stdin
393 393 $ hg --cwd b tip --template '{desc}\n'
394 394 email patch
395 395
396 396 next line
397 397 $ rm -r b
398 398
399 399
400 400 Issue963: Parent of working dir incorrect after import of multiple
401 401 patches and rollback
402 402
403 403 We weren't backing up the correct dirstate file when importing many
404 404 patches: import patch1 patch2; rollback
405 405
406 406 $ echo line 3 >> a/a
407 407 $ hg --cwd a ci -m'third change'
408 408 $ hg --cwd a export -o '../patch%R' 1 2
409 409 $ hg clone -qr0 a b
410 410 $ hg --cwd b parents --template 'parent: {rev}\n'
411 411 parent: 0
412 412 $ hg --cwd b import -v ../patch1 ../patch2
413 413 applying ../patch1
414 414 patching file a
415 415 committing files:
416 416 a
417 417 committing manifest
418 418 committing changelog
419 419 created 1d4bd90af0e4
420 420 applying ../patch2
421 421 patching file a
422 422 committing files:
423 423 a
424 424 committing manifest
425 425 committing changelog
426 426 created 6d019af21222
427 427 $ hg --cwd b rollback
428 428 repository tip rolled back to revision 0 (undo import)
429 429 working directory now based on revision 0
430 430 $ hg --cwd b parents --template 'parent: {rev}\n'
431 431 parent: 0
432 432
433 433 Test that "hg rollback" doesn't restore dirstate to one at the
434 434 beginning of the rollbacked transaction in not-"parent-gone" case.
435 435
436 436 invoking pretxncommit hook will cause marking '.hg/dirstate' as a file
437 437 to be restored at rollbacking, after DirstateTransactionPlan (see wiki
438 438 page for detail).
439 439
440 440 $ hg --cwd b branch -q foobar
441 441 $ hg --cwd b commit -m foobar
442 442 $ hg --cwd b update 0 -q
443 443 $ hg --cwd b import ../patch1 ../patch2 --config hooks.pretxncommit=true
444 444 applying ../patch1
445 445 applying ../patch2
446 446 $ hg --cwd b update -q 1
447 447 $ hg --cwd b rollback -q
448 448 $ hg --cwd b parents --template 'parent: {rev}\n'
449 449 parent: 1
450 450
451 451 $ hg --cwd b update -q -C 0
452 452 $ hg --cwd b --config extensions.strip= strip -q 1
453 453
454 454 Test visibility of in-memory distate changes inside transaction to
455 455 external process
456 456
457 457 $ echo foo > a/foo
458 458 $ hg --cwd a commit -A -m 'adding foo' foo
459 459 $ hg --cwd a export -o '../patch%R' 3
460 460
461 461 $ cat > $TESTTMP/checkvisibility.sh <<EOF
462 462 > echo "===="
463 463 > hg parents --template "VISIBLE {rev}:{node|short}\n"
464 464 > hg status -amr
465 465 > # test that pending changes are hidden
466 466 > unset HG_PENDING
467 467 > hg parents --template "ACTUAL {rev}:{node|short}\n"
468 468 > hg status -amr
469 469 > echo "===="
470 470 > EOF
471 471
472 472 == test visibility to external editor
473 473
474 474 $ (cd b && sh "$TESTTMP/checkvisibility.sh")
475 475 ====
476 476 VISIBLE 0:80971e65b431
477 477 ACTUAL 0:80971e65b431
478 478 ====
479 479
480 480 $ HGEDITOR="sh $TESTTMP/checkvisibility.sh" hg --cwd b import -v --edit ../patch1 ../patch2 ../patch3
481 481 applying ../patch1
482 482 patching file a
483 483 ====
484 484 VISIBLE 0:80971e65b431
485 485 M a
486 486 ACTUAL 0:80971e65b431
487 487 M a
488 488 ====
489 489 committing files:
490 490 a
491 491 committing manifest
492 492 committing changelog
493 493 created 1d4bd90af0e4
494 494 applying ../patch2
495 495 patching file a
496 496 ====
497 497 VISIBLE 1:1d4bd90af0e4
498 498 M a
499 499 ACTUAL 0:80971e65b431
500 500 M a
501 501 ====
502 502 committing files:
503 503 a
504 504 committing manifest
505 505 committing changelog
506 506 created 6d019af21222
507 507 applying ../patch3
508 508 patching file foo
509 509 adding foo
510 510 ====
511 511 VISIBLE 2:6d019af21222
512 512 A foo
513 513 ACTUAL 0:80971e65b431
514 514 M a
515 515 ====
516 516 committing files:
517 517 foo
518 518 committing manifest
519 519 committing changelog
520 520 created 55e3f75b2378
521 521
522 522 $ hg --cwd b rollback -q
523 523
524 524 (content of file "a" is already changed and it should be recognized as
525 525 "M", even though dirstate is restored to one before "hg import")
526 526
527 527 $ (cd b && sh "$TESTTMP/checkvisibility.sh")
528 528 ====
529 529 VISIBLE 0:80971e65b431
530 530 M a
531 531 ACTUAL 0:80971e65b431
532 532 M a
533 533 ====
534 534 $ hg --cwd b revert --no-backup a
535 535 $ rm -f b/foo
536 536
537 537 == test visibility to precommit external hook
538 538
539 539 $ cat >> b/.hg/hgrc <<EOF
540 540 > [hooks]
541 541 > precommit.visibility = sh $TESTTMP/checkvisibility.sh
542 542 > EOF
543 543
544 544 $ (cd b && sh "$TESTTMP/checkvisibility.sh")
545 545 ====
546 546 VISIBLE 0:80971e65b431
547 547 ACTUAL 0:80971e65b431
548 548 ====
549 549
550 550 $ hg --cwd b import ../patch1 ../patch2 ../patch3
551 551 applying ../patch1
552 552 ====
553 553 VISIBLE 0:80971e65b431
554 554 M a
555 555 ACTUAL 0:80971e65b431
556 556 M a
557 557 ====
558 558 applying ../patch2
559 559 ====
560 560 VISIBLE 1:1d4bd90af0e4
561 561 M a
562 562 ACTUAL 0:80971e65b431
563 563 M a
564 564 ====
565 565 applying ../patch3
566 566 ====
567 567 VISIBLE 2:6d019af21222
568 568 A foo
569 569 ACTUAL 0:80971e65b431
570 570 M a
571 571 ====
572 572
573 573 $ hg --cwd b rollback -q
574 574 $ (cd b && sh "$TESTTMP/checkvisibility.sh")
575 575 ====
576 576 VISIBLE 0:80971e65b431
577 577 M a
578 578 ACTUAL 0:80971e65b431
579 579 M a
580 580 ====
581 581 $ hg --cwd b revert --no-backup a
582 582 $ rm -f b/foo
583 583
584 584 $ cat >> b/.hg/hgrc <<EOF
585 585 > [hooks]
586 586 > precommit.visibility =
587 587 > EOF
588 588
589 589 == test visibility to pretxncommit external hook
590 590
591 591 $ cat >> b/.hg/hgrc <<EOF
592 592 > [hooks]
593 593 > pretxncommit.visibility = sh $TESTTMP/checkvisibility.sh
594 594 > EOF
595 595
596 596 $ (cd b && sh "$TESTTMP/checkvisibility.sh")
597 597 ====
598 598 VISIBLE 0:80971e65b431
599 599 ACTUAL 0:80971e65b431
600 600 ====
601 601
602 602 $ hg --cwd b import ../patch1 ../patch2 ../patch3
603 603 applying ../patch1
604 604 ====
605 605 VISIBLE 0:80971e65b431
606 606 M a
607 607 ACTUAL 0:80971e65b431
608 608 M a
609 609 ====
610 610 applying ../patch2
611 611 ====
612 612 VISIBLE 1:1d4bd90af0e4
613 613 M a
614 614 ACTUAL 0:80971e65b431
615 615 M a
616 616 ====
617 617 applying ../patch3
618 618 ====
619 619 VISIBLE 2:6d019af21222
620 620 A foo
621 621 ACTUAL 0:80971e65b431
622 622 M a
623 623 ====
624 624
625 625 $ hg --cwd b rollback -q
626 626 $ (cd b && sh "$TESTTMP/checkvisibility.sh")
627 627 ====
628 628 VISIBLE 0:80971e65b431
629 629 M a
630 630 ACTUAL 0:80971e65b431
631 631 M a
632 632 ====
633 633 $ hg --cwd b revert --no-backup a
634 634 $ rm -f b/foo
635 635
636 636 $ cat >> b/.hg/hgrc <<EOF
637 637 > [hooks]
638 638 > pretxncommit.visibility =
639 639 > EOF
640 640
641 641 $ rm -r b
642 642
643 643
644 644 importing a patch in a subdirectory failed at the commit stage
645 645
646 646 $ echo line 2 >> a/d1/d2/a
647 647 $ hg --cwd a ci -u someoneelse -d '1 0' -m'subdir change'
648 648
649 649 hg import in a subdirectory
650 650
651 651 $ hg clone -r0 a b
652 652 adding changesets
653 653 adding manifests
654 654 adding file changes
655 655 added 1 changesets with 2 changes to 2 files
656 656 updating to branch default
657 657 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
658 658 $ hg --cwd a export tip > tmp
659 659 $ sed -e 's/d1\/d2\///' < tmp > subdir-tip.patch
660 660 $ dir=`pwd`
661 661 $ cd b/d1/d2 2>&1 > /dev/null
662 662 $ hg import ../../../subdir-tip.patch
663 663 applying ../../../subdir-tip.patch
664 664 $ cd "$dir"
665 665
666 666 message should be 'subdir change'
667 667 committer should be 'someoneelse'
668 668
669 669 $ hg --cwd b tip
670 670 changeset: 1:3577f5aea227
671 671 tag: tip
672 672 user: someoneelse
673 673 date: Thu Jan 01 00:00:01 1970 +0000
674 674 summary: subdir change
675 675
676 676
677 677 should be empty
678 678
679 679 $ hg --cwd b status
680 680
681 681
682 682 Test fuzziness (ambiguous patch location, fuzz=2)
683 683
684 684 $ hg init fuzzy
685 685 $ cd fuzzy
686 686 $ echo line1 > a
687 687 $ echo line0 >> a
688 688 $ echo line3 >> a
689 689 $ hg ci -Am adda
690 690 adding a
691 691 $ echo line1 > a
692 692 $ echo line2 >> a
693 693 $ echo line0 >> a
694 694 $ echo line3 >> a
695 695 $ hg ci -m change a
696 696 $ hg export tip > fuzzy-tip.patch
697 697 $ hg up -C 0
698 698 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
699 699 $ echo line1 > a
700 700 $ echo line0 >> a
701 701 $ echo line1 >> a
702 702 $ echo line0 >> a
703 703 $ hg ci -m brancha
704 704 created new head
705 705 $ hg import --config patch.fuzz=0 -v fuzzy-tip.patch
706 706 applying fuzzy-tip.patch
707 707 patching file a
708 708 Hunk #1 FAILED at 0
709 709 1 out of 1 hunks FAILED -- saving rejects to file a.rej
710 710 abort: patch failed to apply
711 711 [255]
712 712 $ hg import --no-commit -v fuzzy-tip.patch
713 713 applying fuzzy-tip.patch
714 714 patching file a
715 715 Hunk #1 succeeded at 2 with fuzz 1 (offset 0 lines).
716 716 applied to working directory
717 717 $ hg revert -a
718 718 reverting a
719 719
720 720
721 721 import with --no-commit should have written .hg/last-message.txt
722 722
723 723 $ cat .hg/last-message.txt
724 724 change (no-eol)
725 725
726 726
727 727 test fuzziness with eol=auto
728 728
729 729 $ hg --config patch.eol=auto import --no-commit -v fuzzy-tip.patch
730 730 applying fuzzy-tip.patch
731 731 patching file a
732 732 Hunk #1 succeeded at 2 with fuzz 1 (offset 0 lines).
733 733 applied to working directory
734 734 $ cd ..
735 735
736 736
737 737 Test hunk touching empty files (issue906)
738 738
739 739 $ hg init empty
740 740 $ cd empty
741 741 $ touch a
742 742 $ touch b1
743 743 $ touch c1
744 744 $ echo d > d
745 745 $ hg ci -Am init
746 746 adding a
747 747 adding b1
748 748 adding c1
749 749 adding d
750 750 $ echo a > a
751 751 $ echo b > b1
752 752 $ hg mv b1 b2
753 753 $ echo c > c1
754 754 $ hg copy c1 c2
755 755 $ rm d
756 756 $ touch d
757 757 $ hg diff --git
758 758 diff --git a/a b/a
759 759 --- a/a
760 760 +++ b/a
761 761 @@ -0,0 +1,1 @@
762 762 +a
763 763 diff --git a/b1 b/b2
764 764 rename from b1
765 765 rename to b2
766 766 --- a/b1
767 767 +++ b/b2
768 768 @@ -0,0 +1,1 @@
769 769 +b
770 770 diff --git a/c1 b/c1
771 771 --- a/c1
772 772 +++ b/c1
773 773 @@ -0,0 +1,1 @@
774 774 +c
775 775 diff --git a/c1 b/c2
776 776 copy from c1
777 777 copy to c2
778 778 --- a/c1
779 779 +++ b/c2
780 780 @@ -0,0 +1,1 @@
781 781 +c
782 782 diff --git a/d b/d
783 783 --- a/d
784 784 +++ b/d
785 785 @@ -1,1 +0,0 @@
786 786 -d
787 787 $ hg ci -m empty
788 788 $ hg export --git tip > empty.diff
789 789 $ hg up -C 0
790 790 4 files updated, 0 files merged, 2 files removed, 0 files unresolved
791 791 $ hg import empty.diff
792 792 applying empty.diff
793 793 $ for name in a b1 b2 c1 c2 d; do
794 794 > echo % $name file
795 795 > test -f $name && cat $name
796 796 > done
797 797 % a file
798 798 a
799 799 % b1 file
800 800 % b2 file
801 801 b
802 802 % c1 file
803 803 c
804 804 % c2 file
805 805 c
806 806 % d file
807 807 $ cd ..
808 808
809 809
810 810 Test importing a patch ending with a binary file removal
811 811
812 812 $ hg init binaryremoval
813 813 $ cd binaryremoval
814 814 $ echo a > a
815 815 $ $PYTHON -c "file('b', 'wb').write('a\x00b')"
816 816 $ hg ci -Am addall
817 817 adding a
818 818 adding b
819 819 $ hg rm a
820 820 $ hg rm b
821 821 $ hg st
822 822 R a
823 823 R b
824 824 $ hg ci -m remove
825 825 $ hg export --git . > remove.diff
826 826 $ cat remove.diff | grep git
827 827 diff --git a/a b/a
828 828 diff --git a/b b/b
829 829 $ hg up -C 0
830 830 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
831 831 $ hg import remove.diff
832 832 applying remove.diff
833 833 $ hg manifest
834 834 $ cd ..
835 835
836 836
837 837 Issue927: test update+rename with common name
838 838
839 839 $ hg init t
840 840 $ cd t
841 841 $ touch a
842 842 $ hg ci -Am t
843 843 adding a
844 844 $ echo a > a
845 845
846 846 Here, bfile.startswith(afile)
847 847
848 848 $ hg copy a a2
849 849 $ hg ci -m copya
850 850 $ hg export --git tip > copy.diff
851 851 $ hg up -C 0
852 852 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
853 853 $ hg import copy.diff
854 854 applying copy.diff
855 855
856 856 a should contain an 'a'
857 857
858 858 $ cat a
859 859 a
860 860
861 861 and a2 should have duplicated it
862 862
863 863 $ cat a2
864 864 a
865 865 $ cd ..
866 866
867 867
868 868 test -p0
869 869
870 870 $ hg init p0
871 871 $ cd p0
872 872 $ echo a > a
873 873 $ hg ci -Am t
874 874 adding a
875 875 $ hg import -p foo
876 876 abort: invalid value 'foo' for option -p, expected int
877 877 [255]
878 878 $ hg import -p0 - << EOF
879 879 > foobar
880 880 > --- a Sat Apr 12 22:43:58 2008 -0400
881 881 > +++ a Sat Apr 12 22:44:05 2008 -0400
882 882 > @@ -1,1 +1,1 @@
883 883 > -a
884 884 > +bb
885 885 > EOF
886 886 applying patch from stdin
887 887 $ hg status
888 888 $ cat a
889 889 bb
890 890
891 891 test --prefix
892 892
893 893 $ mkdir -p dir/dir2
894 894 $ echo b > dir/dir2/b
895 895 $ hg ci -Am b
896 896 adding dir/dir2/b
897 897 $ hg import -p2 --prefix dir - << EOF
898 898 > foobar
899 899 > --- drop1/drop2/dir2/b
900 900 > +++ drop1/drop2/dir2/b
901 901 > @@ -1,1 +1,1 @@
902 902 > -b
903 903 > +cc
904 904 > EOF
905 905 applying patch from stdin
906 906 $ hg status
907 907 $ cat dir/dir2/b
908 908 cc
909 909 $ cd ..
910 910
911 911
912 912 test paths outside repo root
913 913
914 914 $ mkdir outside
915 915 $ touch outside/foo
916 916 $ hg init inside
917 917 $ cd inside
918 918 $ hg import - <<EOF
919 919 > diff --git a/a b/b
920 920 > rename from ../outside/foo
921 921 > rename to bar
922 922 > EOF
923 923 applying patch from stdin
924 924 abort: path contains illegal component: ../outside/foo (glob)
925 925 [255]
926 926 $ cd ..
927 927
928 928
929 929 test import with similarity and git and strip (issue295 et al.)
930 930
931 931 $ hg init sim
932 932 $ cd sim
933 933 $ echo 'this is a test' > a
934 934 $ hg ci -Ama
935 935 adding a
936 936 $ cat > ../rename.diff <<EOF
937 937 > diff --git a/foo/a b/foo/a
938 938 > deleted file mode 100644
939 939 > --- a/foo/a
940 940 > +++ /dev/null
941 941 > @@ -1,1 +0,0 @@
942 942 > -this is a test
943 943 > diff --git a/foo/b b/foo/b
944 944 > new file mode 100644
945 945 > --- /dev/null
946 946 > +++ b/foo/b
947 947 > @@ -0,0 +1,2 @@
948 948 > +this is a test
949 949 > +foo
950 950 > EOF
951 951 $ hg import --no-commit -v -s 1 ../rename.diff -p2
952 952 applying ../rename.diff
953 953 patching file a
954 954 patching file b
955 955 adding b
956 956 recording removal of a as rename to b (88% similar)
957 957 applied to working directory
958 958 $ hg st -C
959 959 A b
960 960 a
961 961 R a
962 962 $ hg revert -a
963 963 undeleting a
964 964 forgetting b
965 965 $ rm b
966 966 $ hg import --no-commit -v -s 100 ../rename.diff -p2
967 967 applying ../rename.diff
968 968 patching file a
969 969 patching file b
970 970 adding b
971 971 applied to working directory
972 972 $ hg st -C
973 973 A b
974 974 R a
975 975 $ cd ..
976 976
977 977
978 978 Issue1495: add empty file from the end of patch
979 979
980 980 $ hg init addemptyend
981 981 $ cd addemptyend
982 982 $ touch a
983 983 $ hg addremove
984 984 adding a
985 985 $ hg ci -m "commit"
986 986 $ cat > a.patch <<EOF
987 987 > add a, b
988 988 > diff --git a/a b/a
989 989 > --- a/a
990 990 > +++ b/a
991 991 > @@ -0,0 +1,1 @@
992 992 > +a
993 993 > diff --git a/b b/b
994 994 > new file mode 100644
995 995 > EOF
996 996 $ hg import --no-commit a.patch
997 997 applying a.patch
998 998
999 999 apply a good patch followed by an empty patch (mainly to ensure
1000 1000 that dirstate is *not* updated when import crashes)
1001 1001 $ hg update -q -C .
1002 1002 $ rm b
1003 1003 $ touch empty.patch
1004 1004 $ hg import a.patch empty.patch
1005 1005 applying a.patch
1006 1006 applying empty.patch
1007 1007 transaction abort!
1008 1008 rollback completed
1009 1009 abort: empty.patch: no diffs found
1010 1010 [255]
1011 1011 $ hg tip --template '{rev} {desc|firstline}\n'
1012 1012 0 commit
1013 1013 $ hg -q status
1014 1014 M a
1015 1015 $ cd ..
1016 1016
1017 1017 create file when source is not /dev/null
1018 1018
1019 1019 $ cat > create.patch <<EOF
1020 1020 > diff -Naur proj-orig/foo proj-new/foo
1021 1021 > --- proj-orig/foo 1969-12-31 16:00:00.000000000 -0800
1022 1022 > +++ proj-new/foo 2009-07-17 16:50:45.801368000 -0700
1023 1023 > @@ -0,0 +1,1 @@
1024 1024 > +a
1025 1025 > EOF
1026 1026
1027 1027 some people have patches like the following too
1028 1028
1029 1029 $ cat > create2.patch <<EOF
1030 1030 > diff -Naur proj-orig/foo proj-new/foo
1031 1031 > --- proj-orig/foo.orig 1969-12-31 16:00:00.000000000 -0800
1032 1032 > +++ proj-new/foo 2009-07-17 16:50:45.801368000 -0700
1033 1033 > @@ -0,0 +1,1 @@
1034 1034 > +a
1035 1035 > EOF
1036 1036 $ hg init oddcreate
1037 1037 $ cd oddcreate
1038 1038 $ hg import --no-commit ../create.patch
1039 1039 applying ../create.patch
1040 1040 $ cat foo
1041 1041 a
1042 1042 $ rm foo
1043 1043 $ hg revert foo
1044 1044 $ hg import --no-commit ../create2.patch
1045 1045 applying ../create2.patch
1046 1046 $ cat foo
1047 1047 a
1048 1048
1049 1049 $ cd ..
1050 1050
1051 1051 Issue1859: first line mistaken for email headers
1052 1052
1053 1053 $ hg init emailconfusion
1054 1054 $ cd emailconfusion
1055 1055 $ cat > a.patch <<EOF
1056 1056 > module: summary
1057 1057 >
1058 1058 > description
1059 1059 >
1060 1060 >
1061 1061 > diff -r 000000000000 -r 9b4c1e343b55 test.txt
1062 1062 > --- /dev/null
1063 1063 > +++ b/a
1064 1064 > @@ -0,0 +1,1 @@
1065 1065 > +a
1066 1066 > EOF
1067 1067 $ hg import -d '0 0' a.patch
1068 1068 applying a.patch
1069 1069 $ hg parents -v
1070 1070 changeset: 0:5a681217c0ad
1071 1071 tag: tip
1072 1072 user: test
1073 1073 date: Thu Jan 01 00:00:00 1970 +0000
1074 1074 files: a
1075 1075 description:
1076 1076 module: summary
1077 1077
1078 1078 description
1079 1079
1080 1080
1081 1081 $ cd ..
1082 1082
1083 1083
1084 1084 in commit message
1085 1085
1086 1086 $ hg init commitconfusion
1087 1087 $ cd commitconfusion
1088 1088 $ cat > a.patch <<EOF
1089 1089 > module: summary
1090 1090 >
1091 1091 > --- description
1092 1092 >
1093 1093 > diff --git a/a b/a
1094 1094 > new file mode 100644
1095 1095 > --- /dev/null
1096 1096 > +++ b/a
1097 1097 > @@ -0,0 +1,1 @@
1098 1098 > +a
1099 1099 > EOF
1100 1100 > hg import -d '0 0' a.patch
1101 1101 > hg parents -v
1102 1102 > cd ..
1103 1103 >
1104 1104 > echo '% tricky header splitting'
1105 1105 > cat > trickyheaders.patch <<EOF
1106 1106 > From: User A <user@a>
1107 1107 > Subject: [PATCH] from: tricky!
1108 1108 >
1109 1109 > # HG changeset patch
1110 1110 > # User User B
1111 1111 > # Date 1266264441 18000
1112 1112 > # Branch stable
1113 1113 > # Node ID f2be6a1170ac83bf31cb4ae0bad00d7678115bc0
1114 1114 > # Parent 0000000000000000000000000000000000000000
1115 1115 > from: tricky!
1116 1116 >
1117 1117 > That is not a header.
1118 1118 >
1119 1119 > diff -r 000000000000 -r f2be6a1170ac foo
1120 1120 > --- /dev/null
1121 1121 > +++ b/foo
1122 1122 > @@ -0,0 +1,1 @@
1123 1123 > +foo
1124 1124 > EOF
1125 1125 applying a.patch
1126 1126 changeset: 0:f34d9187897d
1127 1127 tag: tip
1128 1128 user: test
1129 1129 date: Thu Jan 01 00:00:00 1970 +0000
1130 1130 files: a
1131 1131 description:
1132 1132 module: summary
1133 1133
1134 1134
1135 1135 % tricky header splitting
1136 1136
1137 1137 $ hg init trickyheaders
1138 1138 $ cd trickyheaders
1139 1139 $ hg import -d '0 0' ../trickyheaders.patch
1140 1140 applying ../trickyheaders.patch
1141 1141 $ hg export --git tip
1142 1142 # HG changeset patch
1143 1143 # User User B
1144 1144 # Date 0 0
1145 1145 # Thu Jan 01 00:00:00 1970 +0000
1146 1146 # Node ID eb56ab91903632294ac504838508cb370c0901d2
1147 1147 # Parent 0000000000000000000000000000000000000000
1148 1148 from: tricky!
1149 1149
1150 1150 That is not a header.
1151 1151
1152 1152 diff --git a/foo b/foo
1153 1153 new file mode 100644
1154 1154 --- /dev/null
1155 1155 +++ b/foo
1156 1156 @@ -0,0 +1,1 @@
1157 1157 +foo
1158 1158 $ cd ..
1159 1159
1160 1160
1161 1161 Issue2102: hg export and hg import speak different languages
1162 1162
1163 1163 $ hg init issue2102
1164 1164 $ cd issue2102
1165 1165 $ mkdir -p src/cmd/gc
1166 1166 $ touch src/cmd/gc/mksys.bash
1167 1167 $ hg ci -Am init
1168 1168 adding src/cmd/gc/mksys.bash
1169 1169 $ hg import - <<EOF
1170 1170 > # HG changeset patch
1171 1171 > # User Rob Pike
1172 1172 > # Date 1216685449 25200
1173 1173 > # Node ID 03aa2b206f499ad6eb50e6e207b9e710d6409c98
1174 1174 > # Parent 93d10138ad8df586827ca90b4ddb5033e21a3a84
1175 1175 > help management of empty pkg and lib directories in perforce
1176 1176 >
1177 1177 > R=gri
1178 1178 > DELTA=4 (4 added, 0 deleted, 0 changed)
1179 1179 > OCL=13328
1180 1180 > CL=13328
1181 1181 >
1182 1182 > diff --git a/lib/place-holder b/lib/place-holder
1183 1183 > new file mode 100644
1184 1184 > --- /dev/null
1185 1185 > +++ b/lib/place-holder
1186 1186 > @@ -0,0 +1,2 @@
1187 1187 > +perforce does not maintain empty directories.
1188 1188 > +this file helps.
1189 1189 > diff --git a/pkg/place-holder b/pkg/place-holder
1190 1190 > new file mode 100644
1191 1191 > --- /dev/null
1192 1192 > +++ b/pkg/place-holder
1193 1193 > @@ -0,0 +1,2 @@
1194 1194 > +perforce does not maintain empty directories.
1195 1195 > +this file helps.
1196 1196 > diff --git a/src/cmd/gc/mksys.bash b/src/cmd/gc/mksys.bash
1197 1197 > old mode 100644
1198 1198 > new mode 100755
1199 1199 > EOF
1200 1200 applying patch from stdin
1201 1201
1202 1202 #if execbit
1203 1203
1204 1204 $ hg sum
1205 1205 parent: 1:d59915696727 tip
1206 1206 help management of empty pkg and lib directories in perforce
1207 1207 branch: default
1208 1208 commit: (clean)
1209 1209 update: (current)
1210 1210 phases: 2 draft
1211 1211
1212 1212 $ hg diff --git -c tip
1213 1213 diff --git a/lib/place-holder b/lib/place-holder
1214 1214 new file mode 100644
1215 1215 --- /dev/null
1216 1216 +++ b/lib/place-holder
1217 1217 @@ -0,0 +1,2 @@
1218 1218 +perforce does not maintain empty directories.
1219 1219 +this file helps.
1220 1220 diff --git a/pkg/place-holder b/pkg/place-holder
1221 1221 new file mode 100644
1222 1222 --- /dev/null
1223 1223 +++ b/pkg/place-holder
1224 1224 @@ -0,0 +1,2 @@
1225 1225 +perforce does not maintain empty directories.
1226 1226 +this file helps.
1227 1227 diff --git a/src/cmd/gc/mksys.bash b/src/cmd/gc/mksys.bash
1228 1228 old mode 100644
1229 1229 new mode 100755
1230 1230
1231 1231 #else
1232 1232
1233 1233 $ hg sum
1234 1234 parent: 1:28f089cc9ccc tip
1235 1235 help management of empty pkg and lib directories in perforce
1236 1236 branch: default
1237 1237 commit: (clean)
1238 1238 update: (current)
1239 1239 phases: 2 draft
1240 1240
1241 1241 $ hg diff --git -c tip
1242 1242 diff --git a/lib/place-holder b/lib/place-holder
1243 1243 new file mode 100644
1244 1244 --- /dev/null
1245 1245 +++ b/lib/place-holder
1246 1246 @@ -0,0 +1,2 @@
1247 1247 +perforce does not maintain empty directories.
1248 1248 +this file helps.
1249 1249 diff --git a/pkg/place-holder b/pkg/place-holder
1250 1250 new file mode 100644
1251 1251 --- /dev/null
1252 1252 +++ b/pkg/place-holder
1253 1253 @@ -0,0 +1,2 @@
1254 1254 +perforce does not maintain empty directories.
1255 1255 +this file helps.
1256 1256
1257 1257 /* The mode change for mksys.bash is missing here, because on platforms */
1258 1258 /* that don't support execbits, mode changes in patches are ignored when */
1259 1259 /* they are imported. This is obviously also the reason for why the hash */
1260 1260 /* in the created changeset is different to the one you see above the */
1261 1261 /* #else clause */
1262 1262
1263 1263 #endif
1264 1264 $ cd ..
1265 1265
1266 1266
1267 1267 diff lines looking like headers
1268 1268
1269 1269 $ hg init difflineslikeheaders
1270 1270 $ cd difflineslikeheaders
1271 1271 $ echo a >a
1272 1272 $ echo b >b
1273 1273 $ echo c >c
1274 1274 $ hg ci -Am1
1275 1275 adding a
1276 1276 adding b
1277 1277 adding c
1278 1278
1279 1279 $ echo "key: value" >>a
1280 1280 $ echo "key: value" >>b
1281 1281 $ echo "foo" >>c
1282 1282 $ hg ci -m2
1283 1283
1284 1284 $ hg up -C 0
1285 1285 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
1286 1286 $ hg diff --git -c1 >want
1287 1287 $ hg diff -c1 | hg import --no-commit -
1288 1288 applying patch from stdin
1289 1289 $ hg diff --git >have
1290 1290 $ diff want have
1291 1291 $ cd ..
1292 1292
1293 1293 import a unified diff with no lines of context (diff -U0)
1294 1294
1295 1295 $ hg init diffzero
1296 1296 $ cd diffzero
1297 1297 $ cat > f << EOF
1298 1298 > c2
1299 1299 > c4
1300 1300 > c5
1301 1301 > EOF
1302 1302 $ hg commit -Am0
1303 1303 adding f
1304 1304
1305 1305 $ hg import --no-commit - << EOF
1306 1306 > # HG changeset patch
1307 1307 > # User test
1308 1308 > # Date 0 0
1309 1309 > # Node ID f4974ab632f3dee767567b0576c0ec9a4508575c
1310 1310 > # Parent 8679a12a975b819fae5f7ad3853a2886d143d794
1311 1311 > 1
1312 1312 > diff -r 8679a12a975b -r f4974ab632f3 f
1313 1313 > --- a/f Thu Jan 01 00:00:00 1970 +0000
1314 1314 > +++ b/f Thu Jan 01 00:00:00 1970 +0000
1315 1315 > @@ -0,0 +1,1 @@
1316 1316 > +c1
1317 1317 > @@ -1,0 +3,1 @@
1318 1318 > +c3
1319 1319 > @@ -3,1 +4,0 @@
1320 1320 > -c5
1321 1321 > EOF
1322 1322 applying patch from stdin
1323 1323
1324 1324 $ cat f
1325 1325 c1
1326 1326 c2
1327 1327 c3
1328 1328 c4
1329 1329
1330 1330 $ cd ..
1331 1331
1332 1332 no segfault while importing a unified diff which start line is zero but chunk
1333 1333 size is non-zero
1334 1334
1335 1335 $ hg init startlinezero
1336 1336 $ cd startlinezero
1337 1337 $ echo foo > foo
1338 1338 $ hg commit -Amfoo
1339 1339 adding foo
1340 1340
1341 1341 $ hg import --no-commit - << EOF
1342 1342 > diff a/foo b/foo
1343 1343 > --- a/foo
1344 1344 > +++ b/foo
1345 1345 > @@ -0,1 +0,1 @@
1346 1346 > foo
1347 1347 > EOF
1348 1348 applying patch from stdin
1349 1349
1350 1350 $ cd ..
1351 1351
1352 1352 Test corner case involving fuzz and skew
1353 1353
1354 1354 $ hg init morecornercases
1355 1355 $ cd morecornercases
1356 1356
1357 1357 $ cat > 01-no-context-beginning-of-file.diff <<EOF
1358 1358 > diff --git a/a b/a
1359 1359 > --- a/a
1360 1360 > +++ b/a
1361 1361 > @@ -1,0 +1,1 @@
1362 1362 > +line
1363 1363 > EOF
1364 1364
1365 1365 $ cat > 02-no-context-middle-of-file.diff <<EOF
1366 1366 > diff --git a/a b/a
1367 1367 > --- a/a
1368 1368 > +++ b/a
1369 1369 > @@ -1,1 +1,1 @@
1370 1370 > -2
1371 1371 > +add some skew
1372 1372 > @@ -2,0 +2,1 @@
1373 1373 > +line
1374 1374 > EOF
1375 1375
1376 1376 $ cat > 03-no-context-end-of-file.diff <<EOF
1377 1377 > diff --git a/a b/a
1378 1378 > --- a/a
1379 1379 > +++ b/a
1380 1380 > @@ -10,0 +10,1 @@
1381 1381 > +line
1382 1382 > EOF
1383 1383
1384 1384 $ cat > 04-middle-of-file-completely-fuzzed.diff <<EOF
1385 1385 > diff --git a/a b/a
1386 1386 > --- a/a
1387 1387 > +++ b/a
1388 1388 > @@ -1,1 +1,1 @@
1389 1389 > -2
1390 1390 > +add some skew
1391 1391 > @@ -2,2 +2,3 @@
1392 1392 > not matching, should fuzz
1393 1393 > ... a bit
1394 1394 > +line
1395 1395 > EOF
1396 1396
1397 1397 $ cat > a <<EOF
1398 1398 > 1
1399 1399 > 2
1400 1400 > 3
1401 1401 > 4
1402 1402 > EOF
1403 1403 $ hg ci -Am adda a
1404 1404 $ for p in *.diff; do
1405 1405 > hg import -v --no-commit $p
1406 1406 > cat a
1407 1407 > hg revert -aqC a
1408 1408 > # patch -p1 < $p
1409 1409 > # cat a
1410 1410 > # hg revert -aC a
1411 1411 > done
1412 1412 applying 01-no-context-beginning-of-file.diff
1413 1413 patching file a
1414 1414 applied to working directory
1415 1415 1
1416 1416 line
1417 1417 2
1418 1418 3
1419 1419 4
1420 1420 applying 02-no-context-middle-of-file.diff
1421 1421 patching file a
1422 1422 Hunk #1 succeeded at 2 (offset 1 lines).
1423 1423 Hunk #2 succeeded at 4 (offset 1 lines).
1424 1424 applied to working directory
1425 1425 1
1426 1426 add some skew
1427 1427 3
1428 1428 line
1429 1429 4
1430 1430 applying 03-no-context-end-of-file.diff
1431 1431 patching file a
1432 1432 Hunk #1 succeeded at 5 (offset -6 lines).
1433 1433 applied to working directory
1434 1434 1
1435 1435 2
1436 1436 3
1437 1437 4
1438 1438 line
1439 1439 applying 04-middle-of-file-completely-fuzzed.diff
1440 1440 patching file a
1441 1441 Hunk #1 succeeded at 2 (offset 1 lines).
1442 1442 Hunk #2 succeeded at 5 with fuzz 2 (offset 1 lines).
1443 1443 applied to working directory
1444 1444 1
1445 1445 add some skew
1446 1446 3
1447 1447 4
1448 1448 line
1449 1449 $ cd ..
1450 1450
1451 1451 Test partial application
1452 1452 ------------------------
1453 1453
1454 1454 prepare a stack of patches depending on each other
1455 1455
1456 1456 $ hg init partial
1457 1457 $ cd partial
1458 1458 $ cat << EOF > a
1459 1459 > one
1460 1460 > two
1461 1461 > three
1462 1462 > four
1463 1463 > five
1464 1464 > six
1465 1465 > seven
1466 1466 > EOF
1467 1467 $ hg add a
1468 1468 $ echo 'b' > b
1469 1469 $ hg add b
1470 1470 $ hg commit -m 'initial' -u Babar
1471 1471 $ cat << EOF > a
1472 1472 > one
1473 1473 > two
1474 1474 > 3
1475 1475 > four
1476 1476 > five
1477 1477 > six
1478 1478 > seven
1479 1479 > EOF
1480 1480 $ hg commit -m 'three' -u Celeste
1481 1481 $ cat << EOF > a
1482 1482 > one
1483 1483 > two
1484 1484 > 3
1485 1485 > 4
1486 1486 > five
1487 1487 > six
1488 1488 > seven
1489 1489 > EOF
1490 1490 $ hg commit -m 'four' -u Rataxes
1491 1491 $ cat << EOF > a
1492 1492 > one
1493 1493 > two
1494 1494 > 3
1495 1495 > 4
1496 1496 > 5
1497 1497 > six
1498 1498 > seven
1499 1499 > EOF
1500 1500 $ echo bb >> b
1501 1501 $ hg commit -m 'five' -u Arthur
1502 1502 $ echo 'Babar' > jungle
1503 1503 $ hg add jungle
1504 1504 $ hg ci -m 'jungle' -u Zephir
1505 1505 $ echo 'Celeste' >> jungle
1506 1506 $ hg ci -m 'extended jungle' -u Cornelius
1507 1507 $ hg log -G --template '{desc|firstline} [{author}] {diffstat}\n'
1508 1508 @ extended jungle [Cornelius] 1: +1/-0
1509 1509 |
1510 1510 o jungle [Zephir] 1: +1/-0
1511 1511 |
1512 1512 o five [Arthur] 2: +2/-1
1513 1513 |
1514 1514 o four [Rataxes] 1: +1/-1
1515 1515 |
1516 1516 o three [Celeste] 1: +1/-1
1517 1517 |
1518 1518 o initial [Babar] 2: +8/-0
1519 1519
1520 1520
1521 1521 Importing with some success and some errors:
1522 1522
1523 1523 $ hg update --rev 'desc(initial)'
1524 1524 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
1525 1525 $ hg export --rev 'desc(five)' | hg import --partial -
1526 1526 applying patch from stdin
1527 1527 patching file a
1528 1528 Hunk #1 FAILED at 1
1529 1529 1 out of 1 hunks FAILED -- saving rejects to file a.rej
1530 1530 patch applied partially
1531 1531 (fix the .rej files and run `hg commit --amend`)
1532 1532 [1]
1533 1533
1534 1534 $ hg log -G --template '{desc|firstline} [{author}] {diffstat}\n'
1535 1535 @ five [Arthur] 1: +1/-0
1536 1536 |
1537 1537 | o extended jungle [Cornelius] 1: +1/-0
1538 1538 | |
1539 1539 | o jungle [Zephir] 1: +1/-0
1540 1540 | |
1541 1541 | o five [Arthur] 2: +2/-1
1542 1542 | |
1543 1543 | o four [Rataxes] 1: +1/-1
1544 1544 | |
1545 1545 | o three [Celeste] 1: +1/-1
1546 1546 |/
1547 1547 o initial [Babar] 2: +8/-0
1548 1548
1549 1549 $ hg export
1550 1550 # HG changeset patch
1551 1551 # User Arthur
1552 1552 # Date 0 0
1553 1553 # Thu Jan 01 00:00:00 1970 +0000
1554 1554 # Node ID 26e6446bb2526e2be1037935f5fca2b2706f1509
1555 1555 # Parent 8e4f0351909eae6b9cf68c2c076cb54c42b54b2e
1556 1556 five
1557 1557
1558 1558 diff -r 8e4f0351909e -r 26e6446bb252 b
1559 1559 --- a/b Thu Jan 01 00:00:00 1970 +0000
1560 1560 +++ b/b Thu Jan 01 00:00:00 1970 +0000
1561 1561 @@ -1,1 +1,2 @@
1562 1562 b
1563 1563 +bb
1564 1564 $ hg status -c .
1565 1565 C a
1566 1566 C b
1567 1567 $ ls
1568 1568 a
1569 1569 a.rej
1570 1570 b
1571 1571
1572 1572 Importing with zero success:
1573 1573
1574 1574 $ hg update --rev 'desc(initial)'
1575 1575 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1576 1576 $ hg export --rev 'desc(four)' | hg import --partial -
1577 1577 applying patch from stdin
1578 1578 patching file a
1579 1579 Hunk #1 FAILED at 0
1580 1580 1 out of 1 hunks FAILED -- saving rejects to file a.rej
1581 1581 patch applied partially
1582 1582 (fix the .rej files and run `hg commit --amend`)
1583 1583 [1]
1584 1584
1585 1585 $ hg log -G --template '{desc|firstline} [{author}] {diffstat}\n'
1586 1586 @ four [Rataxes] 0: +0/-0
1587 1587 |
1588 1588 | o five [Arthur] 1: +1/-0
1589 1589 |/
1590 1590 | o extended jungle [Cornelius] 1: +1/-0
1591 1591 | |
1592 1592 | o jungle [Zephir] 1: +1/-0
1593 1593 | |
1594 1594 | o five [Arthur] 2: +2/-1
1595 1595 | |
1596 1596 | o four [Rataxes] 1: +1/-1
1597 1597 | |
1598 1598 | o three [Celeste] 1: +1/-1
1599 1599 |/
1600 1600 o initial [Babar] 2: +8/-0
1601 1601
1602 1602 $ hg export
1603 1603 # HG changeset patch
1604 1604 # User Rataxes
1605 1605 # Date 0 0
1606 1606 # Thu Jan 01 00:00:00 1970 +0000
1607 1607 # Node ID cb9b1847a74d9ad52e93becaf14b98dbcc274e1e
1608 1608 # Parent 8e4f0351909eae6b9cf68c2c076cb54c42b54b2e
1609 1609 four
1610 1610
1611 1611 $ hg status -c .
1612 1612 C a
1613 1613 C b
1614 1614 $ ls
1615 1615 a
1616 1616 a.rej
1617 1617 b
1618 1618
1619 1619 Importing with unknown file:
1620 1620
1621 1621 $ hg update --rev 'desc(initial)'
1622 1622 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1623 1623 $ hg export --rev 'desc("extended jungle")' | hg import --partial -
1624 1624 applying patch from stdin
1625 1625 unable to find 'jungle' for patching
1626 1626 (use '--prefix' to apply patch relative to the current directory)
1627 1627 1 out of 1 hunks FAILED -- saving rejects to file jungle.rej
1628 1628 patch applied partially
1629 1629 (fix the .rej files and run `hg commit --amend`)
1630 1630 [1]
1631 1631
1632 1632 $ hg log -G --template '{desc|firstline} [{author}] {diffstat}\n'
1633 1633 @ extended jungle [Cornelius] 0: +0/-0
1634 1634 |
1635 1635 | o four [Rataxes] 0: +0/-0
1636 1636 |/
1637 1637 | o five [Arthur] 1: +1/-0
1638 1638 |/
1639 1639 | o extended jungle [Cornelius] 1: +1/-0
1640 1640 | |
1641 1641 | o jungle [Zephir] 1: +1/-0
1642 1642 | |
1643 1643 | o five [Arthur] 2: +2/-1
1644 1644 | |
1645 1645 | o four [Rataxes] 1: +1/-1
1646 1646 | |
1647 1647 | o three [Celeste] 1: +1/-1
1648 1648 |/
1649 1649 o initial [Babar] 2: +8/-0
1650 1650
1651 1651 $ hg export
1652 1652 # HG changeset patch
1653 1653 # User Cornelius
1654 1654 # Date 0 0
1655 1655 # Thu Jan 01 00:00:00 1970 +0000
1656 1656 # Node ID 1fb1f86bef43c5a75918178f8d23c29fb0a7398d
1657 1657 # Parent 8e4f0351909eae6b9cf68c2c076cb54c42b54b2e
1658 1658 extended jungle
1659 1659
1660 1660 $ hg status -c .
1661 1661 C a
1662 1662 C b
1663 1663 $ ls
1664 1664 a
1665 1665 a.rej
1666 1666 b
1667 1667 jungle.rej
1668 1668
1669 1669 Importing multiple failing patches:
1670 1670
1671 1671 $ hg update --rev 'desc(initial)'
1672 1672 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1673 1673 $ echo 'B' > b # just to make another commit
1674 1674 $ hg commit -m "a new base"
1675 1675 created new head
1676 1676 $ hg export --rev 'desc("four") + desc("extended jungle")' | hg import --partial -
1677 1677 applying patch from stdin
1678 1678 patching file a
1679 1679 Hunk #1 FAILED at 0
1680 1680 1 out of 1 hunks FAILED -- saving rejects to file a.rej
1681 1681 patch applied partially
1682 1682 (fix the .rej files and run `hg commit --amend`)
1683 1683 [1]
1684 1684 $ hg log -G --template '{desc|firstline} [{author}] {diffstat}\n'
1685 1685 @ four [Rataxes] 0: +0/-0
1686 1686 |
1687 1687 o a new base [test] 1: +1/-1
1688 1688 |
1689 1689 | o extended jungle [Cornelius] 0: +0/-0
1690 1690 |/
1691 1691 | o four [Rataxes] 0: +0/-0
1692 1692 |/
1693 1693 | o five [Arthur] 1: +1/-0
1694 1694 |/
1695 1695 | o extended jungle [Cornelius] 1: +1/-0
1696 1696 | |
1697 1697 | o jungle [Zephir] 1: +1/-0
1698 1698 | |
1699 1699 | o five [Arthur] 2: +2/-1
1700 1700 | |
1701 1701 | o four [Rataxes] 1: +1/-1
1702 1702 | |
1703 1703 | o three [Celeste] 1: +1/-1
1704 1704 |/
1705 1705 o initial [Babar] 2: +8/-0
1706 1706
1707 1707 $ hg export
1708 1708 # HG changeset patch
1709 1709 # User Rataxes
1710 1710 # Date 0 0
1711 1711 # Thu Jan 01 00:00:00 1970 +0000
1712 1712 # Node ID a9d7b6d0ffbb4eb12b7d5939250fcd42e8930a1d
1713 1713 # Parent f59f8d2e95a8ca5b1b4ca64320140da85f3b44fd
1714 1714 four
1715 1715
1716 1716 $ hg status -c .
1717 1717 C a
1718 1718 C b
1719 1719
1720 1720 Importing some extra header
1721 1721 ===========================
1722 1722
1723 1723 $ cat > $TESTTMP/parseextra.py <<EOF
1724 1724 > import mercurial.patch
1725 1725 > import mercurial.cmdutil
1726 1726 >
1727 1727 > def processfoo(repo, data, extra, opts):
1728 1728 > if 'foo' in data:
1729 1729 > extra['foo'] = data['foo']
1730 1730 > def postimport(ctx):
1731 1731 > if 'foo' in ctx.extra():
1732 1732 > ctx.repo().ui.write('imported-foo: %s\n' % ctx.extra()['foo'])
1733 1733 >
1734 1734 > mercurial.patch.patchheadermap.append(('Foo', 'foo'))
1735 1735 > mercurial.cmdutil.extrapreimport.append('foo')
1736 1736 > mercurial.cmdutil.extrapreimportmap['foo'] = processfoo
1737 1737 > mercurial.cmdutil.extrapostimport.append('foo')
1738 1738 > mercurial.cmdutil.extrapostimportmap['foo'] = postimport
1739 1739 > EOF
1740 1740 $ cat >> $HGRCPATH <<EOF
1741 1741 > [extensions]
1742 1742 > parseextra=$TESTTMP/parseextra.py
1743 1743 > EOF
1744 1744 $ hg up -C tip
1745 1745 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1746 1746 $ cat > $TESTTMP/foo.patch <<EOF
1747 1747 > # HG changeset patch
1748 1748 > # User Rataxes
1749 1749 > # Date 0 0
1750 1750 > # Thu Jan 01 00:00:00 1970 +0000
1751 1751 > # Foo bar
1752 1752 > height
1753 1753 >
1754 1754 > --- a/a Thu Jan 01 00:00:00 1970 +0000
1755 1755 > +++ b/a Wed Oct 07 09:17:44 2015 +0000
1756 1756 > @@ -5,3 +5,4 @@
1757 1757 > five
1758 1758 > six
1759 1759 > seven
1760 1760 > +heigt
1761 1761 > EOF
1762 1762 $ hg import $TESTTMP/foo.patch
1763 1763 applying $TESTTMP/foo.patch
1764 1764 imported-foo: bar
1765 1765 $ hg log --debug -r . | grep extra
1766 1766 extra: branch=default
1767 1767 extra: foo=bar
1768 1768
1769 1769 Warn the user that paths are relative to the root of
1770 1770 repository when file not found for patching
1771 1771
1772 1772 $ mkdir filedir
1773 1773 $ echo "file1" >> filedir/file1
1774 1774 $ hg add filedir/file1
1775 1775 $ hg commit -m "file1"
1776 1776 $ cd filedir
1777 1777 $ hg import -p 2 - <<EOF
1778 1778 > # HG changeset patch
1779 1779 > # User test
1780 1780 > # Date 0 0
1781 1781 > file2
1782 1782 >
1783 1783 > diff --git a/filedir/file1 b/filedir/file1
1784 1784 > --- a/filedir/file1
1785 1785 > +++ b/filedir/file1
1786 1786 > @@ -1,1 +1,2 @@
1787 1787 > file1
1788 1788 > +file2
1789 1789 > EOF
1790 1790 applying patch from stdin
1791 1791 unable to find 'file1' for patching
1792 1792 (use '--prefix' to apply patch relative to the current directory)
1793 1793 1 out of 1 hunks FAILED -- saving rejects to file file1.rej
1794 1794 abort: patch failed to apply
1795 1795 [255]
1796
1797 test import crash (issue5375)
1798 $ cd ..
1799 $ hg init repo
1800 $ cd repo
1801 $ printf "diff --git a/a b/b\nrename from a\nrename to b" | hg import -
1802 applying patch from stdin
1803 a not tracked!
1804 abort: source file 'a' does not exist
1805 [255]
General Comments 0
You need to be logged in to leave comments. Login now