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