##// END OF EJS Templates
diff: make sure we output stat even when --git is not passed (issue4037) (BC)...
Pulkit Goyal -
r41950:251332db default
parent child Browse files
Show More
@@ -1,2850 +1,2850 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) ')
867 867 newfile_re = re.compile('(?:new file)')
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 'record': _("record change %d/%d to '%s'?"),
1016 1016 },
1017 1017 'single': {
1018 1018 'apply': _("apply this change to '%s'?"),
1019 1019 'discard': _("discard this change to '%s'?"),
1020 1020 'record': _("record this change to '%s'?"),
1021 1021 },
1022 1022 'help': {
1023 1023 'apply': _('[Ynesfdaq?]'
1024 1024 '$$ &Yes, apply this change'
1025 1025 '$$ &No, skip this change'
1026 1026 '$$ &Edit this change manually'
1027 1027 '$$ &Skip remaining changes to this file'
1028 1028 '$$ Apply remaining changes to this &file'
1029 1029 '$$ &Done, skip remaining changes and files'
1030 1030 '$$ Apply &all changes to all remaining files'
1031 1031 '$$ &Quit, applying no changes'
1032 1032 '$$ &? (display help)'),
1033 1033 'discard': _('[Ynesfdaq?]'
1034 1034 '$$ &Yes, discard this change'
1035 1035 '$$ &No, skip this change'
1036 1036 '$$ &Edit this change manually'
1037 1037 '$$ &Skip remaining changes to this file'
1038 1038 '$$ Discard remaining changes to this &file'
1039 1039 '$$ &Done, skip remaining changes and files'
1040 1040 '$$ Discard &all changes to all remaining files'
1041 1041 '$$ &Quit, discarding no changes'
1042 1042 '$$ &? (display help)'),
1043 1043 'record': _('[Ynesfdaq?]'
1044 1044 '$$ &Yes, record this change'
1045 1045 '$$ &No, skip this change'
1046 1046 '$$ &Edit this change manually'
1047 1047 '$$ &Skip remaining changes to this file'
1048 1048 '$$ Record remaining changes to this &file'
1049 1049 '$$ &Done, skip remaining changes and files'
1050 1050 '$$ Record &all changes to all remaining files'
1051 1051 '$$ &Quit, recording no changes'
1052 1052 '$$ &? (display help)'),
1053 1053 }
1054 1054 }
1055 1055
1056 1056 def filterpatch(ui, headers, operation=None):
1057 1057 """Interactively filter patch chunks into applied-only chunks"""
1058 1058 messages = getmessages()
1059 1059
1060 1060 if operation is None:
1061 1061 operation = 'record'
1062 1062
1063 1063 def prompt(skipfile, skipall, query, chunk):
1064 1064 """prompt query, and process base inputs
1065 1065
1066 1066 - y/n for the rest of file
1067 1067 - y/n for the rest
1068 1068 - ? (help)
1069 1069 - q (quit)
1070 1070
1071 1071 Return True/False and possibly updated skipfile and skipall.
1072 1072 """
1073 1073 newpatches = None
1074 1074 if skipall is not None:
1075 1075 return skipall, skipfile, skipall, newpatches
1076 1076 if skipfile is not None:
1077 1077 return skipfile, skipfile, skipall, newpatches
1078 1078 while True:
1079 1079 resps = messages['help'][operation]
1080 1080 r = ui.promptchoice("%s %s" % (query, resps))
1081 1081 ui.write("\n")
1082 1082 if r == 8: # ?
1083 1083 for c, t in ui.extractchoices(resps)[1]:
1084 1084 ui.write('%s - %s\n' % (c, encoding.lower(t)))
1085 1085 continue
1086 1086 elif r == 0: # yes
1087 1087 ret = True
1088 1088 elif r == 1: # no
1089 1089 ret = False
1090 1090 elif r == 2: # Edit patch
1091 1091 if chunk is None:
1092 1092 ui.write(_('cannot edit patch for whole file'))
1093 1093 ui.write("\n")
1094 1094 continue
1095 1095 if chunk.header.binary():
1096 1096 ui.write(_('cannot edit patch for binary file'))
1097 1097 ui.write("\n")
1098 1098 continue
1099 1099 # Patch comment based on the Git one (based on comment at end of
1100 1100 # https://mercurial-scm.org/wiki/RecordExtension)
1101 1101 phelp = '---' + _("""
1102 1102 To remove '-' lines, make them ' ' lines (context).
1103 1103 To remove '+' lines, delete them.
1104 1104 Lines starting with # will be removed from the patch.
1105 1105
1106 1106 If the patch applies cleanly, the edited hunk will immediately be
1107 1107 added to the record list. If it does not apply cleanly, a rejects
1108 1108 file will be generated: you can use that when you try again. If
1109 1109 all lines of the hunk are removed, then the edit is aborted and
1110 1110 the hunk is left unchanged.
1111 1111 """)
1112 1112 (patchfd, patchfn) = pycompat.mkstemp(prefix="hg-editor-",
1113 1113 suffix=".diff")
1114 1114 ncpatchfp = None
1115 1115 try:
1116 1116 # Write the initial patch
1117 1117 f = util.nativeeolwriter(os.fdopen(patchfd, r'wb'))
1118 1118 chunk.header.write(f)
1119 1119 chunk.write(f)
1120 1120 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
1121 1121 f.close()
1122 1122 # Start the editor and wait for it to complete
1123 1123 editor = ui.geteditor()
1124 1124 ret = ui.system("%s \"%s\"" % (editor, patchfn),
1125 1125 environ={'HGUSER': ui.username()},
1126 1126 blockedtag='filterpatch')
1127 1127 if ret != 0:
1128 1128 ui.warn(_("editor exited with exit code %d\n") % ret)
1129 1129 continue
1130 1130 # Remove comment lines
1131 1131 patchfp = open(patchfn, r'rb')
1132 1132 ncpatchfp = stringio()
1133 1133 for line in util.iterfile(patchfp):
1134 1134 line = util.fromnativeeol(line)
1135 1135 if not line.startswith('#'):
1136 1136 ncpatchfp.write(line)
1137 1137 patchfp.close()
1138 1138 ncpatchfp.seek(0)
1139 1139 newpatches = parsepatch(ncpatchfp)
1140 1140 finally:
1141 1141 os.unlink(patchfn)
1142 1142 del ncpatchfp
1143 1143 # Signal that the chunk shouldn't be applied as-is, but
1144 1144 # provide the new patch to be used instead.
1145 1145 ret = False
1146 1146 elif r == 3: # Skip
1147 1147 ret = skipfile = False
1148 1148 elif r == 4: # file (Record remaining)
1149 1149 ret = skipfile = True
1150 1150 elif r == 5: # done, skip remaining
1151 1151 ret = skipall = False
1152 1152 elif r == 6: # all
1153 1153 ret = skipall = True
1154 1154 elif r == 7: # quit
1155 1155 raise error.Abort(_('user quit'))
1156 1156 return ret, skipfile, skipall, newpatches
1157 1157
1158 1158 seen = set()
1159 1159 applied = {} # 'filename' -> [] of chunks
1160 1160 skipfile, skipall = None, None
1161 1161 pos, total = 1, sum(len(h.hunks) for h in headers)
1162 1162 for h in headers:
1163 1163 pos += len(h.hunks)
1164 1164 skipfile = None
1165 1165 fixoffset = 0
1166 1166 hdr = ''.join(h.header)
1167 1167 if hdr in seen:
1168 1168 continue
1169 1169 seen.add(hdr)
1170 1170 if skipall is None:
1171 1171 h.pretty(ui)
1172 1172 msg = (_('examine changes to %s?') %
1173 1173 _(' and ').join("'%s'" % f for f in h.files()))
1174 1174 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1175 1175 if not r:
1176 1176 continue
1177 1177 applied[h.filename()] = [h]
1178 1178 if h.allhunks():
1179 1179 applied[h.filename()] += h.hunks
1180 1180 continue
1181 1181 for i, chunk in enumerate(h.hunks):
1182 1182 if skipfile is None and skipall is None:
1183 1183 chunk.pretty(ui)
1184 1184 if total == 1:
1185 1185 msg = messages['single'][operation] % chunk.filename()
1186 1186 else:
1187 1187 idx = pos - len(h.hunks) + i
1188 1188 msg = messages['multiple'][operation] % (idx, total,
1189 1189 chunk.filename())
1190 1190 r, skipfile, skipall, newpatches = prompt(skipfile,
1191 1191 skipall, msg, chunk)
1192 1192 if r:
1193 1193 if fixoffset:
1194 1194 chunk = copy.copy(chunk)
1195 1195 chunk.toline += fixoffset
1196 1196 applied[chunk.filename()].append(chunk)
1197 1197 elif newpatches is not None:
1198 1198 for newpatch in newpatches:
1199 1199 for newhunk in newpatch.hunks:
1200 1200 if fixoffset:
1201 1201 newhunk.toline += fixoffset
1202 1202 applied[newhunk.filename()].append(newhunk)
1203 1203 else:
1204 1204 fixoffset += chunk.removed - chunk.added
1205 1205 return (sum([h for h in applied.itervalues()
1206 1206 if h[0].special() or len(h) > 1], []), {})
1207 1207 class hunk(object):
1208 1208 def __init__(self, desc, num, lr, context):
1209 1209 self.number = num
1210 1210 self.desc = desc
1211 1211 self.hunk = [desc]
1212 1212 self.a = []
1213 1213 self.b = []
1214 1214 self.starta = self.lena = None
1215 1215 self.startb = self.lenb = None
1216 1216 if lr is not None:
1217 1217 if context:
1218 1218 self.read_context_hunk(lr)
1219 1219 else:
1220 1220 self.read_unified_hunk(lr)
1221 1221
1222 1222 def getnormalized(self):
1223 1223 """Return a copy with line endings normalized to LF."""
1224 1224
1225 1225 def normalize(lines):
1226 1226 nlines = []
1227 1227 for line in lines:
1228 1228 if line.endswith('\r\n'):
1229 1229 line = line[:-2] + '\n'
1230 1230 nlines.append(line)
1231 1231 return nlines
1232 1232
1233 1233 # Dummy object, it is rebuilt manually
1234 1234 nh = hunk(self.desc, self.number, None, None)
1235 1235 nh.number = self.number
1236 1236 nh.desc = self.desc
1237 1237 nh.hunk = self.hunk
1238 1238 nh.a = normalize(self.a)
1239 1239 nh.b = normalize(self.b)
1240 1240 nh.starta = self.starta
1241 1241 nh.startb = self.startb
1242 1242 nh.lena = self.lena
1243 1243 nh.lenb = self.lenb
1244 1244 return nh
1245 1245
1246 1246 def read_unified_hunk(self, lr):
1247 1247 m = unidesc.match(self.desc)
1248 1248 if not m:
1249 1249 raise PatchError(_("bad hunk #%d") % self.number)
1250 1250 self.starta, self.lena, self.startb, self.lenb = m.groups()
1251 1251 if self.lena is None:
1252 1252 self.lena = 1
1253 1253 else:
1254 1254 self.lena = int(self.lena)
1255 1255 if self.lenb is None:
1256 1256 self.lenb = 1
1257 1257 else:
1258 1258 self.lenb = int(self.lenb)
1259 1259 self.starta = int(self.starta)
1260 1260 self.startb = int(self.startb)
1261 1261 try:
1262 1262 diffhelper.addlines(lr, self.hunk, self.lena, self.lenb,
1263 1263 self.a, self.b)
1264 1264 except error.ParseError as e:
1265 1265 raise PatchError(_("bad hunk #%d: %s") % (self.number, e))
1266 1266 # if we hit eof before finishing out the hunk, the last line will
1267 1267 # be zero length. Lets try to fix it up.
1268 1268 while len(self.hunk[-1]) == 0:
1269 1269 del self.hunk[-1]
1270 1270 del self.a[-1]
1271 1271 del self.b[-1]
1272 1272 self.lena -= 1
1273 1273 self.lenb -= 1
1274 1274 self._fixnewline(lr)
1275 1275
1276 1276 def read_context_hunk(self, lr):
1277 1277 self.desc = lr.readline()
1278 1278 m = contextdesc.match(self.desc)
1279 1279 if not m:
1280 1280 raise PatchError(_("bad hunk #%d") % self.number)
1281 1281 self.starta, aend = m.groups()
1282 1282 self.starta = int(self.starta)
1283 1283 if aend is None:
1284 1284 aend = self.starta
1285 1285 self.lena = int(aend) - self.starta
1286 1286 if self.starta:
1287 1287 self.lena += 1
1288 1288 for x in pycompat.xrange(self.lena):
1289 1289 l = lr.readline()
1290 1290 if l.startswith('---'):
1291 1291 # lines addition, old block is empty
1292 1292 lr.push(l)
1293 1293 break
1294 1294 s = l[2:]
1295 1295 if l.startswith('- ') or l.startswith('! '):
1296 1296 u = '-' + s
1297 1297 elif l.startswith(' '):
1298 1298 u = ' ' + s
1299 1299 else:
1300 1300 raise PatchError(_("bad hunk #%d old text line %d") %
1301 1301 (self.number, x))
1302 1302 self.a.append(u)
1303 1303 self.hunk.append(u)
1304 1304
1305 1305 l = lr.readline()
1306 1306 if l.startswith(br'\ '):
1307 1307 s = self.a[-1][:-1]
1308 1308 self.a[-1] = s
1309 1309 self.hunk[-1] = s
1310 1310 l = lr.readline()
1311 1311 m = contextdesc.match(l)
1312 1312 if not m:
1313 1313 raise PatchError(_("bad hunk #%d") % self.number)
1314 1314 self.startb, bend = m.groups()
1315 1315 self.startb = int(self.startb)
1316 1316 if bend is None:
1317 1317 bend = self.startb
1318 1318 self.lenb = int(bend) - self.startb
1319 1319 if self.startb:
1320 1320 self.lenb += 1
1321 1321 hunki = 1
1322 1322 for x in pycompat.xrange(self.lenb):
1323 1323 l = lr.readline()
1324 1324 if l.startswith(br'\ '):
1325 1325 # XXX: the only way to hit this is with an invalid line range.
1326 1326 # The no-eol marker is not counted in the line range, but I
1327 1327 # guess there are diff(1) out there which behave differently.
1328 1328 s = self.b[-1][:-1]
1329 1329 self.b[-1] = s
1330 1330 self.hunk[hunki - 1] = s
1331 1331 continue
1332 1332 if not l:
1333 1333 # line deletions, new block is empty and we hit EOF
1334 1334 lr.push(l)
1335 1335 break
1336 1336 s = l[2:]
1337 1337 if l.startswith('+ ') or l.startswith('! '):
1338 1338 u = '+' + s
1339 1339 elif l.startswith(' '):
1340 1340 u = ' ' + s
1341 1341 elif len(self.b) == 0:
1342 1342 # line deletions, new block is empty
1343 1343 lr.push(l)
1344 1344 break
1345 1345 else:
1346 1346 raise PatchError(_("bad hunk #%d old text line %d") %
1347 1347 (self.number, x))
1348 1348 self.b.append(s)
1349 1349 while True:
1350 1350 if hunki >= len(self.hunk):
1351 1351 h = ""
1352 1352 else:
1353 1353 h = self.hunk[hunki]
1354 1354 hunki += 1
1355 1355 if h == u:
1356 1356 break
1357 1357 elif h.startswith('-'):
1358 1358 continue
1359 1359 else:
1360 1360 self.hunk.insert(hunki - 1, u)
1361 1361 break
1362 1362
1363 1363 if not self.a:
1364 1364 # this happens when lines were only added to the hunk
1365 1365 for x in self.hunk:
1366 1366 if x.startswith('-') or x.startswith(' '):
1367 1367 self.a.append(x)
1368 1368 if not self.b:
1369 1369 # this happens when lines were only deleted from the hunk
1370 1370 for x in self.hunk:
1371 1371 if x.startswith('+') or x.startswith(' '):
1372 1372 self.b.append(x[1:])
1373 1373 # @@ -start,len +start,len @@
1374 1374 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
1375 1375 self.startb, self.lenb)
1376 1376 self.hunk[0] = self.desc
1377 1377 self._fixnewline(lr)
1378 1378
1379 1379 def _fixnewline(self, lr):
1380 1380 l = lr.readline()
1381 1381 if l.startswith(br'\ '):
1382 1382 diffhelper.fixnewline(self.hunk, self.a, self.b)
1383 1383 else:
1384 1384 lr.push(l)
1385 1385
1386 1386 def complete(self):
1387 1387 return len(self.a) == self.lena and len(self.b) == self.lenb
1388 1388
1389 1389 def _fuzzit(self, old, new, fuzz, toponly):
1390 1390 # this removes context lines from the top and bottom of list 'l'. It
1391 1391 # checks the hunk to make sure only context lines are removed, and then
1392 1392 # returns a new shortened list of lines.
1393 1393 fuzz = min(fuzz, len(old))
1394 1394 if fuzz:
1395 1395 top = 0
1396 1396 bot = 0
1397 1397 hlen = len(self.hunk)
1398 1398 for x in pycompat.xrange(hlen - 1):
1399 1399 # the hunk starts with the @@ line, so use x+1
1400 1400 if self.hunk[x + 1].startswith(' '):
1401 1401 top += 1
1402 1402 else:
1403 1403 break
1404 1404 if not toponly:
1405 1405 for x in pycompat.xrange(hlen - 1):
1406 1406 if self.hunk[hlen - bot - 1].startswith(' '):
1407 1407 bot += 1
1408 1408 else:
1409 1409 break
1410 1410
1411 1411 bot = min(fuzz, bot)
1412 1412 top = min(fuzz, top)
1413 1413 return old[top:len(old) - bot], new[top:len(new) - bot], top
1414 1414 return old, new, 0
1415 1415
1416 1416 def fuzzit(self, fuzz, toponly):
1417 1417 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1418 1418 oldstart = self.starta + top
1419 1419 newstart = self.startb + top
1420 1420 # zero length hunk ranges already have their start decremented
1421 1421 if self.lena and oldstart > 0:
1422 1422 oldstart -= 1
1423 1423 if self.lenb and newstart > 0:
1424 1424 newstart -= 1
1425 1425 return old, oldstart, new, newstart
1426 1426
1427 1427 class binhunk(object):
1428 1428 'A binary patch file.'
1429 1429 def __init__(self, lr, fname):
1430 1430 self.text = None
1431 1431 self.delta = False
1432 1432 self.hunk = ['GIT binary patch\n']
1433 1433 self._fname = fname
1434 1434 self._read(lr)
1435 1435
1436 1436 def complete(self):
1437 1437 return self.text is not None
1438 1438
1439 1439 def new(self, lines):
1440 1440 if self.delta:
1441 1441 return [applybindelta(self.text, ''.join(lines))]
1442 1442 return [self.text]
1443 1443
1444 1444 def _read(self, lr):
1445 1445 def getline(lr, hunk):
1446 1446 l = lr.readline()
1447 1447 hunk.append(l)
1448 1448 return l.rstrip('\r\n')
1449 1449
1450 1450 while True:
1451 1451 line = getline(lr, self.hunk)
1452 1452 if not line:
1453 1453 raise PatchError(_('could not extract "%s" binary data')
1454 1454 % self._fname)
1455 1455 if line.startswith('literal '):
1456 1456 size = int(line[8:].rstrip())
1457 1457 break
1458 1458 if line.startswith('delta '):
1459 1459 size = int(line[6:].rstrip())
1460 1460 self.delta = True
1461 1461 break
1462 1462 dec = []
1463 1463 line = getline(lr, self.hunk)
1464 1464 while len(line) > 1:
1465 1465 l = line[0:1]
1466 1466 if l <= 'Z' and l >= 'A':
1467 1467 l = ord(l) - ord('A') + 1
1468 1468 else:
1469 1469 l = ord(l) - ord('a') + 27
1470 1470 try:
1471 1471 dec.append(util.b85decode(line[1:])[:l])
1472 1472 except ValueError as e:
1473 1473 raise PatchError(_('could not decode "%s" binary patch: %s')
1474 1474 % (self._fname, stringutil.forcebytestr(e)))
1475 1475 line = getline(lr, self.hunk)
1476 1476 text = zlib.decompress(''.join(dec))
1477 1477 if len(text) != size:
1478 1478 raise PatchError(_('"%s" length is %d bytes, should be %d')
1479 1479 % (self._fname, len(text), size))
1480 1480 self.text = text
1481 1481
1482 1482 def parsefilename(str):
1483 1483 # --- filename \t|space stuff
1484 1484 s = str[4:].rstrip('\r\n')
1485 1485 i = s.find('\t')
1486 1486 if i < 0:
1487 1487 i = s.find(' ')
1488 1488 if i < 0:
1489 1489 return s
1490 1490 return s[:i]
1491 1491
1492 1492 def reversehunks(hunks):
1493 1493 '''reverse the signs in the hunks given as argument
1494 1494
1495 1495 This function operates on hunks coming out of patch.filterpatch, that is
1496 1496 a list of the form: [header1, hunk1, hunk2, header2...]. Example usage:
1497 1497
1498 1498 >>> rawpatch = b"""diff --git a/folder1/g b/folder1/g
1499 1499 ... --- a/folder1/g
1500 1500 ... +++ b/folder1/g
1501 1501 ... @@ -1,7 +1,7 @@
1502 1502 ... +firstline
1503 1503 ... c
1504 1504 ... 1
1505 1505 ... 2
1506 1506 ... + 3
1507 1507 ... -4
1508 1508 ... 5
1509 1509 ... d
1510 1510 ... +lastline"""
1511 1511 >>> hunks = parsepatch([rawpatch])
1512 1512 >>> hunkscomingfromfilterpatch = []
1513 1513 >>> for h in hunks:
1514 1514 ... hunkscomingfromfilterpatch.append(h)
1515 1515 ... hunkscomingfromfilterpatch.extend(h.hunks)
1516 1516
1517 1517 >>> reversedhunks = reversehunks(hunkscomingfromfilterpatch)
1518 1518 >>> from . import util
1519 1519 >>> fp = util.stringio()
1520 1520 >>> for c in reversedhunks:
1521 1521 ... c.write(fp)
1522 1522 >>> fp.seek(0) or None
1523 1523 >>> reversedpatch = fp.read()
1524 1524 >>> print(pycompat.sysstr(reversedpatch))
1525 1525 diff --git a/folder1/g b/folder1/g
1526 1526 --- a/folder1/g
1527 1527 +++ b/folder1/g
1528 1528 @@ -1,4 +1,3 @@
1529 1529 -firstline
1530 1530 c
1531 1531 1
1532 1532 2
1533 1533 @@ -2,6 +1,6 @@
1534 1534 c
1535 1535 1
1536 1536 2
1537 1537 - 3
1538 1538 +4
1539 1539 5
1540 1540 d
1541 1541 @@ -6,3 +5,2 @@
1542 1542 5
1543 1543 d
1544 1544 -lastline
1545 1545
1546 1546 '''
1547 1547
1548 1548 newhunks = []
1549 1549 for c in hunks:
1550 1550 if util.safehasattr(c, 'reversehunk'):
1551 1551 c = c.reversehunk()
1552 1552 newhunks.append(c)
1553 1553 return newhunks
1554 1554
1555 1555 def parsepatch(originalchunks, maxcontext=None):
1556 1556 """patch -> [] of headers -> [] of hunks
1557 1557
1558 1558 If maxcontext is not None, trim context lines if necessary.
1559 1559
1560 1560 >>> rawpatch = b'''diff --git a/folder1/g b/folder1/g
1561 1561 ... --- a/folder1/g
1562 1562 ... +++ b/folder1/g
1563 1563 ... @@ -1,8 +1,10 @@
1564 1564 ... 1
1565 1565 ... 2
1566 1566 ... -3
1567 1567 ... 4
1568 1568 ... 5
1569 1569 ... 6
1570 1570 ... +6.1
1571 1571 ... +6.2
1572 1572 ... 7
1573 1573 ... 8
1574 1574 ... +9'''
1575 1575 >>> out = util.stringio()
1576 1576 >>> headers = parsepatch([rawpatch], maxcontext=1)
1577 1577 >>> for header in headers:
1578 1578 ... header.write(out)
1579 1579 ... for hunk in header.hunks:
1580 1580 ... hunk.write(out)
1581 1581 >>> print(pycompat.sysstr(out.getvalue()))
1582 1582 diff --git a/folder1/g b/folder1/g
1583 1583 --- a/folder1/g
1584 1584 +++ b/folder1/g
1585 1585 @@ -2,3 +2,2 @@
1586 1586 2
1587 1587 -3
1588 1588 4
1589 1589 @@ -6,2 +5,4 @@
1590 1590 6
1591 1591 +6.1
1592 1592 +6.2
1593 1593 7
1594 1594 @@ -8,1 +9,2 @@
1595 1595 8
1596 1596 +9
1597 1597 """
1598 1598 class parser(object):
1599 1599 """patch parsing state machine"""
1600 1600 def __init__(self):
1601 1601 self.fromline = 0
1602 1602 self.toline = 0
1603 1603 self.proc = ''
1604 1604 self.header = None
1605 1605 self.context = []
1606 1606 self.before = []
1607 1607 self.hunk = []
1608 1608 self.headers = []
1609 1609
1610 1610 def addrange(self, limits):
1611 1611 self.addcontext([])
1612 1612 fromstart, fromend, tostart, toend, proc = limits
1613 1613 self.fromline = int(fromstart)
1614 1614 self.toline = int(tostart)
1615 1615 self.proc = proc
1616 1616
1617 1617 def addcontext(self, context):
1618 1618 if self.hunk:
1619 1619 h = recordhunk(self.header, self.fromline, self.toline,
1620 1620 self.proc, self.before, self.hunk, context, maxcontext)
1621 1621 self.header.hunks.append(h)
1622 1622 self.fromline += len(self.before) + h.removed
1623 1623 self.toline += len(self.before) + h.added
1624 1624 self.before = []
1625 1625 self.hunk = []
1626 1626 self.context = context
1627 1627
1628 1628 def addhunk(self, hunk):
1629 1629 if self.context:
1630 1630 self.before = self.context
1631 1631 self.context = []
1632 1632 if self.hunk:
1633 1633 self.addcontext([])
1634 1634 self.hunk = hunk
1635 1635
1636 1636 def newfile(self, hdr):
1637 1637 self.addcontext([])
1638 1638 h = header(hdr)
1639 1639 self.headers.append(h)
1640 1640 self.header = h
1641 1641
1642 1642 def addother(self, line):
1643 1643 pass # 'other' lines are ignored
1644 1644
1645 1645 def finished(self):
1646 1646 self.addcontext([])
1647 1647 return self.headers
1648 1648
1649 1649 transitions = {
1650 1650 'file': {'context': addcontext,
1651 1651 'file': newfile,
1652 1652 'hunk': addhunk,
1653 1653 'range': addrange},
1654 1654 'context': {'file': newfile,
1655 1655 'hunk': addhunk,
1656 1656 'range': addrange,
1657 1657 'other': addother},
1658 1658 'hunk': {'context': addcontext,
1659 1659 'file': newfile,
1660 1660 'range': addrange},
1661 1661 'range': {'context': addcontext,
1662 1662 'hunk': addhunk},
1663 1663 'other': {'other': addother},
1664 1664 }
1665 1665
1666 1666 p = parser()
1667 1667 fp = stringio()
1668 1668 fp.write(''.join(originalchunks))
1669 1669 fp.seek(0)
1670 1670
1671 1671 state = 'context'
1672 1672 for newstate, data in scanpatch(fp):
1673 1673 try:
1674 1674 p.transitions[state][newstate](p, data)
1675 1675 except KeyError:
1676 1676 raise PatchError('unhandled transition: %s -> %s' %
1677 1677 (state, newstate))
1678 1678 state = newstate
1679 1679 del fp
1680 1680 return p.finished()
1681 1681
1682 1682 def pathtransform(path, strip, prefix):
1683 1683 '''turn a path from a patch into a path suitable for the repository
1684 1684
1685 1685 prefix, if not empty, is expected to be normalized with a / at the end.
1686 1686
1687 1687 Returns (stripped components, path in repository).
1688 1688
1689 1689 >>> pathtransform(b'a/b/c', 0, b'')
1690 1690 ('', 'a/b/c')
1691 1691 >>> pathtransform(b' a/b/c ', 0, b'')
1692 1692 ('', ' a/b/c')
1693 1693 >>> pathtransform(b' a/b/c ', 2, b'')
1694 1694 ('a/b/', 'c')
1695 1695 >>> pathtransform(b'a/b/c', 0, b'd/e/')
1696 1696 ('', 'd/e/a/b/c')
1697 1697 >>> pathtransform(b' a//b/c ', 2, b'd/e/')
1698 1698 ('a//b/', 'd/e/c')
1699 1699 >>> pathtransform(b'a/b/c', 3, b'')
1700 1700 Traceback (most recent call last):
1701 1701 PatchError: unable to strip away 1 of 3 dirs from a/b/c
1702 1702 '''
1703 1703 pathlen = len(path)
1704 1704 i = 0
1705 1705 if strip == 0:
1706 1706 return '', prefix + path.rstrip()
1707 1707 count = strip
1708 1708 while count > 0:
1709 1709 i = path.find('/', i)
1710 1710 if i == -1:
1711 1711 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1712 1712 (count, strip, path))
1713 1713 i += 1
1714 1714 # consume '//' in the path
1715 1715 while i < pathlen - 1 and path[i:i + 1] == '/':
1716 1716 i += 1
1717 1717 count -= 1
1718 1718 return path[:i].lstrip(), prefix + path[i:].rstrip()
1719 1719
1720 1720 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1721 1721 nulla = afile_orig == "/dev/null"
1722 1722 nullb = bfile_orig == "/dev/null"
1723 1723 create = nulla and hunk.starta == 0 and hunk.lena == 0
1724 1724 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1725 1725 abase, afile = pathtransform(afile_orig, strip, prefix)
1726 1726 gooda = not nulla and backend.exists(afile)
1727 1727 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1728 1728 if afile == bfile:
1729 1729 goodb = gooda
1730 1730 else:
1731 1731 goodb = not nullb and backend.exists(bfile)
1732 1732 missing = not goodb and not gooda and not create
1733 1733
1734 1734 # some diff programs apparently produce patches where the afile is
1735 1735 # not /dev/null, but afile starts with bfile
1736 1736 abasedir = afile[:afile.rfind('/') + 1]
1737 1737 bbasedir = bfile[:bfile.rfind('/') + 1]
1738 1738 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1739 1739 and hunk.starta == 0 and hunk.lena == 0):
1740 1740 create = True
1741 1741 missing = False
1742 1742
1743 1743 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1744 1744 # diff is between a file and its backup. In this case, the original
1745 1745 # file should be patched (see original mpatch code).
1746 1746 isbackup = (abase == bbase and bfile.startswith(afile))
1747 1747 fname = None
1748 1748 if not missing:
1749 1749 if gooda and goodb:
1750 1750 if isbackup:
1751 1751 fname = afile
1752 1752 else:
1753 1753 fname = bfile
1754 1754 elif gooda:
1755 1755 fname = afile
1756 1756
1757 1757 if not fname:
1758 1758 if not nullb:
1759 1759 if isbackup:
1760 1760 fname = afile
1761 1761 else:
1762 1762 fname = bfile
1763 1763 elif not nulla:
1764 1764 fname = afile
1765 1765 else:
1766 1766 raise PatchError(_("undefined source and destination files"))
1767 1767
1768 1768 gp = patchmeta(fname)
1769 1769 if create:
1770 1770 gp.op = 'ADD'
1771 1771 elif remove:
1772 1772 gp.op = 'DELETE'
1773 1773 return gp
1774 1774
1775 1775 def scanpatch(fp):
1776 1776 """like patch.iterhunks, but yield different events
1777 1777
1778 1778 - ('file', [header_lines + fromfile + tofile])
1779 1779 - ('context', [context_lines])
1780 1780 - ('hunk', [hunk_lines])
1781 1781 - ('range', (-start,len, +start,len, proc))
1782 1782 """
1783 1783 lines_re = re.compile(br'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1784 1784 lr = linereader(fp)
1785 1785
1786 1786 def scanwhile(first, p):
1787 1787 """scan lr while predicate holds"""
1788 1788 lines = [first]
1789 1789 for line in iter(lr.readline, ''):
1790 1790 if p(line):
1791 1791 lines.append(line)
1792 1792 else:
1793 1793 lr.push(line)
1794 1794 break
1795 1795 return lines
1796 1796
1797 1797 for line in iter(lr.readline, ''):
1798 1798 if line.startswith('diff --git a/') or line.startswith('diff -r '):
1799 1799 def notheader(line):
1800 1800 s = line.split(None, 1)
1801 1801 return not s or s[0] not in ('---', 'diff')
1802 1802 header = scanwhile(line, notheader)
1803 1803 fromfile = lr.readline()
1804 1804 if fromfile.startswith('---'):
1805 1805 tofile = lr.readline()
1806 1806 header += [fromfile, tofile]
1807 1807 else:
1808 1808 lr.push(fromfile)
1809 1809 yield 'file', header
1810 1810 elif line.startswith(' '):
1811 1811 cs = (' ', '\\')
1812 1812 yield 'context', scanwhile(line, lambda l: l.startswith(cs))
1813 1813 elif line.startswith(('-', '+')):
1814 1814 cs = ('-', '+', '\\')
1815 1815 yield 'hunk', scanwhile(line, lambda l: l.startswith(cs))
1816 1816 else:
1817 1817 m = lines_re.match(line)
1818 1818 if m:
1819 1819 yield 'range', m.groups()
1820 1820 else:
1821 1821 yield 'other', line
1822 1822
1823 1823 def scangitpatch(lr, firstline):
1824 1824 """
1825 1825 Git patches can emit:
1826 1826 - rename a to b
1827 1827 - change b
1828 1828 - copy a to c
1829 1829 - change c
1830 1830
1831 1831 We cannot apply this sequence as-is, the renamed 'a' could not be
1832 1832 found for it would have been renamed already. And we cannot copy
1833 1833 from 'b' instead because 'b' would have been changed already. So
1834 1834 we scan the git patch for copy and rename commands so we can
1835 1835 perform the copies ahead of time.
1836 1836 """
1837 1837 pos = 0
1838 1838 try:
1839 1839 pos = lr.fp.tell()
1840 1840 fp = lr.fp
1841 1841 except IOError:
1842 1842 fp = stringio(lr.fp.read())
1843 1843 gitlr = linereader(fp)
1844 1844 gitlr.push(firstline)
1845 1845 gitpatches = readgitpatch(gitlr)
1846 1846 fp.seek(pos)
1847 1847 return gitpatches
1848 1848
1849 1849 def iterhunks(fp):
1850 1850 """Read a patch and yield the following events:
1851 1851 - ("file", afile, bfile, firsthunk): select a new target file.
1852 1852 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1853 1853 "file" event.
1854 1854 - ("git", gitchanges): current diff is in git format, gitchanges
1855 1855 maps filenames to gitpatch records. Unique event.
1856 1856 """
1857 1857 afile = ""
1858 1858 bfile = ""
1859 1859 state = None
1860 1860 hunknum = 0
1861 1861 emitfile = newfile = False
1862 1862 gitpatches = None
1863 1863
1864 1864 # our states
1865 1865 BFILE = 1
1866 1866 context = None
1867 1867 lr = linereader(fp)
1868 1868
1869 1869 for x in iter(lr.readline, ''):
1870 1870 if state == BFILE and (
1871 1871 (not context and x.startswith('@'))
1872 1872 or (context is not False and x.startswith('***************'))
1873 1873 or x.startswith('GIT binary patch')):
1874 1874 gp = None
1875 1875 if (gitpatches and
1876 1876 gitpatches[-1].ispatching(afile, bfile)):
1877 1877 gp = gitpatches.pop()
1878 1878 if x.startswith('GIT binary patch'):
1879 1879 h = binhunk(lr, gp.path)
1880 1880 else:
1881 1881 if context is None and x.startswith('***************'):
1882 1882 context = True
1883 1883 h = hunk(x, hunknum + 1, lr, context)
1884 1884 hunknum += 1
1885 1885 if emitfile:
1886 1886 emitfile = False
1887 1887 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1888 1888 yield 'hunk', h
1889 1889 elif x.startswith('diff --git a/'):
1890 1890 m = gitre.match(x.rstrip(' \r\n'))
1891 1891 if not m:
1892 1892 continue
1893 1893 if gitpatches is None:
1894 1894 # scan whole input for git metadata
1895 1895 gitpatches = scangitpatch(lr, x)
1896 1896 yield 'git', [g.copy() for g in gitpatches
1897 1897 if g.op in ('COPY', 'RENAME')]
1898 1898 gitpatches.reverse()
1899 1899 afile = 'a/' + m.group(1)
1900 1900 bfile = 'b/' + m.group(2)
1901 1901 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1902 1902 gp = gitpatches.pop()
1903 1903 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1904 1904 if not gitpatches:
1905 1905 raise PatchError(_('failed to synchronize metadata for "%s"')
1906 1906 % afile[2:])
1907 1907 newfile = True
1908 1908 elif x.startswith('---'):
1909 1909 # check for a unified diff
1910 1910 l2 = lr.readline()
1911 1911 if not l2.startswith('+++'):
1912 1912 lr.push(l2)
1913 1913 continue
1914 1914 newfile = True
1915 1915 context = False
1916 1916 afile = parsefilename(x)
1917 1917 bfile = parsefilename(l2)
1918 1918 elif x.startswith('***'):
1919 1919 # check for a context diff
1920 1920 l2 = lr.readline()
1921 1921 if not l2.startswith('---'):
1922 1922 lr.push(l2)
1923 1923 continue
1924 1924 l3 = lr.readline()
1925 1925 lr.push(l3)
1926 1926 if not l3.startswith("***************"):
1927 1927 lr.push(l2)
1928 1928 continue
1929 1929 newfile = True
1930 1930 context = True
1931 1931 afile = parsefilename(x)
1932 1932 bfile = parsefilename(l2)
1933 1933
1934 1934 if newfile:
1935 1935 newfile = False
1936 1936 emitfile = True
1937 1937 state = BFILE
1938 1938 hunknum = 0
1939 1939
1940 1940 while gitpatches:
1941 1941 gp = gitpatches.pop()
1942 1942 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1943 1943
1944 1944 def applybindelta(binchunk, data):
1945 1945 """Apply a binary delta hunk
1946 1946 The algorithm used is the algorithm from git's patch-delta.c
1947 1947 """
1948 1948 def deltahead(binchunk):
1949 1949 i = 0
1950 1950 for c in pycompat.bytestr(binchunk):
1951 1951 i += 1
1952 1952 if not (ord(c) & 0x80):
1953 1953 return i
1954 1954 return i
1955 1955 out = ""
1956 1956 s = deltahead(binchunk)
1957 1957 binchunk = binchunk[s:]
1958 1958 s = deltahead(binchunk)
1959 1959 binchunk = binchunk[s:]
1960 1960 i = 0
1961 1961 while i < len(binchunk):
1962 1962 cmd = ord(binchunk[i:i + 1])
1963 1963 i += 1
1964 1964 if (cmd & 0x80):
1965 1965 offset = 0
1966 1966 size = 0
1967 1967 if (cmd & 0x01):
1968 1968 offset = ord(binchunk[i:i + 1])
1969 1969 i += 1
1970 1970 if (cmd & 0x02):
1971 1971 offset |= ord(binchunk[i:i + 1]) << 8
1972 1972 i += 1
1973 1973 if (cmd & 0x04):
1974 1974 offset |= ord(binchunk[i:i + 1]) << 16
1975 1975 i += 1
1976 1976 if (cmd & 0x08):
1977 1977 offset |= ord(binchunk[i:i + 1]) << 24
1978 1978 i += 1
1979 1979 if (cmd & 0x10):
1980 1980 size = ord(binchunk[i:i + 1])
1981 1981 i += 1
1982 1982 if (cmd & 0x20):
1983 1983 size |= ord(binchunk[i:i + 1]) << 8
1984 1984 i += 1
1985 1985 if (cmd & 0x40):
1986 1986 size |= ord(binchunk[i:i + 1]) << 16
1987 1987 i += 1
1988 1988 if size == 0:
1989 1989 size = 0x10000
1990 1990 offset_end = offset + size
1991 1991 out += data[offset:offset_end]
1992 1992 elif cmd != 0:
1993 1993 offset_end = i + cmd
1994 1994 out += binchunk[i:offset_end]
1995 1995 i += cmd
1996 1996 else:
1997 1997 raise PatchError(_('unexpected delta opcode 0'))
1998 1998 return out
1999 1999
2000 2000 def applydiff(ui, fp, backend, store, strip=1, prefix='', eolmode='strict'):
2001 2001 """Reads a patch from fp and tries to apply it.
2002 2002
2003 2003 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
2004 2004 there was any fuzz.
2005 2005
2006 2006 If 'eolmode' is 'strict', the patch content and patched file are
2007 2007 read in binary mode. Otherwise, line endings are ignored when
2008 2008 patching then normalized according to 'eolmode'.
2009 2009 """
2010 2010 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
2011 2011 prefix=prefix, eolmode=eolmode)
2012 2012
2013 2013 def _canonprefix(repo, prefix):
2014 2014 if prefix:
2015 2015 prefix = pathutil.canonpath(repo.root, repo.getcwd(), prefix)
2016 2016 if prefix != '':
2017 2017 prefix += '/'
2018 2018 return prefix
2019 2019
2020 2020 def _applydiff(ui, fp, patcher, backend, store, strip=1, prefix='',
2021 2021 eolmode='strict'):
2022 2022 prefix = _canonprefix(backend.repo, prefix)
2023 2023 def pstrip(p):
2024 2024 return pathtransform(p, strip - 1, prefix)[1]
2025 2025
2026 2026 rejects = 0
2027 2027 err = 0
2028 2028 current_file = None
2029 2029
2030 2030 for state, values in iterhunks(fp):
2031 2031 if state == 'hunk':
2032 2032 if not current_file:
2033 2033 continue
2034 2034 ret = current_file.apply(values)
2035 2035 if ret > 0:
2036 2036 err = 1
2037 2037 elif state == 'file':
2038 2038 if current_file:
2039 2039 rejects += current_file.close()
2040 2040 current_file = None
2041 2041 afile, bfile, first_hunk, gp = values
2042 2042 if gp:
2043 2043 gp.path = pstrip(gp.path)
2044 2044 if gp.oldpath:
2045 2045 gp.oldpath = pstrip(gp.oldpath)
2046 2046 else:
2047 2047 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
2048 2048 prefix)
2049 2049 if gp.op == 'RENAME':
2050 2050 backend.unlink(gp.oldpath)
2051 2051 if not first_hunk:
2052 2052 if gp.op == 'DELETE':
2053 2053 backend.unlink(gp.path)
2054 2054 continue
2055 2055 data, mode = None, None
2056 2056 if gp.op in ('RENAME', 'COPY'):
2057 2057 data, mode = store.getfile(gp.oldpath)[:2]
2058 2058 if data is None:
2059 2059 # This means that the old path does not exist
2060 2060 raise PatchError(_("source file '%s' does not exist")
2061 2061 % gp.oldpath)
2062 2062 if gp.mode:
2063 2063 mode = gp.mode
2064 2064 if gp.op == 'ADD':
2065 2065 # Added files without content have no hunk and
2066 2066 # must be created
2067 2067 data = ''
2068 2068 if data or mode:
2069 2069 if (gp.op in ('ADD', 'RENAME', 'COPY')
2070 2070 and backend.exists(gp.path)):
2071 2071 raise PatchError(_("cannot create %s: destination "
2072 2072 "already exists") % gp.path)
2073 2073 backend.setfile(gp.path, data, mode, gp.oldpath)
2074 2074 continue
2075 2075 try:
2076 2076 current_file = patcher(ui, gp, backend, store,
2077 2077 eolmode=eolmode)
2078 2078 except PatchError as inst:
2079 2079 ui.warn(str(inst) + '\n')
2080 2080 current_file = None
2081 2081 rejects += 1
2082 2082 continue
2083 2083 elif state == 'git':
2084 2084 for gp in values:
2085 2085 path = pstrip(gp.oldpath)
2086 2086 data, mode = backend.getfile(path)
2087 2087 if data is None:
2088 2088 # The error ignored here will trigger a getfile()
2089 2089 # error in a place more appropriate for error
2090 2090 # handling, and will not interrupt the patching
2091 2091 # process.
2092 2092 pass
2093 2093 else:
2094 2094 store.setfile(path, data, mode)
2095 2095 else:
2096 2096 raise error.Abort(_('unsupported parser state: %s') % state)
2097 2097
2098 2098 if current_file:
2099 2099 rejects += current_file.close()
2100 2100
2101 2101 if rejects:
2102 2102 return -1
2103 2103 return err
2104 2104
2105 2105 def _externalpatch(ui, repo, patcher, patchname, strip, files,
2106 2106 similarity):
2107 2107 """use <patcher> to apply <patchname> to the working directory.
2108 2108 returns whether patch was applied with fuzz factor."""
2109 2109
2110 2110 fuzz = False
2111 2111 args = []
2112 2112 cwd = repo.root
2113 2113 if cwd:
2114 2114 args.append('-d %s' % procutil.shellquote(cwd))
2115 2115 cmd = ('%s %s -p%d < %s'
2116 2116 % (patcher, ' '.join(args), strip, procutil.shellquote(patchname)))
2117 2117 ui.debug('Using external patch tool: %s\n' % cmd)
2118 2118 fp = procutil.popen(cmd, 'rb')
2119 2119 try:
2120 2120 for line in util.iterfile(fp):
2121 2121 line = line.rstrip()
2122 2122 ui.note(line + '\n')
2123 2123 if line.startswith('patching file '):
2124 2124 pf = util.parsepatchoutput(line)
2125 2125 printed_file = False
2126 2126 files.add(pf)
2127 2127 elif line.find('with fuzz') >= 0:
2128 2128 fuzz = True
2129 2129 if not printed_file:
2130 2130 ui.warn(pf + '\n')
2131 2131 printed_file = True
2132 2132 ui.warn(line + '\n')
2133 2133 elif line.find('saving rejects to file') >= 0:
2134 2134 ui.warn(line + '\n')
2135 2135 elif line.find('FAILED') >= 0:
2136 2136 if not printed_file:
2137 2137 ui.warn(pf + '\n')
2138 2138 printed_file = True
2139 2139 ui.warn(line + '\n')
2140 2140 finally:
2141 2141 if files:
2142 2142 scmutil.marktouched(repo, files, similarity)
2143 2143 code = fp.close()
2144 2144 if code:
2145 2145 raise PatchError(_("patch command failed: %s") %
2146 2146 procutil.explainexit(code))
2147 2147 return fuzz
2148 2148
2149 2149 def patchbackend(ui, backend, patchobj, strip, prefix, files=None,
2150 2150 eolmode='strict'):
2151 2151 if files is None:
2152 2152 files = set()
2153 2153 if eolmode is None:
2154 2154 eolmode = ui.config('patch', 'eol')
2155 2155 if eolmode.lower() not in eolmodes:
2156 2156 raise error.Abort(_('unsupported line endings type: %s') % eolmode)
2157 2157 eolmode = eolmode.lower()
2158 2158
2159 2159 store = filestore()
2160 2160 try:
2161 2161 fp = open(patchobj, 'rb')
2162 2162 except TypeError:
2163 2163 fp = patchobj
2164 2164 try:
2165 2165 ret = applydiff(ui, fp, backend, store, strip=strip, prefix=prefix,
2166 2166 eolmode=eolmode)
2167 2167 finally:
2168 2168 if fp != patchobj:
2169 2169 fp.close()
2170 2170 files.update(backend.close())
2171 2171 store.close()
2172 2172 if ret < 0:
2173 2173 raise PatchError(_('patch failed to apply'))
2174 2174 return ret > 0
2175 2175
2176 2176 def internalpatch(ui, repo, patchobj, strip, prefix='', files=None,
2177 2177 eolmode='strict', similarity=0):
2178 2178 """use builtin patch to apply <patchobj> to the working directory.
2179 2179 returns whether patch was applied with fuzz factor."""
2180 2180 backend = workingbackend(ui, repo, similarity)
2181 2181 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2182 2182
2183 2183 def patchrepo(ui, repo, ctx, store, patchobj, strip, prefix, files=None,
2184 2184 eolmode='strict'):
2185 2185 backend = repobackend(ui, repo, ctx, store)
2186 2186 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2187 2187
2188 2188 def patch(ui, repo, patchname, strip=1, prefix='', files=None, eolmode='strict',
2189 2189 similarity=0):
2190 2190 """Apply <patchname> to the working directory.
2191 2191
2192 2192 'eolmode' specifies how end of lines should be handled. It can be:
2193 2193 - 'strict': inputs are read in binary mode, EOLs are preserved
2194 2194 - 'crlf': EOLs are ignored when patching and reset to CRLF
2195 2195 - 'lf': EOLs are ignored when patching and reset to LF
2196 2196 - None: get it from user settings, default to 'strict'
2197 2197 'eolmode' is ignored when using an external patcher program.
2198 2198
2199 2199 Returns whether patch was applied with fuzz factor.
2200 2200 """
2201 2201 patcher = ui.config('ui', 'patch')
2202 2202 if files is None:
2203 2203 files = set()
2204 2204 if patcher:
2205 2205 return _externalpatch(ui, repo, patcher, patchname, strip,
2206 2206 files, similarity)
2207 2207 return internalpatch(ui, repo, patchname, strip, prefix, files, eolmode,
2208 2208 similarity)
2209 2209
2210 2210 def changedfiles(ui, repo, patchpath, strip=1, prefix=''):
2211 2211 backend = fsbackend(ui, repo.root)
2212 2212 prefix = _canonprefix(repo, prefix)
2213 2213 with open(patchpath, 'rb') as fp:
2214 2214 changed = set()
2215 2215 for state, values in iterhunks(fp):
2216 2216 if state == 'file':
2217 2217 afile, bfile, first_hunk, gp = values
2218 2218 if gp:
2219 2219 gp.path = pathtransform(gp.path, strip - 1, prefix)[1]
2220 2220 if gp.oldpath:
2221 2221 gp.oldpath = pathtransform(gp.oldpath, strip - 1,
2222 2222 prefix)[1]
2223 2223 else:
2224 2224 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
2225 2225 prefix)
2226 2226 changed.add(gp.path)
2227 2227 if gp.op == 'RENAME':
2228 2228 changed.add(gp.oldpath)
2229 2229 elif state not in ('hunk', 'git'):
2230 2230 raise error.Abort(_('unsupported parser state: %s') % state)
2231 2231 return changed
2232 2232
2233 2233 class GitDiffRequired(Exception):
2234 2234 pass
2235 2235
2236 2236 diffopts = diffutil.diffallopts
2237 2237 diffallopts = diffutil.diffallopts
2238 2238 difffeatureopts = diffutil.difffeatureopts
2239 2239
2240 2240 def diff(repo, node1=None, node2=None, match=None, changes=None,
2241 2241 opts=None, losedatafn=None, pathfn=None, copy=None,
2242 2242 copysourcematch=None, hunksfilterfn=None):
2243 2243 '''yields diff of changes to files between two nodes, or node and
2244 2244 working directory.
2245 2245
2246 2246 if node1 is None, use first dirstate parent instead.
2247 2247 if node2 is None, compare node1 with working directory.
2248 2248
2249 2249 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2250 2250 every time some change cannot be represented with the current
2251 2251 patch format. Return False to upgrade to git patch format, True to
2252 2252 accept the loss or raise an exception to abort the diff. It is
2253 2253 called with the name of current file being diffed as 'fn'. If set
2254 2254 to None, patches will always be upgraded to git format when
2255 2255 necessary.
2256 2256
2257 2257 prefix is a filename prefix that is prepended to all filenames on
2258 2258 display (used for subrepos).
2259 2259
2260 2260 relroot, if not empty, must be normalized with a trailing /. Any match
2261 2261 patterns that fall outside it will be ignored.
2262 2262
2263 2263 copy, if not empty, should contain mappings {dst@y: src@x} of copy
2264 2264 information.
2265 2265
2266 2266 if copysourcematch is not None, then copy sources will be filtered by this
2267 2267 matcher
2268 2268
2269 2269 hunksfilterfn, if not None, should be a function taking a filectx and
2270 2270 hunks generator that may yield filtered hunks.
2271 2271 '''
2272 2272 if not node1 and not node2:
2273 2273 node1 = repo.dirstate.p1()
2274 2274
2275 2275 ctx1 = repo[node1]
2276 2276 ctx2 = repo[node2]
2277 2277
2278 2278 for fctx1, fctx2, hdr, hunks in diffhunks(
2279 2279 repo, ctx1=ctx1, ctx2=ctx2, match=match, changes=changes, opts=opts,
2280 2280 losedatafn=losedatafn, pathfn=pathfn, copy=copy,
2281 2281 copysourcematch=copysourcematch):
2282 2282 if hunksfilterfn is not None:
2283 2283 # If the file has been removed, fctx2 is None; but this should
2284 2284 # not occur here since we catch removed files early in
2285 2285 # logcmdutil.getlinerangerevs() for 'hg log -L'.
2286 2286 assert fctx2 is not None, (
2287 2287 'fctx2 unexpectly None in diff hunks filtering')
2288 2288 hunks = hunksfilterfn(fctx2, hunks)
2289 2289 text = ''.join(sum((list(hlines) for hrange, hlines in hunks), []))
2290 2290 if hdr and (text or len(hdr) > 1):
2291 2291 yield '\n'.join(hdr) + '\n'
2292 2292 if text:
2293 2293 yield text
2294 2294
2295 2295 def diffhunks(repo, ctx1, ctx2, match=None, changes=None, opts=None,
2296 2296 losedatafn=None, pathfn=None, copy=None, copysourcematch=None):
2297 2297 """Yield diff of changes to files in the form of (`header`, `hunks`) tuples
2298 2298 where `header` is a list of diff headers and `hunks` is an iterable of
2299 2299 (`hunkrange`, `hunklines`) tuples.
2300 2300
2301 2301 See diff() for the meaning of parameters.
2302 2302 """
2303 2303
2304 2304 if opts is None:
2305 2305 opts = mdiff.defaultopts
2306 2306
2307 2307 def lrugetfilectx():
2308 2308 cache = {}
2309 2309 order = collections.deque()
2310 2310 def getfilectx(f, ctx):
2311 2311 fctx = ctx.filectx(f, filelog=cache.get(f))
2312 2312 if f not in cache:
2313 2313 if len(cache) > 20:
2314 2314 del cache[order.popleft()]
2315 2315 cache[f] = fctx.filelog()
2316 2316 else:
2317 2317 order.remove(f)
2318 2318 order.append(f)
2319 2319 return fctx
2320 2320 return getfilectx
2321 2321 getfilectx = lrugetfilectx()
2322 2322
2323 2323 if not changes:
2324 2324 changes = ctx1.status(ctx2, match=match)
2325 2325 modified, added, removed = changes[:3]
2326 2326
2327 2327 if not modified and not added and not removed:
2328 2328 return []
2329 2329
2330 2330 if repo.ui.debugflag:
2331 2331 hexfunc = hex
2332 2332 else:
2333 2333 hexfunc = short
2334 2334 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2335 2335
2336 2336 if copy is None:
2337 2337 copy = {}
2338 2338 if opts.git or opts.upgrade:
2339 2339 copy = copies.pathcopies(ctx1, ctx2, match=match)
2340 2340
2341 2341 if copysourcematch:
2342 2342 # filter out copies where source side isn't inside the matcher
2343 2343 # (copies.pathcopies() already filtered out the destination)
2344 2344 copy = {dst: src for dst, src in copy.iteritems()
2345 2345 if copysourcematch(src)}
2346 2346
2347 2347 modifiedset = set(modified)
2348 2348 addedset = set(added)
2349 2349 removedset = set(removed)
2350 2350 for f in modified:
2351 2351 if f not in ctx1:
2352 2352 # Fix up added, since merged-in additions appear as
2353 2353 # modifications during merges
2354 2354 modifiedset.remove(f)
2355 2355 addedset.add(f)
2356 2356 for f in removed:
2357 2357 if f not in ctx1:
2358 2358 # Merged-in additions that are then removed are reported as removed.
2359 2359 # They are not in ctx1, so We don't want to show them in the diff.
2360 2360 removedset.remove(f)
2361 2361 modified = sorted(modifiedset)
2362 2362 added = sorted(addedset)
2363 2363 removed = sorted(removedset)
2364 2364 for dst, src in list(copy.items()):
2365 2365 if src not in ctx1:
2366 2366 # Files merged in during a merge and then copied/renamed are
2367 2367 # reported as copies. We want to show them in the diff as additions.
2368 2368 del copy[dst]
2369 2369
2370 2370 prefetchmatch = scmutil.matchfiles(
2371 2371 repo, list(modifiedset | addedset | removedset))
2372 2372 scmutil.prefetchfiles(repo, [ctx1.rev(), ctx2.rev()], prefetchmatch)
2373 2373
2374 2374 def difffn(opts, losedata):
2375 2375 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2376 2376 copy, getfilectx, opts, losedata, pathfn)
2377 2377 if opts.upgrade and not opts.git:
2378 2378 try:
2379 2379 def losedata(fn):
2380 2380 if not losedatafn or not losedatafn(fn=fn):
2381 2381 raise GitDiffRequired
2382 2382 # Buffer the whole output until we are sure it can be generated
2383 2383 return list(difffn(opts.copy(git=False), losedata))
2384 2384 except GitDiffRequired:
2385 2385 return difffn(opts.copy(git=True), None)
2386 2386 else:
2387 2387 return difffn(opts, None)
2388 2388
2389 2389 def diffsinglehunk(hunklines):
2390 2390 """yield tokens for a list of lines in a single hunk"""
2391 2391 for line in hunklines:
2392 2392 # chomp
2393 2393 chompline = line.rstrip('\r\n')
2394 2394 # highlight tabs and trailing whitespace
2395 2395 stripline = chompline.rstrip()
2396 2396 if line.startswith('-'):
2397 2397 label = 'diff.deleted'
2398 2398 elif line.startswith('+'):
2399 2399 label = 'diff.inserted'
2400 2400 else:
2401 2401 raise error.ProgrammingError('unexpected hunk line: %s' % line)
2402 2402 for token in tabsplitter.findall(stripline):
2403 2403 if token.startswith('\t'):
2404 2404 yield (token, 'diff.tab')
2405 2405 else:
2406 2406 yield (token, label)
2407 2407
2408 2408 if chompline != stripline:
2409 2409 yield (chompline[len(stripline):], 'diff.trailingwhitespace')
2410 2410 if chompline != line:
2411 2411 yield (line[len(chompline):], '')
2412 2412
2413 2413 def diffsinglehunkinline(hunklines):
2414 2414 """yield tokens for a list of lines in a single hunk, with inline colors"""
2415 2415 # prepare deleted, and inserted content
2416 2416 a = ''
2417 2417 b = ''
2418 2418 for line in hunklines:
2419 2419 if line[0:1] == '-':
2420 2420 a += line[1:]
2421 2421 elif line[0:1] == '+':
2422 2422 b += line[1:]
2423 2423 else:
2424 2424 raise error.ProgrammingError('unexpected hunk line: %s' % line)
2425 2425 # fast path: if either side is empty, use diffsinglehunk
2426 2426 if not a or not b:
2427 2427 for t in diffsinglehunk(hunklines):
2428 2428 yield t
2429 2429 return
2430 2430 # re-split the content into words
2431 2431 al = wordsplitter.findall(a)
2432 2432 bl = wordsplitter.findall(b)
2433 2433 # re-arrange the words to lines since the diff algorithm is line-based
2434 2434 aln = [s if s == '\n' else s + '\n' for s in al]
2435 2435 bln = [s if s == '\n' else s + '\n' for s in bl]
2436 2436 an = ''.join(aln)
2437 2437 bn = ''.join(bln)
2438 2438 # run the diff algorithm, prepare atokens and btokens
2439 2439 atokens = []
2440 2440 btokens = []
2441 2441 blocks = mdiff.allblocks(an, bn, lines1=aln, lines2=bln)
2442 2442 for (a1, a2, b1, b2), btype in blocks:
2443 2443 changed = btype == '!'
2444 2444 for token in mdiff.splitnewlines(''.join(al[a1:a2])):
2445 2445 atokens.append((changed, token))
2446 2446 for token in mdiff.splitnewlines(''.join(bl[b1:b2])):
2447 2447 btokens.append((changed, token))
2448 2448
2449 2449 # yield deleted tokens, then inserted ones
2450 2450 for prefix, label, tokens in [('-', 'diff.deleted', atokens),
2451 2451 ('+', 'diff.inserted', btokens)]:
2452 2452 nextisnewline = True
2453 2453 for changed, token in tokens:
2454 2454 if nextisnewline:
2455 2455 yield (prefix, label)
2456 2456 nextisnewline = False
2457 2457 # special handling line end
2458 2458 isendofline = token.endswith('\n')
2459 2459 if isendofline:
2460 2460 chomp = token[:-1] # chomp
2461 2461 if chomp.endswith('\r'):
2462 2462 chomp = chomp[:-1]
2463 2463 endofline = token[len(chomp):]
2464 2464 token = chomp.rstrip() # detect spaces at the end
2465 2465 endspaces = chomp[len(token):]
2466 2466 # scan tabs
2467 2467 for maybetab in tabsplitter.findall(token):
2468 2468 if b'\t' == maybetab[0:1]:
2469 2469 currentlabel = 'diff.tab'
2470 2470 else:
2471 2471 if changed:
2472 2472 currentlabel = label + '.changed'
2473 2473 else:
2474 2474 currentlabel = label + '.unchanged'
2475 2475 yield (maybetab, currentlabel)
2476 2476 if isendofline:
2477 2477 if endspaces:
2478 2478 yield (endspaces, 'diff.trailingwhitespace')
2479 2479 yield (endofline, '')
2480 2480 nextisnewline = True
2481 2481
2482 2482 def difflabel(func, *args, **kw):
2483 2483 '''yields 2-tuples of (output, label) based on the output of func()'''
2484 2484 if kw.get(r'opts') and kw[r'opts'].worddiff:
2485 2485 dodiffhunk = diffsinglehunkinline
2486 2486 else:
2487 2487 dodiffhunk = diffsinglehunk
2488 2488 headprefixes = [('diff', 'diff.diffline'),
2489 2489 ('copy', 'diff.extended'),
2490 2490 ('rename', 'diff.extended'),
2491 2491 ('old', 'diff.extended'),
2492 2492 ('new', 'diff.extended'),
2493 2493 ('deleted', 'diff.extended'),
2494 2494 ('index', 'diff.extended'),
2495 2495 ('similarity', 'diff.extended'),
2496 2496 ('---', 'diff.file_a'),
2497 2497 ('+++', 'diff.file_b')]
2498 2498 textprefixes = [('@', 'diff.hunk'),
2499 2499 # - and + are handled by diffsinglehunk
2500 2500 ]
2501 2501 head = False
2502 2502
2503 2503 # buffers a hunk, i.e. adjacent "-", "+" lines without other changes.
2504 2504 hunkbuffer = []
2505 2505 def consumehunkbuffer():
2506 2506 if hunkbuffer:
2507 2507 for token in dodiffhunk(hunkbuffer):
2508 2508 yield token
2509 2509 hunkbuffer[:] = []
2510 2510
2511 2511 for chunk in func(*args, **kw):
2512 2512 lines = chunk.split('\n')
2513 2513 linecount = len(lines)
2514 2514 for i, line in enumerate(lines):
2515 2515 if head:
2516 2516 if line.startswith('@'):
2517 2517 head = False
2518 2518 else:
2519 2519 if line and not line.startswith((' ', '+', '-', '@', '\\')):
2520 2520 head = True
2521 2521 diffline = False
2522 2522 if not head and line and line.startswith(('+', '-')):
2523 2523 diffline = True
2524 2524
2525 2525 prefixes = textprefixes
2526 2526 if head:
2527 2527 prefixes = headprefixes
2528 2528 if diffline:
2529 2529 # buffered
2530 2530 bufferedline = line
2531 2531 if i + 1 < linecount:
2532 2532 bufferedline += "\n"
2533 2533 hunkbuffer.append(bufferedline)
2534 2534 else:
2535 2535 # unbuffered
2536 2536 for token in consumehunkbuffer():
2537 2537 yield token
2538 2538 stripline = line.rstrip()
2539 2539 for prefix, label in prefixes:
2540 2540 if stripline.startswith(prefix):
2541 2541 yield (stripline, label)
2542 2542 if line != stripline:
2543 2543 yield (line[len(stripline):],
2544 2544 'diff.trailingwhitespace')
2545 2545 break
2546 2546 else:
2547 2547 yield (line, '')
2548 2548 if i + 1 < linecount:
2549 2549 yield ('\n', '')
2550 2550 for token in consumehunkbuffer():
2551 2551 yield token
2552 2552
2553 2553 def diffui(*args, **kw):
2554 2554 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2555 2555 return difflabel(diff, *args, **kw)
2556 2556
2557 2557 def _filepairs(modified, added, removed, copy, opts):
2558 2558 '''generates tuples (f1, f2, copyop), where f1 is the name of the file
2559 2559 before and f2 is the the name after. For added files, f1 will be None,
2560 2560 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2561 2561 or 'rename' (the latter two only if opts.git is set).'''
2562 2562 gone = set()
2563 2563
2564 2564 copyto = dict([(v, k) for k, v in copy.items()])
2565 2565
2566 2566 addedset, removedset = set(added), set(removed)
2567 2567
2568 2568 for f in sorted(modified + added + removed):
2569 2569 copyop = None
2570 2570 f1, f2 = f, f
2571 2571 if f in addedset:
2572 2572 f1 = None
2573 2573 if f in copy:
2574 2574 if opts.git:
2575 2575 f1 = copy[f]
2576 2576 if f1 in removedset and f1 not in gone:
2577 2577 copyop = 'rename'
2578 2578 gone.add(f1)
2579 2579 else:
2580 2580 copyop = 'copy'
2581 2581 elif f in removedset:
2582 2582 f2 = None
2583 2583 if opts.git:
2584 2584 # have we already reported a copy above?
2585 2585 if (f in copyto and copyto[f] in addedset
2586 2586 and copy[copyto[f]] == f):
2587 2587 continue
2588 2588 yield f1, f2, copyop
2589 2589
2590 2590 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2591 2591 copy, getfilectx, opts, losedatafn, pathfn):
2592 2592 '''given input data, generate a diff and yield it in blocks
2593 2593
2594 2594 If generating a diff would lose data like flags or binary data and
2595 2595 losedatafn is not None, it will be called.
2596 2596
2597 2597 pathfn is applied to every path in the diff output.
2598 2598 '''
2599 2599
2600 2600 def gitindex(text):
2601 2601 if not text:
2602 2602 text = ""
2603 2603 l = len(text)
2604 2604 s = hashlib.sha1('blob %d\0' % l)
2605 2605 s.update(text)
2606 2606 return hex(s.digest())
2607 2607
2608 2608 if opts.noprefix:
2609 2609 aprefix = bprefix = ''
2610 2610 else:
2611 2611 aprefix = 'a/'
2612 2612 bprefix = 'b/'
2613 2613
2614 2614 def diffline(f, revs):
2615 2615 revinfo = ' '.join(["-r %s" % rev for rev in revs])
2616 2616 return 'diff %s %s' % (revinfo, f)
2617 2617
2618 2618 def isempty(fctx):
2619 2619 return fctx is None or fctx.size() == 0
2620 2620
2621 2621 date1 = dateutil.datestr(ctx1.date())
2622 2622 date2 = dateutil.datestr(ctx2.date())
2623 2623
2624 2624 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
2625 2625
2626 2626 if not pathfn:
2627 2627 pathfn = lambda f: f
2628 2628
2629 2629 for f1, f2, copyop in _filepairs(modified, added, removed, copy, opts):
2630 2630 content1 = None
2631 2631 content2 = None
2632 2632 fctx1 = None
2633 2633 fctx2 = None
2634 2634 flag1 = None
2635 2635 flag2 = None
2636 2636 if f1:
2637 2637 fctx1 = getfilectx(f1, ctx1)
2638 2638 if opts.git or losedatafn:
2639 2639 flag1 = ctx1.flags(f1)
2640 2640 if f2:
2641 2641 fctx2 = getfilectx(f2, ctx2)
2642 2642 if opts.git or losedatafn:
2643 2643 flag2 = ctx2.flags(f2)
2644 2644 # if binary is True, output "summary" or "base85", but not "text diff"
2645 2645 if opts.text:
2646 2646 binary = False
2647 2647 else:
2648 2648 binary = any(f.isbinary() for f in [fctx1, fctx2] if f is not None)
2649 2649
2650 2650 if losedatafn and not opts.git:
2651 2651 if (binary or
2652 2652 # copy/rename
2653 2653 f2 in copy or
2654 2654 # empty file creation
2655 2655 (not f1 and isempty(fctx2)) or
2656 2656 # empty file deletion
2657 2657 (isempty(fctx1) and not f2) or
2658 2658 # create with flags
2659 2659 (not f1 and flag2) or
2660 2660 # change flags
2661 2661 (f1 and f2 and flag1 != flag2)):
2662 2662 losedatafn(f2 or f1)
2663 2663
2664 2664 path1 = pathfn(f1 or f2)
2665 2665 path2 = pathfn(f2 or f1)
2666 2666 header = []
2667 2667 if opts.git:
2668 2668 header.append('diff --git %s%s %s%s' %
2669 2669 (aprefix, path1, bprefix, path2))
2670 2670 if not f1: # added
2671 2671 header.append('new file mode %s' % gitmode[flag2])
2672 2672 elif not f2: # removed
2673 2673 header.append('deleted file mode %s' % gitmode[flag1])
2674 2674 else: # modified/copied/renamed
2675 2675 mode1, mode2 = gitmode[flag1], gitmode[flag2]
2676 2676 if mode1 != mode2:
2677 2677 header.append('old mode %s' % mode1)
2678 2678 header.append('new mode %s' % mode2)
2679 2679 if copyop is not None:
2680 2680 if opts.showsimilarity:
2681 2681 sim = similar.score(ctx1[path1], ctx2[path2]) * 100
2682 2682 header.append('similarity index %d%%' % sim)
2683 2683 header.append('%s from %s' % (copyop, path1))
2684 2684 header.append('%s to %s' % (copyop, path2))
2685 elif revs and not repo.ui.quiet:
2685 elif revs:
2686 2686 header.append(diffline(path1, revs))
2687 2687
2688 2688 # fctx.is | diffopts | what to | is fctx.data()
2689 2689 # binary() | text nobinary git index | output? | outputted?
2690 2690 # ------------------------------------|----------------------------
2691 2691 # yes | no no no * | summary | no
2692 2692 # yes | no no yes * | base85 | yes
2693 2693 # yes | no yes no * | summary | no
2694 2694 # yes | no yes yes 0 | summary | no
2695 2695 # yes | no yes yes >0 | summary | semi [1]
2696 2696 # yes | yes * * * | text diff | yes
2697 2697 # no | * * * * | text diff | yes
2698 2698 # [1]: hash(fctx.data()) is outputted. so fctx.data() cannot be faked
2699 2699 if binary and (not opts.git or (opts.git and opts.nobinary and not
2700 2700 opts.index)):
2701 2701 # fast path: no binary content will be displayed, content1 and
2702 2702 # content2 are only used for equivalent test. cmp() could have a
2703 2703 # fast path.
2704 2704 if fctx1 is not None:
2705 2705 content1 = b'\0'
2706 2706 if fctx2 is not None:
2707 2707 if fctx1 is not None and not fctx1.cmp(fctx2):
2708 2708 content2 = b'\0' # not different
2709 2709 else:
2710 2710 content2 = b'\0\0'
2711 2711 else:
2712 2712 # normal path: load contents
2713 2713 if fctx1 is not None:
2714 2714 content1 = fctx1.data()
2715 2715 if fctx2 is not None:
2716 2716 content2 = fctx2.data()
2717 2717
2718 2718 if binary and opts.git and not opts.nobinary:
2719 2719 text = mdiff.b85diff(content1, content2)
2720 2720 if text:
2721 2721 header.append('index %s..%s' %
2722 2722 (gitindex(content1), gitindex(content2)))
2723 2723 hunks = (None, [text]),
2724 2724 else:
2725 2725 if opts.git and opts.index > 0:
2726 2726 flag = flag1
2727 2727 if flag is None:
2728 2728 flag = flag2
2729 2729 header.append('index %s..%s %s' %
2730 2730 (gitindex(content1)[0:opts.index],
2731 2731 gitindex(content2)[0:opts.index],
2732 2732 gitmode[flag]))
2733 2733
2734 2734 uheaders, hunks = mdiff.unidiff(content1, date1,
2735 2735 content2, date2,
2736 2736 path1, path2,
2737 2737 binary=binary, opts=opts)
2738 2738 header.extend(uheaders)
2739 2739 yield fctx1, fctx2, header, hunks
2740 2740
2741 2741 def diffstatsum(stats):
2742 2742 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
2743 2743 for f, a, r, b in stats:
2744 2744 maxfile = max(maxfile, encoding.colwidth(f))
2745 2745 maxtotal = max(maxtotal, a + r)
2746 2746 addtotal += a
2747 2747 removetotal += r
2748 2748 binary = binary or b
2749 2749
2750 2750 return maxfile, maxtotal, addtotal, removetotal, binary
2751 2751
2752 2752 def diffstatdata(lines):
2753 2753 diffre = re.compile(br'^diff .*-r [a-z0-9]+\s(.*)$')
2754 2754
2755 2755 results = []
2756 2756 filename, adds, removes, isbinary = None, 0, 0, False
2757 2757
2758 2758 def addresult():
2759 2759 if filename:
2760 2760 results.append((filename, adds, removes, isbinary))
2761 2761
2762 2762 # inheader is used to track if a line is in the
2763 2763 # header portion of the diff. This helps properly account
2764 2764 # for lines that start with '--' or '++'
2765 2765 inheader = False
2766 2766
2767 2767 for line in lines:
2768 2768 if line.startswith('diff'):
2769 2769 addresult()
2770 2770 # starting a new file diff
2771 2771 # set numbers to 0 and reset inheader
2772 2772 inheader = True
2773 2773 adds, removes, isbinary = 0, 0, False
2774 2774 if line.startswith('diff --git a/'):
2775 2775 filename = gitre.search(line).group(2)
2776 2776 elif line.startswith('diff -r'):
2777 2777 # format: "diff -r ... -r ... filename"
2778 2778 filename = diffre.search(line).group(1)
2779 2779 elif line.startswith('@@'):
2780 2780 inheader = False
2781 2781 elif line.startswith('+') and not inheader:
2782 2782 adds += 1
2783 2783 elif line.startswith('-') and not inheader:
2784 2784 removes += 1
2785 2785 elif (line.startswith('GIT binary patch') or
2786 2786 line.startswith('Binary file')):
2787 2787 isbinary = True
2788 2788 elif line.startswith('rename from'):
2789 2789 filename = line[12:]
2790 2790 elif line.startswith('rename to'):
2791 2791 filename += ' => %s' % line[10:]
2792 2792 addresult()
2793 2793 return results
2794 2794
2795 2795 def diffstat(lines, width=80):
2796 2796 output = []
2797 2797 stats = diffstatdata(lines)
2798 2798 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
2799 2799
2800 2800 countwidth = len(str(maxtotal))
2801 2801 if hasbinary and countwidth < 3:
2802 2802 countwidth = 3
2803 2803 graphwidth = width - countwidth - maxname - 6
2804 2804 if graphwidth < 10:
2805 2805 graphwidth = 10
2806 2806
2807 2807 def scale(i):
2808 2808 if maxtotal <= graphwidth:
2809 2809 return i
2810 2810 # If diffstat runs out of room it doesn't print anything,
2811 2811 # which isn't very useful, so always print at least one + or -
2812 2812 # if there were at least some changes.
2813 2813 return max(i * graphwidth // maxtotal, int(bool(i)))
2814 2814
2815 2815 for filename, adds, removes, isbinary in stats:
2816 2816 if isbinary:
2817 2817 count = 'Bin'
2818 2818 else:
2819 2819 count = '%d' % (adds + removes)
2820 2820 pluses = '+' * scale(adds)
2821 2821 minuses = '-' * scale(removes)
2822 2822 output.append(' %s%s | %*s %s%s\n' %
2823 2823 (filename, ' ' * (maxname - encoding.colwidth(filename)),
2824 2824 countwidth, count, pluses, minuses))
2825 2825
2826 2826 if stats:
2827 2827 output.append(_(' %d files changed, %d insertions(+), '
2828 2828 '%d deletions(-)\n')
2829 2829 % (len(stats), totaladds, totalremoves))
2830 2830
2831 2831 return ''.join(output)
2832 2832
2833 2833 def diffstatui(*args, **kw):
2834 2834 '''like diffstat(), but yields 2-tuples of (output, label) for
2835 2835 ui.write()
2836 2836 '''
2837 2837
2838 2838 for line in diffstat(*args, **kw).splitlines():
2839 2839 if line and line[-1] in '+-':
2840 2840 name, graph = line.rsplit(' ', 1)
2841 2841 yield (name + ' ', '')
2842 2842 m = re.search(br'\++', graph)
2843 2843 if m:
2844 2844 yield (m.group(0), 'diffstat.inserted')
2845 2845 m = re.search(br'-+', graph)
2846 2846 if m:
2847 2847 yield (m.group(0), 'diffstat.deleted')
2848 2848 else:
2849 2849 yield (line, '')
2850 2850 yield ('\n', '')
@@ -1,833 +1,836 b''
1 1 commit date test
2 2
3 3 $ hg init test
4 4 $ cd test
5 5 $ echo foo > foo
6 6 $ hg add foo
7 7 $ cat > $TESTTMP/checkeditform.sh <<EOF
8 8 > env | grep HGEDITFORM
9 9 > true
10 10 > EOF
11 11 $ HGEDITOR="sh $TESTTMP/checkeditform.sh" hg commit -m ""
12 12 HGEDITFORM=commit.normal.normal
13 13 abort: empty commit message
14 14 [255]
15 15 $ hg commit -d '0 0' -m commit-1
16 16 $ echo foo >> foo
17 17 $ hg commit -d '1 4444444' -m commit-3
18 18 hg: parse error: impossible time zone offset: 4444444
19 19 [255]
20 20 $ hg commit -d '1 15.1' -m commit-4
21 21 hg: parse error: invalid date: '1\t15.1'
22 22 [255]
23 23 $ hg commit -d 'foo bar' -m commit-5
24 24 hg: parse error: invalid date: 'foo bar'
25 25 [255]
26 26 $ hg commit -d ' 1 4444' -m commit-6
27 27 $ hg commit -d '111111111111 0' -m commit-7
28 28 hg: parse error: date exceeds 32 bits: 111111111111
29 29 [255]
30 30 $ hg commit -d '-111111111111 0' -m commit-7
31 31 hg: parse error: date exceeds 32 bits: -111111111111
32 32 [255]
33 33 $ echo foo >> foo
34 34 $ hg commit -d '1901-12-13 20:45:52 +0000' -m commit-7-2
35 35 $ echo foo >> foo
36 36 $ hg commit -d '-2147483648 0' -m commit-7-3
37 37 $ hg log -T '{rev} {date|isodatesec}\n' -l2
38 38 3 1901-12-13 20:45:52 +0000
39 39 2 1901-12-13 20:45:52 +0000
40 40 $ hg commit -d '1901-12-13 20:45:51 +0000' -m commit-7
41 41 hg: parse error: date exceeds 32 bits: -2147483649
42 42 [255]
43 43 $ hg commit -d '-2147483649 0' -m commit-7
44 44 hg: parse error: date exceeds 32 bits: -2147483649
45 45 [255]
46 46
47 47 commit added file that has been deleted
48 48
49 49 $ echo bar > bar
50 50 $ hg add bar
51 51 $ rm bar
52 52 $ hg commit -m commit-8
53 53 nothing changed (1 missing files, see 'hg status')
54 54 [1]
55 55 $ hg commit -m commit-8-2 bar
56 56 abort: bar: file not found!
57 57 [255]
58 58
59 59 $ hg -q revert -a --no-backup
60 60
61 61 $ mkdir dir
62 62 $ echo boo > dir/file
63 63 $ hg add
64 64 adding dir/file
65 65 $ hg -v commit -m commit-9 dir
66 66 committing files:
67 67 dir/file
68 68 committing manifest
69 69 committing changelog
70 70 committed changeset 4:1957363f1ced
71 71
72 72 $ echo > dir.file
73 73 $ hg add
74 74 adding dir.file
75 75 $ hg commit -m commit-10 dir dir.file
76 76 abort: dir: no match under directory!
77 77 [255]
78 78
79 79 $ echo >> dir/file
80 80 $ mkdir bleh
81 81 $ mkdir dir2
82 82 $ cd bleh
83 83 $ hg commit -m commit-11 .
84 84 abort: bleh: no match under directory!
85 85 [255]
86 86 $ hg commit -m commit-12 ../dir ../dir2
87 87 abort: dir2: no match under directory!
88 88 [255]
89 89 $ hg -v commit -m commit-13 ../dir
90 90 committing files:
91 91 dir/file
92 92 committing manifest
93 93 committing changelog
94 94 committed changeset 5:a31d8f87544a
95 95 $ cd ..
96 96
97 97 $ hg commit -m commit-14 does-not-exist
98 98 abort: does-not-exist: * (glob)
99 99 [255]
100 100
101 101 #if symlink
102 102 $ ln -s foo baz
103 103 $ hg commit -m commit-15 baz
104 104 abort: baz: file not tracked!
105 105 [255]
106 106 $ rm baz
107 107 #endif
108 108
109 109 $ touch quux
110 110 $ hg commit -m commit-16 quux
111 111 abort: quux: file not tracked!
112 112 [255]
113 113 $ echo >> dir/file
114 114 $ hg -v commit -m commit-17 dir/file
115 115 committing files:
116 116 dir/file
117 117 committing manifest
118 118 committing changelog
119 119 committed changeset 6:32d054c9d085
120 120
121 121 An empty date was interpreted as epoch origin
122 122
123 123 $ echo foo >> foo
124 124 $ hg commit -d '' -m commit-no-date --config devel.default-date=
125 125 $ hg tip --template '{date|isodate}\n' | grep '1970'
126 126 [1]
127 127
128 128 Using the advanced --extra flag
129 129
130 130 $ echo "[extensions]" >> $HGRCPATH
131 131 $ echo "commitextras=" >> $HGRCPATH
132 132 $ hg status
133 133 ? quux
134 134 $ hg add quux
135 135 $ hg commit -m "adding internal used extras" --extra amend_source=hash
136 136 abort: key 'amend_source' is used internally, can't be set manually
137 137 [255]
138 138 $ hg commit -m "special chars in extra" --extra id@phab=214
139 139 abort: keys can only contain ascii letters, digits, '_' and '-'
140 140 [255]
141 141 $ hg commit -m "empty key" --extra =value
142 142 abort: unable to parse '=value', keys can't be empty
143 143 [255]
144 144 $ hg commit -m "adding extras" --extra sourcehash=foo --extra oldhash=bar
145 145 $ hg log -r . -T '{extras % "{extra}\n"}'
146 146 branch=default
147 147 oldhash=bar
148 148 sourcehash=foo
149 149
150 150 Failed commit with --addremove should not update dirstate
151 151
152 152 $ echo foo > newfile
153 153 $ hg status
154 154 ? newfile
155 155 $ HGEDITOR=false hg ci --addremove
156 156 adding newfile
157 157 abort: edit failed: false exited with status 1
158 158 [255]
159 159 $ hg status
160 160 ? newfile
161 161
162 162 Make sure we do not obscure unknown requires file entries (issue2649)
163 163
164 164 $ echo foo >> foo
165 165 $ echo fake >> .hg/requires
166 166 $ hg commit -m bla
167 167 abort: repository requires features unknown to this Mercurial: fake!
168 168 (see https://mercurial-scm.org/wiki/MissingRequirement for more information)
169 169 [255]
170 170
171 171 $ cd ..
172 172
173 173
174 174 partial subdir commit test
175 175
176 176 $ hg init test2
177 177 $ cd test2
178 178 $ mkdir foo
179 179 $ echo foo > foo/foo
180 180 $ mkdir bar
181 181 $ echo bar > bar/bar
182 182 $ hg add
183 183 adding bar/bar
184 184 adding foo/foo
185 185 $ HGEDITOR=cat hg ci -e -m commit-subdir-1 foo
186 186 commit-subdir-1
187 187
188 188
189 189 HG: Enter commit message. Lines beginning with 'HG:' are removed.
190 190 HG: Leave message empty to abort commit.
191 191 HG: --
192 192 HG: user: test
193 193 HG: branch 'default'
194 194 HG: added foo/foo
195 195
196 196
197 197 $ hg ci -m commit-subdir-2 bar
198 198
199 199 subdir log 1
200 200
201 201 $ hg log -v foo
202 202 changeset: 0:f97e73a25882
203 203 user: test
204 204 date: Thu Jan 01 00:00:00 1970 +0000
205 205 files: foo/foo
206 206 description:
207 207 commit-subdir-1
208 208
209 209
210 210
211 211 subdir log 2
212 212
213 213 $ hg log -v bar
214 214 changeset: 1:aa809156d50d
215 215 tag: tip
216 216 user: test
217 217 date: Thu Jan 01 00:00:00 1970 +0000
218 218 files: bar/bar
219 219 description:
220 220 commit-subdir-2
221 221
222 222
223 223
224 224 full log
225 225
226 226 $ hg log -v
227 227 changeset: 1:aa809156d50d
228 228 tag: tip
229 229 user: test
230 230 date: Thu Jan 01 00:00:00 1970 +0000
231 231 files: bar/bar
232 232 description:
233 233 commit-subdir-2
234 234
235 235
236 236 changeset: 0:f97e73a25882
237 237 user: test
238 238 date: Thu Jan 01 00:00:00 1970 +0000
239 239 files: foo/foo
240 240 description:
241 241 commit-subdir-1
242 242
243 243
244 244 $ cd ..
245 245
246 246
247 247 dot and subdir commit test
248 248
249 249 $ hg init test3
250 250 $ echo commit-foo-subdir > commit-log-test
251 251 $ cd test3
252 252 $ mkdir foo
253 253 $ echo foo content > foo/plain-file
254 254 $ hg add foo/plain-file
255 255 $ HGEDITOR=cat hg ci --edit -l ../commit-log-test foo
256 256 commit-foo-subdir
257 257
258 258
259 259 HG: Enter commit message. Lines beginning with 'HG:' are removed.
260 260 HG: Leave message empty to abort commit.
261 261 HG: --
262 262 HG: user: test
263 263 HG: branch 'default'
264 264 HG: added foo/plain-file
265 265
266 266
267 267 $ echo modified foo content > foo/plain-file
268 268 $ hg ci -m commit-foo-dot .
269 269
270 270 full log
271 271
272 272 $ hg log -v
273 273 changeset: 1:95b38e3a5b2e
274 274 tag: tip
275 275 user: test
276 276 date: Thu Jan 01 00:00:00 1970 +0000
277 277 files: foo/plain-file
278 278 description:
279 279 commit-foo-dot
280 280
281 281
282 282 changeset: 0:65d4e9386227
283 283 user: test
284 284 date: Thu Jan 01 00:00:00 1970 +0000
285 285 files: foo/plain-file
286 286 description:
287 287 commit-foo-subdir
288 288
289 289
290 290
291 291 subdir log
292 292
293 293 $ cd foo
294 294 $ hg log .
295 295 changeset: 1:95b38e3a5b2e
296 296 tag: tip
297 297 user: test
298 298 date: Thu Jan 01 00:00:00 1970 +0000
299 299 summary: commit-foo-dot
300 300
301 301 changeset: 0:65d4e9386227
302 302 user: test
303 303 date: Thu Jan 01 00:00:00 1970 +0000
304 304 summary: commit-foo-subdir
305 305
306 306 $ cd ..
307 307 $ cd ..
308 308
309 309 Issue1049: Hg permits partial commit of merge without warning
310 310
311 311 $ hg init issue1049
312 312 $ cd issue1049
313 313 $ echo a > a
314 314 $ hg ci -Ama
315 315 adding a
316 316 $ echo a >> a
317 317 $ hg ci -mb
318 318 $ hg up 0
319 319 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
320 320 $ echo b >> a
321 321 $ hg ci -mc
322 322 created new head
323 323 $ HGMERGE=true hg merge
324 324 merging a
325 325 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
326 326 (branch merge, don't forget to commit)
327 327
328 328 should fail because we are specifying a file name
329 329
330 330 $ hg ci -mmerge a
331 331 abort: cannot partially commit a merge (do not specify files or patterns)
332 332 [255]
333 333
334 334 should fail because we are specifying a pattern
335 335
336 336 $ hg ci -mmerge -I a
337 337 abort: cannot partially commit a merge (do not specify files or patterns)
338 338 [255]
339 339
340 340 should succeed
341 341
342 342 $ HGEDITOR="sh $TESTTMP/checkeditform.sh" hg ci -mmerge --edit
343 343 HGEDITFORM=commit.normal.merge
344 344 $ cd ..
345 345
346 346
347 347 test commit message content
348 348
349 349 $ hg init commitmsg
350 350 $ cd commitmsg
351 351 $ echo changed > changed
352 352 $ echo removed > removed
353 353 $ hg book activebookmark
354 354 $ hg ci -qAm init
355 355
356 356 $ hg rm removed
357 357 $ echo changed >> changed
358 358 $ echo added > added
359 359 $ hg add added
360 360 $ HGEDITOR=cat hg ci -A
361 361
362 362
363 363 HG: Enter commit message. Lines beginning with 'HG:' are removed.
364 364 HG: Leave message empty to abort commit.
365 365 HG: --
366 366 HG: user: test
367 367 HG: branch 'default'
368 368 HG: bookmark 'activebookmark'
369 369 HG: added added
370 370 HG: changed changed
371 371 HG: removed removed
372 372 abort: empty commit message
373 373 [255]
374 374
375 375 test saving last-message.txt
376 376
377 377 $ hg init sub
378 378 $ echo a > sub/a
379 379 $ hg -R sub add sub/a
380 380 $ cat > sub/.hg/hgrc <<EOF
381 381 > [hooks]
382 382 > precommit.test-saving-last-message = false
383 383 > EOF
384 384
385 385 $ echo 'sub = sub' > .hgsub
386 386 $ hg add .hgsub
387 387
388 388 $ cat > $TESTTMP/editor.sh <<EOF
389 389 > echo "==== before editing:"
390 390 > cat \$1
391 391 > echo "===="
392 392 > echo "test saving last-message.txt" >> \$1
393 393 > EOF
394 394
395 395 $ rm -f .hg/last-message.txt
396 396 $ HGEDITOR="sh $TESTTMP/editor.sh" hg commit -S -q
397 397 ==== before editing:
398 398
399 399
400 400 HG: Enter commit message. Lines beginning with 'HG:' are removed.
401 401 HG: Leave message empty to abort commit.
402 402 HG: --
403 403 HG: user: test
404 404 HG: branch 'default'
405 405 HG: bookmark 'activebookmark'
406 406 HG: subrepo sub
407 407 HG: added .hgsub
408 408 HG: added added
409 409 HG: changed .hgsubstate
410 410 HG: changed changed
411 411 HG: removed removed
412 412 ====
413 413 abort: precommit.test-saving-last-message hook exited with status 1 (in subrepository "sub")
414 414 [255]
415 415 $ cat .hg/last-message.txt
416 416
417 417
418 418 test saving last-message.txt
419 419
420 420 test that '[committemplate] changeset' definition and commit log
421 421 specific template keywords work well
422 422
423 423 $ cat >> .hg/hgrc <<EOF
424 424 > [committemplate]
425 425 > changeset.commit.normal = 'HG: this is "commit.normal" template
426 426 > HG: {extramsg}
427 427 > {if(activebookmark,
428 428 > "HG: bookmark '{activebookmark}' is activated\n",
429 429 > "HG: no bookmark is activated\n")}{subrepos %
430 430 > "HG: subrepo '{subrepo}' is changed\n"}'
431 431 >
432 432 > changeset.commit = HG: this is "commit" template
433 433 > HG: {extramsg}
434 434 > {if(activebookmark,
435 435 > "HG: bookmark '{activebookmark}' is activated\n",
436 436 > "HG: no bookmark is activated\n")}{subrepos %
437 437 > "HG: subrepo '{subrepo}' is changed\n"}
438 438 >
439 439 > changeset = HG: this is customized commit template
440 440 > HG: {extramsg}
441 441 > {if(activebookmark,
442 442 > "HG: bookmark '{activebookmark}' is activated\n",
443 443 > "HG: no bookmark is activated\n")}{subrepos %
444 444 > "HG: subrepo '{subrepo}' is changed\n"}
445 445 > EOF
446 446
447 447 $ hg init sub2
448 448 $ echo a > sub2/a
449 449 $ hg -R sub2 add sub2/a
450 450 $ echo 'sub2 = sub2' >> .hgsub
451 451
452 452 $ HGEDITOR=cat hg commit -S -q
453 453 HG: this is "commit.normal" template
454 454 HG: Leave message empty to abort commit.
455 455 HG: bookmark 'activebookmark' is activated
456 456 HG: subrepo 'sub' is changed
457 457 HG: subrepo 'sub2' is changed
458 458 abort: empty commit message
459 459 [255]
460 460
461 461 $ cat >> .hg/hgrc <<EOF
462 462 > [committemplate]
463 463 > changeset.commit.normal =
464 464 > # now, "changeset.commit" should be chosen for "hg commit"
465 465 > EOF
466 466
467 467 $ hg bookmark --inactive activebookmark
468 468 $ hg forget .hgsub
469 469 $ HGEDITOR=cat hg commit -q
470 470 HG: this is "commit" template
471 471 HG: Leave message empty to abort commit.
472 472 HG: no bookmark is activated
473 473 abort: empty commit message
474 474 [255]
475 475
476 476 $ cat >> .hg/hgrc <<EOF
477 477 > [committemplate]
478 478 > changeset.commit =
479 479 > # now, "changeset" should be chosen for "hg commit"
480 480 > EOF
481 481
482 482 $ HGEDITOR=cat hg commit -q
483 483 HG: this is customized commit template
484 484 HG: Leave message empty to abort commit.
485 485 HG: no bookmark is activated
486 486 abort: empty commit message
487 487 [255]
488 488
489 489 $ cat >> .hg/hgrc <<EOF
490 490 > [committemplate]
491 491 > changeset = {desc}
492 492 > HG: mods={file_mods}
493 493 > HG: adds={file_adds}
494 494 > HG: dels={file_dels}
495 495 > HG: files={files}
496 496 > HG:
497 497 > {splitlines(diff()) % 'HG: {line}\n'
498 498 > }HG:
499 499 > HG: mods={file_mods}
500 500 > HG: adds={file_adds}
501 501 > HG: dels={file_dels}
502 502 > HG: files={files}\n
503 503 > EOF
504 504 $ hg status -amr
505 505 M changed
506 506 A added
507 507 R removed
508 508 $ HGEDITOR=cat hg commit -q -e -m "foo bar" changed
509 509 foo bar
510 510 HG: mods=changed
511 511 HG: adds=
512 512 HG: dels=
513 513 HG: files=changed
514 514 HG:
515 HG: diff -r d2313f97106f changed
515 516 HG: --- a/changed Thu Jan 01 00:00:00 1970 +0000
516 517 HG: +++ b/changed Thu Jan 01 00:00:00 1970 +0000
517 518 HG: @@ -1,1 +1,2 @@
518 519 HG: changed
519 520 HG: +changed
520 521 HG:
521 522 HG: mods=changed
522 523 HG: adds=
523 524 HG: dels=
524 525 HG: files=changed
525 526 $ hg status -amr
526 527 A added
527 528 R removed
528 529 $ hg parents --template "M {file_mods}\nA {file_adds}\nR {file_dels}\n"
529 530 M changed
530 531 A
531 532 R
532 533 $ hg rollback -q
533 534
534 535 $ cat >> .hg/hgrc <<EOF
535 536 > [committemplate]
536 537 > changeset = {desc}
537 538 > HG: mods={file_mods}
538 539 > HG: adds={file_adds}
539 540 > HG: dels={file_dels}
540 541 > HG: files={files}
541 542 > HG:
542 543 > {splitlines(diff("changed")) % 'HG: {line}\n'
543 544 > }HG:
544 545 > HG: mods={file_mods}
545 546 > HG: adds={file_adds}
546 547 > HG: dels={file_dels}
547 548 > HG: files={files}
548 549 > HG:
549 550 > {splitlines(diff("added")) % 'HG: {line}\n'
550 551 > }HG:
551 552 > HG: mods={file_mods}
552 553 > HG: adds={file_adds}
553 554 > HG: dels={file_dels}
554 555 > HG: files={files}
555 556 > HG:
556 557 > {splitlines(diff("removed")) % 'HG: {line}\n'
557 558 > }HG:
558 559 > HG: mods={file_mods}
559 560 > HG: adds={file_adds}
560 561 > HG: dels={file_dels}
561 562 > HG: files={files}\n
562 563 > EOF
563 564 $ HGEDITOR=cat hg commit -q -e -m "foo bar" added removed
564 565 foo bar
565 566 HG: mods=
566 567 HG: adds=added
567 568 HG: dels=removed
568 569 HG: files=added removed
569 570 HG:
570 571 HG:
571 572 HG: mods=
572 573 HG: adds=added
573 574 HG: dels=removed
574 575 HG: files=added removed
575 576 HG:
577 HG: diff -r d2313f97106f added
576 578 HG: --- /dev/null Thu Jan 01 00:00:00 1970 +0000
577 579 HG: +++ b/added Thu Jan 01 00:00:00 1970 +0000
578 580 HG: @@ -0,0 +1,1 @@
579 581 HG: +added
580 582 HG:
581 583 HG: mods=
582 584 HG: adds=added
583 585 HG: dels=removed
584 586 HG: files=added removed
585 587 HG:
588 HG: diff -r d2313f97106f removed
586 589 HG: --- a/removed Thu Jan 01 00:00:00 1970 +0000
587 590 HG: +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
588 591 HG: @@ -1,1 +0,0 @@
589 592 HG: -removed
590 593 HG:
591 594 HG: mods=
592 595 HG: adds=added
593 596 HG: dels=removed
594 597 HG: files=added removed
595 598 $ hg status -amr
596 599 M changed
597 600 $ hg parents --template "M {file_mods}\nA {file_adds}\nR {file_dels}\n"
598 601 M
599 602 A added
600 603 R removed
601 604 $ hg rollback -q
602 605
603 606 $ cat >> .hg/hgrc <<EOF
604 607 > # disable customizing for subsequent tests
605 608 > [committemplate]
606 609 > changeset =
607 610 > EOF
608 611
609 612 $ cd ..
610 613
611 614
612 615 commit copy
613 616
614 617 $ hg init dir2
615 618 $ cd dir2
616 619 $ echo bleh > bar
617 620 $ hg add bar
618 621 $ hg ci -m 'add bar'
619 622
620 623 $ hg cp bar foo
621 624 $ echo >> bar
622 625 $ hg ci -m 'cp bar foo; change bar'
623 626
624 627 $ hg debugrename foo
625 628 foo renamed from bar:26d3ca0dfd18e44d796b564e38dd173c9668d3a9
626 629 $ hg debugindex bar
627 630 rev linkrev nodeid p1 p2
628 631 0 0 26d3ca0dfd18 000000000000 000000000000
629 632 1 1 d267bddd54f7 26d3ca0dfd18 000000000000
630 633
631 634 Test making empty commits
632 635 $ hg commit --config ui.allowemptycommit=True -m "empty commit"
633 636 $ hg log -r . -v --stat
634 637 changeset: 2:d809f3644287
635 638 tag: tip
636 639 user: test
637 640 date: Thu Jan 01 00:00:00 1970 +0000
638 641 description:
639 642 empty commit
640 643
641 644
642 645
643 646 verify pathauditor blocks evil filepaths
644 647 $ cat > evil-commit.py <<EOF
645 648 > from __future__ import absolute_import
646 649 > from mercurial import context, hg, node, ui as uimod
647 650 > notrc = u".h\u200cg".encode('utf-8') + b'/hgrc'
648 651 > u = uimod.ui.load()
649 652 > r = hg.repository(u, b'.')
650 653 > def filectxfn(repo, memctx, path):
651 654 > return context.memfilectx(repo, memctx, path,
652 655 > b'[hooks]\nupdate = echo owned')
653 656 > c = context.memctx(r, [r.changelog.tip(), node.nullid],
654 657 > b'evil', [notrc], filectxfn, 0)
655 658 > r.commitctx(c)
656 659 > EOF
657 660 $ "$PYTHON" evil-commit.py
658 661 #if windows
659 662 $ hg co --clean tip
660 663 abort: path contains illegal component: .h\xe2\x80\x8cg\\hgrc (esc)
661 664 [255]
662 665 #else
663 666 $ hg co --clean tip
664 667 abort: path contains illegal component: .h\xe2\x80\x8cg/hgrc (esc)
665 668 [255]
666 669 #endif
667 670
668 671 $ hg rollback -f
669 672 repository tip rolled back to revision 2 (undo commit)
670 673 $ cat > evil-commit.py <<EOF
671 674 > from __future__ import absolute_import
672 675 > from mercurial import context, hg, node, ui as uimod
673 676 > notrc = b"HG~1/hgrc"
674 677 > u = uimod.ui.load()
675 678 > r = hg.repository(u, b'.')
676 679 > def filectxfn(repo, memctx, path):
677 680 > return context.memfilectx(repo, memctx, path,
678 681 > b'[hooks]\nupdate = echo owned')
679 682 > c = context.memctx(r, [r[b'tip'].node(), node.nullid],
680 683 > b'evil', [notrc], filectxfn, 0)
681 684 > r.commitctx(c)
682 685 > EOF
683 686 $ "$PYTHON" evil-commit.py
684 687 $ hg co --clean tip
685 688 abort: path contains illegal component: HG~1/hgrc
686 689 [255]
687 690
688 691 $ hg rollback -f
689 692 repository tip rolled back to revision 2 (undo commit)
690 693 $ cat > evil-commit.py <<EOF
691 694 > from __future__ import absolute_import
692 695 > from mercurial import context, hg, node, ui as uimod
693 696 > notrc = b"HG8B6C~2/hgrc"
694 697 > u = uimod.ui.load()
695 698 > r = hg.repository(u, b'.')
696 699 > def filectxfn(repo, memctx, path):
697 700 > return context.memfilectx(repo, memctx, path,
698 701 > b'[hooks]\nupdate = echo owned')
699 702 > c = context.memctx(r, [r[b'tip'].node(), node.nullid],
700 703 > b'evil', [notrc], filectxfn, 0)
701 704 > r.commitctx(c)
702 705 > EOF
703 706 $ "$PYTHON" evil-commit.py
704 707 $ hg co --clean tip
705 708 abort: path contains illegal component: HG8B6C~2/hgrc
706 709 [255]
707 710
708 711 # test that an unmodified commit template message aborts
709 712
710 713 $ hg init unmodified_commit_template
711 714 $ cd unmodified_commit_template
712 715 $ echo foo > foo
713 716 $ hg add foo
714 717 $ hg commit -m "foo"
715 718 $ cat >> .hg/hgrc <<EOF
716 719 > [committemplate]
717 720 > changeset.commit = HI THIS IS NOT STRIPPED
718 721 > HG: this is customized commit template
719 722 > HG: {extramsg}
720 723 > {if(activebookmark,
721 724 > "HG: bookmark '{activebookmark}' is activated\n",
722 725 > "HG: no bookmark is activated\n")}{subrepos %
723 726 > "HG: subrepo '{subrepo}' is changed\n"}
724 727 > EOF
725 728 $ cat > $TESTTMP/notouching.sh <<EOF
726 729 > true
727 730 > EOF
728 731 $ echo foo2 > foo2
729 732 $ hg add foo2
730 733 $ HGEDITOR="sh $TESTTMP/notouching.sh" hg commit
731 734 abort: commit message unchanged
732 735 [255]
733 736
734 737 test that text below the --- >8 --- special string is ignored
735 738
736 739 $ cat <<'EOF' > $TESTTMP/lowercaseline.sh
737 740 > cat $1 | sed s/LINE/line/ | tee $1.new
738 741 > mv $1.new $1
739 742 > EOF
740 743
741 744 $ hg init ignore_below_special_string
742 745 $ cd ignore_below_special_string
743 746 $ echo foo > foo
744 747 $ hg add foo
745 748 $ hg commit -m "foo"
746 749 $ cat >> .hg/hgrc <<EOF
747 750 > [committemplate]
748 751 > changeset.commit = first LINE
749 752 > HG: this is customized commit template
750 753 > HG: {extramsg}
751 754 > HG: ------------------------ >8 ------------------------
752 755 > {diff()}
753 756 > EOF
754 757 $ echo foo2 > foo2
755 758 $ hg add foo2
756 759 $ HGEDITOR="sh $TESTTMP/notouching.sh" hg ci
757 760 abort: commit message unchanged
758 761 [255]
759 762 $ HGEDITOR="sh $TESTTMP/lowercaseline.sh" hg ci
760 763 first line
761 764 HG: this is customized commit template
762 765 HG: Leave message empty to abort commit.
763 766 HG: ------------------------ >8 ------------------------
764 767 diff -r e63c23eaa88a foo2
765 768 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
766 769 +++ b/foo2 Thu Jan 01 00:00:00 1970 +0000
767 770 @@ -0,0 +1,1 @@
768 771 +foo2
769 772 $ hg log -T '{desc}\n' -r .
770 773 first line
771 774
772 775 test that the special string --- >8 --- isn't used when not at the beginning of
773 776 a line
774 777
775 778 $ cat >> .hg/hgrc <<EOF
776 779 > [committemplate]
777 780 > changeset.commit = first LINE2
778 781 > another line HG: ------------------------ >8 ------------------------
779 782 > HG: this is customized commit template
780 783 > HG: {extramsg}
781 784 > HG: ------------------------ >8 ------------------------
782 785 > {diff()}
783 786 > EOF
784 787 $ echo foo >> foo
785 788 $ HGEDITOR="sh $TESTTMP/lowercaseline.sh" hg ci
786 789 first line2
787 790 another line HG: ------------------------ >8 ------------------------
788 791 HG: this is customized commit template
789 792 HG: Leave message empty to abort commit.
790 793 HG: ------------------------ >8 ------------------------
791 794 diff -r 3661b22b0702 foo
792 795 --- a/foo Thu Jan 01 00:00:00 1970 +0000
793 796 +++ b/foo Thu Jan 01 00:00:00 1970 +0000
794 797 @@ -1,1 +1,2 @@
795 798 foo
796 799 +foo
797 800 $ hg log -T '{desc}\n' -r .
798 801 first line2
799 802 another line HG: ------------------------ >8 ------------------------
800 803
801 804 also test that this special string isn't accepted when there is some extra text
802 805 at the end
803 806
804 807 $ cat >> .hg/hgrc <<EOF
805 808 > [committemplate]
806 809 > changeset.commit = first LINE3
807 810 > HG: ------------------------ >8 ------------------------foobar
808 811 > second line
809 812 > HG: this is customized commit template
810 813 > HG: {extramsg}
811 814 > HG: ------------------------ >8 ------------------------
812 815 > {diff()}
813 816 > EOF
814 817 $ echo foo >> foo
815 818 $ HGEDITOR="sh $TESTTMP/lowercaseline.sh" hg ci
816 819 first line3
817 820 HG: ------------------------ >8 ------------------------foobar
818 821 second line
819 822 HG: this is customized commit template
820 823 HG: Leave message empty to abort commit.
821 824 HG: ------------------------ >8 ------------------------
822 825 diff -r ce648f5f066f foo
823 826 --- a/foo Thu Jan 01 00:00:00 1970 +0000
824 827 +++ b/foo Thu Jan 01 00:00:00 1970 +0000
825 828 @@ -1,2 +1,3 @@
826 829 foo
827 830 foo
828 831 +foo
829 832 $ hg log -T '{desc}\n' -r .
830 833 first line3
831 834 second line
832 835
833 836 $ cd ..
@@ -1,46 +1,47 b''
1 1 $ hg init a
2 2 $ cd a
3 3
4 4 $ hg diff inexistent1 inexistent2
5 5 inexistent1: * (glob)
6 6 inexistent2: * (glob)
7 7
8 8 $ echo bar > foo
9 9 $ hg add foo
10 10 $ hg ci -m 'add foo'
11 11
12 12 $ echo foobar > foo
13 13 $ hg ci -m 'change foo'
14 14
15 15 $ hg --quiet diff -r 0 -r 1
16 diff -r a99fb63adac3 -r 9b8568d3af2f foo
16 17 --- a/foo Thu Jan 01 00:00:00 1970 +0000
17 18 +++ b/foo Thu Jan 01 00:00:00 1970 +0000
18 19 @@ -1,1 +1,1 @@
19 20 -bar
20 21 +foobar
21 22
22 23 $ hg diff -r 0 -r 1
23 24 diff -r a99fb63adac3 -r 9b8568d3af2f foo
24 25 --- a/foo Thu Jan 01 00:00:00 1970 +0000
25 26 +++ b/foo Thu Jan 01 00:00:00 1970 +0000
26 27 @@ -1,1 +1,1 @@
27 28 -bar
28 29 +foobar
29 30
30 31 $ hg --verbose diff -r 0 -r 1
31 32 diff -r a99fb63adac3 -r 9b8568d3af2f foo
32 33 --- a/foo Thu Jan 01 00:00:00 1970 +0000
33 34 +++ b/foo Thu Jan 01 00:00:00 1970 +0000
34 35 @@ -1,1 +1,1 @@
35 36 -bar
36 37 +foobar
37 38
38 39 $ hg --debug diff -r 0 -r 1
39 40 diff -r a99fb63adac3f31816a22f665bc3b7a7655b30f4 -r 9b8568d3af2f1749445eef03aede868a6f39f210 foo
40 41 --- a/foo Thu Jan 01 00:00:00 1970 +0000
41 42 +++ b/foo Thu Jan 01 00:00:00 1970 +0000
42 43 @@ -1,1 +1,1 @@
43 44 -bar
44 45 +foobar
45 46
46 47 $ cd ..
@@ -1,291 +1,294 b''
1 1 $ hg init repo
2 2 $ cd repo
3 3 $ i=0; while [ "$i" -lt 213 ]; do echo a >> a; i=`expr $i + 1`; done
4 4 $ hg add a
5 5 $ cp a b
6 6 $ hg add b
7 7
8 8 Wide diffstat:
9 9
10 10 $ hg diff --stat
11 11 a | 213 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
12 12 b | 213 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
13 13 2 files changed, 426 insertions(+), 0 deletions(-)
14 14
15 15 diffstat width:
16 16
17 17 $ COLUMNS=24 hg diff --config ui.interactive=true --stat
18 18 a | 213 ++++++++++++++
19 19 b | 213 ++++++++++++++
20 20 2 files changed, 426 insertions(+), 0 deletions(-)
21 21
22 22 $ hg ci -m adda
23 23
24 24 $ cat >> a <<EOF
25 25 > a
26 26 > a
27 27 > a
28 28 > EOF
29 29
30 30 Narrow diffstat:
31 31
32 32 $ hg diff --stat
33 33 a | 3 +++
34 34 1 files changed, 3 insertions(+), 0 deletions(-)
35 35
36 36 $ hg ci -m appenda
37 37
38 38 >>> open("c", "wb").write(b"\0") and None
39 39 $ touch d
40 40 $ hg add c d
41 41
42 42 Binary diffstat:
43 43
44 44 $ hg diff --stat
45 45 c | Bin
46 46 1 files changed, 0 insertions(+), 0 deletions(-)
47 47
48 48 Binary git diffstat:
49 49
50 50 $ hg diff --stat --git
51 51 c | Bin
52 52 d | 0
53 53 2 files changed, 0 insertions(+), 0 deletions(-)
54 54
55 55 $ hg ci -m createb
56 56
57 57 >>> open("file with spaces", "wb").write(b"\0") and None
58 58 $ hg add "file with spaces"
59 59
60 60 Filename with spaces diffstat:
61 61
62 62 $ hg diff --stat
63 63 file with spaces | Bin
64 64 1 files changed, 0 insertions(+), 0 deletions(-)
65 65
66 66 Filename with spaces git diffstat:
67 67
68 68 $ hg diff --stat --git
69 69 file with spaces | Bin
70 70 1 files changed, 0 insertions(+), 0 deletions(-)
71 71
72 72 Filename without "a/" or "b/" (issue5759):
73 73
74 74 $ hg diff --config 'diff.noprefix=1' -c1 --stat --git
75 75 a | 3 +++
76 76 1 files changed, 3 insertions(+), 0 deletions(-)
77 77 $ hg diff --config 'diff.noprefix=1' -c2 --stat --git
78 78 c | Bin
79 79 d | 0
80 80 2 files changed, 0 insertions(+), 0 deletions(-)
81 81
82 82 $ hg log --config 'diff.noprefix=1' -r '1:' -p --stat --git
83 83 changeset: 1:3a95b07bb77f
84 84 user: test
85 85 date: Thu Jan 01 00:00:00 1970 +0000
86 86 summary: appenda
87 87
88 88 a | 3 +++
89 89 1 files changed, 3 insertions(+), 0 deletions(-)
90 90
91 91 diff --git a a
92 92 --- a
93 93 +++ a
94 94 @@ -211,3 +211,6 @@
95 95 a
96 96 a
97 97 a
98 98 +a
99 99 +a
100 100 +a
101 101
102 102 changeset: 2:c60a6c753773
103 103 tag: tip
104 104 user: test
105 105 date: Thu Jan 01 00:00:00 1970 +0000
106 106 summary: createb
107 107
108 108 c | Bin
109 109 d | 0
110 110 2 files changed, 0 insertions(+), 0 deletions(-)
111 111
112 112 diff --git c c
113 113 new file mode 100644
114 114 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f76dd238ade08917e6712764a16a22005a50573d
115 115 GIT binary patch
116 116 literal 1
117 117 Ic${MZ000310RR91
118 118
119 119 diff --git d d
120 120 new file mode 100644
121 121
122 122
123 123 diffstat within directories:
124 124
125 125 $ hg rm -f 'file with spaces'
126 126
127 127 $ mkdir dir1 dir2
128 128 $ echo new1 > dir1/new
129 129 $ echo new2 > dir2/new
130 130 $ hg add dir1/new dir2/new
131 131 $ hg diff --stat
132 132 dir1/new | 1 +
133 133 dir2/new | 1 +
134 134 2 files changed, 2 insertions(+), 0 deletions(-)
135 135
136 136 $ hg diff --stat --root dir1
137 137 new | 1 +
138 138 1 files changed, 1 insertions(+), 0 deletions(-)
139 139
140 140 $ hg diff --stat --root dir1 dir2
141 141 warning: dir2 not inside relative root dir1
142 142
143 143 $ hg diff --stat --root dir1 -I dir1/old
144 144
145 145 $ cd dir1
146 146 $ hg diff --stat .
147 147 dir1/new | 1 +
148 148 1 files changed, 1 insertions(+), 0 deletions(-)
149 149 $ hg diff --stat . --config ui.relative-paths=yes
150 150 new | 1 +
151 151 1 files changed, 1 insertions(+), 0 deletions(-)
152 152 $ hg diff --stat --root .
153 153 new | 1 +
154 154 1 files changed, 1 insertions(+), 0 deletions(-)
155 155
156 156 $ hg diff --stat --root . --config ui.relative-paths=yes
157 157 new | 1 +
158 158 1 files changed, 1 insertions(+), 0 deletions(-)
159 159 --root trumps ui.relative-paths
160 160 $ hg diff --stat --root .. --config ui.relative-paths=yes
161 161 new | 1 +
162 162 ../dir2/new | 1 +
163 163 2 files changed, 2 insertions(+), 0 deletions(-)
164 164 $ hg diff --stat --root ../dir1 ../dir2
165 165 warning: ../dir2 not inside relative root .
166 166
167 167 $ hg diff --stat --root . -I old
168 168
169 169 $ cd ..
170 170
171 171 Files with lines beginning with '--' or '++' should be properly counted in diffstat
172 172
173 173 $ hg up -Cr tip
174 174 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
175 175 $ rm dir1/new
176 176 $ rm dir2/new
177 177 $ rm "file with spaces"
178 178 $ cat > file << EOF
179 179 > line 1
180 180 > line 2
181 181 > line 3
182 182 > EOF
183 183 $ hg commit -Am file
184 184 adding file
185 185
186 186 Lines added starting with '--' should count as additions
187 187 $ cat > file << EOF
188 188 > line 1
189 189 > -- line 2, with dashes
190 190 > line 3
191 191 > EOF
192 192
193 193 $ hg diff --root .
194 194 diff -r be1569354b24 file
195 195 --- a/file Thu Jan 01 00:00:00 1970 +0000
196 196 +++ b/file * (glob)
197 197 @@ -1,3 +1,3 @@
198 198 line 1
199 199 -line 2
200 200 +-- line 2, with dashes
201 201 line 3
202 202
203 203 $ hg diff --root . --stat
204 204 file | 2 +-
205 205 1 files changed, 1 insertions(+), 1 deletions(-)
206 206
207 207 Lines changed starting with '--' should count as deletions
208 208 $ hg commit -m filev2
209 209 $ cat > file << EOF
210 210 > line 1
211 211 > -- line 2, with dashes, changed again
212 212 > line 3
213 213 > EOF
214 214
215 215 $ hg diff --root .
216 216 diff -r 160f7c034df6 file
217 217 --- a/file Thu Jan 01 00:00:00 1970 +0000
218 218 +++ b/file * (glob)
219 219 @@ -1,3 +1,3 @@
220 220 line 1
221 221 --- line 2, with dashes
222 222 +-- line 2, with dashes, changed again
223 223 line 3
224 224
225 225 $ hg diff --root . --stat
226 226 file | 2 +-
227 227 1 files changed, 1 insertions(+), 1 deletions(-)
228 228
229 229 Lines changed starting with '--' should count as deletions
230 230 and starting with '++' should count as additions
231 231 $ cat > file << EOF
232 232 > line 1
233 233 > ++ line 2, switched dashes to plusses
234 234 > line 3
235 235 > EOF
236 236
237 237 $ hg diff --root .
238 238 diff -r 160f7c034df6 file
239 239 --- a/file Thu Jan 01 00:00:00 1970 +0000
240 240 +++ b/file * (glob)
241 241 @@ -1,3 +1,3 @@
242 242 line 1
243 243 --- line 2, with dashes
244 244 +++ line 2, switched dashes to plusses
245 245 line 3
246 246
247 247 $ hg diff --root . --stat
248 248 file | 2 +-
249 249 1 files changed, 1 insertions(+), 1 deletions(-)
250 250
251 251 When a file is renamed, --git shouldn't loss the info about old file
252 252 $ hg init issue6025
253 253 $ cd issue6025
254 254 $ echo > a
255 255 $ hg ci -Am 'add a'
256 256 adding a
257 257 $ hg mv a b
258 258 $ hg diff --git
259 259 diff --git a/a b/b
260 260 rename from a
261 261 rename to b
262 262 $ hg diff --stat
263 263 a | 1 -
264 264 b | 1 +
265 265 2 files changed, 1 insertions(+), 1 deletions(-)
266 266 $ hg diff --stat --git
267 267 a => b | 0
268 268 1 files changed, 0 insertions(+), 0 deletions(-)
269 269 -- filename may contain whitespaces
270 270 $ echo > c
271 271 $ hg ci -Am 'add c'
272 272 adding c
273 273 $ hg mv c 'new c'
274 274 $ hg diff --git
275 275 diff --git a/c b/new c
276 276 rename from c
277 277 rename to new c
278 278 $ hg diff --stat
279 279 c | 1 -
280 280 new c | 1 +
281 281 2 files changed, 1 insertions(+), 1 deletions(-)
282 282 $ hg diff --stat --git
283 283 c => new c | 0
284 284 1 files changed, 0 insertions(+), 0 deletions(-)
285 285
286 286 Make sure `diff --stat -q --config diff.git-0` shows stat (issue4037)
287 287
288 288 $ hg status
289 289 A new c
290 290 R c
291 291 $ hg diff --stat -q
292 c | 1 -
293 new c | 1 +
294 2 files changed, 1 insertions(+), 1 deletions(-)
@@ -1,1621 +1,1622 b''
1 1 $ checkundo()
2 2 > {
3 3 > if [ -f .hg/store/undo ]; then
4 4 > echo ".hg/store/undo still exists after $1"
5 5 > fi
6 6 > }
7 7
8 8 $ cat <<EOF >> $HGRCPATH
9 9 > [extensions]
10 10 > mq =
11 11 > [mq]
12 12 > plain = true
13 13 > EOF
14 14
15 15
16 16 help
17 17
18 18 $ hg help mq
19 19 mq extension - manage a stack of patches
20 20
21 21 This extension lets you work with a stack of patches in a Mercurial
22 22 repository. It manages two stacks of patches - all known patches, and applied
23 23 patches (subset of known patches).
24 24
25 25 Known patches are represented as patch files in the .hg/patches directory.
26 26 Applied patches are both patch files and changesets.
27 27
28 28 Common tasks (use 'hg help COMMAND' for more details):
29 29
30 30 create new patch qnew
31 31 import existing patch qimport
32 32
33 33 print patch series qseries
34 34 print applied patches qapplied
35 35
36 36 add known patch to applied stack qpush
37 37 remove patch from applied stack qpop
38 38 refresh contents of top applied patch qrefresh
39 39
40 40 By default, mq will automatically use git patches when required to avoid
41 41 losing file mode changes, copy records, binary files or empty files creations
42 42 or deletions. This behavior can be configured with:
43 43
44 44 [mq]
45 45 git = auto/keep/yes/no
46 46
47 47 If set to 'keep', mq will obey the [diff] section configuration while
48 48 preserving existing git patches upon qrefresh. If set to 'yes' or 'no', mq
49 49 will override the [diff] section and always generate git or regular patches,
50 50 possibly losing data in the second case.
51 51
52 52 It may be desirable for mq changesets to be kept in the secret phase (see 'hg
53 53 help phases'), which can be enabled with the following setting:
54 54
55 55 [mq]
56 56 secret = True
57 57
58 58 You will by default be managing a patch queue named "patches". You can create
59 59 other, independent patch queues with the 'hg qqueue' command.
60 60
61 61 If the working directory contains uncommitted files, qpush, qpop and qgoto
62 62 abort immediately. If -f/--force is used, the changes are discarded. Setting:
63 63
64 64 [mq]
65 65 keepchanges = True
66 66
67 67 make them behave as if --keep-changes were passed, and non-conflicting local
68 68 changes will be tolerated and preserved. If incompatible options such as
69 69 -f/--force or --exact are passed, this setting is ignored.
70 70
71 71 This extension used to provide a strip command. This command now lives in the
72 72 strip extension.
73 73
74 74 list of commands:
75 75
76 76 Repository creation:
77 77
78 78 qclone clone main and patch repository at same time
79 79
80 80 Change creation:
81 81
82 82 qnew create a new patch
83 83 qrefresh update the current patch
84 84
85 85 Change manipulation:
86 86
87 87 qfold fold the named patches into the current patch
88 88
89 89 Change organization:
90 90
91 91 qapplied print the patches already applied
92 92 qdelete remove patches from queue
93 93 qfinish move applied patches into repository history
94 94 qgoto push or pop patches until named patch is at top of stack
95 95 qguard set or print guards for a patch
96 96 qheader print the header of the topmost or specified patch
97 97 qnext print the name of the next pushable patch
98 98 qpop pop the current patch off the stack
99 99 qprev print the name of the preceding applied patch
100 100 qpush push the next patch onto the stack
101 101 qqueue manage multiple patch queues
102 102 qrename rename a patch
103 103 qselect set or print guarded patches to push
104 104 qseries print the entire series file
105 105 qtop print the name of the current patch
106 106 qunapplied print the patches not yet applied
107 107
108 108 File content management:
109 109
110 110 qdiff diff of the current patch and subsequent modifications
111 111
112 112 Change import/export:
113 113
114 114 qimport import a patch or existing changeset
115 115
116 116 (use 'hg help -v mq' to show built-in aliases and global options)
117 117
118 118 $ hg init a
119 119 $ cd a
120 120 $ echo a > a
121 121 $ hg ci -Ama
122 122 adding a
123 123
124 124 $ hg clone . ../k
125 125 updating to branch default
126 126 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
127 127
128 128 $ mkdir b
129 129 $ echo z > b/z
130 130 $ hg ci -Ama
131 131 adding b/z
132 132
133 133
134 134 qinit
135 135
136 136 $ hg qinit
137 137
138 138 $ cd ..
139 139 $ hg init b
140 140
141 141
142 142 -R qinit
143 143
144 144 $ hg -R b qinit
145 145
146 146 $ hg init c
147 147
148 148
149 149 qinit -c
150 150
151 151 $ hg --cwd c qinit -c
152 152 $ hg -R c/.hg/patches st
153 153 A .hgignore
154 154 A series
155 155
156 156
157 157 qinit; qinit -c
158 158
159 159 $ hg init d
160 160 $ cd d
161 161 $ hg qinit
162 162 $ hg qinit -c
163 163
164 164 qinit -c should create both files if they don't exist
165 165
166 166 $ cat .hg/patches/.hgignore
167 167 ^\.hg
168 168 ^\.mq
169 169 syntax: glob
170 170 status
171 171 guards
172 172 $ cat .hg/patches/series
173 173 $ hg qinit -c
174 174 abort: repository $TESTTMP/d/.hg/patches already exists!
175 175 [255]
176 176 $ cd ..
177 177
178 178 $ echo '% qinit; <stuff>; qinit -c'
179 179 % qinit; <stuff>; qinit -c
180 180 $ hg init e
181 181 $ cd e
182 182 $ hg qnew A
183 183 $ checkundo qnew
184 184 $ echo foo > foo
185 185 $ hg phase -r qbase
186 186 0: draft
187 187 $ hg add foo
188 188 $ hg qrefresh
189 189 $ hg phase -r qbase
190 190 0: draft
191 191 $ hg qnew B
192 192 $ echo >> foo
193 193 $ hg qrefresh
194 194 $ echo status >> .hg/patches/.hgignore
195 195 $ echo bleh >> .hg/patches/.hgignore
196 196 $ hg qinit -c
197 197 adding .hg/patches/A
198 198 adding .hg/patches/B
199 199 $ hg -R .hg/patches status
200 200 A .hgignore
201 201 A A
202 202 A B
203 203 A series
204 204
205 205 qinit -c shouldn't touch these files if they already exist
206 206
207 207 $ cat .hg/patches/.hgignore
208 208 status
209 209 bleh
210 210 $ cat .hg/patches/series
211 211 A
212 212 B
213 213
214 214 add an untracked file
215 215
216 216 $ echo >> .hg/patches/flaf
217 217
218 218 status --mq with color (issue2096)
219 219
220 220 $ hg status --mq --config extensions.color= --config color.mode=ansi --color=always
221 221 \x1b[0;32;1mA \x1b[0m\x1b[0;32;1m.hgignore\x1b[0m (esc)
222 222 \x1b[0;32;1mA \x1b[0m\x1b[0;32;1mA\x1b[0m (esc)
223 223 \x1b[0;32;1mA \x1b[0m\x1b[0;32;1mB\x1b[0m (esc)
224 224 \x1b[0;32;1mA \x1b[0m\x1b[0;32;1mseries\x1b[0m (esc)
225 225 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mflaf\x1b[0m (esc)
226 226
227 227 try the --mq option on a command provided by an extension
228 228
229 229 $ hg purge --mq --verbose --config extensions.purge=
230 230 removing file flaf
231 231
232 232 $ cd ..
233 233
234 234 #if no-outer-repo
235 235
236 236 init --mq without repo
237 237
238 238 $ mkdir f
239 239 $ cd f
240 240 $ hg init --mq
241 241 abort: there is no Mercurial repository here (.hg not found)
242 242 [255]
243 243 $ cd ..
244 244
245 245 #endif
246 246
247 247 init --mq with repo path
248 248
249 249 $ hg init g
250 250 $ hg init --mq g
251 251 $ test -d g/.hg/patches/.hg
252 252
253 253 init --mq with nonexistent directory
254 254
255 255 $ hg init --mq nonexistentdir
256 256 abort: repository nonexistentdir not found!
257 257 [255]
258 258
259 259
260 260 init --mq with bundle (non "local")
261 261
262 262 $ hg -R a bundle --all a.bundle >/dev/null
263 263 $ hg init --mq a.bundle
264 264 abort: only a local queue repository may be initialized
265 265 [255]
266 266
267 267 $ cd a
268 268
269 269 $ hg qnew -m 'foo bar' test.patch
270 270
271 271 $ echo '# comment' > .hg/patches/series.tmp
272 272 $ echo >> .hg/patches/series.tmp # empty line
273 273 $ cat .hg/patches/series >> .hg/patches/series.tmp
274 274 $ mv .hg/patches/series.tmp .hg/patches/series
275 275
276 276
277 277 qrefresh
278 278
279 279 $ echo a >> a
280 280 $ hg qrefresh
281 281 $ cat .hg/patches/test.patch
282 282 foo bar
283 283
284 284 diff -r [a-f0-9]* a (re)
285 285 --- a/a\t(?P<date>.*) (re)
286 286 \+\+\+ b/a\t(?P<date2>.*) (re)
287 287 @@ -1,1 +1,2 @@
288 288 a
289 289 +a
290 290
291 291 empty qrefresh
292 292
293 293 $ hg qrefresh -X a
294 294
295 295 revision:
296 296
297 297 $ hg diff -r -2 -r -1
298 298
299 299 patch:
300 300
301 301 $ cat .hg/patches/test.patch
302 302 foo bar
303 303
304 304
305 305 working dir diff:
306 306
307 307 $ hg diff --nodates -q
308 diff -r dde259bd5934 a
308 309 --- a/a
309 310 +++ b/a
310 311 @@ -1,1 +1,2 @@
311 312 a
312 313 +a
313 314
314 315 restore things
315 316
316 317 $ hg qrefresh
317 318 $ checkundo qrefresh
318 319
319 320
320 321 qpop
321 322
322 323 $ hg qpop
323 324 popping test.patch
324 325 patch queue now empty
325 326 $ checkundo qpop
326 327
327 328
328 329 qpush with dump of tag cache
329 330 Dump the tag cache to ensure that it has exactly one head after qpush.
330 331
331 332 $ rm -f .hg/cache/tags2-visible
332 333 $ hg tags > /dev/null
333 334
334 335 .hg/cache/tags2-visible (pre qpush):
335 336
336 337 $ cat .hg/cache/tags2-visible
337 338 1 [\da-f]{40} (re)
338 339 $ hg qpush
339 340 applying test.patch
340 341 now at: test.patch
341 342 $ hg phase -r qbase
342 343 2: draft
343 344 $ hg tags > /dev/null
344 345
345 346 .hg/cache/tags2-visible (post qpush):
346 347
347 348 $ cat .hg/cache/tags2-visible
348 349 2 [\da-f]{40} (re)
349 350 $ checkundo qpush
350 351 $ cd ..
351 352
352 353
353 354 pop/push outside repo
354 355 $ hg -R a qpop
355 356 popping test.patch
356 357 patch queue now empty
357 358 $ hg -R a qpush
358 359 applying test.patch
359 360 now at: test.patch
360 361
361 362 $ cd a
362 363 $ hg qnew test2.patch
363 364
364 365 qrefresh in subdir
365 366
366 367 $ cd b
367 368 $ echo a > a
368 369 $ hg add a
369 370 $ hg qrefresh
370 371
371 372 pop/push -a in subdir
372 373
373 374 $ hg qpop -a
374 375 popping test2.patch
375 376 popping test.patch
376 377 patch queue now empty
377 378 $ hg --traceback qpush -a
378 379 applying test.patch
379 380 applying test2.patch
380 381 now at: test2.patch
381 382
382 383
383 384 setting columns & formatted tests truncating (issue1912)
384 385
385 386 $ COLUMNS=4 hg qseries --config ui.formatted=true --color=no
386 387 test.patch
387 388 test2.patch
388 389 $ COLUMNS=20 hg qseries --config ui.formatted=true -vs --color=no
389 390 0 A test.patch: f...
390 391 1 A test2.patch:
391 392 $ hg qpop
392 393 popping test2.patch
393 394 now at: test.patch
394 395 $ hg qseries -vs
395 396 0 A test.patch: foo bar
396 397 1 U test2.patch:
397 398 $ hg sum | grep mq
398 399 mq: 1 applied, 1 unapplied
399 400 $ hg qpush
400 401 applying test2.patch
401 402 now at: test2.patch
402 403 $ hg sum | grep mq
403 404 mq: 2 applied
404 405 $ hg qapplied
405 406 test.patch
406 407 test2.patch
407 408 $ hg qtop
408 409 test2.patch
409 410
410 411
411 412 prev
412 413
413 414 $ hg qapp -1
414 415 test.patch
415 416
416 417 next
417 418
418 419 $ hg qunapp -1
419 420 all patches applied
420 421 [1]
421 422
422 423 $ hg qpop
423 424 popping test2.patch
424 425 now at: test.patch
425 426
426 427 commit should fail
427 428
428 429 $ hg commit
429 430 abort: cannot commit over an applied mq patch
430 431 [255]
431 432
432 433 push should fail if draft
433 434
434 435 $ hg push ../../k
435 436 pushing to ../../k
436 437 abort: source has mq patches applied
437 438 [255]
438 439
439 440
440 441 import should fail
441 442
442 443 $ hg st .
443 444 $ echo foo >> ../a
444 445 $ hg diff > ../../import.diff
445 446 $ hg revert --no-backup ../a
446 447 $ hg import ../../import.diff
447 448 abort: cannot import over an applied patch
448 449 [255]
449 450 $ hg st
450 451
451 452 import --no-commit should succeed
452 453
453 454 $ hg import --no-commit ../../import.diff
454 455 applying ../../import.diff
455 456 $ hg st
456 457 M a
457 458 $ hg revert --no-backup ../a
458 459
459 460
460 461 qunapplied
461 462
462 463 $ hg qunapplied
463 464 test2.patch
464 465
465 466
466 467 qpush/qpop with index
467 468
468 469 $ hg qnew test1b.patch
469 470 $ echo 1b > 1b
470 471 $ hg add 1b
471 472 $ hg qrefresh
472 473 $ hg qpush 2
473 474 applying test2.patch
474 475 now at: test2.patch
475 476 $ hg qpop 0
476 477 popping test2.patch
477 478 popping test1b.patch
478 479 now at: test.patch
479 480 $ hg qpush test.patch+1
480 481 applying test1b.patch
481 482 now at: test1b.patch
482 483 $ hg qpush test.patch+2
483 484 applying test2.patch
484 485 now at: test2.patch
485 486 $ hg qpop test2.patch-1
486 487 popping test2.patch
487 488 now at: test1b.patch
488 489 $ hg qpop test2.patch-2
489 490 popping test1b.patch
490 491 now at: test.patch
491 492 $ hg qpush test1b.patch+1
492 493 applying test1b.patch
493 494 applying test2.patch
494 495 now at: test2.patch
495 496
496 497
497 498 qpush --move
498 499
499 500 $ hg qpop -a
500 501 popping test2.patch
501 502 popping test1b.patch
502 503 popping test.patch
503 504 patch queue now empty
504 505 $ hg qguard test1b.patch -- -negguard
505 506 $ hg qguard test2.patch -- +posguard
506 507 $ hg qpush --move test2.patch # can't move guarded patch
507 508 cannot push 'test2.patch' - guarded by '+posguard'
508 509 [1]
509 510 $ hg qselect posguard
510 511 number of unguarded, unapplied patches has changed from 2 to 3
511 512 $ hg qpush --move test2.patch # move to front
512 513 applying test2.patch
513 514 now at: test2.patch
514 515 $ hg qpush --move test1b.patch # negative guard unselected
515 516 applying test1b.patch
516 517 now at: test1b.patch
517 518 $ hg qpush --move test.patch # noop move
518 519 applying test.patch
519 520 now at: test.patch
520 521 $ hg qseries -v
521 522 0 A test2.patch
522 523 1 A test1b.patch
523 524 2 A test.patch
524 525 $ hg qpop -a
525 526 popping test.patch
526 527 popping test1b.patch
527 528 popping test2.patch
528 529 patch queue now empty
529 530
530 531 cleaning up
531 532
532 533 $ hg qselect --none
533 534 guards deactivated
534 535 number of unguarded, unapplied patches has changed from 3 to 2
535 536 $ hg qguard --none test1b.patch
536 537 $ hg qguard --none test2.patch
537 538 $ hg qpush --move test.patch
538 539 applying test.patch
539 540 now at: test.patch
540 541 $ hg qpush --move test1b.patch
541 542 applying test1b.patch
542 543 now at: test1b.patch
543 544 $ hg qpush --move bogus # nonexistent patch
544 545 abort: patch bogus not in series
545 546 [255]
546 547 $ hg qpush --move # no patch
547 548 abort: please specify the patch to move
548 549 [255]
549 550 $ hg qpush --move test.patch # already applied
550 551 abort: cannot push to a previous patch: test.patch
551 552 [255]
552 553 $ sed '2i\
553 554 > # make qtip index different in series and fullseries
554 555 > ' `hg root`/.hg/patches/series > $TESTTMP/sedtmp
555 556 $ cp $TESTTMP/sedtmp `hg root`/.hg/patches/series
556 557 $ cat `hg root`/.hg/patches/series
557 558 # comment
558 559 # make qtip index different in series and fullseries
559 560
560 561 test.patch
561 562 test1b.patch
562 563 test2.patch
563 564 $ hg qpush --move test2.patch
564 565 applying test2.patch
565 566 now at: test2.patch
566 567
567 568
568 569 series after move
569 570
570 571 $ cat `hg root`/.hg/patches/series
571 572 # comment
572 573 # make qtip index different in series and fullseries
573 574
574 575 test.patch
575 576 test1b.patch
576 577 test2.patch
577 578
578 579
579 580 pop, qapplied, qunapplied
580 581
581 582 $ hg qseries -v
582 583 0 A test.patch
583 584 1 A test1b.patch
584 585 2 A test2.patch
585 586
586 587 qapplied -1 test.patch
587 588
588 589 $ hg qapplied -1 test.patch
589 590 only one patch applied
590 591 [1]
591 592
592 593 qapplied -1 test1b.patch
593 594
594 595 $ hg qapplied -1 test1b.patch
595 596 test.patch
596 597
597 598 qapplied -1 test2.patch
598 599
599 600 $ hg qapplied -1 test2.patch
600 601 test1b.patch
601 602
602 603 qapplied -1
603 604
604 605 $ hg qapplied -1
605 606 test1b.patch
606 607
607 608 qapplied
608 609
609 610 $ hg qapplied
610 611 test.patch
611 612 test1b.patch
612 613 test2.patch
613 614
614 615 qapplied test1b.patch
615 616
616 617 $ hg qapplied test1b.patch
617 618 test.patch
618 619 test1b.patch
619 620
620 621 qunapplied -1
621 622
622 623 $ hg qunapplied -1
623 624 all patches applied
624 625 [1]
625 626
626 627 qunapplied
627 628
628 629 $ hg qunapplied
629 630
630 631 popping
631 632
632 633 $ hg qpop
633 634 popping test2.patch
634 635 now at: test1b.patch
635 636
636 637 qunapplied -1
637 638
638 639 $ hg qunapplied -1
639 640 test2.patch
640 641
641 642 qunapplied
642 643
643 644 $ hg qunapplied
644 645 test2.patch
645 646
646 647 qunapplied test2.patch
647 648
648 649 $ hg qunapplied test2.patch
649 650
650 651 qunapplied -1 test2.patch
651 652
652 653 $ hg qunapplied -1 test2.patch
653 654 all patches applied
654 655 [1]
655 656
656 657 popping -a
657 658
658 659 $ hg qpop -a
659 660 popping test1b.patch
660 661 popping test.patch
661 662 patch queue now empty
662 663
663 664 qapplied
664 665
665 666 $ hg qapplied
666 667
667 668 qapplied -1
668 669
669 670 $ hg qapplied -1
670 671 no patches applied
671 672 [1]
672 673 $ hg qpush
673 674 applying test.patch
674 675 now at: test.patch
675 676
676 677
677 678 push should succeed
678 679
679 680 $ hg qpop -a
680 681 popping test.patch
681 682 patch queue now empty
682 683 $ hg push ../../k
683 684 pushing to ../../k
684 685 searching for changes
685 686 adding changesets
686 687 adding manifests
687 688 adding file changes
688 689 added 1 changesets with 1 changes to 1 files
689 690
690 691
691 692 we want to start with some patches applied
692 693
693 694 $ hg qpush -a
694 695 applying test.patch
695 696 applying test1b.patch
696 697 applying test2.patch
697 698 now at: test2.patch
698 699
699 700 % pops all patches and succeeds
700 701
701 702 $ hg qpop -a
702 703 popping test2.patch
703 704 popping test1b.patch
704 705 popping test.patch
705 706 patch queue now empty
706 707
707 708 % does nothing and succeeds
708 709
709 710 $ hg qpop -a
710 711 no patches applied
711 712
712 713 % fails - nothing else to pop
713 714
714 715 $ hg qpop
715 716 no patches applied
716 717 [1]
717 718
718 719 % pushes a patch and succeeds
719 720
720 721 $ hg qpush
721 722 applying test.patch
722 723 now at: test.patch
723 724
724 725 % pops a patch and succeeds
725 726
726 727 $ hg qpop
727 728 popping test.patch
728 729 patch queue now empty
729 730
730 731 % pushes up to test1b.patch and succeeds
731 732
732 733 $ hg qpush test1b.patch
733 734 applying test.patch
734 735 applying test1b.patch
735 736 now at: test1b.patch
736 737
737 738 % does nothing and succeeds
738 739
739 740 $ hg qpush test1b.patch
740 741 qpush: test1b.patch is already at the top
741 742
742 743 % does nothing and succeeds
743 744
744 745 $ hg qpop test1b.patch
745 746 qpop: test1b.patch is already at the top
746 747
747 748 % fails - can't push to this patch
748 749
749 750 $ hg qpush test.patch
750 751 abort: cannot push to a previous patch: test.patch
751 752 [255]
752 753
753 754 % fails - can't pop to this patch
754 755
755 756 $ hg qpop test2.patch
756 757 abort: patch test2.patch is not applied
757 758 [255]
758 759
759 760 % pops up to test.patch and succeeds
760 761
761 762 $ hg qpop test.patch
762 763 popping test1b.patch
763 764 now at: test.patch
764 765
765 766 % pushes all patches and succeeds
766 767
767 768 $ hg qpush -a
768 769 applying test1b.patch
769 770 applying test2.patch
770 771 now at: test2.patch
771 772
772 773 % does nothing and succeeds
773 774
774 775 $ hg qpush -a
775 776 all patches are currently applied
776 777
777 778 % fails - nothing else to push
778 779
779 780 $ hg qpush
780 781 patch series already fully applied
781 782 [1]
782 783
783 784 % does nothing and succeeds
784 785
785 786 $ hg qpush test2.patch
786 787 qpush: test2.patch is already at the top
787 788
788 789 strip
789 790
790 791 $ cd ../../b
791 792 $ echo x>x
792 793 $ hg ci -Ama
793 794 adding x
794 795 $ hg strip tip
795 796 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
796 797 saved backup bundle to $TESTTMP/b/.hg/strip-backup/*-backup.hg (glob)
797 798 $ hg unbundle .hg/strip-backup/*
798 799 adding changesets
799 800 adding manifests
800 801 adding file changes
801 802 added 1 changesets with 1 changes to 1 files
802 803 new changesets 770eb8fce608 (1 drafts)
803 804 (run 'hg update' to get a working copy)
804 805
805 806
806 807 strip with local changes, should complain
807 808
808 809 $ hg up
809 810 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
810 811 $ echo y>y
811 812 $ hg add y
812 813 $ hg strip tip
813 814 abort: local changes found
814 815 [255]
815 816
816 817 --force strip with local changes
817 818
818 819 $ hg strip -f tip
819 820 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
820 821 saved backup bundle to $TESTTMP/b/.hg/strip-backup/770eb8fce608-0ddcae0f-backup.hg
821 822 $ cd ..
822 823
823 824
824 825 cd b; hg qrefresh
825 826
826 827 $ hg init refresh
827 828 $ cd refresh
828 829 $ echo a > a
829 830 $ hg ci -Ama
830 831 adding a
831 832 $ hg qnew -mfoo foo
832 833 $ echo a >> a
833 834 $ hg qrefresh
834 835 $ mkdir b
835 836 $ cd b
836 837 $ echo f > f
837 838 $ hg add f
838 839 $ hg qrefresh
839 840 $ cat ../.hg/patches/foo
840 841 foo
841 842
842 843 diff -r cb9a9f314b8b a
843 844 --- a/a\t(?P<date>.*) (re)
844 845 \+\+\+ b/a\t(?P<date>.*) (re)
845 846 @@ -1,1 +1,2 @@
846 847 a
847 848 +a
848 849 diff -r cb9a9f314b8b b/f
849 850 --- /dev/null\t(?P<date>.*) (re)
850 851 \+\+\+ b/b/f\t(?P<date>.*) (re)
851 852 @@ -0,0 +1,1 @@
852 853 +f
853 854
854 855 hg qrefresh .
855 856
856 857 $ hg qrefresh .
857 858 $ cat ../.hg/patches/foo
858 859 foo
859 860
860 861 diff -r cb9a9f314b8b b/f
861 862 --- /dev/null\t(?P<date>.*) (re)
862 863 \+\+\+ b/b/f\t(?P<date>.*) (re)
863 864 @@ -0,0 +1,1 @@
864 865 +f
865 866 $ hg status
866 867 M a
867 868
868 869
869 870 qpush failure
870 871
871 872 $ cd ..
872 873 $ hg qrefresh
873 874 $ hg qnew -mbar bar
874 875 $ echo foo > foo
875 876 $ echo bar > bar
876 877 $ hg add foo bar
877 878 $ hg qrefresh
878 879 $ hg qpop -a
879 880 popping bar
880 881 popping foo
881 882 patch queue now empty
882 883 $ echo bar > foo
883 884 $ hg qpush -a
884 885 applying foo
885 886 applying bar
886 887 file foo already exists
887 888 1 out of 1 hunks FAILED -- saving rejects to file foo.rej
888 889 patch failed, unable to continue (try -v)
889 890 patch failed, rejects left in working directory
890 891 errors during apply, please fix and qrefresh bar
891 892 [2]
892 893 $ hg st
893 894 ? foo
894 895 ? foo.rej
895 896
896 897
897 898 mq tags
898 899
899 900 $ hg log --template '{rev} {tags}\n' -r qparent:qtip
900 901 0 qparent
901 902 1 foo qbase
902 903 2 bar qtip tip
903 904
904 905 mq revset
905 906
906 907 $ hg log -r 'mq()' --template '{rev}\n'
907 908 1
908 909 2
909 910 $ hg help revisions.mq
910 911 "mq()"
911 912 Changesets managed by MQ.
912 913
913 914
914 915 bad node in status
915 916
916 917 $ hg qpop
917 918 popping bar
918 919 now at: foo
919 920 $ hg strip -qn tip
920 921 $ hg tip
921 922 changeset: 0:cb9a9f314b8b
922 923 tag: tip
923 924 user: test
924 925 date: Thu Jan 01 00:00:00 1970 +0000
925 926 summary: a
926 927
927 928 $ hg branches
928 929 default 0:cb9a9f314b8b
929 930 $ hg qpop
930 931 no patches applied
931 932 [1]
932 933
933 934 $ cd ..
934 935
935 936
936 937 git patches
937 938
938 939 $ cat >>$HGRCPATH <<EOF
939 940 > [diff]
940 941 > git = True
941 942 > EOF
942 943 $ hg init git
943 944 $ cd git
944 945 $ hg qinit
945 946
946 947 $ hg qnew -m'new file' new
947 948 $ echo foo > new
948 949 #if execbit
949 950 $ chmod +x new
950 951 #endif
951 952 $ hg add new
952 953 $ hg qrefresh
953 954
954 955 $ cat .hg/patches/new
955 956 new file
956 957
957 958 diff --git a/new b/new
958 959 new file mode 100755 (execbit !)
959 960 new file mode 100644 (no-execbit !)
960 961 --- /dev/null
961 962 +++ b/new
962 963 @@ -0,0 +1,1 @@
963 964 +foo
964 965
965 966 $ hg qnew -m'copy file' copy
966 967 $ hg cp new copy
967 968 $ hg qrefresh
968 969 $ cat .hg/patches/copy
969 970 copy file
970 971
971 972 diff --git a/new b/copy
972 973 copy from new
973 974 copy to copy
974 975
975 976 $ hg qpop
976 977 popping copy
977 978 now at: new
978 979 $ hg qpush
979 980 applying copy
980 981 now at: copy
981 982 $ hg qdiff
982 983 diff --git a/new b/copy
983 984 copy from new
984 985 copy to copy
985 986 $ cat >>$HGRCPATH <<EOF
986 987 > [diff]
987 988 > git = False
988 989 > EOF
989 990 $ hg qdiff --git
990 991 diff --git a/new b/copy
991 992 copy from new
992 993 copy to copy
993 994 $ cd ..
994 995
995 996 empty lines in status
996 997
997 998 $ hg init emptystatus
998 999 $ cd emptystatus
999 1000 $ hg qinit
1000 1001 $ printf '\n\n' > .hg/patches/status
1001 1002 $ hg qser
1002 1003 $ cd ..
1003 1004
1004 1005 bad line in status (without ":")
1005 1006
1006 1007 $ hg init badstatus
1007 1008 $ cd badstatus
1008 1009 $ hg qinit
1009 1010 $ printf 'babar has no colon in this line\n' > .hg/patches/status
1010 1011 $ hg qser
1011 1012 malformated mq status line: ['babar has no colon in this line']
1012 1013 $ cd ..
1013 1014
1014 1015
1015 1016 test file addition in slow path
1016 1017
1017 1018 $ hg init slow
1018 1019 $ cd slow
1019 1020 $ hg qinit
1020 1021 $ echo foo > foo
1021 1022 $ hg add foo
1022 1023 $ hg ci -m 'add foo'
1023 1024 $ hg qnew bar
1024 1025 $ echo bar > bar
1025 1026 $ hg add bar
1026 1027 $ hg mv foo baz
1027 1028 $ hg qrefresh --git
1028 1029 $ hg up -C 0
1029 1030 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
1030 1031 $ echo >> foo
1031 1032 $ hg ci -m 'change foo'
1032 1033 created new head
1033 1034 $ hg up -C 1
1034 1035 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
1035 1036 $ hg qrefresh --git
1036 1037 $ cat .hg/patches/bar
1037 1038 diff --git a/bar b/bar
1038 1039 new file mode 100644
1039 1040 --- /dev/null
1040 1041 +++ b/bar
1041 1042 @@ -0,0 +1,1 @@
1042 1043 +bar
1043 1044 diff --git a/foo b/baz
1044 1045 rename from foo
1045 1046 rename to baz
1046 1047 $ hg log -v --template '{rev} {file_copies}\n' -r .
1047 1048 2 baz (foo)
1048 1049 $ hg qrefresh --git
1049 1050 $ cat .hg/patches/bar
1050 1051 diff --git a/bar b/bar
1051 1052 new file mode 100644
1052 1053 --- /dev/null
1053 1054 +++ b/bar
1054 1055 @@ -0,0 +1,1 @@
1055 1056 +bar
1056 1057 diff --git a/foo b/baz
1057 1058 rename from foo
1058 1059 rename to baz
1059 1060 $ hg log -v --template '{rev} {file_copies}\n' -r .
1060 1061 2 baz (foo)
1061 1062 $ hg qrefresh
1062 1063 $ grep 'diff --git' .hg/patches/bar
1063 1064 diff --git a/bar b/bar
1064 1065 diff --git a/foo b/baz
1065 1066
1066 1067
1067 1068 test file move chains in the slow path
1068 1069
1069 1070 $ hg up -C 1
1070 1071 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
1071 1072 $ echo >> foo
1072 1073 $ hg ci -m 'change foo again'
1073 1074 $ hg up -C 2
1074 1075 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
1075 1076 $ hg mv bar quux
1076 1077 $ hg mv baz bleh
1077 1078 $ hg qrefresh --git
1078 1079 $ cat .hg/patches/bar
1079 1080 diff --git a/foo b/bleh
1080 1081 rename from foo
1081 1082 rename to bleh
1082 1083 diff --git a/quux b/quux
1083 1084 new file mode 100644
1084 1085 --- /dev/null
1085 1086 +++ b/quux
1086 1087 @@ -0,0 +1,1 @@
1087 1088 +bar
1088 1089 $ hg log -v --template '{rev} {file_copies}\n' -r .
1089 1090 3 bleh (foo)
1090 1091 $ hg mv quux fred
1091 1092 $ hg mv bleh barney
1092 1093 $ hg qrefresh --git
1093 1094 $ cat .hg/patches/bar
1094 1095 diff --git a/foo b/barney
1095 1096 rename from foo
1096 1097 rename to barney
1097 1098 diff --git a/fred b/fred
1098 1099 new file mode 100644
1099 1100 --- /dev/null
1100 1101 +++ b/fred
1101 1102 @@ -0,0 +1,1 @@
1102 1103 +bar
1103 1104 $ hg log -v --template '{rev} {file_copies}\n' -r .
1104 1105 3 barney (foo)
1105 1106
1106 1107
1107 1108 refresh omitting an added file
1108 1109
1109 1110 $ hg qnew baz
1110 1111 $ echo newfile > newfile
1111 1112 $ hg add newfile
1112 1113 $ hg qrefresh
1113 1114 $ hg st -A newfile
1114 1115 C newfile
1115 1116 $ hg qrefresh -X newfile
1116 1117 $ hg st -A newfile
1117 1118 A newfile
1118 1119 $ hg revert newfile
1119 1120 $ rm newfile
1120 1121 $ hg qpop
1121 1122 popping baz
1122 1123 now at: bar
1123 1124
1124 1125 test qdel/qrm
1125 1126
1126 1127 $ hg qdel baz
1127 1128 $ echo p >> .hg/patches/series
1128 1129 $ hg qrm p
1129 1130 $ hg qser
1130 1131 bar
1131 1132
1132 1133 create a git patch
1133 1134
1134 1135 $ echo a > alexander
1135 1136 $ hg add alexander
1136 1137 $ hg qnew -f --git addalexander
1137 1138 $ grep diff .hg/patches/addalexander
1138 1139 diff --git a/alexander b/alexander
1139 1140
1140 1141
1141 1142 create a git binary patch
1142 1143
1143 1144 $ cat > writebin.py <<EOF
1144 1145 > import sys
1145 1146 > path = sys.argv[1]
1146 1147 > open(path, 'wb').write(b'BIN\x00ARY')
1147 1148 > EOF
1148 1149 $ "$PYTHON" writebin.py bucephalus
1149 1150
1150 1151 $ "$PYTHON" "$TESTDIR/md5sum.py" bucephalus
1151 1152 8ba2a2f3e77b55d03051ff9c24ad65e7 bucephalus
1152 1153 $ hg add bucephalus
1153 1154 $ hg qnew -f --git addbucephalus
1154 1155 $ grep diff .hg/patches/addbucephalus
1155 1156 diff --git a/bucephalus b/bucephalus
1156 1157
1157 1158
1158 1159 check binary patches can be popped and pushed
1159 1160
1160 1161 $ hg qpop
1161 1162 popping addbucephalus
1162 1163 now at: addalexander
1163 1164 $ test -f bucephalus && echo % bucephalus should not be there
1164 1165 [1]
1165 1166 $ hg qpush
1166 1167 applying addbucephalus
1167 1168 now at: addbucephalus
1168 1169 $ test -f bucephalus
1169 1170 $ "$PYTHON" "$TESTDIR/md5sum.py" bucephalus
1170 1171 8ba2a2f3e77b55d03051ff9c24ad65e7 bucephalus
1171 1172
1172 1173
1173 1174
1174 1175 strip again
1175 1176
1176 1177 $ cd ..
1177 1178 $ hg init strip
1178 1179 $ cd strip
1179 1180 $ touch foo
1180 1181 $ hg add foo
1181 1182 $ hg ci -m 'add foo'
1182 1183 $ echo >> foo
1183 1184 $ hg ci -m 'change foo 1'
1184 1185 $ hg up -C 0
1185 1186 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1186 1187 $ echo 1 >> foo
1187 1188 $ hg ci -m 'change foo 2'
1188 1189 created new head
1189 1190 $ HGMERGE=true hg merge
1190 1191 merging foo
1191 1192 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1192 1193 (branch merge, don't forget to commit)
1193 1194 $ hg ci -m merge
1194 1195 $ hg log
1195 1196 changeset: 3:99615015637b
1196 1197 tag: tip
1197 1198 parent: 2:20cbbe65cff7
1198 1199 parent: 1:d2871fc282d4
1199 1200 user: test
1200 1201 date: Thu Jan 01 00:00:00 1970 +0000
1201 1202 summary: merge
1202 1203
1203 1204 changeset: 2:20cbbe65cff7
1204 1205 parent: 0:53245c60e682
1205 1206 user: test
1206 1207 date: Thu Jan 01 00:00:00 1970 +0000
1207 1208 summary: change foo 2
1208 1209
1209 1210 changeset: 1:d2871fc282d4
1210 1211 user: test
1211 1212 date: Thu Jan 01 00:00:00 1970 +0000
1212 1213 summary: change foo 1
1213 1214
1214 1215 changeset: 0:53245c60e682
1215 1216 user: test
1216 1217 date: Thu Jan 01 00:00:00 1970 +0000
1217 1218 summary: add foo
1218 1219
1219 1220 $ hg strip 1
1220 1221 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1221 1222 saved backup bundle to $TESTTMP/strip/.hg/strip-backup/*-backup.hg (glob)
1222 1223 $ checkundo strip
1223 1224 $ hg log
1224 1225 changeset: 1:20cbbe65cff7
1225 1226 tag: tip
1226 1227 user: test
1227 1228 date: Thu Jan 01 00:00:00 1970 +0000
1228 1229 summary: change foo 2
1229 1230
1230 1231 changeset: 0:53245c60e682
1231 1232 user: test
1232 1233 date: Thu Jan 01 00:00:00 1970 +0000
1233 1234 summary: add foo
1234 1235
1235 1236 $ cd ..
1236 1237
1237 1238
1238 1239 qclone
1239 1240
1240 1241 $ qlog()
1241 1242 > {
1242 1243 > echo 'main repo:'
1243 1244 > hg log --template ' rev {rev}: {desc}\n'
1244 1245 > echo 'patch repo:'
1245 1246 > hg -R .hg/patches log --template ' rev {rev}: {desc}\n'
1246 1247 > }
1247 1248 $ hg init qclonesource
1248 1249 $ cd qclonesource
1249 1250 $ echo foo > foo
1250 1251 $ hg add foo
1251 1252 $ hg ci -m 'add foo'
1252 1253 $ hg qinit
1253 1254 $ hg qnew patch1
1254 1255 $ echo bar >> foo
1255 1256 $ hg qrefresh -m 'change foo'
1256 1257 $ cd ..
1257 1258
1258 1259
1259 1260 repo with unversioned patch dir
1260 1261
1261 1262 $ hg qclone qclonesource failure
1262 1263 abort: versioned patch repository not found (see init --mq)
1263 1264 [255]
1264 1265
1265 1266 $ cd qclonesource
1266 1267 $ hg qinit -c
1267 1268 adding .hg/patches/patch1
1268 1269 $ hg qci -m checkpoint
1269 1270 $ qlog
1270 1271 main repo:
1271 1272 rev 1: change foo
1272 1273 rev 0: add foo
1273 1274 patch repo:
1274 1275 rev 0: checkpoint
1275 1276 $ cd ..
1276 1277
1277 1278
1278 1279 repo with patches applied
1279 1280
1280 1281 $ hg qclone qclonesource qclonedest
1281 1282 updating to branch default
1282 1283 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
1283 1284 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1284 1285 $ cd qclonedest
1285 1286 $ qlog
1286 1287 main repo:
1287 1288 rev 0: add foo
1288 1289 patch repo:
1289 1290 rev 0: checkpoint
1290 1291 $ cd ..
1291 1292
1292 1293
1293 1294 repo with patches unapplied
1294 1295
1295 1296 $ cd qclonesource
1296 1297 $ hg qpop -a
1297 1298 popping patch1
1298 1299 patch queue now empty
1299 1300 $ qlog
1300 1301 main repo:
1301 1302 rev 0: add foo
1302 1303 patch repo:
1303 1304 rev 0: checkpoint
1304 1305 $ cd ..
1305 1306 $ hg qclone qclonesource qclonedest2
1306 1307 updating to branch default
1307 1308 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
1308 1309 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1309 1310 $ cd qclonedest2
1310 1311 $ qlog
1311 1312 main repo:
1312 1313 rev 0: add foo
1313 1314 patch repo:
1314 1315 rev 0: checkpoint
1315 1316 $ cd ..
1316 1317
1317 1318
1318 1319 Issue1033: test applying on an empty file
1319 1320
1320 1321 $ hg init empty
1321 1322 $ cd empty
1322 1323 $ touch a
1323 1324 $ hg ci -Am addempty
1324 1325 adding a
1325 1326 $ echo a > a
1326 1327 $ hg qnew -f -e changea
1327 1328 $ hg qpop
1328 1329 popping changea
1329 1330 patch queue now empty
1330 1331 $ hg qpush
1331 1332 applying changea
1332 1333 now at: changea
1333 1334 $ cd ..
1334 1335
1335 1336 test qpush with --force, issue1087
1336 1337
1337 1338 $ hg init forcepush
1338 1339 $ cd forcepush
1339 1340 $ echo hello > hello.txt
1340 1341 $ echo bye > bye.txt
1341 1342 $ hg ci -Ama
1342 1343 adding bye.txt
1343 1344 adding hello.txt
1344 1345 $ hg qnew -d '0 0' empty
1345 1346 $ hg qpop
1346 1347 popping empty
1347 1348 patch queue now empty
1348 1349 $ echo world >> hello.txt
1349 1350
1350 1351
1351 1352 qpush should fail, local changes
1352 1353
1353 1354 $ hg qpush
1354 1355 abort: local changes found
1355 1356 [255]
1356 1357
1357 1358
1358 1359 apply force, should not discard changes with empty patch
1359 1360
1360 1361 $ hg qpush -f
1361 1362 applying empty
1362 1363 patch empty is empty
1363 1364 now at: empty
1364 1365 $ hg diff --config diff.nodates=True
1365 1366 diff -r d58265112590 hello.txt
1366 1367 --- a/hello.txt
1367 1368 +++ b/hello.txt
1368 1369 @@ -1,1 +1,2 @@
1369 1370 hello
1370 1371 +world
1371 1372 $ hg qdiff --config diff.nodates=True
1372 1373 diff -r 9ecee4f634e3 hello.txt
1373 1374 --- a/hello.txt
1374 1375 +++ b/hello.txt
1375 1376 @@ -1,1 +1,2 @@
1376 1377 hello
1377 1378 +world
1378 1379 $ hg log -l1 -p
1379 1380 changeset: 1:d58265112590
1380 1381 tag: empty
1381 1382 tag: qbase
1382 1383 tag: qtip
1383 1384 tag: tip
1384 1385 user: test
1385 1386 date: Thu Jan 01 00:00:00 1970 +0000
1386 1387 summary: imported patch empty
1387 1388
1388 1389
1389 1390 $ hg qref -d '0 0'
1390 1391 $ hg qpop
1391 1392 popping empty
1392 1393 patch queue now empty
1393 1394 $ echo universe >> hello.txt
1394 1395 $ echo universe >> bye.txt
1395 1396
1396 1397
1397 1398 qpush should fail, local changes
1398 1399
1399 1400 $ hg qpush
1400 1401 abort: local changes found
1401 1402 [255]
1402 1403
1403 1404
1404 1405 apply force, should discard changes in hello, but not bye
1405 1406
1406 1407 $ hg qpush -f --verbose --config 'ui.origbackuppath=.hg/origbackups'
1407 1408 applying empty
1408 1409 creating directory: $TESTTMP/forcepush/.hg/origbackups
1409 1410 saving current version of hello.txt as .hg/origbackups/hello.txt
1410 1411 patching file hello.txt
1411 1412 committing files:
1412 1413 hello.txt
1413 1414 committing manifest
1414 1415 committing changelog
1415 1416 now at: empty
1416 1417 $ hg st
1417 1418 M bye.txt
1418 1419 $ hg diff --config diff.nodates=True
1419 1420 diff -r ba252371dbc1 bye.txt
1420 1421 --- a/bye.txt
1421 1422 +++ b/bye.txt
1422 1423 @@ -1,1 +1,2 @@
1423 1424 bye
1424 1425 +universe
1425 1426 $ hg qdiff --config diff.nodates=True
1426 1427 diff -r 9ecee4f634e3 bye.txt
1427 1428 --- a/bye.txt
1428 1429 +++ b/bye.txt
1429 1430 @@ -1,1 +1,2 @@
1430 1431 bye
1431 1432 +universe
1432 1433 diff -r 9ecee4f634e3 hello.txt
1433 1434 --- a/hello.txt
1434 1435 +++ b/hello.txt
1435 1436 @@ -1,1 +1,3 @@
1436 1437 hello
1437 1438 +world
1438 1439 +universe
1439 1440
1440 1441 test that the previous call to qpush with -f (--force) and --config actually put
1441 1442 the orig files out of the working copy
1442 1443 $ ls .hg/origbackups
1443 1444 hello.txt
1444 1445
1445 1446 test popping revisions not in working dir ancestry
1446 1447
1447 1448 $ hg qseries -v
1448 1449 0 A empty
1449 1450 $ hg up qparent
1450 1451 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1451 1452 $ hg qpop
1452 1453 popping empty
1453 1454 patch queue now empty
1454 1455
1455 1456 $ cd ..
1456 1457 $ hg init deletion-order
1457 1458 $ cd deletion-order
1458 1459
1459 1460 $ touch a
1460 1461 $ hg ci -Aqm0
1461 1462
1462 1463 $ hg qnew rename-dir
1463 1464 $ hg rm a
1464 1465 $ hg qrefresh
1465 1466
1466 1467 $ mkdir a b
1467 1468 $ touch a/a b/b
1468 1469 $ hg add -q a b
1469 1470 $ hg qrefresh
1470 1471
1471 1472
1472 1473 test popping must remove files added in subdirectories first
1473 1474
1474 1475 $ hg qpop
1475 1476 popping rename-dir
1476 1477 patch queue now empty
1477 1478 $ cd ..
1478 1479
1479 1480
1480 1481 test case preservation through patch pushing especially on case
1481 1482 insensitive filesystem
1482 1483
1483 1484 $ hg init casepreserve
1484 1485 $ cd casepreserve
1485 1486
1486 1487 $ hg qnew add-file1
1487 1488 $ echo a > TeXtFiLe.TxT
1488 1489 $ hg add TeXtFiLe.TxT
1489 1490 $ hg qrefresh
1490 1491
1491 1492 $ hg qnew add-file2
1492 1493 $ echo b > AnOtHeRFiLe.TxT
1493 1494 $ hg add AnOtHeRFiLe.TxT
1494 1495 $ hg qrefresh
1495 1496
1496 1497 $ hg qnew modify-file
1497 1498 $ echo c >> AnOtHeRFiLe.TxT
1498 1499 $ hg qrefresh
1499 1500
1500 1501 $ hg qapplied
1501 1502 add-file1
1502 1503 add-file2
1503 1504 modify-file
1504 1505 $ hg qpop -a
1505 1506 popping modify-file
1506 1507 popping add-file2
1507 1508 popping add-file1
1508 1509 patch queue now empty
1509 1510
1510 1511 this qpush causes problems below, if case preservation on case
1511 1512 insensitive filesystem is not enough:
1512 1513 (1) unexpected "adding ..." messages are shown
1513 1514 (2) patching fails in modification of (1) files
1514 1515
1515 1516 $ hg qpush -a
1516 1517 applying add-file1
1517 1518 applying add-file2
1518 1519 applying modify-file
1519 1520 now at: modify-file
1520 1521
1521 1522 Proper phase default with mq:
1522 1523
1523 1524 1. mq.secret=false
1524 1525
1525 1526 $ rm .hg/store/phaseroots
1526 1527 $ hg phase 'qparent::'
1527 1528 -1: public
1528 1529 0: draft
1529 1530 1: draft
1530 1531 2: draft
1531 1532 $ echo '[mq]' >> $HGRCPATH
1532 1533 $ echo 'secret=true' >> $HGRCPATH
1533 1534 $ rm -f .hg/store/phaseroots
1534 1535 $ hg phase 'qparent::'
1535 1536 -1: public
1536 1537 0: secret
1537 1538 1: secret
1538 1539 2: secret
1539 1540
1540 1541 Test that qfinish change phase when mq.secret=true
1541 1542
1542 1543 $ hg qfinish qbase
1543 1544 patch add-file1 finalized without changeset message
1544 1545 $ hg phase 'all()'
1545 1546 0: draft
1546 1547 1: secret
1547 1548 2: secret
1548 1549
1549 1550 Test that qfinish respect phases.new-commit setting
1550 1551
1551 1552 $ echo '[phases]' >> $HGRCPATH
1552 1553 $ echo 'new-commit=secret' >> $HGRCPATH
1553 1554 $ hg qfinish qbase
1554 1555 patch add-file2 finalized without changeset message
1555 1556 $ hg phase 'all()'
1556 1557 0: draft
1557 1558 1: secret
1558 1559 2: secret
1559 1560
1560 1561 (restore env for next test)
1561 1562
1562 1563 $ sed -e 's/new-commit=secret//' $HGRCPATH > $TESTTMP/sedtmp
1563 1564 $ cp $TESTTMP/sedtmp $HGRCPATH
1564 1565 $ hg qimport -r 1 --name add-file2
1565 1566
1566 1567 Test that qfinish preserve phase when mq.secret=false
1567 1568
1568 1569 $ sed -e 's/secret=true/secret=false/' $HGRCPATH > $TESTTMP/sedtmp
1569 1570 $ cp $TESTTMP/sedtmp $HGRCPATH
1570 1571 $ hg qfinish qbase
1571 1572 patch add-file2 finalized without changeset message
1572 1573 $ hg phase 'all()'
1573 1574 0: draft
1574 1575 1: secret
1575 1576 2: secret
1576 1577
1577 1578 Test that secret mq patch does not break hgweb
1578 1579
1579 1580 $ cat > hgweb.cgi <<HGWEB
1580 1581 > from mercurial import demandimport; demandimport.enable()
1581 1582 > from mercurial.hgweb import hgweb
1582 1583 > from mercurial.hgweb import wsgicgi
1583 1584 > import cgitb
1584 1585 > cgitb.enable()
1585 1586 > app = hgweb(b'.', b'test')
1586 1587 > wsgicgi.launch(app)
1587 1588 > HGWEB
1588 1589 $ . "$TESTDIR/cgienv"
1589 1590 #if msys
1590 1591 $ PATH_INFO=//tags; export PATH_INFO
1591 1592 #else
1592 1593 $ PATH_INFO=/tags; export PATH_INFO
1593 1594 #endif
1594 1595 $ QUERY_STRING='style=raw'
1595 1596 $ "$PYTHON" hgweb.cgi | grep '^tip'
1596 1597 tip [0-9a-f]{40} (re)
1597 1598
1598 1599 $ cd ..
1599 1600
1600 1601 Test interaction with revset (issue4426)
1601 1602
1602 1603 $ hg init issue4426
1603 1604 $ cd issue4426
1604 1605
1605 1606 $ echo a > a
1606 1607 $ hg ci -Am a
1607 1608 adding a
1608 1609 $ echo a >> a
1609 1610 $ hg ci -m a
1610 1611 $ echo a >> a
1611 1612 $ hg ci -m a
1612 1613 $ hg qimport -r 0::
1613 1614
1614 1615 reimport things
1615 1616
1616 1617 $ hg qimport -r 1::
1617 1618 abort: revision 2 is already managed
1618 1619 [255]
1619 1620
1620 1621
1621 1622 $ cd ..
@@ -1,1006 +1,1007 b''
1 1 $ cat <<EOF >> $HGRCPATH
2 2 > [extensions]
3 3 > transplant=
4 4 > EOF
5 5
6 6 $ hg init t
7 7 $ cd t
8 8 $ hg transplant
9 9 abort: no source URL, branch revision, or revision list provided
10 10 [255]
11 11 $ hg transplant --continue --all
12 12 abort: --continue is incompatible with --branch, --all and --merge
13 13 [255]
14 14 $ hg transplant --all tip
15 15 abort: --all requires a branch revision
16 16 [255]
17 17 $ hg transplant --all --branch default tip
18 18 abort: --all is incompatible with a revision list
19 19 [255]
20 20 $ echo r1 > r1
21 21 $ hg ci -Amr1 -d'0 0'
22 22 adding r1
23 23 $ hg co -q null
24 24 $ hg transplant tip
25 25 abort: no revision checked out
26 26 [255]
27 27 $ hg up -q
28 28 $ echo r2 > r2
29 29 $ hg ci -Amr2 -d'1 0'
30 30 adding r2
31 31 $ hg up 0
32 32 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
33 33
34 34 $ echo b1 > b1
35 35 $ hg ci -Amb1 -d '0 0'
36 36 adding b1
37 37 created new head
38 38 $ hg merge 1
39 39 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
40 40 (branch merge, don't forget to commit)
41 41 $ hg transplant 1
42 42 abort: outstanding uncommitted merge
43 43 [255]
44 44 $ hg up -qC tip
45 45 $ echo b0 > b1
46 46 $ hg transplant 1
47 47 abort: uncommitted changes
48 48 [255]
49 49 $ hg up -qC tip
50 50 $ echo b2 > b2
51 51 $ hg ci -Amb2 -d '1 0'
52 52 adding b2
53 53 $ echo b3 > b3
54 54 $ hg ci -Amb3 -d '2 0'
55 55 adding b3
56 56
57 57 $ hg log --template '{rev} {parents} {desc}\n'
58 58 4 b3
59 59 3 b2
60 60 2 0:17ab29e464c6 b1
61 61 1 r2
62 62 0 r1
63 63
64 64 $ hg clone . ../rebase
65 65 updating to branch default
66 66 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
67 67 $ hg init ../emptydest
68 68 $ cd ../emptydest
69 69 $ hg transplant --source=../t > /dev/null
70 70 $ cd ../rebase
71 71
72 72 $ hg up -C 1
73 73 1 files updated, 0 files merged, 3 files removed, 0 files unresolved
74 74
75 75 rebase b onto r1
76 76 (this also tests that editor is not invoked if '--edit' is not specified)
77 77
78 78 $ HGEDITOR=cat hg transplant -a -b tip
79 79 applying 37a1297eb21b
80 80 37a1297eb21b transplanted to e234d668f844
81 81 applying 722f4667af76
82 82 722f4667af76 transplanted to 539f377d78df
83 83 applying a53251cdf717
84 84 a53251cdf717 transplanted to ffd6818a3975
85 85 $ hg log --template '{rev} {parents} {desc}\n'
86 86 7 b3
87 87 6 b2
88 88 5 1:d11e3596cc1a b1
89 89 4 b3
90 90 3 b2
91 91 2 0:17ab29e464c6 b1
92 92 1 r2
93 93 0 r1
94 94
95 95 test format of transplant_source
96 96
97 97 $ hg log -r7 --debug | grep transplant_source
98 98 extra: transplant_source=\xa52Q\xcd\xf7\x17g\x9d\x19\x07\xb2\x89\xf9\x91SK\xe0\\\x99z
99 99 $ hg log -r7 -T '{extras}\n'
100 100 branch=defaulttransplant_source=\xa52Q\xcd\xf7\x17g\x9d\x19\x07\xb2\x89\xf9\x91SK\xe0\\\x99z
101 101 $ hg log -r7 -T '{join(extras, " ")}\n'
102 102 branch=default transplant_source=\xa52Q\xcd\xf7\x17g\x9d\x19\x07\xb2\x89\xf9\x91SK\xe0\\\x99z
103 103
104 104 test transplanted revset
105 105
106 106 $ hg log -r 'transplanted()' --template '{rev} {parents} {desc}\n'
107 107 5 1:d11e3596cc1a b1
108 108 6 b2
109 109 7 b3
110 110 $ hg log -r 'transplanted(head())' --template '{rev} {parents} {desc}\n'
111 111 7 b3
112 112 $ hg help revisions.transplanted
113 113 "transplanted([set])"
114 114 Transplanted changesets in set, or all transplanted changesets.
115 115
116 116
117 117 test transplanted keyword
118 118
119 119 $ hg log --template '{rev} {transplanted}\n'
120 120 7 a53251cdf717679d1907b289f991534be05c997a
121 121 6 722f4667af767100cb15b6a79324bf8abbfe1ef4
122 122 5 37a1297eb21b3ef5c5d2ffac22121a0988ed9f21
123 123 4
124 124 3
125 125 2
126 126 1
127 127 0
128 128
129 129 test destination() revset predicate with a transplant of a transplant; new
130 130 clone so subsequent rollback isn't affected
131 131 (this also tests that editor is invoked if '--edit' is specified)
132 132
133 133 $ hg clone -q . ../destination
134 134 $ cd ../destination
135 135 $ hg up -Cq 0
136 136 $ hg branch -q b4
137 137 $ hg ci -qm "b4"
138 138 $ hg status --rev "7^1" --rev 7
139 139 A b3
140 140 $ cat > $TESTTMP/checkeditform.sh <<EOF
141 141 > env | grep HGEDITFORM
142 142 > true
143 143 > EOF
144 144 $ cat > $TESTTMP/checkeditform-n-cat.sh <<EOF
145 145 > env | grep HGEDITFORM
146 146 > cat \$*
147 147 > EOF
148 148 $ HGEDITOR="sh $TESTTMP/checkeditform-n-cat.sh" hg transplant --edit 7
149 149 applying ffd6818a3975
150 150 HGEDITFORM=transplant.normal
151 151 b3
152 152
153 153
154 154 HG: Enter commit message. Lines beginning with 'HG:' are removed.
155 155 HG: Leave message empty to abort commit.
156 156 HG: --
157 157 HG: user: test
158 158 HG: branch 'b4'
159 159 HG: added b3
160 160 ffd6818a3975 transplanted to 502236fa76bb
161 161
162 162
163 163 $ hg log -r 'destination()'
164 164 changeset: 5:e234d668f844
165 165 parent: 1:d11e3596cc1a
166 166 user: test
167 167 date: Thu Jan 01 00:00:00 1970 +0000
168 168 summary: b1
169 169
170 170 changeset: 6:539f377d78df
171 171 user: test
172 172 date: Thu Jan 01 00:00:01 1970 +0000
173 173 summary: b2
174 174
175 175 changeset: 7:ffd6818a3975
176 176 user: test
177 177 date: Thu Jan 01 00:00:02 1970 +0000
178 178 summary: b3
179 179
180 180 changeset: 9:502236fa76bb
181 181 branch: b4
182 182 tag: tip
183 183 user: test
184 184 date: Thu Jan 01 00:00:02 1970 +0000
185 185 summary: b3
186 186
187 187 $ hg log -r 'destination(a53251cdf717)'
188 188 changeset: 7:ffd6818a3975
189 189 user: test
190 190 date: Thu Jan 01 00:00:02 1970 +0000
191 191 summary: b3
192 192
193 193 changeset: 9:502236fa76bb
194 194 branch: b4
195 195 tag: tip
196 196 user: test
197 197 date: Thu Jan 01 00:00:02 1970 +0000
198 198 summary: b3
199 199
200 200
201 201 test subset parameter in reverse order
202 202 $ hg log -r 'reverse(all()) and destination(a53251cdf717)'
203 203 changeset: 9:502236fa76bb
204 204 branch: b4
205 205 tag: tip
206 206 user: test
207 207 date: Thu Jan 01 00:00:02 1970 +0000
208 208 summary: b3
209 209
210 210 changeset: 7:ffd6818a3975
211 211 user: test
212 212 date: Thu Jan 01 00:00:02 1970 +0000
213 213 summary: b3
214 214
215 215
216 216 back to the original dir
217 217 $ cd ../rebase
218 218
219 219 rollback the transplant
220 220 $ hg rollback
221 221 repository tip rolled back to revision 4 (undo transplant)
222 222 working directory now based on revision 1
223 223 $ hg tip -q
224 224 4:a53251cdf717
225 225 $ hg parents -q
226 226 1:d11e3596cc1a
227 227 $ hg status
228 228 ? b1
229 229 ? b2
230 230 ? b3
231 231
232 232 $ hg clone ../t ../prune
233 233 updating to branch default
234 234 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
235 235 $ cd ../prune
236 236
237 237 $ hg up -C 1
238 238 1 files updated, 0 files merged, 3 files removed, 0 files unresolved
239 239
240 240 rebase b onto r1, skipping b2
241 241
242 242 $ hg transplant -a -b tip -p 3
243 243 applying 37a1297eb21b
244 244 37a1297eb21b transplanted to e234d668f844
245 245 applying a53251cdf717
246 246 a53251cdf717 transplanted to 7275fda4d04f
247 247 $ hg log --template '{rev} {parents} {desc}\n'
248 248 6 b3
249 249 5 1:d11e3596cc1a b1
250 250 4 b3
251 251 3 b2
252 252 2 0:17ab29e464c6 b1
253 253 1 r2
254 254 0 r1
255 255
256 256 test same-parent transplant with --log
257 257
258 258 $ hg clone -r 1 ../t ../sameparent
259 259 adding changesets
260 260 adding manifests
261 261 adding file changes
262 262 added 2 changesets with 2 changes to 2 files
263 263 new changesets 17ab29e464c6:d11e3596cc1a
264 264 updating to branch default
265 265 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
266 266 $ cd ../sameparent
267 267 $ hg transplant --log -s ../prune 5
268 268 searching for changes
269 269 applying e234d668f844
270 270 e234d668f844 transplanted to e07aea8ecf9c
271 271 $ hg log --template '{rev} {parents} {desc}\n'
272 272 2 b1
273 273 (transplanted from e234d668f844e1b1a765f01db83a32c0c7bfa170)
274 274 1 r2
275 275 0 r1
276 276 remote transplant, and also test that transplant doesn't break with
277 277 format-breaking diffopts
278 278
279 279 $ hg clone -r 1 ../t ../remote
280 280 adding changesets
281 281 adding manifests
282 282 adding file changes
283 283 added 2 changesets with 2 changes to 2 files
284 284 new changesets 17ab29e464c6:d11e3596cc1a
285 285 updating to branch default
286 286 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
287 287 $ cd ../remote
288 288 $ hg --config diff.noprefix=True transplant --log -s ../t 2 4
289 289 searching for changes
290 290 applying 37a1297eb21b
291 291 37a1297eb21b transplanted to c19cf0ccb069
292 292 applying a53251cdf717
293 293 a53251cdf717 transplanted to f7fe5bf98525
294 294 $ hg log --template '{rev} {parents} {desc}\n'
295 295 3 b3
296 296 (transplanted from a53251cdf717679d1907b289f991534be05c997a)
297 297 2 b1
298 298 (transplanted from 37a1297eb21b3ef5c5d2ffac22121a0988ed9f21)
299 299 1 r2
300 300 0 r1
301 301
302 302 skip previous transplants
303 303
304 304 $ hg transplant -s ../t -a -b 4
305 305 searching for changes
306 306 applying 722f4667af76
307 307 722f4667af76 transplanted to 47156cd86c0b
308 308 $ hg log --template '{rev} {parents} {desc}\n'
309 309 4 b2
310 310 3 b3
311 311 (transplanted from a53251cdf717679d1907b289f991534be05c997a)
312 312 2 b1
313 313 (transplanted from 37a1297eb21b3ef5c5d2ffac22121a0988ed9f21)
314 314 1 r2
315 315 0 r1
316 316
317 317 skip local changes transplanted to the source
318 318
319 319 $ echo b4 > b4
320 320 $ hg ci -Amb4 -d '3 0'
321 321 adding b4
322 322 $ hg clone ../t ../pullback
323 323 updating to branch default
324 324 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
325 325 $ cd ../pullback
326 326 $ hg transplant -s ../remote -a -b tip
327 327 searching for changes
328 328 applying 4333daefcb15
329 329 4333daefcb15 transplanted to 5f42c04e07cc
330 330
331 331
332 332 remote transplant with pull
333 333
334 334 $ hg serve -R ../t -p $HGPORT -d --pid-file=../t.pid
335 335 $ cat ../t.pid >> $DAEMON_PIDS
336 336
337 337 $ hg clone -r 0 ../t ../rp
338 338 adding changesets
339 339 adding manifests
340 340 adding file changes
341 341 added 1 changesets with 1 changes to 1 files
342 342 new changesets 17ab29e464c6
343 343 updating to branch default
344 344 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
345 345 $ cd ../rp
346 346 $ hg transplant -s http://localhost:$HGPORT/ 37a1297eb21b a53251cdf717
347 347 searching for changes
348 348 searching for changes
349 349 adding changesets
350 350 adding manifests
351 351 adding file changes
352 352 added 1 changesets with 1 changes to 1 files
353 353 applying a53251cdf717
354 354 a53251cdf717 transplanted to 8d9279348abb
355 355 $ hg log --template '{rev} {parents} {desc}\n'
356 356 2 b3
357 357 1 b1
358 358 0 r1
359 359
360 360 remote transplant without pull
361 361 (It was using "2" and "4" (as the previous transplant used to) which referenced
362 362 revision different from one run to another)
363 363
364 364 $ hg pull -q http://localhost:$HGPORT/
365 365 $ hg transplant -s http://localhost:$HGPORT/ 8d9279348abb 722f4667af76
366 366 skipping already applied revision 2:8d9279348abb
367 367 applying 722f4667af76
368 368 722f4667af76 transplanted to 76e321915884
369 369
370 370 transplant --continue
371 371
372 372 $ hg init ../tc
373 373 $ cd ../tc
374 374 $ cat <<EOF > foo
375 375 > foo
376 376 > bar
377 377 > baz
378 378 > EOF
379 379 $ echo toremove > toremove
380 380 $ echo baz > baz
381 381 $ hg ci -Amfoo
382 382 adding baz
383 383 adding foo
384 384 adding toremove
385 385 $ cat <<EOF > foo
386 386 > foo2
387 387 > bar2
388 388 > baz2
389 389 > EOF
390 390 $ rm toremove
391 391 $ echo added > added
392 392 $ hg ci -Amfoo2
393 393 adding added
394 394 removing toremove
395 395 $ echo bar > bar
396 396 $ cat > baz <<EOF
397 397 > before baz
398 398 > baz
399 399 > after baz
400 400 > EOF
401 401 $ hg ci -Ambar
402 402 adding bar
403 403 $ echo bar2 >> bar
404 404 $ hg ci -mbar2
405 405 $ hg up 0
406 406 3 files updated, 0 files merged, 2 files removed, 0 files unresolved
407 407 $ echo foobar > foo
408 408 $ hg ci -mfoobar
409 409 created new head
410 410 $ hg transplant 1:3
411 411 applying 46ae92138f3c
412 412 patching file foo
413 413 Hunk #1 FAILED at 0
414 414 1 out of 1 hunks FAILED -- saving rejects to file foo.rej
415 415 patch failed to apply
416 416 abort: fix up the working directory and run hg transplant --continue
417 417 [255]
418 418
419 419 transplant -c shouldn't use an old changeset
420 420
421 421 $ hg up -C
422 422 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
423 423 updated to "e8643552fde5: foobar"
424 424 1 other heads for branch "default"
425 425 $ rm added
426 426 $ hg transplant --continue
427 427 abort: no transplant to continue
428 428 [255]
429 429 $ hg transplant 1
430 430 applying 46ae92138f3c
431 431 patching file foo
432 432 Hunk #1 FAILED at 0
433 433 1 out of 1 hunks FAILED -- saving rejects to file foo.rej
434 434 patch failed to apply
435 435 abort: fix up the working directory and run hg transplant --continue
436 436 [255]
437 437 $ cp .hg/transplant/journal .hg/transplant/journal.orig
438 438 $ cat .hg/transplant/journal
439 439 # User test
440 440 # Date 0 0
441 441 # Node ID 46ae92138f3ce0249f6789650403286ead052b6d
442 442 # Parent e8643552fde58f57515e19c4b373a57c96e62af3
443 443 foo2
444 444 $ grep -v 'Date' .hg/transplant/journal.orig > .hg/transplant/journal
445 445 $ HGEDITOR="sh $TESTTMP/checkeditform.sh" hg transplant --continue -e
446 446 abort: filter corrupted changeset (no user or date)
447 447 [255]
448 448 $ cp .hg/transplant/journal.orig .hg/transplant/journal
449 449 $ HGEDITOR="sh $TESTTMP/checkeditform.sh" hg transplant --continue -e
450 450 HGEDITFORM=transplant.normal
451 451 46ae92138f3c transplanted as 9159dada197d
452 452 $ hg transplant 1:3
453 453 skipping already applied revision 1:46ae92138f3c
454 454 applying 9d6d6b5a8275
455 455 9d6d6b5a8275 transplanted to 2d17a10c922f
456 456 applying 1dab759070cf
457 457 1dab759070cf transplanted to e06a69927eb0
458 458 $ hg locate
459 459 added
460 460 bar
461 461 baz
462 462 foo
463 463
464 464 test multiple revisions and --continue
465 465
466 466 $ hg up -qC 0
467 467 $ echo bazbaz > baz
468 468 $ hg ci -Am anotherbaz baz
469 469 created new head
470 470 $ hg transplant 1:3
471 471 applying 46ae92138f3c
472 472 46ae92138f3c transplanted to 1024233ea0ba
473 473 applying 9d6d6b5a8275
474 474 patching file baz
475 475 Hunk #1 FAILED at 0
476 476 1 out of 1 hunks FAILED -- saving rejects to file baz.rej
477 477 patch failed to apply
478 478 abort: fix up the working directory and run hg transplant --continue
479 479 [255]
480 480 $ hg transplant 1:3
481 481 abort: transplant in progress
482 482 (use 'hg transplant --continue' or 'hg update' to abort)
483 483 [255]
484 484 $ echo fixed > baz
485 485 $ hg transplant --continue
486 486 9d6d6b5a8275 transplanted as d80c49962290
487 487 applying 1dab759070cf
488 488 1dab759070cf transplanted to aa0ffe6bd5ae
489 489
490 490 $ cd ..
491 491
492 492 Issue1111: Test transplant --merge
493 493
494 494 $ hg init t1111
495 495 $ cd t1111
496 496 $ echo a > a
497 497 $ hg ci -Am adda
498 498 adding a
499 499 $ echo b >> a
500 500 $ hg ci -m appendb
501 501 $ echo c >> a
502 502 $ hg ci -m appendc
503 503 $ hg up -C 0
504 504 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
505 505 $ echo d >> a
506 506 $ hg ci -m appendd
507 507 created new head
508 508
509 509 transplant
510 510
511 511 $ HGEDITOR="sh $TESTTMP/checkeditform.sh" hg transplant -m 1 -e
512 512 applying 42dc4432fd35
513 513 HGEDITFORM=transplant.merge
514 514 1:42dc4432fd35 merged at a9f4acbac129
515 515 $ hg update -q -C 2
516 516 $ cat > a <<EOF
517 517 > x
518 518 > y
519 519 > z
520 520 > EOF
521 521 $ hg commit -m replace
522 522 $ hg update -q -C 4
523 523 $ hg transplant -m 5
524 524 applying 600a3cdcb41d
525 525 patching file a
526 526 Hunk #1 FAILED at 0
527 527 1 out of 1 hunks FAILED -- saving rejects to file a.rej
528 528 patch failed to apply
529 529 abort: fix up the working directory and run hg transplant --continue
530 530 [255]
531 531 $ HGEDITOR="sh $TESTTMP/checkeditform.sh" hg transplant --continue -e
532 532 HGEDITFORM=transplant.merge
533 533 600a3cdcb41d transplanted as a3f88be652e0
534 534
535 535 $ cd ..
536 536
537 537 test transplant into empty repository
538 538
539 539 $ hg init empty
540 540 $ cd empty
541 541 $ hg transplant -s ../t -b tip -a
542 542 adding changesets
543 543 adding manifests
544 544 adding file changes
545 545 added 4 changesets with 4 changes to 4 files
546 546 new changesets 17ab29e464c6:a53251cdf717
547 547
548 548 test "--merge" causing pull from source repository on local host
549 549
550 550 $ hg --config extensions.mq= -q strip 2
551 551 $ hg transplant -s ../t --merge tip
552 552 searching for changes
553 553 searching for changes
554 554 adding changesets
555 555 adding manifests
556 556 adding file changes
557 557 added 2 changesets with 2 changes to 2 files
558 558 applying a53251cdf717
559 559 4:a53251cdf717 merged at 4831f4dc831a
560 560
561 561 test interactive transplant
562 562
563 563 $ hg --config extensions.strip= -q strip 0
564 564 $ hg -R ../t log -G --template "{rev}:{node|short}"
565 565 @ 4:a53251cdf717
566 566 |
567 567 o 3:722f4667af76
568 568 |
569 569 o 2:37a1297eb21b
570 570 |
571 571 | o 1:d11e3596cc1a
572 572 |/
573 573 o 0:17ab29e464c6
574 574
575 575 $ hg transplant -q --config ui.interactive=true -s ../t <<EOF
576 576 > ?
577 577 > x
578 578 > q
579 579 > EOF
580 580 0:17ab29e464c6
581 581 apply changeset? [ynmpcq?]: ?
582 582 y: yes, transplant this changeset
583 583 n: no, skip this changeset
584 584 m: merge at this changeset
585 585 p: show patch
586 586 c: commit selected changesets
587 587 q: quit and cancel transplant
588 588 ?: ? (show this help)
589 589 apply changeset? [ynmpcq?]: x
590 590 unrecognized response
591 591 apply changeset? [ynmpcq?]: q
592 592 $ hg transplant -q --config ui.interactive=true -s ../t <<EOF
593 593 > p
594 594 > y
595 595 > n
596 596 > n
597 597 > m
598 598 > c
599 599 > EOF
600 600 0:17ab29e464c6
601 601 apply changeset? [ynmpcq?]: p
602 diff -r 000000000000 -r 17ab29e464c6 r1
602 603 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
603 604 +++ b/r1 Thu Jan 01 00:00:00 1970 +0000
604 605 @@ -0,0 +1,1 @@
605 606 +r1
606 607 apply changeset? [ynmpcq?]: y
607 608 1:d11e3596cc1a
608 609 apply changeset? [ynmpcq?]: n
609 610 2:37a1297eb21b
610 611 apply changeset? [ynmpcq?]: n
611 612 3:722f4667af76
612 613 apply changeset? [ynmpcq?]: m
613 614 4:a53251cdf717
614 615 apply changeset? [ynmpcq?]: c
615 616 $ hg log -G --template "{node|short}"
616 617 @ 88be5dde5260
617 618 |\
618 619 | o 722f4667af76
619 620 | |
620 621 | o 37a1297eb21b
621 622 |/
622 623 o 17ab29e464c6
623 624
624 625 $ hg transplant -q --config ui.interactive=true -s ../t <<EOF
625 626 > x
626 627 > ?
627 628 > y
628 629 > q
629 630 > EOF
630 631 1:d11e3596cc1a
631 632 apply changeset? [ynmpcq?]: x
632 633 unrecognized response
633 634 apply changeset? [ynmpcq?]: ?
634 635 y: yes, transplant this changeset
635 636 n: no, skip this changeset
636 637 m: merge at this changeset
637 638 p: show patch
638 639 c: commit selected changesets
639 640 q: quit and cancel transplant
640 641 ?: ? (show this help)
641 642 apply changeset? [ynmpcq?]: y
642 643 4:a53251cdf717
643 644 apply changeset? [ynmpcq?]: q
644 645 $ hg heads --template "{node|short}\n"
645 646 88be5dde5260
646 647
647 648 $ cd ..
648 649
649 650
650 651 #if unix-permissions system-sh
651 652
652 653 test filter
653 654
654 655 $ hg init filter
655 656 $ cd filter
656 657 $ cat <<'EOF' >test-filter
657 658 > #!/bin/sh
658 659 > sed 's/r1/r2/' $1 > $1.new
659 660 > mv $1.new $1
660 661 > EOF
661 662 $ chmod +x test-filter
662 663 $ hg transplant -s ../t -b tip -a --filter ./test-filter
663 664 filtering * (glob)
664 665 applying 17ab29e464c6
665 666 17ab29e464c6 transplanted to e9ffc54ea104
666 667 filtering * (glob)
667 668 applying 37a1297eb21b
668 669 37a1297eb21b transplanted to 348b36d0b6a5
669 670 filtering * (glob)
670 671 applying 722f4667af76
671 672 722f4667af76 transplanted to 0aa6979afb95
672 673 filtering * (glob)
673 674 applying a53251cdf717
674 675 a53251cdf717 transplanted to 14f8512272b5
675 676 $ hg log --template '{rev} {parents} {desc}\n'
676 677 3 b3
677 678 2 b2
678 679 1 b1
679 680 0 r2
680 681 $ cd ..
681 682
682 683
683 684 test filter with failed patch
684 685
685 686 $ cd filter
686 687 $ hg up 0
687 688 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
688 689 $ echo foo > b1
689 690 $ hg ci -Am foo
690 691 adding b1
691 692 adding test-filter
692 693 created new head
693 694 $ hg transplant 1 --filter ./test-filter
694 695 filtering * (glob)
695 696 applying 348b36d0b6a5
696 697 file b1 already exists
697 698 1 out of 1 hunks FAILED -- saving rejects to file b1.rej
698 699 patch failed to apply
699 700 abort: fix up the working directory and run hg transplant --continue
700 701 [255]
701 702 $ cd ..
702 703
703 704 test environment passed to filter
704 705
705 706 $ hg init filter-environment
706 707 $ cd filter-environment
707 708 $ cat <<'EOF' >test-filter-environment
708 709 > #!/bin/sh
709 710 > echo "Transplant by $HGUSER" >> $1
710 711 > echo "Transplant from rev $HGREVISION" >> $1
711 712 > EOF
712 713 $ chmod +x test-filter-environment
713 714 $ hg transplant -s ../t --filter ./test-filter-environment 0
714 715 filtering * (glob)
715 716 applying 17ab29e464c6
716 717 17ab29e464c6 transplanted to 5190e68026a0
717 718
718 719 $ hg log --template '{rev} {parents} {desc}\n'
719 720 0 r1
720 721 Transplant by test
721 722 Transplant from rev 17ab29e464c6ca53e329470efe2a9918ac617a6f
722 723 $ cd ..
723 724
724 725 test transplant with filter handles invalid changelog
725 726
726 727 $ hg init filter-invalid-log
727 728 $ cd filter-invalid-log
728 729 $ cat <<'EOF' >test-filter-invalid-log
729 730 > #!/bin/sh
730 731 > echo "" > $1
731 732 > EOF
732 733 $ chmod +x test-filter-invalid-log
733 734 $ hg transplant -s ../t --filter ./test-filter-invalid-log 0
734 735 filtering * (glob)
735 736 abort: filter corrupted changeset (no user or date)
736 737 [255]
737 738 $ cd ..
738 739
739 740 #endif
740 741
741 742
742 743 test with a win32ext like setup (differing EOLs)
743 744
744 745 $ hg init twin1
745 746 $ cd twin1
746 747 $ echo a > a
747 748 $ echo b > b
748 749 $ echo b >> b
749 750 $ hg ci -Am t
750 751 adding a
751 752 adding b
752 753 $ echo a > b
753 754 $ echo b >> b
754 755 $ hg ci -m changeb
755 756 $ cd ..
756 757
757 758 $ hg init twin2
758 759 $ cd twin2
759 760 $ echo '[patch]' >> .hg/hgrc
760 761 $ echo 'eol = crlf' >> .hg/hgrc
761 762 $ "$PYTHON" -c "open('b', 'wb').write(b'b\r\nb\r\n')"
762 763 $ hg ci -Am addb
763 764 adding b
764 765 $ hg transplant -s ../twin1 tip
765 766 searching for changes
766 767 warning: repository is unrelated
767 768 applying 2e849d776c17
768 769 2e849d776c17 transplanted to 8e65bebc063e
769 770 $ cat b
770 771 a\r (esc)
771 772 b\r (esc)
772 773 $ cd ..
773 774
774 775 test transplant with merge changeset is skipped
775 776
776 777 $ hg init merge1a
777 778 $ cd merge1a
778 779 $ echo a > a
779 780 $ hg ci -Am a
780 781 adding a
781 782 $ hg branch b
782 783 marked working directory as branch b
783 784 (branches are permanent and global, did you want a bookmark?)
784 785 $ hg ci -m branchb
785 786 $ echo b > b
786 787 $ hg ci -Am b
787 788 adding b
788 789 $ hg update default
789 790 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
790 791 $ hg merge b
791 792 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
792 793 (branch merge, don't forget to commit)
793 794 $ hg ci -m mergeb
794 795 $ cd ..
795 796
796 797 $ hg init merge1b
797 798 $ cd merge1b
798 799 $ hg transplant -s ../merge1a tip
799 800 $ cd ..
800 801
801 802 test transplant with merge changeset accepts --parent
802 803
803 804 $ hg init merge2a
804 805 $ cd merge2a
805 806 $ echo a > a
806 807 $ hg ci -Am a
807 808 adding a
808 809 $ hg branch b
809 810 marked working directory as branch b
810 811 (branches are permanent and global, did you want a bookmark?)
811 812 $ hg ci -m branchb
812 813 $ echo b > b
813 814 $ hg ci -Am b
814 815 adding b
815 816 $ hg update default
816 817 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
817 818 $ hg merge b
818 819 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
819 820 (branch merge, don't forget to commit)
820 821 $ hg ci -m mergeb
821 822 $ cd ..
822 823
823 824 $ hg init merge2b
824 825 $ cd merge2b
825 826 $ hg transplant -s ../merge2a --parent tip tip
826 827 abort: be9f9b39483f is not a parent of be9f9b39483f
827 828 [255]
828 829 $ hg transplant -s ../merge2a --parent 0 tip
829 830 applying be9f9b39483f
830 831 be9f9b39483f transplanted to 9959e51f94d1
831 832 $ cd ..
832 833
833 834 test transplanting a patch turning into a no-op
834 835
835 836 $ hg init binarysource
836 837 $ cd binarysource
837 838 $ echo a > a
838 839 $ hg ci -Am adda a
839 840 >>> open('b', 'wb').write(b'\0b1') and None
840 841 $ hg ci -Am addb b
841 842 >>> open('b', 'wb').write(b'\0b2') and None
842 843 $ hg ci -m changeb b
843 844 $ cd ..
844 845
845 846 $ hg clone -r0 binarysource binarydest
846 847 adding changesets
847 848 adding manifests
848 849 adding file changes
849 850 added 1 changesets with 1 changes to 1 files
850 851 new changesets 07f494440405
851 852 updating to branch default
852 853 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
853 854 $ cd binarydest
854 855 $ cp ../binarysource/b b
855 856 $ hg ci -Am addb2 b
856 857 $ hg transplant -s ../binarysource 2
857 858 searching for changes
858 859 applying 7a7d57e15850
859 860 skipping emptied changeset 7a7d57e15850
860 861
861 862 Test empty result in --continue
862 863
863 864 $ hg transplant -s ../binarysource 1
864 865 searching for changes
865 866 applying 645035761929
866 867 file b already exists
867 868 1 out of 1 hunks FAILED -- saving rejects to file b.rej
868 869 patch failed to apply
869 870 abort: fix up the working directory and run hg transplant --continue
870 871 [255]
871 872 $ hg status
872 873 ? b.rej
873 874 $ hg transplant --continue
874 875 645035761929 skipped due to empty diff
875 876
876 877 $ cd ..
877 878
878 879 Explicitly kill daemons to let the test exit on Windows
879 880
880 881 $ killdaemons.py
881 882
882 883 Test that patch-ed files are treated as "modified", when transplant is
883 884 aborted by failure of patching, even if none of mode, size and
884 885 timestamp of them isn't changed on the filesystem (see also issue4583)
885 886
886 887 $ cd t
887 888
888 889 $ cat > $TESTTMP/abort.py <<EOF
889 890 > # emulate that patch.patch() is aborted at patching on "abort" file
890 891 > from mercurial import error, extensions, patch as patchmod
891 892 > def patch(orig, ui, repo, patchname,
892 893 > strip=1, prefix=b'', files=None,
893 894 > eolmode=b'strict', similarity=0):
894 895 > if files is None:
895 896 > files = set()
896 897 > r = orig(ui, repo, patchname,
897 898 > strip=strip, prefix=prefix, files=files,
898 899 > eolmode=eolmode, similarity=similarity)
899 900 > if b'abort' in files:
900 901 > raise error.PatchError('intentional error while patching')
901 902 > return r
902 903 > def extsetup(ui):
903 904 > extensions.wrapfunction(patchmod, 'patch', patch)
904 905 > EOF
905 906
906 907 $ echo X1 > r1
907 908 $ hg diff --nodates r1
908 909 diff -r a53251cdf717 r1
909 910 --- a/r1
910 911 +++ b/r1
911 912 @@ -1,1 +1,1 @@
912 913 -r1
913 914 +X1
914 915 $ hg commit -m "X1 as r1"
915 916
916 917 $ echo 'marking to abort patching' > abort
917 918 $ hg add abort
918 919 $ echo Y1 > r1
919 920 $ hg diff --nodates r1
920 921 diff -r 22c515968f13 r1
921 922 --- a/r1
922 923 +++ b/r1
923 924 @@ -1,1 +1,1 @@
924 925 -X1
925 926 +Y1
926 927 $ hg commit -m "Y1 as r1"
927 928
928 929 $ hg update -q -C d11e3596cc1a
929 930 $ cat r1
930 931 r1
931 932
932 933 $ cat >> .hg/hgrc <<EOF
933 934 > [fakedirstatewritetime]
934 935 > # emulate invoking dirstate.write() via repo.status() or markcommitted()
935 936 > # at 2000-01-01 00:00
936 937 > fakenow = 200001010000
937 938 >
938 939 > # emulate invoking patch.internalpatch() at 2000-01-01 00:00
939 940 > [fakepatchtime]
940 941 > fakenow = 200001010000
941 942 >
942 943 > [extensions]
943 944 > fakedirstatewritetime = $TESTDIR/fakedirstatewritetime.py
944 945 > fakepatchtime = $TESTDIR/fakepatchtime.py
945 946 > abort = $TESTTMP/abort.py
946 947 > EOF
947 948 $ hg transplant "22c515968f13::"
948 949 applying 22c515968f13
949 950 22c515968f13 transplanted to * (glob)
950 951 applying e38700ba9dd3
951 952 intentional error while patching
952 953 abort: fix up the working directory and run hg transplant --continue
953 954 [255]
954 955 $ cat >> .hg/hgrc <<EOF
955 956 > [hooks]
956 957 > fakedirstatewritetime = !
957 958 > fakepatchtime = !
958 959 > [extensions]
959 960 > abort = !
960 961 > EOF
961 962
962 963 $ cat r1
963 964 Y1
964 965 $ hg debugstate | grep ' r1$'
965 966 n 644 3 unset r1
966 967 $ hg status -A r1
967 968 M r1
968 969
969 970 Test that rollback by unexpected failure after transplanting the first
970 971 revision restores dirstate correctly.
971 972
972 973 $ hg rollback -q
973 974 $ rm -f abort
974 975 $ hg update -q -C d11e3596cc1a
975 976 $ hg parents -T "{node|short}\n"
976 977 d11e3596cc1a
977 978 $ hg status -A
978 979 C r1
979 980 C r2
980 981
981 982 $ cat >> .hg/hgrc <<EOF
982 983 > [hooks]
983 984 > # emulate failure at transplanting the 2nd revision
984 985 > pretxncommit.abort = test ! -f abort
985 986 > EOF
986 987 $ hg transplant "22c515968f13::"
987 988 applying 22c515968f13
988 989 22c515968f13 transplanted to * (glob)
989 990 applying e38700ba9dd3
990 991 transaction abort!
991 992 rollback completed
992 993 abort: pretxncommit.abort hook exited with status 1
993 994 [255]
994 995 $ cat >> .hg/hgrc <<EOF
995 996 > [hooks]
996 997 > pretxncommit.abort = !
997 998 > EOF
998 999
999 1000 $ hg parents -T "{node|short}\n"
1000 1001 d11e3596cc1a
1001 1002 $ hg status -A
1002 1003 M r1
1003 1004 ? abort
1004 1005 C r2
1005 1006
1006 1007 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now