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