##// END OF EJS Templates
commit: use cmdutil.check_at_most_one_arg()...
Martin von Zweigbergk -
r44345:2e672ccc default
parent child Browse files
Show More
@@ -1,4036 +1,4034 b''
1 1 # cmdutil.py - help for command processing in mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import copy as copymod
11 11 import errno
12 12 import os
13 13 import re
14 14
15 15 from .i18n import _
16 16 from .node import (
17 17 hex,
18 18 nullid,
19 19 nullrev,
20 20 short,
21 21 )
22 22 from .pycompat import (
23 23 getattr,
24 24 open,
25 25 setattr,
26 26 )
27 27 from .thirdparty import attr
28 28
29 29 from . import (
30 30 bookmarks,
31 31 changelog,
32 32 copies,
33 33 crecord as crecordmod,
34 34 dirstateguard,
35 35 encoding,
36 36 error,
37 37 formatter,
38 38 logcmdutil,
39 39 match as matchmod,
40 40 merge as mergemod,
41 41 mergeutil,
42 42 obsolete,
43 43 patch,
44 44 pathutil,
45 45 phases,
46 46 pycompat,
47 47 repair,
48 48 revlog,
49 49 rewriteutil,
50 50 scmutil,
51 51 smartset,
52 52 state as statemod,
53 53 subrepoutil,
54 54 templatekw,
55 55 templater,
56 56 util,
57 57 vfs as vfsmod,
58 58 )
59 59
60 60 from .utils import (
61 61 dateutil,
62 62 stringutil,
63 63 )
64 64
65 65 if pycompat.TYPE_CHECKING:
66 66 from typing import (
67 67 Any,
68 68 Dict,
69 69 )
70 70
71 71 for t in (Any, Dict):
72 72 assert t
73 73
74 74 stringio = util.stringio
75 75
76 76 # templates of common command options
77 77
78 78 dryrunopts = [
79 79 (b'n', b'dry-run', None, _(b'do not perform actions, just print output')),
80 80 ]
81 81
82 82 confirmopts = [
83 83 (b'', b'confirm', None, _(b'ask before applying actions')),
84 84 ]
85 85
86 86 remoteopts = [
87 87 (b'e', b'ssh', b'', _(b'specify ssh command to use'), _(b'CMD')),
88 88 (
89 89 b'',
90 90 b'remotecmd',
91 91 b'',
92 92 _(b'specify hg command to run on the remote side'),
93 93 _(b'CMD'),
94 94 ),
95 95 (
96 96 b'',
97 97 b'insecure',
98 98 None,
99 99 _(b'do not verify server certificate (ignoring web.cacerts config)'),
100 100 ),
101 101 ]
102 102
103 103 walkopts = [
104 104 (
105 105 b'I',
106 106 b'include',
107 107 [],
108 108 _(b'include names matching the given patterns'),
109 109 _(b'PATTERN'),
110 110 ),
111 111 (
112 112 b'X',
113 113 b'exclude',
114 114 [],
115 115 _(b'exclude names matching the given patterns'),
116 116 _(b'PATTERN'),
117 117 ),
118 118 ]
119 119
120 120 commitopts = [
121 121 (b'm', b'message', b'', _(b'use text as commit message'), _(b'TEXT')),
122 122 (b'l', b'logfile', b'', _(b'read commit message from file'), _(b'FILE')),
123 123 ]
124 124
125 125 commitopts2 = [
126 126 (
127 127 b'd',
128 128 b'date',
129 129 b'',
130 130 _(b'record the specified date as commit date'),
131 131 _(b'DATE'),
132 132 ),
133 133 (
134 134 b'u',
135 135 b'user',
136 136 b'',
137 137 _(b'record the specified user as committer'),
138 138 _(b'USER'),
139 139 ),
140 140 ]
141 141
142 142 commitopts3 = [
143 143 (b'D', b'currentdate', None, _(b'record the current date as commit date')),
144 144 (b'U', b'currentuser', None, _(b'record the current user as committer')),
145 145 ]
146 146
147 147 formatteropts = [
148 148 (b'T', b'template', b'', _(b'display with template'), _(b'TEMPLATE')),
149 149 ]
150 150
151 151 templateopts = [
152 152 (
153 153 b'',
154 154 b'style',
155 155 b'',
156 156 _(b'display using template map file (DEPRECATED)'),
157 157 _(b'STYLE'),
158 158 ),
159 159 (b'T', b'template', b'', _(b'display with template'), _(b'TEMPLATE')),
160 160 ]
161 161
162 162 logopts = [
163 163 (b'p', b'patch', None, _(b'show patch')),
164 164 (b'g', b'git', None, _(b'use git extended diff format')),
165 165 (b'l', b'limit', b'', _(b'limit number of changes displayed'), _(b'NUM')),
166 166 (b'M', b'no-merges', None, _(b'do not show merges')),
167 167 (b'', b'stat', None, _(b'output diffstat-style summary of changes')),
168 168 (b'G', b'graph', None, _(b"show the revision DAG")),
169 169 ] + templateopts
170 170
171 171 diffopts = [
172 172 (b'a', b'text', None, _(b'treat all files as text')),
173 173 (b'g', b'git', None, _(b'use git extended diff format')),
174 174 (b'', b'binary', None, _(b'generate binary diffs in git mode (default)')),
175 175 (b'', b'nodates', None, _(b'omit dates from diff headers')),
176 176 ]
177 177
178 178 diffwsopts = [
179 179 (
180 180 b'w',
181 181 b'ignore-all-space',
182 182 None,
183 183 _(b'ignore white space when comparing lines'),
184 184 ),
185 185 (
186 186 b'b',
187 187 b'ignore-space-change',
188 188 None,
189 189 _(b'ignore changes in the amount of white space'),
190 190 ),
191 191 (
192 192 b'B',
193 193 b'ignore-blank-lines',
194 194 None,
195 195 _(b'ignore changes whose lines are all blank'),
196 196 ),
197 197 (
198 198 b'Z',
199 199 b'ignore-space-at-eol',
200 200 None,
201 201 _(b'ignore changes in whitespace at EOL'),
202 202 ),
203 203 ]
204 204
205 205 diffopts2 = (
206 206 [
207 207 (b'', b'noprefix', None, _(b'omit a/ and b/ prefixes from filenames')),
208 208 (
209 209 b'p',
210 210 b'show-function',
211 211 None,
212 212 _(b'show which function each change is in'),
213 213 ),
214 214 (b'', b'reverse', None, _(b'produce a diff that undoes the changes')),
215 215 ]
216 216 + diffwsopts
217 217 + [
218 218 (
219 219 b'U',
220 220 b'unified',
221 221 b'',
222 222 _(b'number of lines of context to show'),
223 223 _(b'NUM'),
224 224 ),
225 225 (b'', b'stat', None, _(b'output diffstat-style summary of changes')),
226 226 (
227 227 b'',
228 228 b'root',
229 229 b'',
230 230 _(b'produce diffs relative to subdirectory'),
231 231 _(b'DIR'),
232 232 ),
233 233 ]
234 234 )
235 235
236 236 mergetoolopts = [
237 237 (b't', b'tool', b'', _(b'specify merge tool'), _(b'TOOL')),
238 238 ]
239 239
240 240 similarityopts = [
241 241 (
242 242 b's',
243 243 b'similarity',
244 244 b'',
245 245 _(b'guess renamed files by similarity (0<=s<=100)'),
246 246 _(b'SIMILARITY'),
247 247 )
248 248 ]
249 249
250 250 subrepoopts = [(b'S', b'subrepos', None, _(b'recurse into subrepositories'))]
251 251
252 252 debugrevlogopts = [
253 253 (b'c', b'changelog', False, _(b'open changelog')),
254 254 (b'm', b'manifest', False, _(b'open manifest')),
255 255 (b'', b'dir', b'', _(b'open directory manifest')),
256 256 ]
257 257
258 258 # special string such that everything below this line will be ingored in the
259 259 # editor text
260 260 _linebelow = b"^HG: ------------------------ >8 ------------------------$"
261 261
262 262
263 263 def check_at_most_one_arg(opts, *args):
264 264 """abort if more than one of the arguments are in opts"""
265 265 previous = None
266 266 for x in args:
267 267 if opts.get(x):
268 268 if previous:
269 269 raise error.Abort(
270 270 _(b'cannot specify both --%s and --%s') % (previous, x)
271 271 )
272 272 previous = x
273 273
274 274
275 275 def resolvecommitoptions(ui, opts):
276 276 """modify commit options dict to handle related options
277 277
278 278 The return value indicates that ``rewrite.update-timestamp`` is the reason
279 279 the ``date`` option is set.
280 280 """
281 if opts.get(b'date') and opts.get(b'currentdate'):
282 raise error.Abort(_(b'--date and --currentdate are mutually exclusive'))
283 if opts.get(b'user') and opts.get(b'currentuser'):
284 raise error.Abort(_(b'--user and --currentuser are mutually exclusive'))
281 check_at_most_one_arg(opts, b'date', b'currentdate')
282 check_at_most_one_arg(opts, b'user', b'currentuser')
285 283
286 284 datemaydiffer = False # date-only change should be ignored?
287 285
288 286 if opts.get(b'currentdate'):
289 287 opts[b'date'] = b'%d %d' % dateutil.makedate()
290 288 elif (
291 289 not opts.get(b'date')
292 290 and ui.configbool(b'rewrite', b'update-timestamp')
293 291 and opts.get(b'currentdate') is None
294 292 ):
295 293 opts[b'date'] = b'%d %d' % dateutil.makedate()
296 294 datemaydiffer = True
297 295
298 296 if opts.get(b'currentuser'):
299 297 opts[b'user'] = ui.username()
300 298
301 299 return datemaydiffer
302 300
303 301
304 302 def checknotesize(ui, opts):
305 303 """ make sure note is of valid format """
306 304
307 305 note = opts.get(b'note')
308 306 if not note:
309 307 return
310 308
311 309 if len(note) > 255:
312 310 raise error.Abort(_(b"cannot store a note of more than 255 bytes"))
313 311 if b'\n' in note:
314 312 raise error.Abort(_(b"note cannot contain a newline"))
315 313
316 314
317 315 def ishunk(x):
318 316 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
319 317 return isinstance(x, hunkclasses)
320 318
321 319
322 320 def newandmodified(chunks, originalchunks):
323 321 newlyaddedandmodifiedfiles = set()
324 322 alsorestore = set()
325 323 for chunk in chunks:
326 324 if (
327 325 ishunk(chunk)
328 326 and chunk.header.isnewfile()
329 327 and chunk not in originalchunks
330 328 ):
331 329 newlyaddedandmodifiedfiles.add(chunk.header.filename())
332 330 alsorestore.update(
333 331 set(chunk.header.files()) - {chunk.header.filename()}
334 332 )
335 333 return newlyaddedandmodifiedfiles, alsorestore
336 334
337 335
338 336 def parsealiases(cmd):
339 337 return cmd.split(b"|")
340 338
341 339
342 340 def setupwrapcolorwrite(ui):
343 341 # wrap ui.write so diff output can be labeled/colorized
344 342 def wrapwrite(orig, *args, **kw):
345 343 label = kw.pop('label', b'')
346 344 for chunk, l in patch.difflabel(lambda: args):
347 345 orig(chunk, label=label + l)
348 346
349 347 oldwrite = ui.write
350 348
351 349 def wrap(*args, **kwargs):
352 350 return wrapwrite(oldwrite, *args, **kwargs)
353 351
354 352 setattr(ui, 'write', wrap)
355 353 return oldwrite
356 354
357 355
358 356 def filterchunks(ui, originalhunks, usecurses, testfile, match, operation=None):
359 357 try:
360 358 if usecurses:
361 359 if testfile:
362 360 recordfn = crecordmod.testdecorator(
363 361 testfile, crecordmod.testchunkselector
364 362 )
365 363 else:
366 364 recordfn = crecordmod.chunkselector
367 365
368 366 return crecordmod.filterpatch(
369 367 ui, originalhunks, recordfn, operation
370 368 )
371 369 except crecordmod.fallbackerror as e:
372 370 ui.warn(b'%s\n' % e.message) # pytype: disable=attribute-error
373 371 ui.warn(_(b'falling back to text mode\n'))
374 372
375 373 return patch.filterpatch(ui, originalhunks, match, operation)
376 374
377 375
378 376 def recordfilter(ui, originalhunks, match, operation=None):
379 377 """ Prompts the user to filter the originalhunks and return a list of
380 378 selected hunks.
381 379 *operation* is used for to build ui messages to indicate the user what
382 380 kind of filtering they are doing: reverting, committing, shelving, etc.
383 381 (see patch.filterpatch).
384 382 """
385 383 usecurses = crecordmod.checkcurses(ui)
386 384 testfile = ui.config(b'experimental', b'crecordtest')
387 385 oldwrite = setupwrapcolorwrite(ui)
388 386 try:
389 387 newchunks, newopts = filterchunks(
390 388 ui, originalhunks, usecurses, testfile, match, operation
391 389 )
392 390 finally:
393 391 ui.write = oldwrite
394 392 return newchunks, newopts
395 393
396 394
397 395 def dorecord(
398 396 ui, repo, commitfunc, cmdsuggest, backupall, filterfn, *pats, **opts
399 397 ):
400 398 opts = pycompat.byteskwargs(opts)
401 399 if not ui.interactive():
402 400 if cmdsuggest:
403 401 msg = _(b'running non-interactively, use %s instead') % cmdsuggest
404 402 else:
405 403 msg = _(b'running non-interactively')
406 404 raise error.Abort(msg)
407 405
408 406 # make sure username is set before going interactive
409 407 if not opts.get(b'user'):
410 408 ui.username() # raise exception, username not provided
411 409
412 410 def recordfunc(ui, repo, message, match, opts):
413 411 """This is generic record driver.
414 412
415 413 Its job is to interactively filter local changes, and
416 414 accordingly prepare working directory into a state in which the
417 415 job can be delegated to a non-interactive commit command such as
418 416 'commit' or 'qrefresh'.
419 417
420 418 After the actual job is done by non-interactive command, the
421 419 working directory is restored to its original state.
422 420
423 421 In the end we'll record interesting changes, and everything else
424 422 will be left in place, so the user can continue working.
425 423 """
426 424 if not opts.get(b'interactive-unshelve'):
427 425 checkunfinished(repo, commit=True)
428 426 wctx = repo[None]
429 427 merge = len(wctx.parents()) > 1
430 428 if merge:
431 429 raise error.Abort(
432 430 _(
433 431 b'cannot partially commit a merge '
434 432 b'(use "hg commit" instead)'
435 433 )
436 434 )
437 435
438 436 def fail(f, msg):
439 437 raise error.Abort(b'%s: %s' % (f, msg))
440 438
441 439 force = opts.get(b'force')
442 440 if not force:
443 441 match = matchmod.badmatch(match, fail)
444 442
445 443 status = repo.status(match=match)
446 444
447 445 overrides = {(b'ui', b'commitsubrepos'): True}
448 446
449 447 with repo.ui.configoverride(overrides, b'record'):
450 448 # subrepoutil.precommit() modifies the status
451 449 tmpstatus = scmutil.status(
452 450 copymod.copy(status.modified),
453 451 copymod.copy(status.added),
454 452 copymod.copy(status.removed),
455 453 copymod.copy(status.deleted),
456 454 copymod.copy(status.unknown),
457 455 copymod.copy(status.ignored),
458 456 copymod.copy(status.clean), # pytype: disable=wrong-arg-count
459 457 )
460 458
461 459 # Force allows -X subrepo to skip the subrepo.
462 460 subs, commitsubs, newstate = subrepoutil.precommit(
463 461 repo.ui, wctx, tmpstatus, match, force=True
464 462 )
465 463 for s in subs:
466 464 if s in commitsubs:
467 465 dirtyreason = wctx.sub(s).dirtyreason(True)
468 466 raise error.Abort(dirtyreason)
469 467
470 468 if not force:
471 469 repo.checkcommitpatterns(wctx, match, status, fail)
472 470 diffopts = patch.difffeatureopts(
473 471 ui,
474 472 opts=opts,
475 473 whitespace=True,
476 474 section=b'commands',
477 475 configprefix=b'commit.interactive.',
478 476 )
479 477 diffopts.nodates = True
480 478 diffopts.git = True
481 479 diffopts.showfunc = True
482 480 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
483 481 originalchunks = patch.parsepatch(originaldiff)
484 482 match = scmutil.match(repo[None], pats)
485 483
486 484 # 1. filter patch, since we are intending to apply subset of it
487 485 try:
488 486 chunks, newopts = filterfn(ui, originalchunks, match)
489 487 except error.PatchError as err:
490 488 raise error.Abort(_(b'error parsing patch: %s') % err)
491 489 opts.update(newopts)
492 490
493 491 # We need to keep a backup of files that have been newly added and
494 492 # modified during the recording process because there is a previous
495 493 # version without the edit in the workdir. We also will need to restore
496 494 # files that were the sources of renames so that the patch application
497 495 # works.
498 496 newlyaddedandmodifiedfiles, alsorestore = newandmodified(
499 497 chunks, originalchunks
500 498 )
501 499 contenders = set()
502 500 for h in chunks:
503 501 try:
504 502 contenders.update(set(h.files()))
505 503 except AttributeError:
506 504 pass
507 505
508 506 changed = status.modified + status.added + status.removed
509 507 newfiles = [f for f in changed if f in contenders]
510 508 if not newfiles:
511 509 ui.status(_(b'no changes to record\n'))
512 510 return 0
513 511
514 512 modified = set(status.modified)
515 513
516 514 # 2. backup changed files, so we can restore them in the end
517 515
518 516 if backupall:
519 517 tobackup = changed
520 518 else:
521 519 tobackup = [
522 520 f
523 521 for f in newfiles
524 522 if f in modified or f in newlyaddedandmodifiedfiles
525 523 ]
526 524 backups = {}
527 525 if tobackup:
528 526 backupdir = repo.vfs.join(b'record-backups')
529 527 try:
530 528 os.mkdir(backupdir)
531 529 except OSError as err:
532 530 if err.errno != errno.EEXIST:
533 531 raise
534 532 try:
535 533 # backup continues
536 534 for f in tobackup:
537 535 fd, tmpname = pycompat.mkstemp(
538 536 prefix=f.replace(b'/', b'_') + b'.', dir=backupdir
539 537 )
540 538 os.close(fd)
541 539 ui.debug(b'backup %r as %r\n' % (f, tmpname))
542 540 util.copyfile(repo.wjoin(f), tmpname, copystat=True)
543 541 backups[f] = tmpname
544 542
545 543 fp = stringio()
546 544 for c in chunks:
547 545 fname = c.filename()
548 546 if fname in backups:
549 547 c.write(fp)
550 548 dopatch = fp.tell()
551 549 fp.seek(0)
552 550
553 551 # 2.5 optionally review / modify patch in text editor
554 552 if opts.get(b'review', False):
555 553 patchtext = (
556 554 crecordmod.diffhelptext
557 555 + crecordmod.patchhelptext
558 556 + fp.read()
559 557 )
560 558 reviewedpatch = ui.edit(
561 559 patchtext, b"", action=b"diff", repopath=repo.path
562 560 )
563 561 fp.truncate(0)
564 562 fp.write(reviewedpatch)
565 563 fp.seek(0)
566 564
567 565 [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
568 566 # 3a. apply filtered patch to clean repo (clean)
569 567 if backups:
570 568 # Equivalent to hg.revert
571 569 m = scmutil.matchfiles(repo, set(backups.keys()) | alsorestore)
572 570 mergemod.update(
573 571 repo,
574 572 repo.dirstate.p1(),
575 573 branchmerge=False,
576 574 force=True,
577 575 matcher=m,
578 576 )
579 577
580 578 # 3b. (apply)
581 579 if dopatch:
582 580 try:
583 581 ui.debug(b'applying patch\n')
584 582 ui.debug(fp.getvalue())
585 583 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
586 584 except error.PatchError as err:
587 585 raise error.Abort(pycompat.bytestr(err))
588 586 del fp
589 587
590 588 # 4. We prepared working directory according to filtered
591 589 # patch. Now is the time to delegate the job to
592 590 # commit/qrefresh or the like!
593 591
594 592 # Make all of the pathnames absolute.
595 593 newfiles = [repo.wjoin(nf) for nf in newfiles]
596 594 return commitfunc(ui, repo, *newfiles, **pycompat.strkwargs(opts))
597 595 finally:
598 596 # 5. finally restore backed-up files
599 597 try:
600 598 dirstate = repo.dirstate
601 599 for realname, tmpname in pycompat.iteritems(backups):
602 600 ui.debug(b'restoring %r to %r\n' % (tmpname, realname))
603 601
604 602 if dirstate[realname] == b'n':
605 603 # without normallookup, restoring timestamp
606 604 # may cause partially committed files
607 605 # to be treated as unmodified
608 606 dirstate.normallookup(realname)
609 607
610 608 # copystat=True here and above are a hack to trick any
611 609 # editors that have f open that we haven't modified them.
612 610 #
613 611 # Also note that this racy as an editor could notice the
614 612 # file's mtime before we've finished writing it.
615 613 util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
616 614 os.unlink(tmpname)
617 615 if tobackup:
618 616 os.rmdir(backupdir)
619 617 except OSError:
620 618 pass
621 619
622 620 def recordinwlock(ui, repo, message, match, opts):
623 621 with repo.wlock():
624 622 return recordfunc(ui, repo, message, match, opts)
625 623
626 624 return commit(ui, repo, recordinwlock, pats, opts)
627 625
628 626
629 627 class dirnode(object):
630 628 """
631 629 Represent a directory in user working copy with information required for
632 630 the purpose of tersing its status.
633 631
634 632 path is the path to the directory, without a trailing '/'
635 633
636 634 statuses is a set of statuses of all files in this directory (this includes
637 635 all the files in all the subdirectories too)
638 636
639 637 files is a list of files which are direct child of this directory
640 638
641 639 subdirs is a dictionary of sub-directory name as the key and it's own
642 640 dirnode object as the value
643 641 """
644 642
645 643 def __init__(self, dirpath):
646 644 self.path = dirpath
647 645 self.statuses = set()
648 646 self.files = []
649 647 self.subdirs = {}
650 648
651 649 def _addfileindir(self, filename, status):
652 650 """Add a file in this directory as a direct child."""
653 651 self.files.append((filename, status))
654 652
655 653 def addfile(self, filename, status):
656 654 """
657 655 Add a file to this directory or to its direct parent directory.
658 656
659 657 If the file is not direct child of this directory, we traverse to the
660 658 directory of which this file is a direct child of and add the file
661 659 there.
662 660 """
663 661
664 662 # the filename contains a path separator, it means it's not the direct
665 663 # child of this directory
666 664 if b'/' in filename:
667 665 subdir, filep = filename.split(b'/', 1)
668 666
669 667 # does the dirnode object for subdir exists
670 668 if subdir not in self.subdirs:
671 669 subdirpath = pathutil.join(self.path, subdir)
672 670 self.subdirs[subdir] = dirnode(subdirpath)
673 671
674 672 # try adding the file in subdir
675 673 self.subdirs[subdir].addfile(filep, status)
676 674
677 675 else:
678 676 self._addfileindir(filename, status)
679 677
680 678 if status not in self.statuses:
681 679 self.statuses.add(status)
682 680
683 681 def iterfilepaths(self):
684 682 """Yield (status, path) for files directly under this directory."""
685 683 for f, st in self.files:
686 684 yield st, pathutil.join(self.path, f)
687 685
688 686 def tersewalk(self, terseargs):
689 687 """
690 688 Yield (status, path) obtained by processing the status of this
691 689 dirnode.
692 690
693 691 terseargs is the string of arguments passed by the user with `--terse`
694 692 flag.
695 693
696 694 Following are the cases which can happen:
697 695
698 696 1) All the files in the directory (including all the files in its
699 697 subdirectories) share the same status and the user has asked us to terse
700 698 that status. -> yield (status, dirpath). dirpath will end in '/'.
701 699
702 700 2) Otherwise, we do following:
703 701
704 702 a) Yield (status, filepath) for all the files which are in this
705 703 directory (only the ones in this directory, not the subdirs)
706 704
707 705 b) Recurse the function on all the subdirectories of this
708 706 directory
709 707 """
710 708
711 709 if len(self.statuses) == 1:
712 710 onlyst = self.statuses.pop()
713 711
714 712 # Making sure we terse only when the status abbreviation is
715 713 # passed as terse argument
716 714 if onlyst in terseargs:
717 715 yield onlyst, self.path + b'/'
718 716 return
719 717
720 718 # add the files to status list
721 719 for st, fpath in self.iterfilepaths():
722 720 yield st, fpath
723 721
724 722 # recurse on the subdirs
725 723 for dirobj in self.subdirs.values():
726 724 for st, fpath in dirobj.tersewalk(terseargs):
727 725 yield st, fpath
728 726
729 727
730 728 def tersedir(statuslist, terseargs):
731 729 """
732 730 Terse the status if all the files in a directory shares the same status.
733 731
734 732 statuslist is scmutil.status() object which contains a list of files for
735 733 each status.
736 734 terseargs is string which is passed by the user as the argument to `--terse`
737 735 flag.
738 736
739 737 The function makes a tree of objects of dirnode class, and at each node it
740 738 stores the information required to know whether we can terse a certain
741 739 directory or not.
742 740 """
743 741 # the order matters here as that is used to produce final list
744 742 allst = (b'm', b'a', b'r', b'd', b'u', b'i', b'c')
745 743
746 744 # checking the argument validity
747 745 for s in pycompat.bytestr(terseargs):
748 746 if s not in allst:
749 747 raise error.Abort(_(b"'%s' not recognized") % s)
750 748
751 749 # creating a dirnode object for the root of the repo
752 750 rootobj = dirnode(b'')
753 751 pstatus = (
754 752 b'modified',
755 753 b'added',
756 754 b'deleted',
757 755 b'clean',
758 756 b'unknown',
759 757 b'ignored',
760 758 b'removed',
761 759 )
762 760
763 761 tersedict = {}
764 762 for attrname in pstatus:
765 763 statuschar = attrname[0:1]
766 764 for f in getattr(statuslist, attrname):
767 765 rootobj.addfile(f, statuschar)
768 766 tersedict[statuschar] = []
769 767
770 768 # we won't be tersing the root dir, so add files in it
771 769 for st, fpath in rootobj.iterfilepaths():
772 770 tersedict[st].append(fpath)
773 771
774 772 # process each sub-directory and build tersedict
775 773 for subdir in rootobj.subdirs.values():
776 774 for st, f in subdir.tersewalk(terseargs):
777 775 tersedict[st].append(f)
778 776
779 777 tersedlist = []
780 778 for st in allst:
781 779 tersedict[st].sort()
782 780 tersedlist.append(tersedict[st])
783 781
784 782 return scmutil.status(*tersedlist)
785 783
786 784
787 785 def _commentlines(raw):
788 786 '''Surround lineswith a comment char and a new line'''
789 787 lines = raw.splitlines()
790 788 commentedlines = [b'# %s' % line for line in lines]
791 789 return b'\n'.join(commentedlines) + b'\n'
792 790
793 791
794 792 @attr.s(frozen=True)
795 793 class morestatus(object):
796 794 reporoot = attr.ib()
797 795 unfinishedop = attr.ib()
798 796 unfinishedmsg = attr.ib()
799 797 inmergestate = attr.ib()
800 798 unresolvedpaths = attr.ib()
801 799 _label = b'status.morestatus'
802 800
803 801 def formatfile(self, path, fm):
804 802 if self.inmergestate and path in self.unresolvedpaths:
805 803 fm.data(unresolved=True)
806 804
807 805 def formatfooter(self, fm):
808 806 fm.startitem()
809 807 fm.data(
810 808 itemtype=b'morestatus',
811 809 unfinished=self.unfinishedop,
812 810 unfinishedmsg=self.unfinishedmsg,
813 811 )
814 812
815 813 statemsg = (
816 814 _(b'The repository is in an unfinished *%s* state.')
817 815 % self.unfinishedop
818 816 )
819 817 fm.plain(b'%s\n' % _commentlines(statemsg), label=self._label)
820 818
821 819 self._formatconflicts(fm)
822 820 if self.unfinishedmsg:
823 821 fm.plain(
824 822 b'%s\n' % _commentlines(self.unfinishedmsg), label=self._label
825 823 )
826 824
827 825 def _formatconflicts(self, fm):
828 826 if not self.inmergestate:
829 827 return
830 828
831 829 if self.unresolvedpaths:
832 830 mergeliststr = b'\n'.join(
833 831 [
834 832 b' %s'
835 833 % util.pathto(self.reporoot, encoding.getcwd(), path)
836 834 for path in self.unresolvedpaths
837 835 ]
838 836 )
839 837 msg = (
840 838 _(
841 839 '''Unresolved merge conflicts:
842 840
843 841 %s
844 842
845 843 To mark files as resolved: hg resolve --mark FILE'''
846 844 )
847 845 % mergeliststr
848 846 )
849 847 else:
850 848 msg = _(b'No unresolved merge conflicts.')
851 849
852 850 fm.plain(b'%s\n' % _commentlines(msg), label=self._label)
853 851
854 852
855 853 def readmorestatus(repo):
856 854 """Returns a morestatus object if the repo has unfinished state."""
857 855 statetuple = statemod.getrepostate(repo)
858 856 if not statetuple:
859 857 return None
860 858
861 859 unfinishedop, unfinishedmsg = statetuple
862 860 mergestate = mergemod.mergestate.read(repo)
863 861 unresolved = None
864 862 if mergestate.active():
865 863 unresolved = sorted(mergestate.unresolved())
866 864 return morestatus(
867 865 repo.root,
868 866 unfinishedop,
869 867 unfinishedmsg,
870 868 unresolved is not None,
871 869 unresolved,
872 870 )
873 871
874 872
875 873 def findpossible(cmd, table, strict=False):
876 874 """
877 875 Return cmd -> (aliases, command table entry)
878 876 for each matching command.
879 877 Return debug commands (or their aliases) only if no normal command matches.
880 878 """
881 879 choice = {}
882 880 debugchoice = {}
883 881
884 882 if cmd in table:
885 883 # short-circuit exact matches, "log" alias beats "log|history"
886 884 keys = [cmd]
887 885 else:
888 886 keys = table.keys()
889 887
890 888 allcmds = []
891 889 for e in keys:
892 890 aliases = parsealiases(e)
893 891 allcmds.extend(aliases)
894 892 found = None
895 893 if cmd in aliases:
896 894 found = cmd
897 895 elif not strict:
898 896 for a in aliases:
899 897 if a.startswith(cmd):
900 898 found = a
901 899 break
902 900 if found is not None:
903 901 if aliases[0].startswith(b"debug") or found.startswith(b"debug"):
904 902 debugchoice[found] = (aliases, table[e])
905 903 else:
906 904 choice[found] = (aliases, table[e])
907 905
908 906 if not choice and debugchoice:
909 907 choice = debugchoice
910 908
911 909 return choice, allcmds
912 910
913 911
914 912 def findcmd(cmd, table, strict=True):
915 913 """Return (aliases, command table entry) for command string."""
916 914 choice, allcmds = findpossible(cmd, table, strict)
917 915
918 916 if cmd in choice:
919 917 return choice[cmd]
920 918
921 919 if len(choice) > 1:
922 920 clist = sorted(choice)
923 921 raise error.AmbiguousCommand(cmd, clist)
924 922
925 923 if choice:
926 924 return list(choice.values())[0]
927 925
928 926 raise error.UnknownCommand(cmd, allcmds)
929 927
930 928
931 929 def changebranch(ui, repo, revs, label):
932 930 """ Change the branch name of given revs to label """
933 931
934 932 with repo.wlock(), repo.lock(), repo.transaction(b'branches'):
935 933 # abort in case of uncommitted merge or dirty wdir
936 934 bailifchanged(repo)
937 935 revs = scmutil.revrange(repo, revs)
938 936 if not revs:
939 937 raise error.Abort(b"empty revision set")
940 938 roots = repo.revs(b'roots(%ld)', revs)
941 939 if len(roots) > 1:
942 940 raise error.Abort(
943 941 _(b"cannot change branch of non-linear revisions")
944 942 )
945 943 rewriteutil.precheck(repo, revs, b'change branch of')
946 944
947 945 root = repo[roots.first()]
948 946 rpb = {parent.branch() for parent in root.parents()}
949 947 if label not in rpb and label in repo.branchmap():
950 948 raise error.Abort(_(b"a branch of the same name already exists"))
951 949
952 950 if repo.revs(b'obsolete() and %ld', revs):
953 951 raise error.Abort(
954 952 _(b"cannot change branch of a obsolete changeset")
955 953 )
956 954
957 955 # make sure only topological heads
958 956 if repo.revs(b'heads(%ld) - head()', revs):
959 957 raise error.Abort(_(b"cannot change branch in middle of a stack"))
960 958
961 959 replacements = {}
962 960 # avoid import cycle mercurial.cmdutil -> mercurial.context ->
963 961 # mercurial.subrepo -> mercurial.cmdutil
964 962 from . import context
965 963
966 964 for rev in revs:
967 965 ctx = repo[rev]
968 966 oldbranch = ctx.branch()
969 967 # check if ctx has same branch
970 968 if oldbranch == label:
971 969 continue
972 970
973 971 def filectxfn(repo, newctx, path):
974 972 try:
975 973 return ctx[path]
976 974 except error.ManifestLookupError:
977 975 return None
978 976
979 977 ui.debug(
980 978 b"changing branch of '%s' from '%s' to '%s'\n"
981 979 % (hex(ctx.node()), oldbranch, label)
982 980 )
983 981 extra = ctx.extra()
984 982 extra[b'branch_change'] = hex(ctx.node())
985 983 # While changing branch of set of linear commits, make sure that
986 984 # we base our commits on new parent rather than old parent which
987 985 # was obsoleted while changing the branch
988 986 p1 = ctx.p1().node()
989 987 p2 = ctx.p2().node()
990 988 if p1 in replacements:
991 989 p1 = replacements[p1][0]
992 990 if p2 in replacements:
993 991 p2 = replacements[p2][0]
994 992
995 993 mc = context.memctx(
996 994 repo,
997 995 (p1, p2),
998 996 ctx.description(),
999 997 ctx.files(),
1000 998 filectxfn,
1001 999 user=ctx.user(),
1002 1000 date=ctx.date(),
1003 1001 extra=extra,
1004 1002 branch=label,
1005 1003 )
1006 1004
1007 1005 newnode = repo.commitctx(mc)
1008 1006 replacements[ctx.node()] = (newnode,)
1009 1007 ui.debug(b'new node id is %s\n' % hex(newnode))
1010 1008
1011 1009 # create obsmarkers and move bookmarks
1012 1010 scmutil.cleanupnodes(
1013 1011 repo, replacements, b'branch-change', fixphase=True
1014 1012 )
1015 1013
1016 1014 # move the working copy too
1017 1015 wctx = repo[None]
1018 1016 # in-progress merge is a bit too complex for now.
1019 1017 if len(wctx.parents()) == 1:
1020 1018 newid = replacements.get(wctx.p1().node())
1021 1019 if newid is not None:
1022 1020 # avoid import cycle mercurial.cmdutil -> mercurial.hg ->
1023 1021 # mercurial.cmdutil
1024 1022 from . import hg
1025 1023
1026 1024 hg.update(repo, newid[0], quietempty=True)
1027 1025
1028 1026 ui.status(_(b"changed branch on %d changesets\n") % len(replacements))
1029 1027
1030 1028
1031 1029 def findrepo(p):
1032 1030 while not os.path.isdir(os.path.join(p, b".hg")):
1033 1031 oldp, p = p, os.path.dirname(p)
1034 1032 if p == oldp:
1035 1033 return None
1036 1034
1037 1035 return p
1038 1036
1039 1037
1040 1038 def bailifchanged(repo, merge=True, hint=None):
1041 1039 """ enforce the precondition that working directory must be clean.
1042 1040
1043 1041 'merge' can be set to false if a pending uncommitted merge should be
1044 1042 ignored (such as when 'update --check' runs).
1045 1043
1046 1044 'hint' is the usual hint given to Abort exception.
1047 1045 """
1048 1046
1049 1047 if merge and repo.dirstate.p2() != nullid:
1050 1048 raise error.Abort(_(b'outstanding uncommitted merge'), hint=hint)
1051 1049 st = repo.status()
1052 1050 if st.modified or st.added or st.removed or st.deleted:
1053 1051 raise error.Abort(_(b'uncommitted changes'), hint=hint)
1054 1052 ctx = repo[None]
1055 1053 for s in sorted(ctx.substate):
1056 1054 ctx.sub(s).bailifchanged(hint=hint)
1057 1055
1058 1056
1059 1057 def logmessage(ui, opts):
1060 1058 """ get the log message according to -m and -l option """
1061 1059 message = opts.get(b'message')
1062 1060 logfile = opts.get(b'logfile')
1063 1061
1064 1062 if message and logfile:
1065 1063 raise error.Abort(
1066 1064 _(b'options --message and --logfile are mutually exclusive')
1067 1065 )
1068 1066 if not message and logfile:
1069 1067 try:
1070 1068 if isstdiofilename(logfile):
1071 1069 message = ui.fin.read()
1072 1070 else:
1073 1071 message = b'\n'.join(util.readfile(logfile).splitlines())
1074 1072 except IOError as inst:
1075 1073 raise error.Abort(
1076 1074 _(b"can't read commit message '%s': %s")
1077 1075 % (logfile, encoding.strtolocal(inst.strerror))
1078 1076 )
1079 1077 return message
1080 1078
1081 1079
1082 1080 def mergeeditform(ctxorbool, baseformname):
1083 1081 """return appropriate editform name (referencing a committemplate)
1084 1082
1085 1083 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
1086 1084 merging is committed.
1087 1085
1088 1086 This returns baseformname with '.merge' appended if it is a merge,
1089 1087 otherwise '.normal' is appended.
1090 1088 """
1091 1089 if isinstance(ctxorbool, bool):
1092 1090 if ctxorbool:
1093 1091 return baseformname + b".merge"
1094 1092 elif len(ctxorbool.parents()) > 1:
1095 1093 return baseformname + b".merge"
1096 1094
1097 1095 return baseformname + b".normal"
1098 1096
1099 1097
1100 1098 def getcommiteditor(
1101 1099 edit=False, finishdesc=None, extramsg=None, editform=b'', **opts
1102 1100 ):
1103 1101 """get appropriate commit message editor according to '--edit' option
1104 1102
1105 1103 'finishdesc' is a function to be called with edited commit message
1106 1104 (= 'description' of the new changeset) just after editing, but
1107 1105 before checking empty-ness. It should return actual text to be
1108 1106 stored into history. This allows to change description before
1109 1107 storing.
1110 1108
1111 1109 'extramsg' is a extra message to be shown in the editor instead of
1112 1110 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
1113 1111 is automatically added.
1114 1112
1115 1113 'editform' is a dot-separated list of names, to distinguish
1116 1114 the purpose of commit text editing.
1117 1115
1118 1116 'getcommiteditor' returns 'commitforceeditor' regardless of
1119 1117 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
1120 1118 they are specific for usage in MQ.
1121 1119 """
1122 1120 if edit or finishdesc or extramsg:
1123 1121 return lambda r, c, s: commitforceeditor(
1124 1122 r, c, s, finishdesc=finishdesc, extramsg=extramsg, editform=editform
1125 1123 )
1126 1124 elif editform:
1127 1125 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
1128 1126 else:
1129 1127 return commiteditor
1130 1128
1131 1129
1132 1130 def _escapecommandtemplate(tmpl):
1133 1131 parts = []
1134 1132 for typ, start, end in templater.scantemplate(tmpl, raw=True):
1135 1133 if typ == b'string':
1136 1134 parts.append(stringutil.escapestr(tmpl[start:end]))
1137 1135 else:
1138 1136 parts.append(tmpl[start:end])
1139 1137 return b''.join(parts)
1140 1138
1141 1139
1142 1140 def rendercommandtemplate(ui, tmpl, props):
1143 1141 r"""Expand a literal template 'tmpl' in a way suitable for command line
1144 1142
1145 1143 '\' in outermost string is not taken as an escape character because it
1146 1144 is a directory separator on Windows.
1147 1145
1148 1146 >>> from . import ui as uimod
1149 1147 >>> ui = uimod.ui()
1150 1148 >>> rendercommandtemplate(ui, b'c:\\{path}', {b'path': b'foo'})
1151 1149 'c:\\foo'
1152 1150 >>> rendercommandtemplate(ui, b'{"c:\\{path}"}', {'path': b'foo'})
1153 1151 'c:{path}'
1154 1152 """
1155 1153 if not tmpl:
1156 1154 return tmpl
1157 1155 t = formatter.maketemplater(ui, _escapecommandtemplate(tmpl))
1158 1156 return t.renderdefault(props)
1159 1157
1160 1158
1161 1159 def rendertemplate(ctx, tmpl, props=None):
1162 1160 """Expand a literal template 'tmpl' byte-string against one changeset
1163 1161
1164 1162 Each props item must be a stringify-able value or a callable returning
1165 1163 such value, i.e. no bare list nor dict should be passed.
1166 1164 """
1167 1165 repo = ctx.repo()
1168 1166 tres = formatter.templateresources(repo.ui, repo)
1169 1167 t = formatter.maketemplater(
1170 1168 repo.ui, tmpl, defaults=templatekw.keywords, resources=tres
1171 1169 )
1172 1170 mapping = {b'ctx': ctx}
1173 1171 if props:
1174 1172 mapping.update(props)
1175 1173 return t.renderdefault(mapping)
1176 1174
1177 1175
1178 1176 def _buildfntemplate(pat, total=None, seqno=None, revwidth=None, pathname=None):
1179 1177 r"""Convert old-style filename format string to template string
1180 1178
1181 1179 >>> _buildfntemplate(b'foo-%b-%n.patch', seqno=0)
1182 1180 'foo-{reporoot|basename}-{seqno}.patch'
1183 1181 >>> _buildfntemplate(b'%R{tags % "{tag}"}%H')
1184 1182 '{rev}{tags % "{tag}"}{node}'
1185 1183
1186 1184 '\' in outermost strings has to be escaped because it is a directory
1187 1185 separator on Windows:
1188 1186
1189 1187 >>> _buildfntemplate(b'c:\\tmp\\%R\\%n.patch', seqno=0)
1190 1188 'c:\\\\tmp\\\\{rev}\\\\{seqno}.patch'
1191 1189 >>> _buildfntemplate(b'\\\\foo\\bar.patch')
1192 1190 '\\\\\\\\foo\\\\bar.patch'
1193 1191 >>> _buildfntemplate(b'\\{tags % "{tag}"}')
1194 1192 '\\\\{tags % "{tag}"}'
1195 1193
1196 1194 but inner strings follow the template rules (i.e. '\' is taken as an
1197 1195 escape character):
1198 1196
1199 1197 >>> _buildfntemplate(br'{"c:\tmp"}', seqno=0)
1200 1198 '{"c:\\tmp"}'
1201 1199 """
1202 1200 expander = {
1203 1201 b'H': b'{node}',
1204 1202 b'R': b'{rev}',
1205 1203 b'h': b'{node|short}',
1206 1204 b'm': br'{sub(r"[^\w]", "_", desc|firstline)}',
1207 1205 b'r': b'{if(revwidth, pad(rev, revwidth, "0", left=True), rev)}',
1208 1206 b'%': b'%',
1209 1207 b'b': b'{reporoot|basename}',
1210 1208 }
1211 1209 if total is not None:
1212 1210 expander[b'N'] = b'{total}'
1213 1211 if seqno is not None:
1214 1212 expander[b'n'] = b'{seqno}'
1215 1213 if total is not None and seqno is not None:
1216 1214 expander[b'n'] = b'{pad(seqno, total|stringify|count, "0", left=True)}'
1217 1215 if pathname is not None:
1218 1216 expander[b's'] = b'{pathname|basename}'
1219 1217 expander[b'd'] = b'{if(pathname|dirname, pathname|dirname, ".")}'
1220 1218 expander[b'p'] = b'{pathname}'
1221 1219
1222 1220 newname = []
1223 1221 for typ, start, end in templater.scantemplate(pat, raw=True):
1224 1222 if typ != b'string':
1225 1223 newname.append(pat[start:end])
1226 1224 continue
1227 1225 i = start
1228 1226 while i < end:
1229 1227 n = pat.find(b'%', i, end)
1230 1228 if n < 0:
1231 1229 newname.append(stringutil.escapestr(pat[i:end]))
1232 1230 break
1233 1231 newname.append(stringutil.escapestr(pat[i:n]))
1234 1232 if n + 2 > end:
1235 1233 raise error.Abort(
1236 1234 _(b"incomplete format spec in output filename")
1237 1235 )
1238 1236 c = pat[n + 1 : n + 2]
1239 1237 i = n + 2
1240 1238 try:
1241 1239 newname.append(expander[c])
1242 1240 except KeyError:
1243 1241 raise error.Abort(
1244 1242 _(b"invalid format spec '%%%s' in output filename") % c
1245 1243 )
1246 1244 return b''.join(newname)
1247 1245
1248 1246
1249 1247 def makefilename(ctx, pat, **props):
1250 1248 if not pat:
1251 1249 return pat
1252 1250 tmpl = _buildfntemplate(pat, **props)
1253 1251 # BUG: alias expansion shouldn't be made against template fragments
1254 1252 # rewritten from %-format strings, but we have no easy way to partially
1255 1253 # disable the expansion.
1256 1254 return rendertemplate(ctx, tmpl, pycompat.byteskwargs(props))
1257 1255
1258 1256
1259 1257 def isstdiofilename(pat):
1260 1258 """True if the given pat looks like a filename denoting stdin/stdout"""
1261 1259 return not pat or pat == b'-'
1262 1260
1263 1261
1264 1262 class _unclosablefile(object):
1265 1263 def __init__(self, fp):
1266 1264 self._fp = fp
1267 1265
1268 1266 def close(self):
1269 1267 pass
1270 1268
1271 1269 def __iter__(self):
1272 1270 return iter(self._fp)
1273 1271
1274 1272 def __getattr__(self, attr):
1275 1273 return getattr(self._fp, attr)
1276 1274
1277 1275 def __enter__(self):
1278 1276 return self
1279 1277
1280 1278 def __exit__(self, exc_type, exc_value, exc_tb):
1281 1279 pass
1282 1280
1283 1281
1284 1282 def makefileobj(ctx, pat, mode=b'wb', **props):
1285 1283 writable = mode not in (b'r', b'rb')
1286 1284
1287 1285 if isstdiofilename(pat):
1288 1286 repo = ctx.repo()
1289 1287 if writable:
1290 1288 fp = repo.ui.fout
1291 1289 else:
1292 1290 fp = repo.ui.fin
1293 1291 return _unclosablefile(fp)
1294 1292 fn = makefilename(ctx, pat, **props)
1295 1293 return open(fn, mode)
1296 1294
1297 1295
1298 1296 def openstorage(repo, cmd, file_, opts, returnrevlog=False):
1299 1297 """opens the changelog, manifest, a filelog or a given revlog"""
1300 1298 cl = opts[b'changelog']
1301 1299 mf = opts[b'manifest']
1302 1300 dir = opts[b'dir']
1303 1301 msg = None
1304 1302 if cl and mf:
1305 1303 msg = _(b'cannot specify --changelog and --manifest at the same time')
1306 1304 elif cl and dir:
1307 1305 msg = _(b'cannot specify --changelog and --dir at the same time')
1308 1306 elif cl or mf or dir:
1309 1307 if file_:
1310 1308 msg = _(b'cannot specify filename with --changelog or --manifest')
1311 1309 elif not repo:
1312 1310 msg = _(
1313 1311 b'cannot specify --changelog or --manifest or --dir '
1314 1312 b'without a repository'
1315 1313 )
1316 1314 if msg:
1317 1315 raise error.Abort(msg)
1318 1316
1319 1317 r = None
1320 1318 if repo:
1321 1319 if cl:
1322 1320 r = repo.unfiltered().changelog
1323 1321 elif dir:
1324 1322 if b'treemanifest' not in repo.requirements:
1325 1323 raise error.Abort(
1326 1324 _(
1327 1325 b"--dir can only be used on repos with "
1328 1326 b"treemanifest enabled"
1329 1327 )
1330 1328 )
1331 1329 if not dir.endswith(b'/'):
1332 1330 dir = dir + b'/'
1333 1331 dirlog = repo.manifestlog.getstorage(dir)
1334 1332 if len(dirlog):
1335 1333 r = dirlog
1336 1334 elif mf:
1337 1335 r = repo.manifestlog.getstorage(b'')
1338 1336 elif file_:
1339 1337 filelog = repo.file(file_)
1340 1338 if len(filelog):
1341 1339 r = filelog
1342 1340
1343 1341 # Not all storage may be revlogs. If requested, try to return an actual
1344 1342 # revlog instance.
1345 1343 if returnrevlog:
1346 1344 if isinstance(r, revlog.revlog):
1347 1345 pass
1348 1346 elif util.safehasattr(r, b'_revlog'):
1349 1347 r = r._revlog # pytype: disable=attribute-error
1350 1348 elif r is not None:
1351 1349 raise error.Abort(_(b'%r does not appear to be a revlog') % r)
1352 1350
1353 1351 if not r:
1354 1352 if not returnrevlog:
1355 1353 raise error.Abort(_(b'cannot give path to non-revlog'))
1356 1354
1357 1355 if not file_:
1358 1356 raise error.CommandError(cmd, _(b'invalid arguments'))
1359 1357 if not os.path.isfile(file_):
1360 1358 raise error.Abort(_(b"revlog '%s' not found") % file_)
1361 1359 r = revlog.revlog(
1362 1360 vfsmod.vfs(encoding.getcwd(), audit=False), file_[:-2] + b".i"
1363 1361 )
1364 1362 return r
1365 1363
1366 1364
1367 1365 def openrevlog(repo, cmd, file_, opts):
1368 1366 """Obtain a revlog backing storage of an item.
1369 1367
1370 1368 This is similar to ``openstorage()`` except it always returns a revlog.
1371 1369
1372 1370 In most cases, a caller cares about the main storage object - not the
1373 1371 revlog backing it. Therefore, this function should only be used by code
1374 1372 that needs to examine low-level revlog implementation details. e.g. debug
1375 1373 commands.
1376 1374 """
1377 1375 return openstorage(repo, cmd, file_, opts, returnrevlog=True)
1378 1376
1379 1377
1380 1378 def copy(ui, repo, pats, opts, rename=False):
1381 1379 # called with the repo lock held
1382 1380 #
1383 1381 # hgsep => pathname that uses "/" to separate directories
1384 1382 # ossep => pathname that uses os.sep to separate directories
1385 1383 cwd = repo.getcwd()
1386 1384 targets = {}
1387 1385 after = opts.get(b"after")
1388 1386 dryrun = opts.get(b"dry_run")
1389 1387 wctx = repo[None]
1390 1388
1391 1389 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
1392 1390
1393 1391 def walkpat(pat):
1394 1392 srcs = []
1395 1393 if after:
1396 1394 badstates = b'?'
1397 1395 else:
1398 1396 badstates = b'?r'
1399 1397 m = scmutil.match(wctx, [pat], opts, globbed=True)
1400 1398 for abs in wctx.walk(m):
1401 1399 state = repo.dirstate[abs]
1402 1400 rel = uipathfn(abs)
1403 1401 exact = m.exact(abs)
1404 1402 if state in badstates:
1405 1403 if exact and state == b'?':
1406 1404 ui.warn(_(b'%s: not copying - file is not managed\n') % rel)
1407 1405 if exact and state == b'r':
1408 1406 ui.warn(
1409 1407 _(
1410 1408 b'%s: not copying - file has been marked for'
1411 1409 b' remove\n'
1412 1410 )
1413 1411 % rel
1414 1412 )
1415 1413 continue
1416 1414 # abs: hgsep
1417 1415 # rel: ossep
1418 1416 srcs.append((abs, rel, exact))
1419 1417 return srcs
1420 1418
1421 1419 # abssrc: hgsep
1422 1420 # relsrc: ossep
1423 1421 # otarget: ossep
1424 1422 def copyfile(abssrc, relsrc, otarget, exact):
1425 1423 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
1426 1424 if b'/' in abstarget:
1427 1425 # We cannot normalize abstarget itself, this would prevent
1428 1426 # case only renames, like a => A.
1429 1427 abspath, absname = abstarget.rsplit(b'/', 1)
1430 1428 abstarget = repo.dirstate.normalize(abspath) + b'/' + absname
1431 1429 reltarget = repo.pathto(abstarget, cwd)
1432 1430 target = repo.wjoin(abstarget)
1433 1431 src = repo.wjoin(abssrc)
1434 1432 state = repo.dirstate[abstarget]
1435 1433
1436 1434 scmutil.checkportable(ui, abstarget)
1437 1435
1438 1436 # check for collisions
1439 1437 prevsrc = targets.get(abstarget)
1440 1438 if prevsrc is not None:
1441 1439 ui.warn(
1442 1440 _(b'%s: not overwriting - %s collides with %s\n')
1443 1441 % (
1444 1442 reltarget,
1445 1443 repo.pathto(abssrc, cwd),
1446 1444 repo.pathto(prevsrc, cwd),
1447 1445 )
1448 1446 )
1449 1447 return True # report a failure
1450 1448
1451 1449 # check for overwrites
1452 1450 exists = os.path.lexists(target)
1453 1451 samefile = False
1454 1452 if exists and abssrc != abstarget:
1455 1453 if repo.dirstate.normalize(abssrc) == repo.dirstate.normalize(
1456 1454 abstarget
1457 1455 ):
1458 1456 if not rename:
1459 1457 ui.warn(_(b"%s: can't copy - same file\n") % reltarget)
1460 1458 return True # report a failure
1461 1459 exists = False
1462 1460 samefile = True
1463 1461
1464 1462 if not after and exists or after and state in b'mn':
1465 1463 if not opts[b'force']:
1466 1464 if state in b'mn':
1467 1465 msg = _(b'%s: not overwriting - file already committed\n')
1468 1466 if after:
1469 1467 flags = b'--after --force'
1470 1468 else:
1471 1469 flags = b'--force'
1472 1470 if rename:
1473 1471 hint = (
1474 1472 _(
1475 1473 b"('hg rename %s' to replace the file by "
1476 1474 b'recording a rename)\n'
1477 1475 )
1478 1476 % flags
1479 1477 )
1480 1478 else:
1481 1479 hint = (
1482 1480 _(
1483 1481 b"('hg copy %s' to replace the file by "
1484 1482 b'recording a copy)\n'
1485 1483 )
1486 1484 % flags
1487 1485 )
1488 1486 else:
1489 1487 msg = _(b'%s: not overwriting - file exists\n')
1490 1488 if rename:
1491 1489 hint = _(
1492 1490 b"('hg rename --after' to record the rename)\n"
1493 1491 )
1494 1492 else:
1495 1493 hint = _(b"('hg copy --after' to record the copy)\n")
1496 1494 ui.warn(msg % reltarget)
1497 1495 ui.warn(hint)
1498 1496 return True # report a failure
1499 1497
1500 1498 if after:
1501 1499 if not exists:
1502 1500 if rename:
1503 1501 ui.warn(
1504 1502 _(b'%s: not recording move - %s does not exist\n')
1505 1503 % (relsrc, reltarget)
1506 1504 )
1507 1505 else:
1508 1506 ui.warn(
1509 1507 _(b'%s: not recording copy - %s does not exist\n')
1510 1508 % (relsrc, reltarget)
1511 1509 )
1512 1510 return True # report a failure
1513 1511 elif not dryrun:
1514 1512 try:
1515 1513 if exists:
1516 1514 os.unlink(target)
1517 1515 targetdir = os.path.dirname(target) or b'.'
1518 1516 if not os.path.isdir(targetdir):
1519 1517 os.makedirs(targetdir)
1520 1518 if samefile:
1521 1519 tmp = target + b"~hgrename"
1522 1520 os.rename(src, tmp)
1523 1521 os.rename(tmp, target)
1524 1522 else:
1525 1523 # Preserve stat info on renames, not on copies; this matches
1526 1524 # Linux CLI behavior.
1527 1525 util.copyfile(src, target, copystat=rename)
1528 1526 srcexists = True
1529 1527 except IOError as inst:
1530 1528 if inst.errno == errno.ENOENT:
1531 1529 ui.warn(_(b'%s: deleted in working directory\n') % relsrc)
1532 1530 srcexists = False
1533 1531 else:
1534 1532 ui.warn(
1535 1533 _(b'%s: cannot copy - %s\n')
1536 1534 % (relsrc, encoding.strtolocal(inst.strerror))
1537 1535 )
1538 1536 return True # report a failure
1539 1537
1540 1538 if ui.verbose or not exact:
1541 1539 if rename:
1542 1540 ui.status(_(b'moving %s to %s\n') % (relsrc, reltarget))
1543 1541 else:
1544 1542 ui.status(_(b'copying %s to %s\n') % (relsrc, reltarget))
1545 1543
1546 1544 targets[abstarget] = abssrc
1547 1545
1548 1546 # fix up dirstate
1549 1547 scmutil.dirstatecopy(
1550 1548 ui, repo, wctx, abssrc, abstarget, dryrun=dryrun, cwd=cwd
1551 1549 )
1552 1550 if rename and not dryrun:
1553 1551 if not after and srcexists and not samefile:
1554 1552 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
1555 1553 repo.wvfs.unlinkpath(abssrc, rmdir=rmdir)
1556 1554 wctx.forget([abssrc])
1557 1555
1558 1556 # pat: ossep
1559 1557 # dest ossep
1560 1558 # srcs: list of (hgsep, hgsep, ossep, bool)
1561 1559 # return: function that takes hgsep and returns ossep
1562 1560 def targetpathfn(pat, dest, srcs):
1563 1561 if os.path.isdir(pat):
1564 1562 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1565 1563 abspfx = util.localpath(abspfx)
1566 1564 if destdirexists:
1567 1565 striplen = len(os.path.split(abspfx)[0])
1568 1566 else:
1569 1567 striplen = len(abspfx)
1570 1568 if striplen:
1571 1569 striplen += len(pycompat.ossep)
1572 1570 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1573 1571 elif destdirexists:
1574 1572 res = lambda p: os.path.join(
1575 1573 dest, os.path.basename(util.localpath(p))
1576 1574 )
1577 1575 else:
1578 1576 res = lambda p: dest
1579 1577 return res
1580 1578
1581 1579 # pat: ossep
1582 1580 # dest ossep
1583 1581 # srcs: list of (hgsep, hgsep, ossep, bool)
1584 1582 # return: function that takes hgsep and returns ossep
1585 1583 def targetpathafterfn(pat, dest, srcs):
1586 1584 if matchmod.patkind(pat):
1587 1585 # a mercurial pattern
1588 1586 res = lambda p: os.path.join(
1589 1587 dest, os.path.basename(util.localpath(p))
1590 1588 )
1591 1589 else:
1592 1590 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1593 1591 if len(abspfx) < len(srcs[0][0]):
1594 1592 # A directory. Either the target path contains the last
1595 1593 # component of the source path or it does not.
1596 1594 def evalpath(striplen):
1597 1595 score = 0
1598 1596 for s in srcs:
1599 1597 t = os.path.join(dest, util.localpath(s[0])[striplen:])
1600 1598 if os.path.lexists(t):
1601 1599 score += 1
1602 1600 return score
1603 1601
1604 1602 abspfx = util.localpath(abspfx)
1605 1603 striplen = len(abspfx)
1606 1604 if striplen:
1607 1605 striplen += len(pycompat.ossep)
1608 1606 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
1609 1607 score = evalpath(striplen)
1610 1608 striplen1 = len(os.path.split(abspfx)[0])
1611 1609 if striplen1:
1612 1610 striplen1 += len(pycompat.ossep)
1613 1611 if evalpath(striplen1) > score:
1614 1612 striplen = striplen1
1615 1613 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1616 1614 else:
1617 1615 # a file
1618 1616 if destdirexists:
1619 1617 res = lambda p: os.path.join(
1620 1618 dest, os.path.basename(util.localpath(p))
1621 1619 )
1622 1620 else:
1623 1621 res = lambda p: dest
1624 1622 return res
1625 1623
1626 1624 pats = scmutil.expandpats(pats)
1627 1625 if not pats:
1628 1626 raise error.Abort(_(b'no source or destination specified'))
1629 1627 if len(pats) == 1:
1630 1628 raise error.Abort(_(b'no destination specified'))
1631 1629 dest = pats.pop()
1632 1630 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
1633 1631 if not destdirexists:
1634 1632 if len(pats) > 1 or matchmod.patkind(pats[0]):
1635 1633 raise error.Abort(
1636 1634 _(
1637 1635 b'with multiple sources, destination must be an '
1638 1636 b'existing directory'
1639 1637 )
1640 1638 )
1641 1639 if util.endswithsep(dest):
1642 1640 raise error.Abort(_(b'destination %s is not a directory') % dest)
1643 1641
1644 1642 tfn = targetpathfn
1645 1643 if after:
1646 1644 tfn = targetpathafterfn
1647 1645 copylist = []
1648 1646 for pat in pats:
1649 1647 srcs = walkpat(pat)
1650 1648 if not srcs:
1651 1649 continue
1652 1650 copylist.append((tfn(pat, dest, srcs), srcs))
1653 1651 if not copylist:
1654 1652 raise error.Abort(_(b'no files to copy'))
1655 1653
1656 1654 errors = 0
1657 1655 for targetpath, srcs in copylist:
1658 1656 for abssrc, relsrc, exact in srcs:
1659 1657 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
1660 1658 errors += 1
1661 1659
1662 1660 return errors != 0
1663 1661
1664 1662
1665 1663 ## facility to let extension process additional data into an import patch
1666 1664 # list of identifier to be executed in order
1667 1665 extrapreimport = [] # run before commit
1668 1666 extrapostimport = [] # run after commit
1669 1667 # mapping from identifier to actual import function
1670 1668 #
1671 1669 # 'preimport' are run before the commit is made and are provided the following
1672 1670 # arguments:
1673 1671 # - repo: the localrepository instance,
1674 1672 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
1675 1673 # - extra: the future extra dictionary of the changeset, please mutate it,
1676 1674 # - opts: the import options.
1677 1675 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
1678 1676 # mutation of in memory commit and more. Feel free to rework the code to get
1679 1677 # there.
1680 1678 extrapreimportmap = {}
1681 1679 # 'postimport' are run after the commit is made and are provided the following
1682 1680 # argument:
1683 1681 # - ctx: the changectx created by import.
1684 1682 extrapostimportmap = {}
1685 1683
1686 1684
1687 1685 def tryimportone(ui, repo, patchdata, parents, opts, msgs, updatefunc):
1688 1686 """Utility function used by commands.import to import a single patch
1689 1687
1690 1688 This function is explicitly defined here to help the evolve extension to
1691 1689 wrap this part of the import logic.
1692 1690
1693 1691 The API is currently a bit ugly because it a simple code translation from
1694 1692 the import command. Feel free to make it better.
1695 1693
1696 1694 :patchdata: a dictionary containing parsed patch data (such as from
1697 1695 ``patch.extract()``)
1698 1696 :parents: nodes that will be parent of the created commit
1699 1697 :opts: the full dict of option passed to the import command
1700 1698 :msgs: list to save commit message to.
1701 1699 (used in case we need to save it when failing)
1702 1700 :updatefunc: a function that update a repo to a given node
1703 1701 updatefunc(<repo>, <node>)
1704 1702 """
1705 1703 # avoid cycle context -> subrepo -> cmdutil
1706 1704 from . import context
1707 1705
1708 1706 tmpname = patchdata.get(b'filename')
1709 1707 message = patchdata.get(b'message')
1710 1708 user = opts.get(b'user') or patchdata.get(b'user')
1711 1709 date = opts.get(b'date') or patchdata.get(b'date')
1712 1710 branch = patchdata.get(b'branch')
1713 1711 nodeid = patchdata.get(b'nodeid')
1714 1712 p1 = patchdata.get(b'p1')
1715 1713 p2 = patchdata.get(b'p2')
1716 1714
1717 1715 nocommit = opts.get(b'no_commit')
1718 1716 importbranch = opts.get(b'import_branch')
1719 1717 update = not opts.get(b'bypass')
1720 1718 strip = opts[b"strip"]
1721 1719 prefix = opts[b"prefix"]
1722 1720 sim = float(opts.get(b'similarity') or 0)
1723 1721
1724 1722 if not tmpname:
1725 1723 return None, None, False
1726 1724
1727 1725 rejects = False
1728 1726
1729 1727 cmdline_message = logmessage(ui, opts)
1730 1728 if cmdline_message:
1731 1729 # pickup the cmdline msg
1732 1730 message = cmdline_message
1733 1731 elif message:
1734 1732 # pickup the patch msg
1735 1733 message = message.strip()
1736 1734 else:
1737 1735 # launch the editor
1738 1736 message = None
1739 1737 ui.debug(b'message:\n%s\n' % (message or b''))
1740 1738
1741 1739 if len(parents) == 1:
1742 1740 parents.append(repo[nullid])
1743 1741 if opts.get(b'exact'):
1744 1742 if not nodeid or not p1:
1745 1743 raise error.Abort(_(b'not a Mercurial patch'))
1746 1744 p1 = repo[p1]
1747 1745 p2 = repo[p2 or nullid]
1748 1746 elif p2:
1749 1747 try:
1750 1748 p1 = repo[p1]
1751 1749 p2 = repo[p2]
1752 1750 # Without any options, consider p2 only if the
1753 1751 # patch is being applied on top of the recorded
1754 1752 # first parent.
1755 1753 if p1 != parents[0]:
1756 1754 p1 = parents[0]
1757 1755 p2 = repo[nullid]
1758 1756 except error.RepoError:
1759 1757 p1, p2 = parents
1760 1758 if p2.node() == nullid:
1761 1759 ui.warn(
1762 1760 _(
1763 1761 b"warning: import the patch as a normal revision\n"
1764 1762 b"(use --exact to import the patch as a merge)\n"
1765 1763 )
1766 1764 )
1767 1765 else:
1768 1766 p1, p2 = parents
1769 1767
1770 1768 n = None
1771 1769 if update:
1772 1770 if p1 != parents[0]:
1773 1771 updatefunc(repo, p1.node())
1774 1772 if p2 != parents[1]:
1775 1773 repo.setparents(p1.node(), p2.node())
1776 1774
1777 1775 if opts.get(b'exact') or importbranch:
1778 1776 repo.dirstate.setbranch(branch or b'default')
1779 1777
1780 1778 partial = opts.get(b'partial', False)
1781 1779 files = set()
1782 1780 try:
1783 1781 patch.patch(
1784 1782 ui,
1785 1783 repo,
1786 1784 tmpname,
1787 1785 strip=strip,
1788 1786 prefix=prefix,
1789 1787 files=files,
1790 1788 eolmode=None,
1791 1789 similarity=sim / 100.0,
1792 1790 )
1793 1791 except error.PatchError as e:
1794 1792 if not partial:
1795 1793 raise error.Abort(pycompat.bytestr(e))
1796 1794 if partial:
1797 1795 rejects = True
1798 1796
1799 1797 files = list(files)
1800 1798 if nocommit:
1801 1799 if message:
1802 1800 msgs.append(message)
1803 1801 else:
1804 1802 if opts.get(b'exact') or p2:
1805 1803 # If you got here, you either use --force and know what
1806 1804 # you are doing or used --exact or a merge patch while
1807 1805 # being updated to its first parent.
1808 1806 m = None
1809 1807 else:
1810 1808 m = scmutil.matchfiles(repo, files or [])
1811 1809 editform = mergeeditform(repo[None], b'import.normal')
1812 1810 if opts.get(b'exact'):
1813 1811 editor = None
1814 1812 else:
1815 1813 editor = getcommiteditor(
1816 1814 editform=editform, **pycompat.strkwargs(opts)
1817 1815 )
1818 1816 extra = {}
1819 1817 for idfunc in extrapreimport:
1820 1818 extrapreimportmap[idfunc](repo, patchdata, extra, opts)
1821 1819 overrides = {}
1822 1820 if partial:
1823 1821 overrides[(b'ui', b'allowemptycommit')] = True
1824 1822 if opts.get(b'secret'):
1825 1823 overrides[(b'phases', b'new-commit')] = b'secret'
1826 1824 with repo.ui.configoverride(overrides, b'import'):
1827 1825 n = repo.commit(
1828 1826 message, user, date, match=m, editor=editor, extra=extra
1829 1827 )
1830 1828 for idfunc in extrapostimport:
1831 1829 extrapostimportmap[idfunc](repo[n])
1832 1830 else:
1833 1831 if opts.get(b'exact') or importbranch:
1834 1832 branch = branch or b'default'
1835 1833 else:
1836 1834 branch = p1.branch()
1837 1835 store = patch.filestore()
1838 1836 try:
1839 1837 files = set()
1840 1838 try:
1841 1839 patch.patchrepo(
1842 1840 ui,
1843 1841 repo,
1844 1842 p1,
1845 1843 store,
1846 1844 tmpname,
1847 1845 strip,
1848 1846 prefix,
1849 1847 files,
1850 1848 eolmode=None,
1851 1849 )
1852 1850 except error.PatchError as e:
1853 1851 raise error.Abort(stringutil.forcebytestr(e))
1854 1852 if opts.get(b'exact'):
1855 1853 editor = None
1856 1854 else:
1857 1855 editor = getcommiteditor(editform=b'import.bypass')
1858 1856 memctx = context.memctx(
1859 1857 repo,
1860 1858 (p1.node(), p2.node()),
1861 1859 message,
1862 1860 files=files,
1863 1861 filectxfn=store,
1864 1862 user=user,
1865 1863 date=date,
1866 1864 branch=branch,
1867 1865 editor=editor,
1868 1866 )
1869 1867 n = memctx.commit()
1870 1868 finally:
1871 1869 store.close()
1872 1870 if opts.get(b'exact') and nocommit:
1873 1871 # --exact with --no-commit is still useful in that it does merge
1874 1872 # and branch bits
1875 1873 ui.warn(_(b"warning: can't check exact import with --no-commit\n"))
1876 1874 elif opts.get(b'exact') and (not n or hex(n) != nodeid):
1877 1875 raise error.Abort(_(b'patch is damaged or loses information'))
1878 1876 msg = _(b'applied to working directory')
1879 1877 if n:
1880 1878 # i18n: refers to a short changeset id
1881 1879 msg = _(b'created %s') % short(n)
1882 1880 return msg, n, rejects
1883 1881
1884 1882
1885 1883 # facility to let extensions include additional data in an exported patch
1886 1884 # list of identifiers to be executed in order
1887 1885 extraexport = []
1888 1886 # mapping from identifier to actual export function
1889 1887 # function as to return a string to be added to the header or None
1890 1888 # it is given two arguments (sequencenumber, changectx)
1891 1889 extraexportmap = {}
1892 1890
1893 1891
1894 1892 def _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts):
1895 1893 node = scmutil.binnode(ctx)
1896 1894 parents = [p.node() for p in ctx.parents() if p]
1897 1895 branch = ctx.branch()
1898 1896 if switch_parent:
1899 1897 parents.reverse()
1900 1898
1901 1899 if parents:
1902 1900 prev = parents[0]
1903 1901 else:
1904 1902 prev = nullid
1905 1903
1906 1904 fm.context(ctx=ctx)
1907 1905 fm.plain(b'# HG changeset patch\n')
1908 1906 fm.write(b'user', b'# User %s\n', ctx.user())
1909 1907 fm.plain(b'# Date %d %d\n' % ctx.date())
1910 1908 fm.write(b'date', b'# %s\n', fm.formatdate(ctx.date()))
1911 1909 fm.condwrite(
1912 1910 branch and branch != b'default', b'branch', b'# Branch %s\n', branch
1913 1911 )
1914 1912 fm.write(b'node', b'# Node ID %s\n', hex(node))
1915 1913 fm.plain(b'# Parent %s\n' % hex(prev))
1916 1914 if len(parents) > 1:
1917 1915 fm.plain(b'# Parent %s\n' % hex(parents[1]))
1918 1916 fm.data(parents=fm.formatlist(pycompat.maplist(hex, parents), name=b'node'))
1919 1917
1920 1918 # TODO: redesign extraexportmap function to support formatter
1921 1919 for headerid in extraexport:
1922 1920 header = extraexportmap[headerid](seqno, ctx)
1923 1921 if header is not None:
1924 1922 fm.plain(b'# %s\n' % header)
1925 1923
1926 1924 fm.write(b'desc', b'%s\n', ctx.description().rstrip())
1927 1925 fm.plain(b'\n')
1928 1926
1929 1927 if fm.isplain():
1930 1928 chunkiter = patch.diffui(repo, prev, node, match, opts=diffopts)
1931 1929 for chunk, label in chunkiter:
1932 1930 fm.plain(chunk, label=label)
1933 1931 else:
1934 1932 chunkiter = patch.diff(repo, prev, node, match, opts=diffopts)
1935 1933 # TODO: make it structured?
1936 1934 fm.data(diff=b''.join(chunkiter))
1937 1935
1938 1936
1939 1937 def _exportfile(repo, revs, fm, dest, switch_parent, diffopts, match):
1940 1938 """Export changesets to stdout or a single file"""
1941 1939 for seqno, rev in enumerate(revs, 1):
1942 1940 ctx = repo[rev]
1943 1941 if not dest.startswith(b'<'):
1944 1942 repo.ui.note(b"%s\n" % dest)
1945 1943 fm.startitem()
1946 1944 _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts)
1947 1945
1948 1946
1949 1947 def _exportfntemplate(
1950 1948 repo, revs, basefm, fntemplate, switch_parent, diffopts, match
1951 1949 ):
1952 1950 """Export changesets to possibly multiple files"""
1953 1951 total = len(revs)
1954 1952 revwidth = max(len(str(rev)) for rev in revs)
1955 1953 filemap = util.sortdict() # filename: [(seqno, rev), ...]
1956 1954
1957 1955 for seqno, rev in enumerate(revs, 1):
1958 1956 ctx = repo[rev]
1959 1957 dest = makefilename(
1960 1958 ctx, fntemplate, total=total, seqno=seqno, revwidth=revwidth
1961 1959 )
1962 1960 filemap.setdefault(dest, []).append((seqno, rev))
1963 1961
1964 1962 for dest in filemap:
1965 1963 with formatter.maybereopen(basefm, dest) as fm:
1966 1964 repo.ui.note(b"%s\n" % dest)
1967 1965 for seqno, rev in filemap[dest]:
1968 1966 fm.startitem()
1969 1967 ctx = repo[rev]
1970 1968 _exportsingle(
1971 1969 repo, ctx, fm, match, switch_parent, seqno, diffopts
1972 1970 )
1973 1971
1974 1972
1975 1973 def _prefetchchangedfiles(repo, revs, match):
1976 1974 allfiles = set()
1977 1975 for rev in revs:
1978 1976 for file in repo[rev].files():
1979 1977 if not match or match(file):
1980 1978 allfiles.add(file)
1981 1979 scmutil.prefetchfiles(repo, revs, scmutil.matchfiles(repo, allfiles))
1982 1980
1983 1981
1984 1982 def export(
1985 1983 repo,
1986 1984 revs,
1987 1985 basefm,
1988 1986 fntemplate=b'hg-%h.patch',
1989 1987 switch_parent=False,
1990 1988 opts=None,
1991 1989 match=None,
1992 1990 ):
1993 1991 '''export changesets as hg patches
1994 1992
1995 1993 Args:
1996 1994 repo: The repository from which we're exporting revisions.
1997 1995 revs: A list of revisions to export as revision numbers.
1998 1996 basefm: A formatter to which patches should be written.
1999 1997 fntemplate: An optional string to use for generating patch file names.
2000 1998 switch_parent: If True, show diffs against second parent when not nullid.
2001 1999 Default is false, which always shows diff against p1.
2002 2000 opts: diff options to use for generating the patch.
2003 2001 match: If specified, only export changes to files matching this matcher.
2004 2002
2005 2003 Returns:
2006 2004 Nothing.
2007 2005
2008 2006 Side Effect:
2009 2007 "HG Changeset Patch" data is emitted to one of the following
2010 2008 destinations:
2011 2009 fntemplate specified: Each rev is written to a unique file named using
2012 2010 the given template.
2013 2011 Otherwise: All revs will be written to basefm.
2014 2012 '''
2015 2013 _prefetchchangedfiles(repo, revs, match)
2016 2014
2017 2015 if not fntemplate:
2018 2016 _exportfile(
2019 2017 repo, revs, basefm, b'<unnamed>', switch_parent, opts, match
2020 2018 )
2021 2019 else:
2022 2020 _exportfntemplate(
2023 2021 repo, revs, basefm, fntemplate, switch_parent, opts, match
2024 2022 )
2025 2023
2026 2024
2027 2025 def exportfile(repo, revs, fp, switch_parent=False, opts=None, match=None):
2028 2026 """Export changesets to the given file stream"""
2029 2027 _prefetchchangedfiles(repo, revs, match)
2030 2028
2031 2029 dest = getattr(fp, 'name', b'<unnamed>')
2032 2030 with formatter.formatter(repo.ui, fp, b'export', {}) as fm:
2033 2031 _exportfile(repo, revs, fm, dest, switch_parent, opts, match)
2034 2032
2035 2033
2036 2034 def showmarker(fm, marker, index=None):
2037 2035 """utility function to display obsolescence marker in a readable way
2038 2036
2039 2037 To be used by debug function."""
2040 2038 if index is not None:
2041 2039 fm.write(b'index', b'%i ', index)
2042 2040 fm.write(b'prednode', b'%s ', hex(marker.prednode()))
2043 2041 succs = marker.succnodes()
2044 2042 fm.condwrite(
2045 2043 succs,
2046 2044 b'succnodes',
2047 2045 b'%s ',
2048 2046 fm.formatlist(map(hex, succs), name=b'node'),
2049 2047 )
2050 2048 fm.write(b'flag', b'%X ', marker.flags())
2051 2049 parents = marker.parentnodes()
2052 2050 if parents is not None:
2053 2051 fm.write(
2054 2052 b'parentnodes',
2055 2053 b'{%s} ',
2056 2054 fm.formatlist(map(hex, parents), name=b'node', sep=b', '),
2057 2055 )
2058 2056 fm.write(b'date', b'(%s) ', fm.formatdate(marker.date()))
2059 2057 meta = marker.metadata().copy()
2060 2058 meta.pop(b'date', None)
2061 2059 smeta = pycompat.rapply(pycompat.maybebytestr, meta)
2062 2060 fm.write(
2063 2061 b'metadata', b'{%s}', fm.formatdict(smeta, fmt=b'%r: %r', sep=b', ')
2064 2062 )
2065 2063 fm.plain(b'\n')
2066 2064
2067 2065
2068 2066 def finddate(ui, repo, date):
2069 2067 """Find the tipmost changeset that matches the given date spec"""
2070 2068
2071 2069 df = dateutil.matchdate(date)
2072 2070 m = scmutil.matchall(repo)
2073 2071 results = {}
2074 2072
2075 2073 def prep(ctx, fns):
2076 2074 d = ctx.date()
2077 2075 if df(d[0]):
2078 2076 results[ctx.rev()] = d
2079 2077
2080 2078 for ctx in walkchangerevs(repo, m, {b'rev': None}, prep):
2081 2079 rev = ctx.rev()
2082 2080 if rev in results:
2083 2081 ui.status(
2084 2082 _(b"found revision %d from %s\n")
2085 2083 % (rev, dateutil.datestr(results[rev]))
2086 2084 )
2087 2085 return b'%d' % rev
2088 2086
2089 2087 raise error.Abort(_(b"revision matching date not found"))
2090 2088
2091 2089
2092 2090 def increasingwindows(windowsize=8, sizelimit=512):
2093 2091 while True:
2094 2092 yield windowsize
2095 2093 if windowsize < sizelimit:
2096 2094 windowsize *= 2
2097 2095
2098 2096
2099 2097 def _walkrevs(repo, opts):
2100 2098 # Default --rev value depends on --follow but --follow behavior
2101 2099 # depends on revisions resolved from --rev...
2102 2100 follow = opts.get(b'follow') or opts.get(b'follow_first')
2103 2101 if opts.get(b'rev'):
2104 2102 revs = scmutil.revrange(repo, opts[b'rev'])
2105 2103 elif follow and repo.dirstate.p1() == nullid:
2106 2104 revs = smartset.baseset()
2107 2105 elif follow:
2108 2106 revs = repo.revs(b'reverse(:.)')
2109 2107 else:
2110 2108 revs = smartset.spanset(repo)
2111 2109 revs.reverse()
2112 2110 return revs
2113 2111
2114 2112
2115 2113 class FileWalkError(Exception):
2116 2114 pass
2117 2115
2118 2116
2119 2117 def walkfilerevs(repo, match, follow, revs, fncache):
2120 2118 '''Walks the file history for the matched files.
2121 2119
2122 2120 Returns the changeset revs that are involved in the file history.
2123 2121
2124 2122 Throws FileWalkError if the file history can't be walked using
2125 2123 filelogs alone.
2126 2124 '''
2127 2125 wanted = set()
2128 2126 copies = []
2129 2127 minrev, maxrev = min(revs), max(revs)
2130 2128
2131 2129 def filerevs(filelog, last):
2132 2130 """
2133 2131 Only files, no patterns. Check the history of each file.
2134 2132
2135 2133 Examines filelog entries within minrev, maxrev linkrev range
2136 2134 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
2137 2135 tuples in backwards order
2138 2136 """
2139 2137 cl_count = len(repo)
2140 2138 revs = []
2141 2139 for j in pycompat.xrange(0, last + 1):
2142 2140 linkrev = filelog.linkrev(j)
2143 2141 if linkrev < minrev:
2144 2142 continue
2145 2143 # only yield rev for which we have the changelog, it can
2146 2144 # happen while doing "hg log" during a pull or commit
2147 2145 if linkrev >= cl_count:
2148 2146 break
2149 2147
2150 2148 parentlinkrevs = []
2151 2149 for p in filelog.parentrevs(j):
2152 2150 if p != nullrev:
2153 2151 parentlinkrevs.append(filelog.linkrev(p))
2154 2152 n = filelog.node(j)
2155 2153 revs.append(
2156 2154 (linkrev, parentlinkrevs, follow and filelog.renamed(n))
2157 2155 )
2158 2156
2159 2157 return reversed(revs)
2160 2158
2161 2159 def iterfiles():
2162 2160 pctx = repo[b'.']
2163 2161 for filename in match.files():
2164 2162 if follow:
2165 2163 if filename not in pctx:
2166 2164 raise error.Abort(
2167 2165 _(
2168 2166 b'cannot follow file not in parent '
2169 2167 b'revision: "%s"'
2170 2168 )
2171 2169 % filename
2172 2170 )
2173 2171 yield filename, pctx[filename].filenode()
2174 2172 else:
2175 2173 yield filename, None
2176 2174 for filename_node in copies:
2177 2175 yield filename_node
2178 2176
2179 2177 for file_, node in iterfiles():
2180 2178 filelog = repo.file(file_)
2181 2179 if not len(filelog):
2182 2180 if node is None:
2183 2181 # A zero count may be a directory or deleted file, so
2184 2182 # try to find matching entries on the slow path.
2185 2183 if follow:
2186 2184 raise error.Abort(
2187 2185 _(b'cannot follow nonexistent file: "%s"') % file_
2188 2186 )
2189 2187 raise FileWalkError(b"Cannot walk via filelog")
2190 2188 else:
2191 2189 continue
2192 2190
2193 2191 if node is None:
2194 2192 last = len(filelog) - 1
2195 2193 else:
2196 2194 last = filelog.rev(node)
2197 2195
2198 2196 # keep track of all ancestors of the file
2199 2197 ancestors = {filelog.linkrev(last)}
2200 2198
2201 2199 # iterate from latest to oldest revision
2202 2200 for rev, flparentlinkrevs, copied in filerevs(filelog, last):
2203 2201 if not follow:
2204 2202 if rev > maxrev:
2205 2203 continue
2206 2204 else:
2207 2205 # Note that last might not be the first interesting
2208 2206 # rev to us:
2209 2207 # if the file has been changed after maxrev, we'll
2210 2208 # have linkrev(last) > maxrev, and we still need
2211 2209 # to explore the file graph
2212 2210 if rev not in ancestors:
2213 2211 continue
2214 2212 # XXX insert 1327 fix here
2215 2213 if flparentlinkrevs:
2216 2214 ancestors.update(flparentlinkrevs)
2217 2215
2218 2216 fncache.setdefault(rev, []).append(file_)
2219 2217 wanted.add(rev)
2220 2218 if copied:
2221 2219 copies.append(copied)
2222 2220
2223 2221 return wanted
2224 2222
2225 2223
2226 2224 class _followfilter(object):
2227 2225 def __init__(self, repo, onlyfirst=False):
2228 2226 self.repo = repo
2229 2227 self.startrev = nullrev
2230 2228 self.roots = set()
2231 2229 self.onlyfirst = onlyfirst
2232 2230
2233 2231 def match(self, rev):
2234 2232 def realparents(rev):
2235 2233 if self.onlyfirst:
2236 2234 return self.repo.changelog.parentrevs(rev)[0:1]
2237 2235 else:
2238 2236 return filter(
2239 2237 lambda x: x != nullrev, self.repo.changelog.parentrevs(rev)
2240 2238 )
2241 2239
2242 2240 if self.startrev == nullrev:
2243 2241 self.startrev = rev
2244 2242 return True
2245 2243
2246 2244 if rev > self.startrev:
2247 2245 # forward: all descendants
2248 2246 if not self.roots:
2249 2247 self.roots.add(self.startrev)
2250 2248 for parent in realparents(rev):
2251 2249 if parent in self.roots:
2252 2250 self.roots.add(rev)
2253 2251 return True
2254 2252 else:
2255 2253 # backwards: all parents
2256 2254 if not self.roots:
2257 2255 self.roots.update(realparents(self.startrev))
2258 2256 if rev in self.roots:
2259 2257 self.roots.remove(rev)
2260 2258 self.roots.update(realparents(rev))
2261 2259 return True
2262 2260
2263 2261 return False
2264 2262
2265 2263
2266 2264 def walkchangerevs(repo, match, opts, prepare):
2267 2265 '''Iterate over files and the revs in which they changed.
2268 2266
2269 2267 Callers most commonly need to iterate backwards over the history
2270 2268 in which they are interested. Doing so has awful (quadratic-looking)
2271 2269 performance, so we use iterators in a "windowed" way.
2272 2270
2273 2271 We walk a window of revisions in the desired order. Within the
2274 2272 window, we first walk forwards to gather data, then in the desired
2275 2273 order (usually backwards) to display it.
2276 2274
2277 2275 This function returns an iterator yielding contexts. Before
2278 2276 yielding each context, the iterator will first call the prepare
2279 2277 function on each context in the window in forward order.'''
2280 2278
2281 2279 allfiles = opts.get(b'all_files')
2282 2280 follow = opts.get(b'follow') or opts.get(b'follow_first')
2283 2281 revs = _walkrevs(repo, opts)
2284 2282 if not revs:
2285 2283 return []
2286 2284 wanted = set()
2287 2285 slowpath = match.anypats() or (not match.always() and opts.get(b'removed'))
2288 2286 fncache = {}
2289 2287 change = repo.__getitem__
2290 2288
2291 2289 # First step is to fill wanted, the set of revisions that we want to yield.
2292 2290 # When it does not induce extra cost, we also fill fncache for revisions in
2293 2291 # wanted: a cache of filenames that were changed (ctx.files()) and that
2294 2292 # match the file filtering conditions.
2295 2293
2296 2294 if match.always() or allfiles:
2297 2295 # No files, no patterns. Display all revs.
2298 2296 wanted = revs
2299 2297 elif not slowpath:
2300 2298 # We only have to read through the filelog to find wanted revisions
2301 2299
2302 2300 try:
2303 2301 wanted = walkfilerevs(repo, match, follow, revs, fncache)
2304 2302 except FileWalkError:
2305 2303 slowpath = True
2306 2304
2307 2305 # We decided to fall back to the slowpath because at least one
2308 2306 # of the paths was not a file. Check to see if at least one of them
2309 2307 # existed in history, otherwise simply return
2310 2308 for path in match.files():
2311 2309 if path == b'.' or path in repo.store:
2312 2310 break
2313 2311 else:
2314 2312 return []
2315 2313
2316 2314 if slowpath:
2317 2315 # We have to read the changelog to match filenames against
2318 2316 # changed files
2319 2317
2320 2318 if follow:
2321 2319 raise error.Abort(
2322 2320 _(b'can only follow copies/renames for explicit filenames')
2323 2321 )
2324 2322
2325 2323 # The slow path checks files modified in every changeset.
2326 2324 # This is really slow on large repos, so compute the set lazily.
2327 2325 class lazywantedset(object):
2328 2326 def __init__(self):
2329 2327 self.set = set()
2330 2328 self.revs = set(revs)
2331 2329
2332 2330 # No need to worry about locality here because it will be accessed
2333 2331 # in the same order as the increasing window below.
2334 2332 def __contains__(self, value):
2335 2333 if value in self.set:
2336 2334 return True
2337 2335 elif not value in self.revs:
2338 2336 return False
2339 2337 else:
2340 2338 self.revs.discard(value)
2341 2339 ctx = change(value)
2342 2340 if allfiles:
2343 2341 matches = list(ctx.manifest().walk(match))
2344 2342 else:
2345 2343 matches = [f for f in ctx.files() if match(f)]
2346 2344 if matches:
2347 2345 fncache[value] = matches
2348 2346 self.set.add(value)
2349 2347 return True
2350 2348 return False
2351 2349
2352 2350 def discard(self, value):
2353 2351 self.revs.discard(value)
2354 2352 self.set.discard(value)
2355 2353
2356 2354 wanted = lazywantedset()
2357 2355
2358 2356 # it might be worthwhile to do this in the iterator if the rev range
2359 2357 # is descending and the prune args are all within that range
2360 2358 for rev in opts.get(b'prune', ()):
2361 2359 rev = repo[rev].rev()
2362 2360 ff = _followfilter(repo)
2363 2361 stop = min(revs[0], revs[-1])
2364 2362 for x in pycompat.xrange(rev, stop - 1, -1):
2365 2363 if ff.match(x):
2366 2364 wanted = wanted - [x]
2367 2365
2368 2366 # Now that wanted is correctly initialized, we can iterate over the
2369 2367 # revision range, yielding only revisions in wanted.
2370 2368 def iterate():
2371 2369 if follow and match.always():
2372 2370 ff = _followfilter(repo, onlyfirst=opts.get(b'follow_first'))
2373 2371
2374 2372 def want(rev):
2375 2373 return ff.match(rev) and rev in wanted
2376 2374
2377 2375 else:
2378 2376
2379 2377 def want(rev):
2380 2378 return rev in wanted
2381 2379
2382 2380 it = iter(revs)
2383 2381 stopiteration = False
2384 2382 for windowsize in increasingwindows():
2385 2383 nrevs = []
2386 2384 for i in pycompat.xrange(windowsize):
2387 2385 rev = next(it, None)
2388 2386 if rev is None:
2389 2387 stopiteration = True
2390 2388 break
2391 2389 elif want(rev):
2392 2390 nrevs.append(rev)
2393 2391 for rev in sorted(nrevs):
2394 2392 fns = fncache.get(rev)
2395 2393 ctx = change(rev)
2396 2394 if not fns:
2397 2395
2398 2396 def fns_generator():
2399 2397 if allfiles:
2400 2398 fiter = iter(ctx)
2401 2399 else:
2402 2400 fiter = ctx.files()
2403 2401 for f in fiter:
2404 2402 if match(f):
2405 2403 yield f
2406 2404
2407 2405 fns = fns_generator()
2408 2406 prepare(ctx, fns)
2409 2407 for rev in nrevs:
2410 2408 yield change(rev)
2411 2409
2412 2410 if stopiteration:
2413 2411 break
2414 2412
2415 2413 return iterate()
2416 2414
2417 2415
2418 2416 def add(ui, repo, match, prefix, uipathfn, explicitonly, **opts):
2419 2417 bad = []
2420 2418
2421 2419 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2422 2420 names = []
2423 2421 wctx = repo[None]
2424 2422 cca = None
2425 2423 abort, warn = scmutil.checkportabilityalert(ui)
2426 2424 if abort or warn:
2427 2425 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2428 2426
2429 2427 match = repo.narrowmatch(match, includeexact=True)
2430 2428 badmatch = matchmod.badmatch(match, badfn)
2431 2429 dirstate = repo.dirstate
2432 2430 # We don't want to just call wctx.walk here, since it would return a lot of
2433 2431 # clean files, which we aren't interested in and takes time.
2434 2432 for f in sorted(
2435 2433 dirstate.walk(
2436 2434 badmatch,
2437 2435 subrepos=sorted(wctx.substate),
2438 2436 unknown=True,
2439 2437 ignored=False,
2440 2438 full=False,
2441 2439 )
2442 2440 ):
2443 2441 exact = match.exact(f)
2444 2442 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2445 2443 if cca:
2446 2444 cca(f)
2447 2445 names.append(f)
2448 2446 if ui.verbose or not exact:
2449 2447 ui.status(
2450 2448 _(b'adding %s\n') % uipathfn(f), label=b'ui.addremove.added'
2451 2449 )
2452 2450
2453 2451 for subpath in sorted(wctx.substate):
2454 2452 sub = wctx.sub(subpath)
2455 2453 try:
2456 2454 submatch = matchmod.subdirmatcher(subpath, match)
2457 2455 subprefix = repo.wvfs.reljoin(prefix, subpath)
2458 2456 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2459 2457 if opts.get('subrepos'):
2460 2458 bad.extend(
2461 2459 sub.add(ui, submatch, subprefix, subuipathfn, False, **opts)
2462 2460 )
2463 2461 else:
2464 2462 bad.extend(
2465 2463 sub.add(ui, submatch, subprefix, subuipathfn, True, **opts)
2466 2464 )
2467 2465 except error.LookupError:
2468 2466 ui.status(
2469 2467 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2470 2468 )
2471 2469
2472 2470 if not opts.get('dry_run'):
2473 2471 rejected = wctx.add(names, prefix)
2474 2472 bad.extend(f for f in rejected if f in match.files())
2475 2473 return bad
2476 2474
2477 2475
2478 2476 def addwebdirpath(repo, serverpath, webconf):
2479 2477 webconf[serverpath] = repo.root
2480 2478 repo.ui.debug(b'adding %s = %s\n' % (serverpath, repo.root))
2481 2479
2482 2480 for r in repo.revs(b'filelog("path:.hgsub")'):
2483 2481 ctx = repo[r]
2484 2482 for subpath in ctx.substate:
2485 2483 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2486 2484
2487 2485
2488 2486 def forget(
2489 2487 ui, repo, match, prefix, uipathfn, explicitonly, dryrun, interactive
2490 2488 ):
2491 2489 if dryrun and interactive:
2492 2490 raise error.Abort(_(b"cannot specify both --dry-run and --interactive"))
2493 2491 bad = []
2494 2492 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2495 2493 wctx = repo[None]
2496 2494 forgot = []
2497 2495
2498 2496 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2499 2497 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2500 2498 if explicitonly:
2501 2499 forget = [f for f in forget if match.exact(f)]
2502 2500
2503 2501 for subpath in sorted(wctx.substate):
2504 2502 sub = wctx.sub(subpath)
2505 2503 submatch = matchmod.subdirmatcher(subpath, match)
2506 2504 subprefix = repo.wvfs.reljoin(prefix, subpath)
2507 2505 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2508 2506 try:
2509 2507 subbad, subforgot = sub.forget(
2510 2508 submatch,
2511 2509 subprefix,
2512 2510 subuipathfn,
2513 2511 dryrun=dryrun,
2514 2512 interactive=interactive,
2515 2513 )
2516 2514 bad.extend([subpath + b'/' + f for f in subbad])
2517 2515 forgot.extend([subpath + b'/' + f for f in subforgot])
2518 2516 except error.LookupError:
2519 2517 ui.status(
2520 2518 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2521 2519 )
2522 2520
2523 2521 if not explicitonly:
2524 2522 for f in match.files():
2525 2523 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2526 2524 if f not in forgot:
2527 2525 if repo.wvfs.exists(f):
2528 2526 # Don't complain if the exact case match wasn't given.
2529 2527 # But don't do this until after checking 'forgot', so
2530 2528 # that subrepo files aren't normalized, and this op is
2531 2529 # purely from data cached by the status walk above.
2532 2530 if repo.dirstate.normalize(f) in repo.dirstate:
2533 2531 continue
2534 2532 ui.warn(
2535 2533 _(
2536 2534 b'not removing %s: '
2537 2535 b'file is already untracked\n'
2538 2536 )
2539 2537 % uipathfn(f)
2540 2538 )
2541 2539 bad.append(f)
2542 2540
2543 2541 if interactive:
2544 2542 responses = _(
2545 2543 b'[Ynsa?]'
2546 2544 b'$$ &Yes, forget this file'
2547 2545 b'$$ &No, skip this file'
2548 2546 b'$$ &Skip remaining files'
2549 2547 b'$$ Include &all remaining files'
2550 2548 b'$$ &? (display help)'
2551 2549 )
2552 2550 for filename in forget[:]:
2553 2551 r = ui.promptchoice(
2554 2552 _(b'forget %s %s') % (uipathfn(filename), responses)
2555 2553 )
2556 2554 if r == 4: # ?
2557 2555 while r == 4:
2558 2556 for c, t in ui.extractchoices(responses)[1]:
2559 2557 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
2560 2558 r = ui.promptchoice(
2561 2559 _(b'forget %s %s') % (uipathfn(filename), responses)
2562 2560 )
2563 2561 if r == 0: # yes
2564 2562 continue
2565 2563 elif r == 1: # no
2566 2564 forget.remove(filename)
2567 2565 elif r == 2: # Skip
2568 2566 fnindex = forget.index(filename)
2569 2567 del forget[fnindex:]
2570 2568 break
2571 2569 elif r == 3: # All
2572 2570 break
2573 2571
2574 2572 for f in forget:
2575 2573 if ui.verbose or not match.exact(f) or interactive:
2576 2574 ui.status(
2577 2575 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2578 2576 )
2579 2577
2580 2578 if not dryrun:
2581 2579 rejected = wctx.forget(forget, prefix)
2582 2580 bad.extend(f for f in rejected if f in match.files())
2583 2581 forgot.extend(f for f in forget if f not in rejected)
2584 2582 return bad, forgot
2585 2583
2586 2584
2587 2585 def files(ui, ctx, m, uipathfn, fm, fmt, subrepos):
2588 2586 ret = 1
2589 2587
2590 2588 needsfctx = ui.verbose or {b'size', b'flags'} & fm.datahint()
2591 2589 for f in ctx.matches(m):
2592 2590 fm.startitem()
2593 2591 fm.context(ctx=ctx)
2594 2592 if needsfctx:
2595 2593 fc = ctx[f]
2596 2594 fm.write(b'size flags', b'% 10d % 1s ', fc.size(), fc.flags())
2597 2595 fm.data(path=f)
2598 2596 fm.plain(fmt % uipathfn(f))
2599 2597 ret = 0
2600 2598
2601 2599 for subpath in sorted(ctx.substate):
2602 2600 submatch = matchmod.subdirmatcher(subpath, m)
2603 2601 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2604 2602 if subrepos or m.exact(subpath) or any(submatch.files()):
2605 2603 sub = ctx.sub(subpath)
2606 2604 try:
2607 2605 recurse = m.exact(subpath) or subrepos
2608 2606 if (
2609 2607 sub.printfiles(ui, submatch, subuipathfn, fm, fmt, recurse)
2610 2608 == 0
2611 2609 ):
2612 2610 ret = 0
2613 2611 except error.LookupError:
2614 2612 ui.status(
2615 2613 _(b"skipping missing subrepository: %s\n")
2616 2614 % uipathfn(subpath)
2617 2615 )
2618 2616
2619 2617 return ret
2620 2618
2621 2619
2622 2620 def remove(
2623 2621 ui, repo, m, prefix, uipathfn, after, force, subrepos, dryrun, warnings=None
2624 2622 ):
2625 2623 ret = 0
2626 2624 s = repo.status(match=m, clean=True)
2627 2625 modified, added, deleted, clean = s.modified, s.added, s.deleted, s.clean
2628 2626
2629 2627 wctx = repo[None]
2630 2628
2631 2629 if warnings is None:
2632 2630 warnings = []
2633 2631 warn = True
2634 2632 else:
2635 2633 warn = False
2636 2634
2637 2635 subs = sorted(wctx.substate)
2638 2636 progress = ui.makeprogress(
2639 2637 _(b'searching'), total=len(subs), unit=_(b'subrepos')
2640 2638 )
2641 2639 for subpath in subs:
2642 2640 submatch = matchmod.subdirmatcher(subpath, m)
2643 2641 subprefix = repo.wvfs.reljoin(prefix, subpath)
2644 2642 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2645 2643 if subrepos or m.exact(subpath) or any(submatch.files()):
2646 2644 progress.increment()
2647 2645 sub = wctx.sub(subpath)
2648 2646 try:
2649 2647 if sub.removefiles(
2650 2648 submatch,
2651 2649 subprefix,
2652 2650 subuipathfn,
2653 2651 after,
2654 2652 force,
2655 2653 subrepos,
2656 2654 dryrun,
2657 2655 warnings,
2658 2656 ):
2659 2657 ret = 1
2660 2658 except error.LookupError:
2661 2659 warnings.append(
2662 2660 _(b"skipping missing subrepository: %s\n")
2663 2661 % uipathfn(subpath)
2664 2662 )
2665 2663 progress.complete()
2666 2664
2667 2665 # warn about failure to delete explicit files/dirs
2668 2666 deleteddirs = pathutil.dirs(deleted)
2669 2667 files = m.files()
2670 2668 progress = ui.makeprogress(
2671 2669 _(b'deleting'), total=len(files), unit=_(b'files')
2672 2670 )
2673 2671 for f in files:
2674 2672
2675 2673 def insubrepo():
2676 2674 for subpath in wctx.substate:
2677 2675 if f.startswith(subpath + b'/'):
2678 2676 return True
2679 2677 return False
2680 2678
2681 2679 progress.increment()
2682 2680 isdir = f in deleteddirs or wctx.hasdir(f)
2683 2681 if f in repo.dirstate or isdir or f == b'.' or insubrepo() or f in subs:
2684 2682 continue
2685 2683
2686 2684 if repo.wvfs.exists(f):
2687 2685 if repo.wvfs.isdir(f):
2688 2686 warnings.append(
2689 2687 _(b'not removing %s: no tracked files\n') % uipathfn(f)
2690 2688 )
2691 2689 else:
2692 2690 warnings.append(
2693 2691 _(b'not removing %s: file is untracked\n') % uipathfn(f)
2694 2692 )
2695 2693 # missing files will generate a warning elsewhere
2696 2694 ret = 1
2697 2695 progress.complete()
2698 2696
2699 2697 if force:
2700 2698 list = modified + deleted + clean + added
2701 2699 elif after:
2702 2700 list = deleted
2703 2701 remaining = modified + added + clean
2704 2702 progress = ui.makeprogress(
2705 2703 _(b'skipping'), total=len(remaining), unit=_(b'files')
2706 2704 )
2707 2705 for f in remaining:
2708 2706 progress.increment()
2709 2707 if ui.verbose or (f in files):
2710 2708 warnings.append(
2711 2709 _(b'not removing %s: file still exists\n') % uipathfn(f)
2712 2710 )
2713 2711 ret = 1
2714 2712 progress.complete()
2715 2713 else:
2716 2714 list = deleted + clean
2717 2715 progress = ui.makeprogress(
2718 2716 _(b'skipping'), total=(len(modified) + len(added)), unit=_(b'files')
2719 2717 )
2720 2718 for f in modified:
2721 2719 progress.increment()
2722 2720 warnings.append(
2723 2721 _(
2724 2722 b'not removing %s: file is modified (use -f'
2725 2723 b' to force removal)\n'
2726 2724 )
2727 2725 % uipathfn(f)
2728 2726 )
2729 2727 ret = 1
2730 2728 for f in added:
2731 2729 progress.increment()
2732 2730 warnings.append(
2733 2731 _(
2734 2732 b"not removing %s: file has been marked for add"
2735 2733 b" (use 'hg forget' to undo add)\n"
2736 2734 )
2737 2735 % uipathfn(f)
2738 2736 )
2739 2737 ret = 1
2740 2738 progress.complete()
2741 2739
2742 2740 list = sorted(list)
2743 2741 progress = ui.makeprogress(
2744 2742 _(b'deleting'), total=len(list), unit=_(b'files')
2745 2743 )
2746 2744 for f in list:
2747 2745 if ui.verbose or not m.exact(f):
2748 2746 progress.increment()
2749 2747 ui.status(
2750 2748 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2751 2749 )
2752 2750 progress.complete()
2753 2751
2754 2752 if not dryrun:
2755 2753 with repo.wlock():
2756 2754 if not after:
2757 2755 for f in list:
2758 2756 if f in added:
2759 2757 continue # we never unlink added files on remove
2760 2758 rmdir = repo.ui.configbool(
2761 2759 b'experimental', b'removeemptydirs'
2762 2760 )
2763 2761 repo.wvfs.unlinkpath(f, ignoremissing=True, rmdir=rmdir)
2764 2762 repo[None].forget(list)
2765 2763
2766 2764 if warn:
2767 2765 for warning in warnings:
2768 2766 ui.warn(warning)
2769 2767
2770 2768 return ret
2771 2769
2772 2770
2773 2771 def _catfmtneedsdata(fm):
2774 2772 return not fm.datahint() or b'data' in fm.datahint()
2775 2773
2776 2774
2777 2775 def _updatecatformatter(fm, ctx, matcher, path, decode):
2778 2776 """Hook for adding data to the formatter used by ``hg cat``.
2779 2777
2780 2778 Extensions (e.g., lfs) can wrap this to inject keywords/data, but must call
2781 2779 this method first."""
2782 2780
2783 2781 # data() can be expensive to fetch (e.g. lfs), so don't fetch it if it
2784 2782 # wasn't requested.
2785 2783 data = b''
2786 2784 if _catfmtneedsdata(fm):
2787 2785 data = ctx[path].data()
2788 2786 if decode:
2789 2787 data = ctx.repo().wwritedata(path, data)
2790 2788 fm.startitem()
2791 2789 fm.context(ctx=ctx)
2792 2790 fm.write(b'data', b'%s', data)
2793 2791 fm.data(path=path)
2794 2792
2795 2793
2796 2794 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2797 2795 err = 1
2798 2796 opts = pycompat.byteskwargs(opts)
2799 2797
2800 2798 def write(path):
2801 2799 filename = None
2802 2800 if fntemplate:
2803 2801 filename = makefilename(
2804 2802 ctx, fntemplate, pathname=os.path.join(prefix, path)
2805 2803 )
2806 2804 # attempt to create the directory if it does not already exist
2807 2805 try:
2808 2806 os.makedirs(os.path.dirname(filename))
2809 2807 except OSError:
2810 2808 pass
2811 2809 with formatter.maybereopen(basefm, filename) as fm:
2812 2810 _updatecatformatter(fm, ctx, matcher, path, opts.get(b'decode'))
2813 2811
2814 2812 # Automation often uses hg cat on single files, so special case it
2815 2813 # for performance to avoid the cost of parsing the manifest.
2816 2814 if len(matcher.files()) == 1 and not matcher.anypats():
2817 2815 file = matcher.files()[0]
2818 2816 mfl = repo.manifestlog
2819 2817 mfnode = ctx.manifestnode()
2820 2818 try:
2821 2819 if mfnode and mfl[mfnode].find(file)[0]:
2822 2820 if _catfmtneedsdata(basefm):
2823 2821 scmutil.prefetchfiles(repo, [ctx.rev()], matcher)
2824 2822 write(file)
2825 2823 return 0
2826 2824 except KeyError:
2827 2825 pass
2828 2826
2829 2827 if _catfmtneedsdata(basefm):
2830 2828 scmutil.prefetchfiles(repo, [ctx.rev()], matcher)
2831 2829
2832 2830 for abs in ctx.walk(matcher):
2833 2831 write(abs)
2834 2832 err = 0
2835 2833
2836 2834 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2837 2835 for subpath in sorted(ctx.substate):
2838 2836 sub = ctx.sub(subpath)
2839 2837 try:
2840 2838 submatch = matchmod.subdirmatcher(subpath, matcher)
2841 2839 subprefix = os.path.join(prefix, subpath)
2842 2840 if not sub.cat(
2843 2841 submatch,
2844 2842 basefm,
2845 2843 fntemplate,
2846 2844 subprefix,
2847 2845 **pycompat.strkwargs(opts)
2848 2846 ):
2849 2847 err = 0
2850 2848 except error.RepoLookupError:
2851 2849 ui.status(
2852 2850 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2853 2851 )
2854 2852
2855 2853 return err
2856 2854
2857 2855
2858 2856 def commit(ui, repo, commitfunc, pats, opts):
2859 2857 '''commit the specified files or all outstanding changes'''
2860 2858 date = opts.get(b'date')
2861 2859 if date:
2862 2860 opts[b'date'] = dateutil.parsedate(date)
2863 2861 message = logmessage(ui, opts)
2864 2862 matcher = scmutil.match(repo[None], pats, opts)
2865 2863
2866 2864 dsguard = None
2867 2865 # extract addremove carefully -- this function can be called from a command
2868 2866 # that doesn't support addremove
2869 2867 if opts.get(b'addremove'):
2870 2868 dsguard = dirstateguard.dirstateguard(repo, b'commit')
2871 2869 with dsguard or util.nullcontextmanager():
2872 2870 if dsguard:
2873 2871 relative = scmutil.anypats(pats, opts)
2874 2872 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2875 2873 if scmutil.addremove(repo, matcher, b"", uipathfn, opts) != 0:
2876 2874 raise error.Abort(
2877 2875 _(b"failed to mark all new/missing files as added/removed")
2878 2876 )
2879 2877
2880 2878 return commitfunc(ui, repo, message, matcher, opts)
2881 2879
2882 2880
2883 2881 def samefile(f, ctx1, ctx2):
2884 2882 if f in ctx1.manifest():
2885 2883 a = ctx1.filectx(f)
2886 2884 if f in ctx2.manifest():
2887 2885 b = ctx2.filectx(f)
2888 2886 return not a.cmp(b) and a.flags() == b.flags()
2889 2887 else:
2890 2888 return False
2891 2889 else:
2892 2890 return f not in ctx2.manifest()
2893 2891
2894 2892
2895 2893 def amend(ui, repo, old, extra, pats, opts):
2896 2894 # avoid cycle context -> subrepo -> cmdutil
2897 2895 from . import context
2898 2896
2899 2897 # amend will reuse the existing user if not specified, but the obsolete
2900 2898 # marker creation requires that the current user's name is specified.
2901 2899 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2902 2900 ui.username() # raise exception if username not set
2903 2901
2904 2902 ui.note(_(b'amending changeset %s\n') % old)
2905 2903 base = old.p1()
2906 2904
2907 2905 with repo.wlock(), repo.lock(), repo.transaction(b'amend'):
2908 2906 # Participating changesets:
2909 2907 #
2910 2908 # wctx o - workingctx that contains changes from working copy
2911 2909 # | to go into amending commit
2912 2910 # |
2913 2911 # old o - changeset to amend
2914 2912 # |
2915 2913 # base o - first parent of the changeset to amend
2916 2914 wctx = repo[None]
2917 2915
2918 2916 # Copy to avoid mutating input
2919 2917 extra = extra.copy()
2920 2918 # Update extra dict from amended commit (e.g. to preserve graft
2921 2919 # source)
2922 2920 extra.update(old.extra())
2923 2921
2924 2922 # Also update it from the from the wctx
2925 2923 extra.update(wctx.extra())
2926 2924
2927 2925 # date-only change should be ignored?
2928 2926 datemaydiffer = resolvecommitoptions(ui, opts)
2929 2927
2930 2928 date = old.date()
2931 2929 if opts.get(b'date'):
2932 2930 date = dateutil.parsedate(opts.get(b'date'))
2933 2931 user = opts.get(b'user') or old.user()
2934 2932
2935 2933 if len(old.parents()) > 1:
2936 2934 # ctx.files() isn't reliable for merges, so fall back to the
2937 2935 # slower repo.status() method
2938 2936 st = base.status(old)
2939 2937 files = set(st.modified) | set(st.added) | set(st.removed)
2940 2938 else:
2941 2939 files = set(old.files())
2942 2940
2943 2941 # add/remove the files to the working copy if the "addremove" option
2944 2942 # was specified.
2945 2943 matcher = scmutil.match(wctx, pats, opts)
2946 2944 relative = scmutil.anypats(pats, opts)
2947 2945 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2948 2946 if opts.get(b'addremove') and scmutil.addremove(
2949 2947 repo, matcher, b"", uipathfn, opts
2950 2948 ):
2951 2949 raise error.Abort(
2952 2950 _(b"failed to mark all new/missing files as added/removed")
2953 2951 )
2954 2952
2955 2953 # Check subrepos. This depends on in-place wctx._status update in
2956 2954 # subrepo.precommit(). To minimize the risk of this hack, we do
2957 2955 # nothing if .hgsub does not exist.
2958 2956 if b'.hgsub' in wctx or b'.hgsub' in old:
2959 2957 subs, commitsubs, newsubstate = subrepoutil.precommit(
2960 2958 ui, wctx, wctx._status, matcher
2961 2959 )
2962 2960 # amend should abort if commitsubrepos is enabled
2963 2961 assert not commitsubs
2964 2962 if subs:
2965 2963 subrepoutil.writestate(repo, newsubstate)
2966 2964
2967 2965 ms = mergemod.mergestate.read(repo)
2968 2966 mergeutil.checkunresolved(ms)
2969 2967
2970 2968 filestoamend = set(f for f in wctx.files() if matcher(f))
2971 2969
2972 2970 changes = len(filestoamend) > 0
2973 2971 if changes:
2974 2972 # Recompute copies (avoid recording a -> b -> a)
2975 2973 copied = copies.pathcopies(base, wctx, matcher)
2976 2974 if old.p2:
2977 2975 copied.update(copies.pathcopies(old.p2(), wctx, matcher))
2978 2976
2979 2977 # Prune files which were reverted by the updates: if old
2980 2978 # introduced file X and the file was renamed in the working
2981 2979 # copy, then those two files are the same and
2982 2980 # we can discard X from our list of files. Likewise if X
2983 2981 # was removed, it's no longer relevant. If X is missing (aka
2984 2982 # deleted), old X must be preserved.
2985 2983 files.update(filestoamend)
2986 2984 files = [
2987 2985 f
2988 2986 for f in files
2989 2987 if (f not in filestoamend or not samefile(f, wctx, base))
2990 2988 ]
2991 2989
2992 2990 def filectxfn(repo, ctx_, path):
2993 2991 try:
2994 2992 # If the file being considered is not amongst the files
2995 2993 # to be amended, we should return the file context from the
2996 2994 # old changeset. This avoids issues when only some files in
2997 2995 # the working copy are being amended but there are also
2998 2996 # changes to other files from the old changeset.
2999 2997 if path not in filestoamend:
3000 2998 return old.filectx(path)
3001 2999
3002 3000 # Return None for removed files.
3003 3001 if path in wctx.removed():
3004 3002 return None
3005 3003
3006 3004 fctx = wctx[path]
3007 3005 flags = fctx.flags()
3008 3006 mctx = context.memfilectx(
3009 3007 repo,
3010 3008 ctx_,
3011 3009 fctx.path(),
3012 3010 fctx.data(),
3013 3011 islink=b'l' in flags,
3014 3012 isexec=b'x' in flags,
3015 3013 copysource=copied.get(path),
3016 3014 )
3017 3015 return mctx
3018 3016 except KeyError:
3019 3017 return None
3020 3018
3021 3019 else:
3022 3020 ui.note(_(b'copying changeset %s to %s\n') % (old, base))
3023 3021
3024 3022 # Use version of files as in the old cset
3025 3023 def filectxfn(repo, ctx_, path):
3026 3024 try:
3027 3025 return old.filectx(path)
3028 3026 except KeyError:
3029 3027 return None
3030 3028
3031 3029 # See if we got a message from -m or -l, if not, open the editor with
3032 3030 # the message of the changeset to amend.
3033 3031 message = logmessage(ui, opts)
3034 3032
3035 3033 editform = mergeeditform(old, b'commit.amend')
3036 3034
3037 3035 if not message:
3038 3036 message = old.description()
3039 3037 # Default if message isn't provided and --edit is not passed is to
3040 3038 # invoke editor, but allow --no-edit. If somehow we don't have any
3041 3039 # description, let's always start the editor.
3042 3040 doedit = not message or opts.get(b'edit') in [True, None]
3043 3041 else:
3044 3042 # Default if message is provided is to not invoke editor, but allow
3045 3043 # --edit.
3046 3044 doedit = opts.get(b'edit') is True
3047 3045 editor = getcommiteditor(edit=doedit, editform=editform)
3048 3046
3049 3047 pureextra = extra.copy()
3050 3048 extra[b'amend_source'] = old.hex()
3051 3049
3052 3050 new = context.memctx(
3053 3051 repo,
3054 3052 parents=[base.node(), old.p2().node()],
3055 3053 text=message,
3056 3054 files=files,
3057 3055 filectxfn=filectxfn,
3058 3056 user=user,
3059 3057 date=date,
3060 3058 extra=extra,
3061 3059 editor=editor,
3062 3060 )
3063 3061
3064 3062 newdesc = changelog.stripdesc(new.description())
3065 3063 if (
3066 3064 (not changes)
3067 3065 and newdesc == old.description()
3068 3066 and user == old.user()
3069 3067 and (date == old.date() or datemaydiffer)
3070 3068 and pureextra == old.extra()
3071 3069 ):
3072 3070 # nothing changed. continuing here would create a new node
3073 3071 # anyway because of the amend_source noise.
3074 3072 #
3075 3073 # This not what we expect from amend.
3076 3074 return old.node()
3077 3075
3078 3076 commitphase = None
3079 3077 if opts.get(b'secret'):
3080 3078 commitphase = phases.secret
3081 3079 newid = repo.commitctx(new)
3082 3080
3083 3081 # Reroute the working copy parent to the new changeset
3084 3082 repo.setparents(newid, nullid)
3085 3083 mapping = {old.node(): (newid,)}
3086 3084 obsmetadata = None
3087 3085 if opts.get(b'note'):
3088 3086 obsmetadata = {b'note': encoding.fromlocal(opts[b'note'])}
3089 3087 backup = ui.configbool(b'rewrite', b'backup-bundle')
3090 3088 scmutil.cleanupnodes(
3091 3089 repo,
3092 3090 mapping,
3093 3091 b'amend',
3094 3092 metadata=obsmetadata,
3095 3093 fixphase=True,
3096 3094 targetphase=commitphase,
3097 3095 backup=backup,
3098 3096 )
3099 3097
3100 3098 # Fixing the dirstate because localrepo.commitctx does not update
3101 3099 # it. This is rather convenient because we did not need to update
3102 3100 # the dirstate for all the files in the new commit which commitctx
3103 3101 # could have done if it updated the dirstate. Now, we can
3104 3102 # selectively update the dirstate only for the amended files.
3105 3103 dirstate = repo.dirstate
3106 3104
3107 3105 # Update the state of the files which were added and modified in the
3108 3106 # amend to "normal" in the dirstate. We need to use "normallookup" since
3109 3107 # the files may have changed since the command started; using "normal"
3110 3108 # would mark them as clean but with uncommitted contents.
3111 3109 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
3112 3110 for f in normalfiles:
3113 3111 dirstate.normallookup(f)
3114 3112
3115 3113 # Update the state of files which were removed in the amend
3116 3114 # to "removed" in the dirstate.
3117 3115 removedfiles = set(wctx.removed()) & filestoamend
3118 3116 for f in removedfiles:
3119 3117 dirstate.drop(f)
3120 3118
3121 3119 return newid
3122 3120
3123 3121
3124 3122 def commiteditor(repo, ctx, subs, editform=b''):
3125 3123 if ctx.description():
3126 3124 return ctx.description()
3127 3125 return commitforceeditor(
3128 3126 repo, ctx, subs, editform=editform, unchangedmessagedetection=True
3129 3127 )
3130 3128
3131 3129
3132 3130 def commitforceeditor(
3133 3131 repo,
3134 3132 ctx,
3135 3133 subs,
3136 3134 finishdesc=None,
3137 3135 extramsg=None,
3138 3136 editform=b'',
3139 3137 unchangedmessagedetection=False,
3140 3138 ):
3141 3139 if not extramsg:
3142 3140 extramsg = _(b"Leave message empty to abort commit.")
3143 3141
3144 3142 forms = [e for e in editform.split(b'.') if e]
3145 3143 forms.insert(0, b'changeset')
3146 3144 templatetext = None
3147 3145 while forms:
3148 3146 ref = b'.'.join(forms)
3149 3147 if repo.ui.config(b'committemplate', ref):
3150 3148 templatetext = committext = buildcommittemplate(
3151 3149 repo, ctx, subs, extramsg, ref
3152 3150 )
3153 3151 break
3154 3152 forms.pop()
3155 3153 else:
3156 3154 committext = buildcommittext(repo, ctx, subs, extramsg)
3157 3155
3158 3156 # run editor in the repository root
3159 3157 olddir = encoding.getcwd()
3160 3158 os.chdir(repo.root)
3161 3159
3162 3160 # make in-memory changes visible to external process
3163 3161 tr = repo.currenttransaction()
3164 3162 repo.dirstate.write(tr)
3165 3163 pending = tr and tr.writepending() and repo.root
3166 3164
3167 3165 editortext = repo.ui.edit(
3168 3166 committext,
3169 3167 ctx.user(),
3170 3168 ctx.extra(),
3171 3169 editform=editform,
3172 3170 pending=pending,
3173 3171 repopath=repo.path,
3174 3172 action=b'commit',
3175 3173 )
3176 3174 text = editortext
3177 3175
3178 3176 # strip away anything below this special string (used for editors that want
3179 3177 # to display the diff)
3180 3178 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
3181 3179 if stripbelow:
3182 3180 text = text[: stripbelow.start()]
3183 3181
3184 3182 text = re.sub(b"(?m)^HG:.*(\n|$)", b"", text)
3185 3183 os.chdir(olddir)
3186 3184
3187 3185 if finishdesc:
3188 3186 text = finishdesc(text)
3189 3187 if not text.strip():
3190 3188 raise error.Abort(_(b"empty commit message"))
3191 3189 if unchangedmessagedetection and editortext == templatetext:
3192 3190 raise error.Abort(_(b"commit message unchanged"))
3193 3191
3194 3192 return text
3195 3193
3196 3194
3197 3195 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
3198 3196 ui = repo.ui
3199 3197 spec = formatter.templatespec(ref, None, None)
3200 3198 t = logcmdutil.changesettemplater(ui, repo, spec)
3201 3199 t.t.cache.update(
3202 3200 (k, templater.unquotestring(v))
3203 3201 for k, v in repo.ui.configitems(b'committemplate')
3204 3202 )
3205 3203
3206 3204 if not extramsg:
3207 3205 extramsg = b'' # ensure that extramsg is string
3208 3206
3209 3207 ui.pushbuffer()
3210 3208 t.show(ctx, extramsg=extramsg)
3211 3209 return ui.popbuffer()
3212 3210
3213 3211
3214 3212 def hgprefix(msg):
3215 3213 return b"\n".join([b"HG: %s" % a for a in msg.split(b"\n") if a])
3216 3214
3217 3215
3218 3216 def buildcommittext(repo, ctx, subs, extramsg):
3219 3217 edittext = []
3220 3218 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
3221 3219 if ctx.description():
3222 3220 edittext.append(ctx.description())
3223 3221 edittext.append(b"")
3224 3222 edittext.append(b"") # Empty line between message and comments.
3225 3223 edittext.append(
3226 3224 hgprefix(
3227 3225 _(
3228 3226 b"Enter commit message."
3229 3227 b" Lines beginning with 'HG:' are removed."
3230 3228 )
3231 3229 )
3232 3230 )
3233 3231 edittext.append(hgprefix(extramsg))
3234 3232 edittext.append(b"HG: --")
3235 3233 edittext.append(hgprefix(_(b"user: %s") % ctx.user()))
3236 3234 if ctx.p2():
3237 3235 edittext.append(hgprefix(_(b"branch merge")))
3238 3236 if ctx.branch():
3239 3237 edittext.append(hgprefix(_(b"branch '%s'") % ctx.branch()))
3240 3238 if bookmarks.isactivewdirparent(repo):
3241 3239 edittext.append(hgprefix(_(b"bookmark '%s'") % repo._activebookmark))
3242 3240 edittext.extend([hgprefix(_(b"subrepo %s") % s) for s in subs])
3243 3241 edittext.extend([hgprefix(_(b"added %s") % f) for f in added])
3244 3242 edittext.extend([hgprefix(_(b"changed %s") % f) for f in modified])
3245 3243 edittext.extend([hgprefix(_(b"removed %s") % f) for f in removed])
3246 3244 if not added and not modified and not removed:
3247 3245 edittext.append(hgprefix(_(b"no files changed")))
3248 3246 edittext.append(b"")
3249 3247
3250 3248 return b"\n".join(edittext)
3251 3249
3252 3250
3253 3251 def commitstatus(repo, node, branch, bheads=None, opts=None):
3254 3252 if opts is None:
3255 3253 opts = {}
3256 3254 ctx = repo[node]
3257 3255 parents = ctx.parents()
3258 3256
3259 3257 if (
3260 3258 not opts.get(b'amend')
3261 3259 and bheads
3262 3260 and node not in bheads
3263 3261 and not [
3264 3262 x for x in parents if x.node() in bheads and x.branch() == branch
3265 3263 ]
3266 3264 ):
3267 3265 repo.ui.status(_(b'created new head\n'))
3268 3266 # The message is not printed for initial roots. For the other
3269 3267 # changesets, it is printed in the following situations:
3270 3268 #
3271 3269 # Par column: for the 2 parents with ...
3272 3270 # N: null or no parent
3273 3271 # B: parent is on another named branch
3274 3272 # C: parent is a regular non head changeset
3275 3273 # H: parent was a branch head of the current branch
3276 3274 # Msg column: whether we print "created new head" message
3277 3275 # In the following, it is assumed that there already exists some
3278 3276 # initial branch heads of the current branch, otherwise nothing is
3279 3277 # printed anyway.
3280 3278 #
3281 3279 # Par Msg Comment
3282 3280 # N N y additional topo root
3283 3281 #
3284 3282 # B N y additional branch root
3285 3283 # C N y additional topo head
3286 3284 # H N n usual case
3287 3285 #
3288 3286 # B B y weird additional branch root
3289 3287 # C B y branch merge
3290 3288 # H B n merge with named branch
3291 3289 #
3292 3290 # C C y additional head from merge
3293 3291 # C H n merge with a head
3294 3292 #
3295 3293 # H H n head merge: head count decreases
3296 3294
3297 3295 if not opts.get(b'close_branch'):
3298 3296 for r in parents:
3299 3297 if r.closesbranch() and r.branch() == branch:
3300 3298 repo.ui.status(
3301 3299 _(b'reopening closed branch head %d\n') % r.rev()
3302 3300 )
3303 3301
3304 3302 if repo.ui.debugflag:
3305 3303 repo.ui.write(
3306 3304 _(b'committed changeset %d:%s\n') % (ctx.rev(), ctx.hex())
3307 3305 )
3308 3306 elif repo.ui.verbose:
3309 3307 repo.ui.write(_(b'committed changeset %d:%s\n') % (ctx.rev(), ctx))
3310 3308
3311 3309
3312 3310 def postcommitstatus(repo, pats, opts):
3313 3311 return repo.status(match=scmutil.match(repo[None], pats, opts))
3314 3312
3315 3313
3316 3314 def revert(ui, repo, ctx, parents, *pats, **opts):
3317 3315 opts = pycompat.byteskwargs(opts)
3318 3316 parent, p2 = parents
3319 3317 node = ctx.node()
3320 3318
3321 3319 mf = ctx.manifest()
3322 3320 if node == p2:
3323 3321 parent = p2
3324 3322
3325 3323 # need all matching names in dirstate and manifest of target rev,
3326 3324 # so have to walk both. do not print errors if files exist in one
3327 3325 # but not other. in both cases, filesets should be evaluated against
3328 3326 # workingctx to get consistent result (issue4497). this means 'set:**'
3329 3327 # cannot be used to select missing files from target rev.
3330 3328
3331 3329 # `names` is a mapping for all elements in working copy and target revision
3332 3330 # The mapping is in the form:
3333 3331 # <abs path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
3334 3332 names = {}
3335 3333 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
3336 3334
3337 3335 with repo.wlock():
3338 3336 ## filling of the `names` mapping
3339 3337 # walk dirstate to fill `names`
3340 3338
3341 3339 interactive = opts.get(b'interactive', False)
3342 3340 wctx = repo[None]
3343 3341 m = scmutil.match(wctx, pats, opts)
3344 3342
3345 3343 # we'll need this later
3346 3344 targetsubs = sorted(s for s in wctx.substate if m(s))
3347 3345
3348 3346 if not m.always():
3349 3347 matcher = matchmod.badmatch(m, lambda x, y: False)
3350 3348 for abs in wctx.walk(matcher):
3351 3349 names[abs] = m.exact(abs)
3352 3350
3353 3351 # walk target manifest to fill `names`
3354 3352
3355 3353 def badfn(path, msg):
3356 3354 if path in names:
3357 3355 return
3358 3356 if path in ctx.substate:
3359 3357 return
3360 3358 path_ = path + b'/'
3361 3359 for f in names:
3362 3360 if f.startswith(path_):
3363 3361 return
3364 3362 ui.warn(b"%s: %s\n" % (uipathfn(path), msg))
3365 3363
3366 3364 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
3367 3365 if abs not in names:
3368 3366 names[abs] = m.exact(abs)
3369 3367
3370 3368 # Find status of all file in `names`.
3371 3369 m = scmutil.matchfiles(repo, names)
3372 3370
3373 3371 changes = repo.status(
3374 3372 node1=node, match=m, unknown=True, ignored=True, clean=True
3375 3373 )
3376 3374 else:
3377 3375 changes = repo.status(node1=node, match=m)
3378 3376 for kind in changes:
3379 3377 for abs in kind:
3380 3378 names[abs] = m.exact(abs)
3381 3379
3382 3380 m = scmutil.matchfiles(repo, names)
3383 3381
3384 3382 modified = set(changes.modified)
3385 3383 added = set(changes.added)
3386 3384 removed = set(changes.removed)
3387 3385 _deleted = set(changes.deleted)
3388 3386 unknown = set(changes.unknown)
3389 3387 unknown.update(changes.ignored)
3390 3388 clean = set(changes.clean)
3391 3389 modadded = set()
3392 3390
3393 3391 # We need to account for the state of the file in the dirstate,
3394 3392 # even when we revert against something else than parent. This will
3395 3393 # slightly alter the behavior of revert (doing back up or not, delete
3396 3394 # or just forget etc).
3397 3395 if parent == node:
3398 3396 dsmodified = modified
3399 3397 dsadded = added
3400 3398 dsremoved = removed
3401 3399 # store all local modifications, useful later for rename detection
3402 3400 localchanges = dsmodified | dsadded
3403 3401 modified, added, removed = set(), set(), set()
3404 3402 else:
3405 3403 changes = repo.status(node1=parent, match=m)
3406 3404 dsmodified = set(changes.modified)
3407 3405 dsadded = set(changes.added)
3408 3406 dsremoved = set(changes.removed)
3409 3407 # store all local modifications, useful later for rename detection
3410 3408 localchanges = dsmodified | dsadded
3411 3409
3412 3410 # only take into account for removes between wc and target
3413 3411 clean |= dsremoved - removed
3414 3412 dsremoved &= removed
3415 3413 # distinct between dirstate remove and other
3416 3414 removed -= dsremoved
3417 3415
3418 3416 modadded = added & dsmodified
3419 3417 added -= modadded
3420 3418
3421 3419 # tell newly modified apart.
3422 3420 dsmodified &= modified
3423 3421 dsmodified |= modified & dsadded # dirstate added may need backup
3424 3422 modified -= dsmodified
3425 3423
3426 3424 # We need to wait for some post-processing to update this set
3427 3425 # before making the distinction. The dirstate will be used for
3428 3426 # that purpose.
3429 3427 dsadded = added
3430 3428
3431 3429 # in case of merge, files that are actually added can be reported as
3432 3430 # modified, we need to post process the result
3433 3431 if p2 != nullid:
3434 3432 mergeadd = set(dsmodified)
3435 3433 for path in dsmodified:
3436 3434 if path in mf:
3437 3435 mergeadd.remove(path)
3438 3436 dsadded |= mergeadd
3439 3437 dsmodified -= mergeadd
3440 3438
3441 3439 # if f is a rename, update `names` to also revert the source
3442 3440 for f in localchanges:
3443 3441 src = repo.dirstate.copied(f)
3444 3442 # XXX should we check for rename down to target node?
3445 3443 if src and src not in names and repo.dirstate[src] == b'r':
3446 3444 dsremoved.add(src)
3447 3445 names[src] = True
3448 3446
3449 3447 # determine the exact nature of the deleted changesets
3450 3448 deladded = set(_deleted)
3451 3449 for path in _deleted:
3452 3450 if path in mf:
3453 3451 deladded.remove(path)
3454 3452 deleted = _deleted - deladded
3455 3453
3456 3454 # distinguish between file to forget and the other
3457 3455 added = set()
3458 3456 for abs in dsadded:
3459 3457 if repo.dirstate[abs] != b'a':
3460 3458 added.add(abs)
3461 3459 dsadded -= added
3462 3460
3463 3461 for abs in deladded:
3464 3462 if repo.dirstate[abs] == b'a':
3465 3463 dsadded.add(abs)
3466 3464 deladded -= dsadded
3467 3465
3468 3466 # For files marked as removed, we check if an unknown file is present at
3469 3467 # the same path. If a such file exists it may need to be backed up.
3470 3468 # Making the distinction at this stage helps have simpler backup
3471 3469 # logic.
3472 3470 removunk = set()
3473 3471 for abs in removed:
3474 3472 target = repo.wjoin(abs)
3475 3473 if os.path.lexists(target):
3476 3474 removunk.add(abs)
3477 3475 removed -= removunk
3478 3476
3479 3477 dsremovunk = set()
3480 3478 for abs in dsremoved:
3481 3479 target = repo.wjoin(abs)
3482 3480 if os.path.lexists(target):
3483 3481 dsremovunk.add(abs)
3484 3482 dsremoved -= dsremovunk
3485 3483
3486 3484 # action to be actually performed by revert
3487 3485 # (<list of file>, message>) tuple
3488 3486 actions = {
3489 3487 b'revert': ([], _(b'reverting %s\n')),
3490 3488 b'add': ([], _(b'adding %s\n')),
3491 3489 b'remove': ([], _(b'removing %s\n')),
3492 3490 b'drop': ([], _(b'removing %s\n')),
3493 3491 b'forget': ([], _(b'forgetting %s\n')),
3494 3492 b'undelete': ([], _(b'undeleting %s\n')),
3495 3493 b'noop': (None, _(b'no changes needed to %s\n')),
3496 3494 b'unknown': (None, _(b'file not managed: %s\n')),
3497 3495 }
3498 3496
3499 3497 # "constant" that convey the backup strategy.
3500 3498 # All set to `discard` if `no-backup` is set do avoid checking
3501 3499 # no_backup lower in the code.
3502 3500 # These values are ordered for comparison purposes
3503 3501 backupinteractive = 3 # do backup if interactively modified
3504 3502 backup = 2 # unconditionally do backup
3505 3503 check = 1 # check if the existing file differs from target
3506 3504 discard = 0 # never do backup
3507 3505 if opts.get(b'no_backup'):
3508 3506 backupinteractive = backup = check = discard
3509 3507 if interactive:
3510 3508 dsmodifiedbackup = backupinteractive
3511 3509 else:
3512 3510 dsmodifiedbackup = backup
3513 3511 tobackup = set()
3514 3512
3515 3513 backupanddel = actions[b'remove']
3516 3514 if not opts.get(b'no_backup'):
3517 3515 backupanddel = actions[b'drop']
3518 3516
3519 3517 disptable = (
3520 3518 # dispatch table:
3521 3519 # file state
3522 3520 # action
3523 3521 # make backup
3524 3522 ## Sets that results that will change file on disk
3525 3523 # Modified compared to target, no local change
3526 3524 (modified, actions[b'revert'], discard),
3527 3525 # Modified compared to target, but local file is deleted
3528 3526 (deleted, actions[b'revert'], discard),
3529 3527 # Modified compared to target, local change
3530 3528 (dsmodified, actions[b'revert'], dsmodifiedbackup),
3531 3529 # Added since target
3532 3530 (added, actions[b'remove'], discard),
3533 3531 # Added in working directory
3534 3532 (dsadded, actions[b'forget'], discard),
3535 3533 # Added since target, have local modification
3536 3534 (modadded, backupanddel, backup),
3537 3535 # Added since target but file is missing in working directory
3538 3536 (deladded, actions[b'drop'], discard),
3539 3537 # Removed since target, before working copy parent
3540 3538 (removed, actions[b'add'], discard),
3541 3539 # Same as `removed` but an unknown file exists at the same path
3542 3540 (removunk, actions[b'add'], check),
3543 3541 # Removed since targe, marked as such in working copy parent
3544 3542 (dsremoved, actions[b'undelete'], discard),
3545 3543 # Same as `dsremoved` but an unknown file exists at the same path
3546 3544 (dsremovunk, actions[b'undelete'], check),
3547 3545 ## the following sets does not result in any file changes
3548 3546 # File with no modification
3549 3547 (clean, actions[b'noop'], discard),
3550 3548 # Existing file, not tracked anywhere
3551 3549 (unknown, actions[b'unknown'], discard),
3552 3550 )
3553 3551
3554 3552 for abs, exact in sorted(names.items()):
3555 3553 # target file to be touch on disk (relative to cwd)
3556 3554 target = repo.wjoin(abs)
3557 3555 # search the entry in the dispatch table.
3558 3556 # if the file is in any of these sets, it was touched in the working
3559 3557 # directory parent and we are sure it needs to be reverted.
3560 3558 for table, (xlist, msg), dobackup in disptable:
3561 3559 if abs not in table:
3562 3560 continue
3563 3561 if xlist is not None:
3564 3562 xlist.append(abs)
3565 3563 if dobackup:
3566 3564 # If in interactive mode, don't automatically create
3567 3565 # .orig files (issue4793)
3568 3566 if dobackup == backupinteractive:
3569 3567 tobackup.add(abs)
3570 3568 elif backup <= dobackup or wctx[abs].cmp(ctx[abs]):
3571 3569 absbakname = scmutil.backuppath(ui, repo, abs)
3572 3570 bakname = os.path.relpath(
3573 3571 absbakname, start=repo.root
3574 3572 )
3575 3573 ui.note(
3576 3574 _(b'saving current version of %s as %s\n')
3577 3575 % (uipathfn(abs), uipathfn(bakname))
3578 3576 )
3579 3577 if not opts.get(b'dry_run'):
3580 3578 if interactive:
3581 3579 util.copyfile(target, absbakname)
3582 3580 else:
3583 3581 util.rename(target, absbakname)
3584 3582 if opts.get(b'dry_run'):
3585 3583 if ui.verbose or not exact:
3586 3584 ui.status(msg % uipathfn(abs))
3587 3585 elif exact:
3588 3586 ui.warn(msg % uipathfn(abs))
3589 3587 break
3590 3588
3591 3589 if not opts.get(b'dry_run'):
3592 3590 needdata = (b'revert', b'add', b'undelete')
3593 3591 oplist = [actions[name][0] for name in needdata]
3594 3592 prefetch = scmutil.prefetchfiles
3595 3593 matchfiles = scmutil.matchfiles
3596 3594 prefetch(
3597 3595 repo,
3598 3596 [ctx.rev()],
3599 3597 matchfiles(repo, [f for sublist in oplist for f in sublist]),
3600 3598 )
3601 3599 match = scmutil.match(repo[None], pats)
3602 3600 _performrevert(
3603 3601 repo,
3604 3602 parents,
3605 3603 ctx,
3606 3604 names,
3607 3605 uipathfn,
3608 3606 actions,
3609 3607 match,
3610 3608 interactive,
3611 3609 tobackup,
3612 3610 )
3613 3611
3614 3612 if targetsubs:
3615 3613 # Revert the subrepos on the revert list
3616 3614 for sub in targetsubs:
3617 3615 try:
3618 3616 wctx.sub(sub).revert(
3619 3617 ctx.substate[sub], *pats, **pycompat.strkwargs(opts)
3620 3618 )
3621 3619 except KeyError:
3622 3620 raise error.Abort(
3623 3621 b"subrepository '%s' does not exist in %s!"
3624 3622 % (sub, short(ctx.node()))
3625 3623 )
3626 3624
3627 3625
3628 3626 def _performrevert(
3629 3627 repo,
3630 3628 parents,
3631 3629 ctx,
3632 3630 names,
3633 3631 uipathfn,
3634 3632 actions,
3635 3633 match,
3636 3634 interactive=False,
3637 3635 tobackup=None,
3638 3636 ):
3639 3637 """function that actually perform all the actions computed for revert
3640 3638
3641 3639 This is an independent function to let extension to plug in and react to
3642 3640 the imminent revert.
3643 3641
3644 3642 Make sure you have the working directory locked when calling this function.
3645 3643 """
3646 3644 parent, p2 = parents
3647 3645 node = ctx.node()
3648 3646 excluded_files = []
3649 3647
3650 3648 def checkout(f):
3651 3649 fc = ctx[f]
3652 3650 repo.wwrite(f, fc.data(), fc.flags())
3653 3651
3654 3652 def doremove(f):
3655 3653 try:
3656 3654 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
3657 3655 repo.wvfs.unlinkpath(f, rmdir=rmdir)
3658 3656 except OSError:
3659 3657 pass
3660 3658 repo.dirstate.remove(f)
3661 3659
3662 3660 def prntstatusmsg(action, f):
3663 3661 exact = names[f]
3664 3662 if repo.ui.verbose or not exact:
3665 3663 repo.ui.status(actions[action][1] % uipathfn(f))
3666 3664
3667 3665 audit_path = pathutil.pathauditor(repo.root, cached=True)
3668 3666 for f in actions[b'forget'][0]:
3669 3667 if interactive:
3670 3668 choice = repo.ui.promptchoice(
3671 3669 _(b"forget added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3672 3670 )
3673 3671 if choice == 0:
3674 3672 prntstatusmsg(b'forget', f)
3675 3673 repo.dirstate.drop(f)
3676 3674 else:
3677 3675 excluded_files.append(f)
3678 3676 else:
3679 3677 prntstatusmsg(b'forget', f)
3680 3678 repo.dirstate.drop(f)
3681 3679 for f in actions[b'remove'][0]:
3682 3680 audit_path(f)
3683 3681 if interactive:
3684 3682 choice = repo.ui.promptchoice(
3685 3683 _(b"remove added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3686 3684 )
3687 3685 if choice == 0:
3688 3686 prntstatusmsg(b'remove', f)
3689 3687 doremove(f)
3690 3688 else:
3691 3689 excluded_files.append(f)
3692 3690 else:
3693 3691 prntstatusmsg(b'remove', f)
3694 3692 doremove(f)
3695 3693 for f in actions[b'drop'][0]:
3696 3694 audit_path(f)
3697 3695 prntstatusmsg(b'drop', f)
3698 3696 repo.dirstate.remove(f)
3699 3697
3700 3698 normal = None
3701 3699 if node == parent:
3702 3700 # We're reverting to our parent. If possible, we'd like status
3703 3701 # to report the file as clean. We have to use normallookup for
3704 3702 # merges to avoid losing information about merged/dirty files.
3705 3703 if p2 != nullid:
3706 3704 normal = repo.dirstate.normallookup
3707 3705 else:
3708 3706 normal = repo.dirstate.normal
3709 3707
3710 3708 newlyaddedandmodifiedfiles = set()
3711 3709 if interactive:
3712 3710 # Prompt the user for changes to revert
3713 3711 torevert = [f for f in actions[b'revert'][0] if f not in excluded_files]
3714 3712 m = scmutil.matchfiles(repo, torevert)
3715 3713 diffopts = patch.difffeatureopts(
3716 3714 repo.ui,
3717 3715 whitespace=True,
3718 3716 section=b'commands',
3719 3717 configprefix=b'revert.interactive.',
3720 3718 )
3721 3719 diffopts.nodates = True
3722 3720 diffopts.git = True
3723 3721 operation = b'apply'
3724 3722 if node == parent:
3725 3723 if repo.ui.configbool(
3726 3724 b'experimental', b'revert.interactive.select-to-keep'
3727 3725 ):
3728 3726 operation = b'keep'
3729 3727 else:
3730 3728 operation = b'discard'
3731 3729
3732 3730 if operation == b'apply':
3733 3731 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3734 3732 else:
3735 3733 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3736 3734 originalchunks = patch.parsepatch(diff)
3737 3735
3738 3736 try:
3739 3737
3740 3738 chunks, opts = recordfilter(
3741 3739 repo.ui, originalchunks, match, operation=operation
3742 3740 )
3743 3741 if operation == b'discard':
3744 3742 chunks = patch.reversehunks(chunks)
3745 3743
3746 3744 except error.PatchError as err:
3747 3745 raise error.Abort(_(b'error parsing patch: %s') % err)
3748 3746
3749 3747 # FIXME: when doing an interactive revert of a copy, there's no way of
3750 3748 # performing a partial revert of the added file, the only option is
3751 3749 # "remove added file <name> (Yn)?", so we don't need to worry about the
3752 3750 # alsorestore value. Ideally we'd be able to partially revert
3753 3751 # copied/renamed files.
3754 3752 newlyaddedandmodifiedfiles, unusedalsorestore = newandmodified(
3755 3753 chunks, originalchunks
3756 3754 )
3757 3755 if tobackup is None:
3758 3756 tobackup = set()
3759 3757 # Apply changes
3760 3758 fp = stringio()
3761 3759 # chunks are serialized per file, but files aren't sorted
3762 3760 for f in sorted(set(c.header.filename() for c in chunks if ishunk(c))):
3763 3761 prntstatusmsg(b'revert', f)
3764 3762 files = set()
3765 3763 for c in chunks:
3766 3764 if ishunk(c):
3767 3765 abs = c.header.filename()
3768 3766 # Create a backup file only if this hunk should be backed up
3769 3767 if c.header.filename() in tobackup:
3770 3768 target = repo.wjoin(abs)
3771 3769 bakname = scmutil.backuppath(repo.ui, repo, abs)
3772 3770 util.copyfile(target, bakname)
3773 3771 tobackup.remove(abs)
3774 3772 if abs not in files:
3775 3773 files.add(abs)
3776 3774 if operation == b'keep':
3777 3775 checkout(abs)
3778 3776 c.write(fp)
3779 3777 dopatch = fp.tell()
3780 3778 fp.seek(0)
3781 3779 if dopatch:
3782 3780 try:
3783 3781 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3784 3782 except error.PatchError as err:
3785 3783 raise error.Abort(pycompat.bytestr(err))
3786 3784 del fp
3787 3785 else:
3788 3786 for f in actions[b'revert'][0]:
3789 3787 prntstatusmsg(b'revert', f)
3790 3788 checkout(f)
3791 3789 if normal:
3792 3790 normal(f)
3793 3791
3794 3792 for f in actions[b'add'][0]:
3795 3793 # Don't checkout modified files, they are already created by the diff
3796 3794 if f not in newlyaddedandmodifiedfiles:
3797 3795 prntstatusmsg(b'add', f)
3798 3796 checkout(f)
3799 3797 repo.dirstate.add(f)
3800 3798
3801 3799 normal = repo.dirstate.normallookup
3802 3800 if node == parent and p2 == nullid:
3803 3801 normal = repo.dirstate.normal
3804 3802 for f in actions[b'undelete'][0]:
3805 3803 if interactive:
3806 3804 choice = repo.ui.promptchoice(
3807 3805 _(b"add back removed file %s (Yn)?$$ &Yes $$ &No") % f
3808 3806 )
3809 3807 if choice == 0:
3810 3808 prntstatusmsg(b'undelete', f)
3811 3809 checkout(f)
3812 3810 normal(f)
3813 3811 else:
3814 3812 excluded_files.append(f)
3815 3813 else:
3816 3814 prntstatusmsg(b'undelete', f)
3817 3815 checkout(f)
3818 3816 normal(f)
3819 3817
3820 3818 copied = copies.pathcopies(repo[parent], ctx)
3821 3819
3822 3820 for f in (
3823 3821 actions[b'add'][0] + actions[b'undelete'][0] + actions[b'revert'][0]
3824 3822 ):
3825 3823 if f in copied:
3826 3824 repo.dirstate.copy(copied[f], f)
3827 3825
3828 3826
3829 3827 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3830 3828 # commands.outgoing. "missing" is "missing" of the result of
3831 3829 # "findcommonoutgoing()"
3832 3830 outgoinghooks = util.hooks()
3833 3831
3834 3832 # a list of (ui, repo) functions called by commands.summary
3835 3833 summaryhooks = util.hooks()
3836 3834
3837 3835 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3838 3836 #
3839 3837 # functions should return tuple of booleans below, if 'changes' is None:
3840 3838 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3841 3839 #
3842 3840 # otherwise, 'changes' is a tuple of tuples below:
3843 3841 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3844 3842 # - (desturl, destbranch, destpeer, outgoing)
3845 3843 summaryremotehooks = util.hooks()
3846 3844
3847 3845
3848 3846 def checkunfinished(repo, commit=False, skipmerge=False):
3849 3847 '''Look for an unfinished multistep operation, like graft, and abort
3850 3848 if found. It's probably good to check this right before
3851 3849 bailifchanged().
3852 3850 '''
3853 3851 # Check for non-clearable states first, so things like rebase will take
3854 3852 # precedence over update.
3855 3853 for state in statemod._unfinishedstates:
3856 3854 if (
3857 3855 state._clearable
3858 3856 or (commit and state._allowcommit)
3859 3857 or state._reportonly
3860 3858 ):
3861 3859 continue
3862 3860 if state.isunfinished(repo):
3863 3861 raise error.Abort(state.msg(), hint=state.hint())
3864 3862
3865 3863 for s in statemod._unfinishedstates:
3866 3864 if (
3867 3865 not s._clearable
3868 3866 or (commit and s._allowcommit)
3869 3867 or (s._opname == b'merge' and skipmerge)
3870 3868 or s._reportonly
3871 3869 ):
3872 3870 continue
3873 3871 if s.isunfinished(repo):
3874 3872 raise error.Abort(s.msg(), hint=s.hint())
3875 3873
3876 3874
3877 3875 def clearunfinished(repo):
3878 3876 '''Check for unfinished operations (as above), and clear the ones
3879 3877 that are clearable.
3880 3878 '''
3881 3879 for state in statemod._unfinishedstates:
3882 3880 if state._reportonly:
3883 3881 continue
3884 3882 if not state._clearable and state.isunfinished(repo):
3885 3883 raise error.Abort(state.msg(), hint=state.hint())
3886 3884
3887 3885 for s in statemod._unfinishedstates:
3888 3886 if s._opname == b'merge' or state._reportonly:
3889 3887 continue
3890 3888 if s._clearable and s.isunfinished(repo):
3891 3889 util.unlink(repo.vfs.join(s._fname))
3892 3890
3893 3891
3894 3892 def getunfinishedstate(repo):
3895 3893 ''' Checks for unfinished operations and returns statecheck object
3896 3894 for it'''
3897 3895 for state in statemod._unfinishedstates:
3898 3896 if state.isunfinished(repo):
3899 3897 return state
3900 3898 return None
3901 3899
3902 3900
3903 3901 def howtocontinue(repo):
3904 3902 '''Check for an unfinished operation and return the command to finish
3905 3903 it.
3906 3904
3907 3905 statemod._unfinishedstates list is checked for an unfinished operation
3908 3906 and the corresponding message to finish it is generated if a method to
3909 3907 continue is supported by the operation.
3910 3908
3911 3909 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3912 3910 a boolean.
3913 3911 '''
3914 3912 contmsg = _(b"continue: %s")
3915 3913 for state in statemod._unfinishedstates:
3916 3914 if not state._continueflag:
3917 3915 continue
3918 3916 if state.isunfinished(repo):
3919 3917 return contmsg % state.continuemsg(), True
3920 3918 if repo[None].dirty(missing=True, merge=False, branch=False):
3921 3919 return contmsg % _(b"hg commit"), False
3922 3920 return None, None
3923 3921
3924 3922
3925 3923 def checkafterresolved(repo):
3926 3924 '''Inform the user about the next action after completing hg resolve
3927 3925
3928 3926 If there's a an unfinished operation that supports continue flag,
3929 3927 howtocontinue will yield repo.ui.warn as the reporter.
3930 3928
3931 3929 Otherwise, it will yield repo.ui.note.
3932 3930 '''
3933 3931 msg, warning = howtocontinue(repo)
3934 3932 if msg is not None:
3935 3933 if warning:
3936 3934 repo.ui.warn(b"%s\n" % msg)
3937 3935 else:
3938 3936 repo.ui.note(b"%s\n" % msg)
3939 3937
3940 3938
3941 3939 def wrongtooltocontinue(repo, task):
3942 3940 '''Raise an abort suggesting how to properly continue if there is an
3943 3941 active task.
3944 3942
3945 3943 Uses howtocontinue() to find the active task.
3946 3944
3947 3945 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3948 3946 a hint.
3949 3947 '''
3950 3948 after = howtocontinue(repo)
3951 3949 hint = None
3952 3950 if after[1]:
3953 3951 hint = after[0]
3954 3952 raise error.Abort(_(b'no %s in progress') % task, hint=hint)
3955 3953
3956 3954
3957 3955 def abortgraft(ui, repo, graftstate):
3958 3956 """abort the interrupted graft and rollbacks to the state before interrupted
3959 3957 graft"""
3960 3958 if not graftstate.exists():
3961 3959 raise error.Abort(_(b"no interrupted graft to abort"))
3962 3960 statedata = readgraftstate(repo, graftstate)
3963 3961 newnodes = statedata.get(b'newnodes')
3964 3962 if newnodes is None:
3965 3963 # and old graft state which does not have all the data required to abort
3966 3964 # the graft
3967 3965 raise error.Abort(_(b"cannot abort using an old graftstate"))
3968 3966
3969 3967 # changeset from which graft operation was started
3970 3968 if len(newnodes) > 0:
3971 3969 startctx = repo[newnodes[0]].p1()
3972 3970 else:
3973 3971 startctx = repo[b'.']
3974 3972 # whether to strip or not
3975 3973 cleanup = False
3976 3974 from . import hg
3977 3975
3978 3976 if newnodes:
3979 3977 newnodes = [repo[r].rev() for r in newnodes]
3980 3978 cleanup = True
3981 3979 # checking that none of the newnodes turned public or is public
3982 3980 immutable = [c for c in newnodes if not repo[c].mutable()]
3983 3981 if immutable:
3984 3982 repo.ui.warn(
3985 3983 _(b"cannot clean up public changesets %s\n")
3986 3984 % b', '.join(bytes(repo[r]) for r in immutable),
3987 3985 hint=_(b"see 'hg help phases' for details"),
3988 3986 )
3989 3987 cleanup = False
3990 3988
3991 3989 # checking that no new nodes are created on top of grafted revs
3992 3990 desc = set(repo.changelog.descendants(newnodes))
3993 3991 if desc - set(newnodes):
3994 3992 repo.ui.warn(
3995 3993 _(
3996 3994 b"new changesets detected on destination "
3997 3995 b"branch, can't strip\n"
3998 3996 )
3999 3997 )
4000 3998 cleanup = False
4001 3999
4002 4000 if cleanup:
4003 4001 with repo.wlock(), repo.lock():
4004 4002 hg.updaterepo(repo, startctx.node(), overwrite=True)
4005 4003 # stripping the new nodes created
4006 4004 strippoints = [
4007 4005 c.node() for c in repo.set(b"roots(%ld)", newnodes)
4008 4006 ]
4009 4007 repair.strip(repo.ui, repo, strippoints, backup=False)
4010 4008
4011 4009 if not cleanup:
4012 4010 # we don't update to the startnode if we can't strip
4013 4011 startctx = repo[b'.']
4014 4012 hg.updaterepo(repo, startctx.node(), overwrite=True)
4015 4013
4016 4014 ui.status(_(b"graft aborted\n"))
4017 4015 ui.status(_(b"working directory is now at %s\n") % startctx.hex()[:12])
4018 4016 graftstate.delete()
4019 4017 return 0
4020 4018
4021 4019
4022 4020 def readgraftstate(repo, graftstate):
4023 4021 # type: (Any, statemod.cmdstate) -> Dict[bytes, Any]
4024 4022 """read the graft state file and return a dict of the data stored in it"""
4025 4023 try:
4026 4024 return graftstate.read()
4027 4025 except error.CorruptedState:
4028 4026 nodes = repo.vfs.read(b'graftstate').splitlines()
4029 4027 return {b'nodes': nodes}
4030 4028
4031 4029
4032 4030 def hgabortgraft(ui, repo):
4033 4031 """ abort logic for aborting graft using 'hg abort'"""
4034 4032 with repo.wlock():
4035 4033 graftstate = statemod.cmdstate(repo, b'graftstate')
4036 4034 return abortgraft(ui, repo, graftstate)
@@ -1,508 +1,508 b''
1 1 #testcases obsstore-off obsstore-on
2 2
3 3 $ cat << EOF >> $HGRCPATH
4 4 > [extensions]
5 5 > amend=
6 6 > debugdrawdag=$TESTDIR/drawdag.py
7 7 > [diff]
8 8 > git=1
9 9 > EOF
10 10
11 11 #if obsstore-on
12 12 $ cat << EOF >> $HGRCPATH
13 13 > [experimental]
14 14 > evolution.createmarkers=True
15 15 > EOF
16 16 #endif
17 17
18 18 Basic amend
19 19
20 20 $ hg init repo1
21 21 $ cd repo1
22 22 $ hg debugdrawdag <<'EOS'
23 23 > B
24 24 > |
25 25 > A
26 26 > EOS
27 27
28 28 $ hg update B -q
29 29 $ echo 2 >> B
30 30
31 31 $ hg amend
32 32 saved backup bundle to $TESTTMP/repo1/.hg/strip-backup/112478962961-7e959a55-amend.hg (obsstore-off !)
33 33 #if obsstore-off
34 34 $ hg log -p -G --hidden -T '{rev} {node|short} {desc}\n'
35 35 @ 1 be169c7e8dbe B
36 36 | diff --git a/B b/B
37 37 | new file mode 100644
38 38 | --- /dev/null
39 39 | +++ b/B
40 40 | @@ -0,0 +1,1 @@
41 41 | +B2
42 42 |
43 43 o 0 426bada5c675 A
44 44 diff --git a/A b/A
45 45 new file mode 100644
46 46 --- /dev/null
47 47 +++ b/A
48 48 @@ -0,0 +1,1 @@
49 49 +A
50 50 \ No newline at end of file
51 51
52 52 #else
53 53 $ hg log -p -G --hidden -T '{rev} {node|short} {desc}\n'
54 54 @ 2 be169c7e8dbe B
55 55 | diff --git a/B b/B
56 56 | new file mode 100644
57 57 | --- /dev/null
58 58 | +++ b/B
59 59 | @@ -0,0 +1,1 @@
60 60 | +B2
61 61 |
62 62 | x 1 112478962961 B
63 63 |/ diff --git a/B b/B
64 64 | new file mode 100644
65 65 | --- /dev/null
66 66 | +++ b/B
67 67 | @@ -0,0 +1,1 @@
68 68 | +B
69 69 | \ No newline at end of file
70 70 |
71 71 o 0 426bada5c675 A
72 72 diff --git a/A b/A
73 73 new file mode 100644
74 74 --- /dev/null
75 75 +++ b/A
76 76 @@ -0,0 +1,1 @@
77 77 +A
78 78 \ No newline at end of file
79 79
80 80 #endif
81 81
82 82 Nothing changed
83 83
84 84 $ hg amend
85 85 nothing changed
86 86 [1]
87 87
88 88 $ hg amend -d "0 0"
89 89 nothing changed
90 90 [1]
91 91
92 92 $ hg amend -d "Thu Jan 01 00:00:00 1970 UTC"
93 93 nothing changed
94 94 [1]
95 95
96 96 Matcher and metadata options
97 97
98 98 $ echo 3 > C
99 99 $ echo 4 > D
100 100 $ hg add C D
101 101 $ hg amend -m NEWMESSAGE -I C
102 102 saved backup bundle to $TESTTMP/repo1/.hg/strip-backup/be169c7e8dbe-7684ddc5-amend.hg (obsstore-off !)
103 103 $ hg log -r . -T '{node|short} {desc} {files}\n'
104 104 c7ba14d9075b NEWMESSAGE B C
105 105 $ echo 5 > E
106 106 $ rm C
107 107 $ hg amend -d '2000 1000' -u 'Foo <foo@example.com>' -A C D
108 108 saved backup bundle to $TESTTMP/repo1/.hg/strip-backup/c7ba14d9075b-b3e76daa-amend.hg (obsstore-off !)
109 109 $ hg log -r . -T '{node|short} {desc} {files} {author} {date}\n'
110 110 14f6c4bcc865 NEWMESSAGE B D Foo <foo@example.com> 2000.01000
111 111
112 112 Amend with editor
113 113
114 114 $ cat > $TESTTMP/prefix.sh <<'EOF'
115 115 > printf 'EDITED: ' > $TESTTMP/msg
116 116 > cat "$1" >> $TESTTMP/msg
117 117 > mv $TESTTMP/msg "$1"
118 118 > EOF
119 119 $ chmod +x $TESTTMP/prefix.sh
120 120
121 121 $ HGEDITOR="sh $TESTTMP/prefix.sh" hg amend --edit
122 122 saved backup bundle to $TESTTMP/repo1/.hg/strip-backup/14f6c4bcc865-6591f15d-amend.hg (obsstore-off !)
123 123 $ hg log -r . -T '{node|short} {desc}\n'
124 124 298f085230c3 EDITED: NEWMESSAGE
125 125 $ HGEDITOR="sh $TESTTMP/prefix.sh" hg amend -e -m MSG
126 126 saved backup bundle to $TESTTMP/repo1/.hg/strip-backup/298f085230c3-d81a6ad3-amend.hg (obsstore-off !)
127 127 $ hg log -r . -T '{node|short} {desc}\n'
128 128 974f07f28537 EDITED: MSG
129 129
130 130 $ echo FOO > $TESTTMP/msg
131 131 $ hg amend -l $TESTTMP/msg -m BAR
132 132 abort: options --message and --logfile are mutually exclusive
133 133 [255]
134 134 $ hg amend -l $TESTTMP/msg
135 135 saved backup bundle to $TESTTMP/repo1/.hg/strip-backup/974f07f28537-edb6470a-amend.hg (obsstore-off !)
136 136 $ hg log -r . -T '{node|short} {desc}\n'
137 137 507be9bdac71 FOO
138 138
139 139 Interactive mode
140 140
141 141 $ touch F G
142 142 $ hg add F G
143 143 $ cat <<EOS | hg amend -i --config ui.interactive=1
144 144 > y
145 145 > n
146 146 > EOS
147 147 diff --git a/F b/F
148 148 new file mode 100644
149 149 examine changes to 'F'?
150 150 (enter ? for help) [Ynesfdaq?] y
151 151
152 152 diff --git a/G b/G
153 153 new file mode 100644
154 154 examine changes to 'G'?
155 155 (enter ? for help) [Ynesfdaq?] n
156 156
157 157 saved backup bundle to $TESTTMP/repo1/.hg/strip-backup/507be9bdac71-c8077452-amend.hg (obsstore-off !)
158 158 $ hg log -r . -T '{files}\n'
159 159 B D F
160 160
161 161 Amend in the middle of a stack
162 162
163 163 $ hg init $TESTTMP/repo2
164 164 $ cd $TESTTMP/repo2
165 165 $ hg debugdrawdag <<'EOS'
166 166 > C
167 167 > |
168 168 > B
169 169 > |
170 170 > A
171 171 > EOS
172 172
173 173 $ hg update -q B
174 174 $ echo 2 >> B
175 175 $ hg amend
176 176 abort: cannot amend changeset with children
177 177 [255]
178 178
179 179 #if obsstore-on
180 180
181 181 With allowunstable, amend could work in the middle of a stack
182 182
183 183 $ cat >> $HGRCPATH <<EOF
184 184 > [experimental]
185 185 > evolution.createmarkers=True
186 186 > evolution.allowunstable=True
187 187 > EOF
188 188
189 189 $ hg amend
190 190 1 new orphan changesets
191 191 $ hg log -T '{rev} {node|short} {desc}\n' -G
192 192 @ 3 be169c7e8dbe B
193 193 |
194 194 | * 2 26805aba1e60 C
195 195 | |
196 196 | x 1 112478962961 B
197 197 |/
198 198 o 0 426bada5c675 A
199 199
200 200 Checking the note stored in the obsmarker
201 201
202 202 $ echo foo > bar
203 203 $ hg add bar
204 204 $ hg amend --note 'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy'
205 205 abort: cannot store a note of more than 255 bytes
206 206 [255]
207 207 $ hg amend --note "adding bar"
208 208 $ hg debugobsolete -r .
209 209 112478962961147124edd43549aedd1a335e44bf be169c7e8dbe21cd10b3d79691cbe7f241e3c21c 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '8', 'operation': 'amend', 'user': 'test'}
210 210 be169c7e8dbe21cd10b3d79691cbe7f241e3c21c 16084da537dd8f84cfdb3055c633772269d62e1b 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '8', 'note': 'adding bar', 'operation': 'amend', 'user': 'test'}
211 211 #endif
212 212
213 213 Cannot amend public changeset
214 214
215 215 $ hg phase -r A --public
216 216 $ hg update -C -q A
217 217 $ hg amend -m AMEND
218 218 abort: cannot amend public changesets
219 219 (see 'hg help phases' for details)
220 220 [255]
221 221
222 222 Amend a merge changeset
223 223
224 224 $ hg init $TESTTMP/repo3
225 225 $ cd $TESTTMP/repo3
226 226 $ hg debugdrawdag <<'EOS'
227 227 > C
228 228 > /|
229 229 > A B
230 230 > EOS
231 231 $ hg update -q C
232 232 $ hg amend -m FOO
233 233 saved backup bundle to $TESTTMP/repo3/.hg/strip-backup/a35c07e8a2a4-15ff4612-amend.hg (obsstore-off !)
234 234 $ rm .hg/localtags
235 235 $ hg log -G -T '{desc}\n'
236 236 @ FOO
237 237 |\
238 238 | o B
239 239 |
240 240 o A
241 241
242 242
243 243 More complete test for status changes (issue5732)
244 244 -------------------------------------------------
245 245
246 246 Generates history of files having 3 states, r0_r1_wc:
247 247
248 248 r0: ground (content/missing)
249 249 r1: old state to be amended (content/missing, where missing means removed)
250 250 wc: changes to be included in r1 (content/missing-tracked/untracked)
251 251
252 252 $ hg init $TESTTMP/wcstates
253 253 $ cd $TESTTMP/wcstates
254 254
255 255 $ "$PYTHON" $TESTDIR/generate-working-copy-states.py state 2 1
256 256 $ hg addremove -q --similarity 0
257 257 $ hg commit -m0
258 258
259 259 $ "$PYTHON" $TESTDIR/generate-working-copy-states.py state 2 2
260 260 $ hg addremove -q --similarity 0
261 261 $ hg commit -m1
262 262
263 263 $ "$PYTHON" $TESTDIR/generate-working-copy-states.py state 2 wc
264 264 $ hg addremove -q --similarity 0
265 265 $ hg forget *_*_*-untracked
266 266 $ rm *_*_missing-*
267 267
268 268 amend r1 to include wc changes
269 269
270 270 $ hg amend
271 271 saved backup bundle to * (glob) (obsstore-off !)
272 272
273 273 clean/modified/removed/added states of the amended revision
274 274
275 275 $ hg status --all --change . 'glob:content1_*_content1-tracked'
276 276 C content1_content1_content1-tracked
277 277 C content1_content2_content1-tracked
278 278 C content1_missing_content1-tracked
279 279 $ hg status --all --change . 'glob:content1_*_content[23]-tracked'
280 280 M content1_content1_content3-tracked
281 281 M content1_content2_content2-tracked
282 282 M content1_content2_content3-tracked
283 283 M content1_missing_content3-tracked
284 284 $ hg status --all --change . 'glob:content1_*_missing-tracked'
285 285 M content1_content2_missing-tracked
286 286 R content1_missing_missing-tracked
287 287 C content1_content1_missing-tracked
288 288 $ hg status --all --change . 'glob:content1_*_*-untracked'
289 289 R content1_content1_content1-untracked
290 290 R content1_content1_content3-untracked
291 291 R content1_content1_missing-untracked
292 292 R content1_content2_content1-untracked
293 293 R content1_content2_content2-untracked
294 294 R content1_content2_content3-untracked
295 295 R content1_content2_missing-untracked
296 296 R content1_missing_content1-untracked
297 297 R content1_missing_content3-untracked
298 298 R content1_missing_missing-untracked
299 299 $ hg status --all --change . 'glob:missing_content2_*'
300 300 A missing_content2_content2-tracked
301 301 A missing_content2_content3-tracked
302 302 A missing_content2_missing-tracked
303 303 $ hg status --all --change . 'glob:missing_missing_*'
304 304 A missing_missing_content3-tracked
305 305
306 306 working directory should be all clean (with some missing/untracked files)
307 307
308 308 $ hg status --all 'glob:*_content?-tracked'
309 309 C content1_content1_content1-tracked
310 310 C content1_content1_content3-tracked
311 311 C content1_content2_content1-tracked
312 312 C content1_content2_content2-tracked
313 313 C content1_content2_content3-tracked
314 314 C content1_missing_content1-tracked
315 315 C content1_missing_content3-tracked
316 316 C missing_content2_content2-tracked
317 317 C missing_content2_content3-tracked
318 318 C missing_missing_content3-tracked
319 319 $ hg status --all 'glob:*_missing-tracked'
320 320 ! content1_content1_missing-tracked
321 321 ! content1_content2_missing-tracked
322 322 ! content1_missing_missing-tracked
323 323 ! missing_content2_missing-tracked
324 324 ! missing_missing_missing-tracked
325 325 $ hg status --all 'glob:*-untracked'
326 326 ? content1_content1_content1-untracked
327 327 ? content1_content1_content3-untracked
328 328 ? content1_content2_content1-untracked
329 329 ? content1_content2_content2-untracked
330 330 ? content1_content2_content3-untracked
331 331 ? content1_missing_content1-untracked
332 332 ? content1_missing_content3-untracked
333 333 ? missing_content2_content2-untracked
334 334 ? missing_content2_content3-untracked
335 335 ? missing_missing_content3-untracked
336 336
337 337 =================================
338 338 Test backup-bundle config option|
339 339 =================================
340 340 $ hg init $TESTTMP/repo4
341 341 $ cd $TESTTMP/repo4
342 342 $ echo a>a
343 343 $ hg ci -Aqma
344 344 $ echo oops>b
345 345 $ hg ci -Aqm "b"
346 346 $ echo partiallyfixed > b
347 347
348 348 #if obsstore-off
349 349 $ hg amend
350 350 saved backup bundle to $TESTTMP/repo4/.hg/strip-backup/95e899acf2ce-f11cb050-amend.hg
351 351 When backup-bundle config option is set:
352 352 $ cat << EOF >> $HGRCPATH
353 353 > [rewrite]
354 354 > backup-bundle = False
355 355 > EOF
356 356 $ echo fixed > b
357 357 $ hg amend
358 358
359 359 #else
360 360 $ hg amend
361 361 When backup-bundle config option is set:
362 362 $ cat << EOF >> $HGRCPATH
363 363 > [rewrite]
364 364 > backup-bundle = False
365 365 > EOF
366 366 $ echo fixed > b
367 367 $ hg amend
368 368
369 369 #endif
370 370 ==========================================
371 371 Test update-timestamp config option|
372 372 ==========================================
373 373
374 374 $ cat >> $HGRCPATH << EOF
375 375 > [extensions]
376 376 > amend=
377 377 > mockmakedate = $TESTDIR/mockmakedate.py
378 378 > EOF
379 379
380 380 $ hg init $TESTTMP/repo5
381 381 $ cd $TESTTMP/repo5
382 382 $ cat <<'EOF' >> .hg/hgrc
383 383 > [ui]
384 384 > logtemplate = 'user: {user}
385 385 > date: {date|date}
386 386 > summary: {desc|firstline}\n'
387 387 > EOF
388 388
389 389 $ echo a>a
390 390 $ hg ci -Am 'commit 1'
391 391 adding a
392 392
393 393 When updatetimestamp is False
394 394
395 395 $ hg amend --date '1997-1-1 0:1'
396 396 $ hg log --limit 1
397 397 user: test
398 398 date: Wed Jan 01 00:01:00 1997 +0000
399 399 summary: commit 1
400 400
401 401 When update-timestamp is True and no other change than the date
402 402
403 403 $ hg amend --config rewrite.update-timestamp=True
404 404 nothing changed
405 405 [1]
406 406 $ hg log --limit 1
407 407 user: test
408 408 date: Wed Jan 01 00:01:00 1997 +0000
409 409 summary: commit 1
410 410
411 411 When update-timestamp is True and there is other change than the date
412 412 $ hg amend --user foobar --config rewrite.update-timestamp=True
413 413 $ hg log --limit 1
414 414 user: foobar
415 415 date: Thu Jan 01 00:00:02 1970 +0000
416 416 summary: commit 1
417 417
418 418 When date option is applicable and update-timestamp is True
419 419 $ hg amend --date '1998-1-1 0:1' --config rewrite.update-timestamp=True
420 420 $ hg log --limit 1
421 421 user: foobar
422 422 date: Thu Jan 01 00:01:00 1998 +0000
423 423 summary: commit 1
424 424
425 425 Unlike rewrite.update-timestamp, -D/--currentdate always updates the timestamp
426 426
427 427 $ hg amend -D
428 428 $ hg log --limit 1
429 429 user: foobar
430 430 date: Thu Jan 01 00:00:04 1970 +0000
431 431 summary: commit 1
432 432
433 433 $ hg amend -D --config rewrite.update-timestamp=True
434 434 $ hg log --limit 1
435 435 user: foobar
436 436 date: Thu Jan 01 00:00:05 1970 +0000
437 437 summary: commit 1
438 438
439 439 rewrite.update-timestamp can be negated by --no-currentdate
440 440
441 441 $ hg amend --config rewrite.update-timestamp=True --no-currentdate -u baz
442 442 $ hg log --limit 1
443 443 user: baz
444 444 date: Thu Jan 01 00:00:05 1970 +0000
445 445 summary: commit 1
446 446
447 447 Bad combination of date options:
448 448
449 449 $ hg amend -D --date '0 0'
450 abort: --date and --currentdate are mutually exclusive
450 abort: cannot specify both --date and --currentdate
451 451 [255]
452 452
453 453 Close branch
454 454
455 455 $ hg amend --secret --close-branch
456 456 $ hg log --limit 1 -T 'close={get(extras, "close")}\nphase={phase}\n'
457 457 close=1
458 458 phase=secret
459 459
460 460 $ cd ..
461 461
462 462 Corner case of amend from issue6157:
463 463 - working copy parent has a change to file `a`
464 464 - working copy has the inverse change
465 465 - we amend the working copy parent for files other than `a`
466 466 hg used to include the changes to `a` anyway.
467 467
468 468 $ hg init 6157; cd 6157
469 469 $ echo a > a; echo b > b; hg commit -qAm_
470 470 $ echo a2 > a; hg commit -qm_
471 471 $ hg diff --stat -c .
472 472 a | 2 +-
473 473 1 files changed, 1 insertions(+), 1 deletions(-)
474 474 $ echo a > a; echo b2 > b; hg amend -q b
475 475 $ hg diff --stat -c .
476 476 a | 2 +-
477 477 b | 2 +-
478 478 2 files changed, 2 insertions(+), 2 deletions(-)
479 479
480 480 Modifying a file while the editor is open can cause dirstate corruption
481 481 (issue6233)
482 482
483 483 $ cd $TESTTMP
484 484 $ hg init modify-during-amend; cd modify-during-amend
485 485 $ echo r0 > foo; hg commit -qAm "r0"
486 486 $ echo alpha > foo; hg commit -qm "alpha"
487 487 $ echo beta >> foo
488 488 $ cat > $TESTTMP/sleepy_editor.sh <<EOF
489 489 > echo hi > "\$1"
490 490 > sleep 3
491 491 > EOF
492 492 $ HGEDITOR="sh $TESTTMP/sleepy_editor.sh" hg commit --amend &
493 493 $ sleep 1
494 494 $ echo delta >> foo
495 495 $ sleep 3
496 496 $ if (hg diff -c . | grep 'delta' >/dev/null) || [ -n "$(hg status)" ]; then
497 497 > echo "OK."
498 498 > else
499 499 > echo "Bug detected. 'delta' is not part of the commit OR the wdir"
500 500 > echo "Diff and status before rebuild:"
501 501 > hg diff
502 502 > hg status
503 503 > hg debugrebuilddirstate
504 504 > echo "Diff and status after rebuild:"
505 505 > hg diff
506 506 > hg status
507 507 > fi
508 508 OK.
@@ -1,2427 +1,2427 b''
1 1 #testcases abortcommand abortflag
2 2
3 3 $ cat >> $HGRCPATH <<EOF
4 4 > [extdiff]
5 5 > # for portability:
6 6 > pdiff = sh "$RUNTESTDIR/pdiff"
7 7 > EOF
8 8
9 9 #if abortflag
10 10 $ cat >> $HGRCPATH <<EOF
11 11 > [alias]
12 12 > abort = graft --abort
13 13 > EOF
14 14 #endif
15 15
16 16 Create a repo with some stuff in it:
17 17
18 18 $ hg init a
19 19 $ cd a
20 20 $ echo a > a
21 21 $ echo a > d
22 22 $ echo a > e
23 23 $ hg ci -qAm0
24 24 $ echo b > a
25 25 $ hg ci -m1 -u bar
26 26 $ hg mv a b
27 27 $ hg ci -m2
28 28 $ hg cp b c
29 29 $ hg ci -m3 -u baz
30 30 $ echo b > d
31 31 $ echo f > e
32 32 $ hg ci -m4
33 33 $ hg up -q 3
34 34 $ echo b > e
35 35 $ hg branch -q stable
36 36 $ hg ci -m5
37 37 $ hg merge -q default --tool internal:local # for conflicts in e, choose 5 and ignore 4
38 38 $ hg branch -q default
39 39 $ hg ci -m6
40 40 $ hg phase --public 3
41 41 $ hg phase --force --secret 6
42 42
43 43 $ hg log -G --template '{author}@{rev}.{phase}: {desc}\n'
44 44 @ test@6.secret: 6
45 45 |\
46 46 | o test@5.draft: 5
47 47 | |
48 48 o | test@4.draft: 4
49 49 |/
50 50 o baz@3.public: 3
51 51 |
52 52 o test@2.public: 2
53 53 |
54 54 o bar@1.public: 1
55 55 |
56 56 o test@0.public: 0
57 57
58 58 Test --base for grafting the merge of 4 from the perspective of 5, thus only getting the change to d
59 59
60 60 $ hg up -cqr 3
61 61 $ hg graft -r 6 --base 5
62 62 grafting 6:25a2b029d3ae "6" (tip)
63 63 merging e
64 64 $ hg st --change .
65 65 M d
66 66
67 67 $ hg -q strip . --config extensions.strip=
68 68
69 69 Test --base for collapsing changesets 2 and 3, thus getting both b and c
70 70
71 71 $ hg up -cqr 0
72 72 $ hg graft -r 3 --base 1
73 73 grafting 3:4c60f11aa304 "3"
74 74 merging a and b to b
75 75 merging a and c to c
76 76 $ hg st --change .
77 77 A b
78 78 A c
79 79 R a
80 80
81 81 $ hg -q strip . --config extensions.strip=
82 82
83 83 Specifying child as --base revision fails safely (perhaps slightly confusing, but consistent)
84 84
85 85 $ hg graft -r 2 --base 3
86 86 grafting 2:5c095ad7e90f "2"
87 87 note: possible conflict - c was deleted and renamed to:
88 88 a
89 89 note: graft of 2:5c095ad7e90f created no changes to commit
90 90
91 91 Can't continue without starting:
92 92
93 93 $ hg -q up -cr tip
94 94 $ hg rm -q e
95 95 $ hg graft --continue
96 96 abort: no graft in progress
97 97 [255]
98 98 $ hg revert -r . -q e
99 99
100 100 Need to specify a rev:
101 101
102 102 $ hg graft
103 103 abort: no revisions specified
104 104 [255]
105 105
106 106 Can't graft ancestor:
107 107
108 108 $ hg graft 1 2
109 109 skipping ancestor revision 1:5d205f8b35b6
110 110 skipping ancestor revision 2:5c095ad7e90f
111 111 [255]
112 112
113 113 Specify revisions with -r:
114 114
115 115 $ hg graft -r 1 -r 2
116 116 skipping ancestor revision 1:5d205f8b35b6
117 117 skipping ancestor revision 2:5c095ad7e90f
118 118 [255]
119 119
120 120 $ hg graft -r 1 2
121 121 warning: inconsistent use of --rev might give unexpected revision ordering!
122 122 skipping ancestor revision 2:5c095ad7e90f
123 123 skipping ancestor revision 1:5d205f8b35b6
124 124 [255]
125 125
126 126 Conflicting date/user options:
127 127
128 128 $ hg up -q 0
129 129 $ hg graft -U --user foo 2
130 abort: --user and --currentuser are mutually exclusive
130 abort: cannot specify both --user and --currentuser
131 131 [255]
132 132 $ hg graft -D --date '0 0' 2
133 abort: --date and --currentdate are mutually exclusive
133 abort: cannot specify both --date and --currentdate
134 134 [255]
135 135
136 136 Can't graft with dirty wd:
137 137
138 138 $ hg up -q 0
139 139 $ echo foo > a
140 140 $ hg graft 1
141 141 abort: uncommitted changes
142 142 [255]
143 143 $ hg revert a
144 144
145 145 Graft a rename:
146 146 (this also tests that editor is invoked if '--edit' is specified)
147 147
148 148 $ hg status --rev "2^1" --rev 2
149 149 A b
150 150 R a
151 151 $ HGEDITOR=cat hg graft 2 -u foo --edit
152 152 grafting 2:5c095ad7e90f "2"
153 153 merging a and b to b
154 154 2
155 155
156 156
157 157 HG: Enter commit message. Lines beginning with 'HG:' are removed.
158 158 HG: Leave message empty to abort commit.
159 159 HG: --
160 160 HG: user: foo
161 161 HG: branch 'default'
162 162 HG: added b
163 163 HG: removed a
164 164 $ hg export tip --git
165 165 # HG changeset patch
166 166 # User foo
167 167 # Date 0 0
168 168 # Thu Jan 01 00:00:00 1970 +0000
169 169 # Node ID ef0ef43d49e79e81ddafdc7997401ba0041efc82
170 170 # Parent 68795b066622ca79a25816a662041d8f78f3cd9e
171 171 2
172 172
173 173 diff --git a/a b/b
174 174 rename from a
175 175 rename to b
176 176
177 177 Look for extra:source
178 178
179 179 $ hg log --debug -r tip
180 180 changeset: 7:ef0ef43d49e79e81ddafdc7997401ba0041efc82
181 181 tag: tip
182 182 phase: draft
183 183 parent: 0:68795b066622ca79a25816a662041d8f78f3cd9e
184 184 parent: -1:0000000000000000000000000000000000000000
185 185 manifest: 7:e59b6b228f9cbf9903d5e9abf996e083a1f533eb
186 186 user: foo
187 187 date: Thu Jan 01 00:00:00 1970 +0000
188 188 files+: b
189 189 files-: a
190 190 extra: branch=default
191 191 extra: source=5c095ad7e90f871700f02dd1fa5012cb4498a2d4
192 192 description:
193 193 2
194 194
195 195
196 196
197 197 Graft out of order, skipping a merge and a duplicate
198 198 (this also tests that editor is not invoked if '--edit' is not specified)
199 199
200 200 $ hg graft 1 5 4 3 'merge()' 2 -n
201 201 skipping ungraftable merge revision 6
202 202 skipping revision 2:5c095ad7e90f (already grafted to 7:ef0ef43d49e7)
203 203 grafting 1:5d205f8b35b6 "1"
204 204 grafting 5:97f8bfe72746 "5"
205 205 grafting 4:9c233e8e184d "4"
206 206 grafting 3:4c60f11aa304 "3"
207 207
208 208 $ HGEDITOR=cat hg graft 1 5 'merge()' 2 --debug
209 209 skipping ungraftable merge revision 6
210 210 scanning for duplicate grafts
211 211 skipping revision 2:5c095ad7e90f (already grafted to 7:ef0ef43d49e7)
212 212 grafting 1:5d205f8b35b6 "1"
213 213 unmatched files in local:
214 214 b
215 215 all copies found (* = to merge, ! = divergent, % = renamed and deleted):
216 216 src: 'a' -> dst: 'b' *
217 217 checking for directory renames
218 218 resolving manifests
219 219 branchmerge: True, force: True, partial: False
220 220 ancestor: 68795b066622, local: ef0ef43d49e7+, remote: 5d205f8b35b6
221 221 preserving b for resolve of b
222 222 starting 4 threads for background file closing (?)
223 223 b: local copied/moved from a -> m (premerge)
224 224 picked tool ':merge' for b (binary False symlink False changedelete False)
225 225 merging b and a to b
226 226 my b@ef0ef43d49e7+ other a@5d205f8b35b6 ancestor a@68795b066622
227 227 premerge successful
228 228 committing files:
229 229 b
230 230 committing manifest
231 231 committing changelog
232 232 updating the branch cache
233 233 grafting 5:97f8bfe72746 "5"
234 234 all copies found (* = to merge, ! = divergent, % = renamed and deleted):
235 235 src: 'c' -> dst: 'b'
236 236 checking for directory renames
237 237 resolving manifests
238 238 branchmerge: True, force: True, partial: False
239 239 ancestor: 4c60f11aa304, local: 6b9e5368ca4e+, remote: 97f8bfe72746
240 240 e: remote is newer -> g
241 241 getting e
242 242 committing files:
243 243 e
244 244 committing manifest
245 245 committing changelog
246 246 updating the branch cache
247 247 $ HGEDITOR=cat hg graft 4 3 --log --debug
248 248 scanning for duplicate grafts
249 249 grafting 4:9c233e8e184d "4"
250 250 all copies found (* = to merge, ! = divergent, % = renamed and deleted):
251 251 src: 'c' -> dst: 'b'
252 252 checking for directory renames
253 253 resolving manifests
254 254 branchmerge: True, force: True, partial: False
255 255 ancestor: 4c60f11aa304, local: 1905859650ec+, remote: 9c233e8e184d
256 256 preserving e for resolve of e
257 257 d: remote is newer -> g
258 258 getting d
259 259 e: versions differ -> m (premerge)
260 260 picked tool ':merge' for e (binary False symlink False changedelete False)
261 261 merging e
262 262 my e@1905859650ec+ other e@9c233e8e184d ancestor e@4c60f11aa304
263 263 e: versions differ -> m (merge)
264 264 picked tool ':merge' for e (binary False symlink False changedelete False)
265 265 my e@1905859650ec+ other e@9c233e8e184d ancestor e@4c60f11aa304
266 266 warning: conflicts while merging e! (edit, then use 'hg resolve --mark')
267 267 abort: unresolved conflicts, can't continue
268 268 (use 'hg resolve' and 'hg graft --continue')
269 269 [255]
270 270
271 271 Summary should mention graft:
272 272
273 273 $ hg summary |grep graft
274 274 commit: 2 modified, 2 unknown, 1 unresolved (graft in progress)
275 275
276 276 Using status to get more context
277 277
278 278 $ hg status --verbose
279 279 M d
280 280 M e
281 281 ? a.orig
282 282 ? e.orig
283 283 # The repository is in an unfinished *graft* state.
284 284
285 285 # Unresolved merge conflicts:
286 286 #
287 287 # e
288 288 #
289 289 # To mark files as resolved: hg resolve --mark FILE
290 290
291 291 # To continue: hg graft --continue
292 292 # To abort: hg graft --abort
293 293 # To stop: hg graft --stop
294 294
295 295
296 296 Commit while interrupted should fail:
297 297
298 298 $ hg ci -m 'commit interrupted graft'
299 299 abort: graft in progress
300 300 (use 'hg graft --continue' or 'hg graft --stop' to stop)
301 301 [255]
302 302
303 303 Abort the graft and try committing:
304 304
305 305 $ hg up -C .
306 306 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
307 307 $ echo c >> e
308 308 $ hg ci -mtest
309 309
310 310 $ hg strip . --config extensions.strip=
311 311 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
312 312 saved backup bundle to $TESTTMP/a/.hg/strip-backup/*-backup.hg (glob)
313 313
314 314 Graft again:
315 315
316 316 $ hg graft 1 5 4 3 'merge()' 2
317 317 skipping ungraftable merge revision 6
318 318 skipping revision 2:5c095ad7e90f (already grafted to 7:ef0ef43d49e7)
319 319 skipping revision 1:5d205f8b35b6 (already grafted to 8:6b9e5368ca4e)
320 320 skipping revision 5:97f8bfe72746 (already grafted to 9:1905859650ec)
321 321 grafting 4:9c233e8e184d "4"
322 322 merging e
323 323 warning: conflicts while merging e! (edit, then use 'hg resolve --mark')
324 324 abort: unresolved conflicts, can't continue
325 325 (use 'hg resolve' and 'hg graft --continue')
326 326 [255]
327 327
328 328 Continue without resolve should fail:
329 329
330 330 $ hg graft -c
331 331 grafting 4:9c233e8e184d "4"
332 332 abort: unresolved merge conflicts (see 'hg help resolve')
333 333 [255]
334 334
335 335 Fix up:
336 336
337 337 $ echo b > e
338 338 $ hg resolve -m e
339 339 (no more unresolved files)
340 340 continue: hg graft --continue
341 341
342 342 Continue with a revision should fail:
343 343
344 344 $ hg graft -c 6
345 345 abort: can't specify --continue and revisions
346 346 [255]
347 347
348 348 $ hg graft -c -r 6
349 349 abort: can't specify --continue and revisions
350 350 [255]
351 351
352 352 Continue for real, clobber usernames
353 353
354 354 $ hg graft -c -U
355 355 grafting 4:9c233e8e184d "4"
356 356 grafting 3:4c60f11aa304 "3"
357 357
358 358 Compare with original:
359 359
360 360 $ hg diff -r 6
361 361 $ hg status --rev 0:. -C
362 362 M d
363 363 M e
364 364 A b
365 365 a
366 366 A c
367 367 a
368 368 R a
369 369
370 370 View graph:
371 371
372 372 $ hg log -G --template '{author}@{rev}.{phase}: {desc}\n'
373 373 @ test@11.draft: 3
374 374 |
375 375 o test@10.draft: 4
376 376 |
377 377 o test@9.draft: 5
378 378 |
379 379 o bar@8.draft: 1
380 380 |
381 381 o foo@7.draft: 2
382 382 |
383 383 | o test@6.secret: 6
384 384 | |\
385 385 | | o test@5.draft: 5
386 386 | | |
387 387 | o | test@4.draft: 4
388 388 | |/
389 389 | o baz@3.public: 3
390 390 | |
391 391 | o test@2.public: 2
392 392 | |
393 393 | o bar@1.public: 1
394 394 |/
395 395 o test@0.public: 0
396 396
397 397 Graft again onto another branch should preserve the original source
398 398 $ hg up -q 0
399 399 $ echo 'g'>g
400 400 $ hg add g
401 401 $ hg ci -m 7
402 402 created new head
403 403 $ hg graft 7
404 404 grafting 7:ef0ef43d49e7 "2"
405 405
406 406 $ hg log -r 7 --template '{rev}:{node}\n'
407 407 7:ef0ef43d49e79e81ddafdc7997401ba0041efc82
408 408 $ hg log -r 2 --template '{rev}:{node}\n'
409 409 2:5c095ad7e90f871700f02dd1fa5012cb4498a2d4
410 410
411 411 $ hg log --debug -r tip
412 412 changeset: 13:7a4785234d87ec1aa420ed6b11afe40fa73e12a9
413 413 tag: tip
414 414 phase: draft
415 415 parent: 12:b592ea63bb0c19a6c5c44685ee29a2284f9f1b8f
416 416 parent: -1:0000000000000000000000000000000000000000
417 417 manifest: 13:dc313617b8c32457c0d589e0dbbedfe71f3cd637
418 418 user: foo
419 419 date: Thu Jan 01 00:00:00 1970 +0000
420 420 files+: b
421 421 files-: a
422 422 extra: branch=default
423 423 extra: intermediate-source=ef0ef43d49e79e81ddafdc7997401ba0041efc82
424 424 extra: source=5c095ad7e90f871700f02dd1fa5012cb4498a2d4
425 425 description:
426 426 2
427 427
428 428
429 429 Disallow grafting an already grafted cset onto its original branch
430 430 $ hg up -q 6
431 431 $ hg graft 7
432 432 skipping already grafted revision 7:ef0ef43d49e7 (was grafted from 2:5c095ad7e90f)
433 433 [255]
434 434
435 435 $ hg pdiff --config extensions.extdiff= --patch -r 2 -r 13
436 436 --- */hg-5c095ad7e90f.patch * (glob)
437 437 +++ */hg-7a4785234d87.patch * (glob)
438 438 @@ -1,18 +1,18 @@
439 439 # HG changeset patch
440 440 -# User test
441 441 +# User foo
442 442 # Date 0 0
443 443 # Thu Jan 01 00:00:00 1970 +0000
444 444 -# Node ID 5c095ad7e90f871700f02dd1fa5012cb4498a2d4
445 445 -# Parent 5d205f8b35b66bc36375c9534ffd3237730e8f04
446 446 +# Node ID 7a4785234d87ec1aa420ed6b11afe40fa73e12a9
447 447 +# Parent b592ea63bb0c19a6c5c44685ee29a2284f9f1b8f
448 448 2
449 449
450 450 -diff -r 5d205f8b35b6 -r 5c095ad7e90f a
451 451 +diff -r b592ea63bb0c -r 7a4785234d87 a
452 452 --- a/a Thu Jan 01 00:00:00 1970 +0000
453 453 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
454 454 @@ -1,1 +0,0 @@
455 455 --b
456 456 -diff -r 5d205f8b35b6 -r 5c095ad7e90f b
457 457 +-a
458 458 +diff -r b592ea63bb0c -r 7a4785234d87 b
459 459 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
460 460 +++ b/b Thu Jan 01 00:00:00 1970 +0000
461 461 @@ -0,0 +1,1 @@
462 462 -+b
463 463 ++a
464 464 [1]
465 465
466 466 $ hg pdiff --config extensions.extdiff= --patch -r 2 -r 13 -X .
467 467 --- */hg-5c095ad7e90f.patch * (glob)
468 468 +++ */hg-7a4785234d87.patch * (glob)
469 469 @@ -1,8 +1,8 @@
470 470 # HG changeset patch
471 471 -# User test
472 472 +# User foo
473 473 # Date 0 0
474 474 # Thu Jan 01 00:00:00 1970 +0000
475 475 -# Node ID 5c095ad7e90f871700f02dd1fa5012cb4498a2d4
476 476 -# Parent 5d205f8b35b66bc36375c9534ffd3237730e8f04
477 477 +# Node ID 7a4785234d87ec1aa420ed6b11afe40fa73e12a9
478 478 +# Parent b592ea63bb0c19a6c5c44685ee29a2284f9f1b8f
479 479 2
480 480
481 481 [1]
482 482
483 483 Disallow grafting already grafted csets with the same origin onto each other
484 484 $ hg up -q 13
485 485 $ hg graft 2
486 486 skipping revision 2:5c095ad7e90f (already grafted to 13:7a4785234d87)
487 487 [255]
488 488 $ hg graft 7
489 489 skipping already grafted revision 7:ef0ef43d49e7 (13:7a4785234d87 also has origin 2:5c095ad7e90f)
490 490 [255]
491 491
492 492 $ hg up -q 7
493 493 $ hg graft 2
494 494 skipping revision 2:5c095ad7e90f (already grafted to 7:ef0ef43d49e7)
495 495 [255]
496 496 $ hg graft tip
497 497 skipping already grafted revision 13:7a4785234d87 (7:ef0ef43d49e7 also has origin 2:5c095ad7e90f)
498 498 [255]
499 499
500 500 Graft with --log
501 501
502 502 $ hg up -Cq 1
503 503 $ hg graft 3 --log -u foo
504 504 grafting 3:4c60f11aa304 "3"
505 505 warning: can't find ancestor for 'c' copied from 'b'!
506 506 $ hg log --template '{rev}:{node|short} {parents} {desc}\n' -r tip
507 507 14:0c921c65ef1e 1:5d205f8b35b6 3
508 508 (grafted from 4c60f11aa304a54ae1c199feb94e7fc771e51ed8)
509 509
510 510 Resolve conflicted graft
511 511 $ hg up -q 0
512 512 $ echo b > a
513 513 $ hg ci -m 8
514 514 created new head
515 515 $ echo c > a
516 516 $ hg ci -m 9
517 517 $ hg graft 1 --tool internal:fail
518 518 grafting 1:5d205f8b35b6 "1"
519 519 abort: unresolved conflicts, can't continue
520 520 (use 'hg resolve' and 'hg graft --continue')
521 521 [255]
522 522 $ hg resolve --all
523 523 merging a
524 524 warning: conflicts while merging a! (edit, then use 'hg resolve --mark')
525 525 [1]
526 526 $ cat a
527 527 <<<<<<< local: aaa4406d4f0a - test: 9
528 528 c
529 529 =======
530 530 b
531 531 >>>>>>> graft: 5d205f8b35b6 - bar: 1
532 532 $ echo b > a
533 533 $ hg resolve -m a
534 534 (no more unresolved files)
535 535 continue: hg graft --continue
536 536 $ hg graft -c
537 537 grafting 1:5d205f8b35b6 "1"
538 538 $ hg export tip --git
539 539 # HG changeset patch
540 540 # User bar
541 541 # Date 0 0
542 542 # Thu Jan 01 00:00:00 1970 +0000
543 543 # Node ID f67661df0c4804d301f064f332b57e7d5ddaf2be
544 544 # Parent aaa4406d4f0ae9befd6e58c82ec63706460cbca6
545 545 1
546 546
547 547 diff --git a/a b/a
548 548 --- a/a
549 549 +++ b/a
550 550 @@ -1,1 +1,1 @@
551 551 -c
552 552 +b
553 553
554 554 Resolve conflicted graft with rename
555 555 $ echo c > a
556 556 $ hg ci -m 10
557 557 $ hg graft 2 --tool internal:fail
558 558 grafting 2:5c095ad7e90f "2"
559 559 abort: unresolved conflicts, can't continue
560 560 (use 'hg resolve' and 'hg graft --continue')
561 561 [255]
562 562 $ hg resolve --all
563 563 merging a and b to b
564 564 (no more unresolved files)
565 565 continue: hg graft --continue
566 566 $ hg graft -c
567 567 grafting 2:5c095ad7e90f "2"
568 568 $ hg export tip --git
569 569 # HG changeset patch
570 570 # User test
571 571 # Date 0 0
572 572 # Thu Jan 01 00:00:00 1970 +0000
573 573 # Node ID 9627f653b421c61fc1ea4c4e366745070fa3d2bc
574 574 # Parent ee295f490a40b97f3d18dd4c4f1c8936c233b612
575 575 2
576 576
577 577 diff --git a/a b/b
578 578 rename from a
579 579 rename to b
580 580
581 581 Test simple origin(), with and without args
582 582 $ hg log -r 'origin()'
583 583 changeset: 1:5d205f8b35b6
584 584 user: bar
585 585 date: Thu Jan 01 00:00:00 1970 +0000
586 586 summary: 1
587 587
588 588 changeset: 2:5c095ad7e90f
589 589 user: test
590 590 date: Thu Jan 01 00:00:00 1970 +0000
591 591 summary: 2
592 592
593 593 changeset: 3:4c60f11aa304
594 594 user: baz
595 595 date: Thu Jan 01 00:00:00 1970 +0000
596 596 summary: 3
597 597
598 598 changeset: 4:9c233e8e184d
599 599 user: test
600 600 date: Thu Jan 01 00:00:00 1970 +0000
601 601 summary: 4
602 602
603 603 changeset: 5:97f8bfe72746
604 604 branch: stable
605 605 parent: 3:4c60f11aa304
606 606 user: test
607 607 date: Thu Jan 01 00:00:00 1970 +0000
608 608 summary: 5
609 609
610 610 $ hg log -r 'origin(7)'
611 611 changeset: 2:5c095ad7e90f
612 612 user: test
613 613 date: Thu Jan 01 00:00:00 1970 +0000
614 614 summary: 2
615 615
616 616 Now transplant a graft to test following through copies
617 617 $ hg up -q 0
618 618 $ hg branch -q dev
619 619 $ hg ci -qm "dev branch"
620 620 $ hg --config extensions.transplant= transplant -q 7
621 621 $ hg log -r 'origin(.)'
622 622 changeset: 2:5c095ad7e90f
623 623 user: test
624 624 date: Thu Jan 01 00:00:00 1970 +0000
625 625 summary: 2
626 626
627 627 Test that the graft and transplant markers in extra are converted, allowing
628 628 origin() to still work. Note that these recheck the immediately preceeding two
629 629 tests.
630 630 $ hg --quiet --config extensions.convert= --config convert.hg.saverev=True convert . ../converted
631 631
632 632 The graft case
633 633 $ hg -R ../converted log -r 7 --template "{rev}: {node}\n{join(extras, '\n')}\n"
634 634 7: 7ae846e9111fc8f57745634250c7b9ac0a60689b
635 635 branch=default
636 636 convert_revision=ef0ef43d49e79e81ddafdc7997401ba0041efc82
637 637 source=e0213322b2c1a5d5d236c74e79666441bee67a7d
638 638 $ hg -R ../converted log -r 'origin(7)'
639 639 changeset: 2:e0213322b2c1
640 640 user: test
641 641 date: Thu Jan 01 00:00:00 1970 +0000
642 642 summary: 2
643 643
644 644 Test that template correctly expands more than one 'extra' (issue4362), and that
645 645 'intermediate-source' is converted.
646 646 $ hg -R ../converted log -r 13 --template "{extras % ' Extra: {extra}\n'}"
647 647 Extra: branch=default
648 648 Extra: convert_revision=7a4785234d87ec1aa420ed6b11afe40fa73e12a9
649 649 Extra: intermediate-source=7ae846e9111fc8f57745634250c7b9ac0a60689b
650 650 Extra: source=e0213322b2c1a5d5d236c74e79666441bee67a7d
651 651
652 652 The transplant case
653 653 $ hg -R ../converted log -r tip --template "{rev}: {node}\n{join(extras, '\n')}\n"
654 654 21: fbb6c5cc81002f2b4b49c9d731404688bcae5ade
655 655 branch=dev
656 656 convert_revision=7e61b508e709a11d28194a5359bc3532d910af21
657 657 transplant_source=z\xe8F\xe9\x11\x1f\xc8\xf5wEcBP\xc7\xb9\xac\n`h\x9b
658 658 $ hg -R ../converted log -r 'origin(tip)'
659 659 changeset: 2:e0213322b2c1
660 660 user: test
661 661 date: Thu Jan 01 00:00:00 1970 +0000
662 662 summary: 2
663 663
664 664
665 665 Test simple destination
666 666 $ hg log -r 'destination()'
667 667 changeset: 7:ef0ef43d49e7
668 668 parent: 0:68795b066622
669 669 user: foo
670 670 date: Thu Jan 01 00:00:00 1970 +0000
671 671 summary: 2
672 672
673 673 changeset: 8:6b9e5368ca4e
674 674 user: bar
675 675 date: Thu Jan 01 00:00:00 1970 +0000
676 676 summary: 1
677 677
678 678 changeset: 9:1905859650ec
679 679 user: test
680 680 date: Thu Jan 01 00:00:00 1970 +0000
681 681 summary: 5
682 682
683 683 changeset: 10:52dc0b4c6907
684 684 user: test
685 685 date: Thu Jan 01 00:00:00 1970 +0000
686 686 summary: 4
687 687
688 688 changeset: 11:882b35362a6b
689 689 user: test
690 690 date: Thu Jan 01 00:00:00 1970 +0000
691 691 summary: 3
692 692
693 693 changeset: 13:7a4785234d87
694 694 user: foo
695 695 date: Thu Jan 01 00:00:00 1970 +0000
696 696 summary: 2
697 697
698 698 changeset: 14:0c921c65ef1e
699 699 parent: 1:5d205f8b35b6
700 700 user: foo
701 701 date: Thu Jan 01 00:00:00 1970 +0000
702 702 summary: 3
703 703
704 704 changeset: 17:f67661df0c48
705 705 user: bar
706 706 date: Thu Jan 01 00:00:00 1970 +0000
707 707 summary: 1
708 708
709 709 changeset: 19:9627f653b421
710 710 user: test
711 711 date: Thu Jan 01 00:00:00 1970 +0000
712 712 summary: 2
713 713
714 714 changeset: 21:7e61b508e709
715 715 branch: dev
716 716 tag: tip
717 717 user: foo
718 718 date: Thu Jan 01 00:00:00 1970 +0000
719 719 summary: 2
720 720
721 721 $ hg log -r 'destination(2)'
722 722 changeset: 7:ef0ef43d49e7
723 723 parent: 0:68795b066622
724 724 user: foo
725 725 date: Thu Jan 01 00:00:00 1970 +0000
726 726 summary: 2
727 727
728 728 changeset: 13:7a4785234d87
729 729 user: foo
730 730 date: Thu Jan 01 00:00:00 1970 +0000
731 731 summary: 2
732 732
733 733 changeset: 19:9627f653b421
734 734 user: test
735 735 date: Thu Jan 01 00:00:00 1970 +0000
736 736 summary: 2
737 737
738 738 changeset: 21:7e61b508e709
739 739 branch: dev
740 740 tag: tip
741 741 user: foo
742 742 date: Thu Jan 01 00:00:00 1970 +0000
743 743 summary: 2
744 744
745 745 Transplants of grafts can find a destination...
746 746 $ hg log -r 'destination(7)'
747 747 changeset: 21:7e61b508e709
748 748 branch: dev
749 749 tag: tip
750 750 user: foo
751 751 date: Thu Jan 01 00:00:00 1970 +0000
752 752 summary: 2
753 753
754 754 ... grafts of grafts unfortunately can't
755 755 $ hg graft -q 13 --debug
756 756 scanning for duplicate grafts
757 757 grafting 13:7a4785234d87 "2"
758 758 all copies found (* = to merge, ! = divergent, % = renamed and deleted):
759 759 src: 'a' -> dst: 'b' *
760 760 checking for directory renames
761 761 resolving manifests
762 762 branchmerge: True, force: True, partial: False
763 763 ancestor: b592ea63bb0c, local: 7e61b508e709+, remote: 7a4785234d87
764 764 starting 4 threads for background file closing (?)
765 765 committing files:
766 766 b
767 767 warning: can't find ancestor for 'b' copied from 'a'!
768 768 reusing manifest from p1 (listed files actually unchanged)
769 769 committing changelog
770 770 updating the branch cache
771 771 $ hg log -r 'destination(13)'
772 772 All copies of a cset
773 773 $ hg log -r 'origin(13) or destination(origin(13))'
774 774 changeset: 2:5c095ad7e90f
775 775 user: test
776 776 date: Thu Jan 01 00:00:00 1970 +0000
777 777 summary: 2
778 778
779 779 changeset: 7:ef0ef43d49e7
780 780 parent: 0:68795b066622
781 781 user: foo
782 782 date: Thu Jan 01 00:00:00 1970 +0000
783 783 summary: 2
784 784
785 785 changeset: 13:7a4785234d87
786 786 user: foo
787 787 date: Thu Jan 01 00:00:00 1970 +0000
788 788 summary: 2
789 789
790 790 changeset: 19:9627f653b421
791 791 user: test
792 792 date: Thu Jan 01 00:00:00 1970 +0000
793 793 summary: 2
794 794
795 795 changeset: 21:7e61b508e709
796 796 branch: dev
797 797 user: foo
798 798 date: Thu Jan 01 00:00:00 1970 +0000
799 799 summary: 2
800 800
801 801 changeset: 22:3a4e92d81b97
802 802 branch: dev
803 803 tag: tip
804 804 user: foo
805 805 date: Thu Jan 01 00:00:00 1970 +0000
806 806 summary: 2
807 807
808 808
809 809 graft works on complex revset
810 810
811 811 $ hg graft 'origin(13) or destination(origin(13))'
812 812 skipping ancestor revision 21:7e61b508e709
813 813 skipping ancestor revision 22:3a4e92d81b97
814 814 skipping revision 2:5c095ad7e90f (already grafted to 22:3a4e92d81b97)
815 815 grafting 7:ef0ef43d49e7 "2"
816 816 warning: can't find ancestor for 'b' copied from 'a'!
817 817 grafting 13:7a4785234d87 "2"
818 818 warning: can't find ancestor for 'b' copied from 'a'!
819 819 grafting 19:9627f653b421 "2"
820 820 merging b
821 821 warning: can't find ancestor for 'b' copied from 'a'!
822 822
823 823 graft with --force (still doesn't graft merges)
824 824
825 825 $ hg graft 19 0 6
826 826 skipping ungraftable merge revision 6
827 827 skipping ancestor revision 0:68795b066622
828 828 skipping already grafted revision 19:9627f653b421 (22:3a4e92d81b97 also has origin 2:5c095ad7e90f)
829 829 [255]
830 830 $ hg graft 19 0 6 --force
831 831 skipping ungraftable merge revision 6
832 832 grafting 19:9627f653b421 "2"
833 833 merging b
834 834 warning: can't find ancestor for 'b' copied from 'a'!
835 835 grafting 0:68795b066622 "0"
836 836
837 837 graft --force after backout
838 838
839 839 $ echo abc > a
840 840 $ hg ci -m 28
841 841 $ hg backout 28
842 842 reverting a
843 843 changeset 29:9d95e865b00c backs out changeset 28:cc20d29aec8d
844 844 $ hg graft 28
845 845 skipping ancestor revision 28:cc20d29aec8d
846 846 [255]
847 847 $ hg graft 28 --force
848 848 grafting 28:cc20d29aec8d "28"
849 849 merging a
850 850 $ cat a
851 851 abc
852 852
853 853 graft --continue after --force
854 854
855 855 $ echo def > a
856 856 $ hg ci -m 31
857 857 $ hg graft 28 --force --tool internal:fail
858 858 grafting 28:cc20d29aec8d "28"
859 859 abort: unresolved conflicts, can't continue
860 860 (use 'hg resolve' and 'hg graft --continue')
861 861 [255]
862 862 $ hg resolve --all
863 863 merging a
864 864 warning: conflicts while merging a! (edit, then use 'hg resolve --mark')
865 865 [1]
866 866 $ echo abc > a
867 867 $ hg resolve -m a
868 868 (no more unresolved files)
869 869 continue: hg graft --continue
870 870 $ hg graft -c
871 871 grafting 28:cc20d29aec8d "28"
872 872 $ cat a
873 873 abc
874 874
875 875 Continue testing same origin policy, using revision numbers from test above
876 876 but do some destructive editing of the repo:
877 877
878 878 $ hg up -qC 7
879 879 $ hg tag -l -r 13 tmp
880 880 $ hg --config extensions.strip= strip 2
881 881 saved backup bundle to $TESTTMP/a/.hg/strip-backup/5c095ad7e90f-d323a1e4-backup.hg
882 882 $ hg graft tmp
883 883 skipping already grafted revision 8:7a4785234d87 (2:ef0ef43d49e7 also has unknown origin 5c095ad7e90f)
884 884 [255]
885 885
886 886 Empty graft
887 887
888 888 $ hg up -qr 26
889 889 $ hg tag -f something
890 890 $ hg graft -qr 27
891 891 $ hg graft -f 27
892 892 grafting 27:17d42b8f5d50 "28"
893 893 note: graft of 27:17d42b8f5d50 created no changes to commit
894 894
895 895 $ cd ..
896 896
897 897 Graft to duplicate a commit
898 898
899 899 $ hg init graftsibling
900 900 $ cd graftsibling
901 901 $ touch a
902 902 $ hg commit -qAm a
903 903 $ touch b
904 904 $ hg commit -qAm b
905 905 $ hg log -G -T '{rev}\n'
906 906 @ 1
907 907 |
908 908 o 0
909 909
910 910 $ hg up -q 0
911 911 $ hg graft -r 1
912 912 grafting 1:0e067c57feba "b" (tip)
913 913 $ hg log -G -T '{rev}\n'
914 914 @ 2
915 915 |
916 916 | o 1
917 917 |/
918 918 o 0
919 919
920 920 Graft to duplicate a commit twice
921 921
922 922 $ hg up -q 0
923 923 $ hg graft -r 2
924 924 grafting 2:044ec77f6389 "b" (tip)
925 925 $ hg log -G -T '{rev}\n'
926 926 @ 3
927 927 |
928 928 | o 2
929 929 |/
930 930 | o 1
931 931 |/
932 932 o 0
933 933
934 934 Graft from behind a move or rename
935 935 ==================================
936 936
937 937 NOTE: This is affected by issue5343, and will need updating when it's fixed
938 938
939 939 Consider this topology for a regular graft:
940 940
941 941 o c1
942 942 |
943 943 | o c2
944 944 | |
945 945 | o ca # stands for "common ancestor"
946 946 |/
947 947 o cta # stands for "common topological ancestor"
948 948
949 949 Note that in issue5343, ca==cta.
950 950
951 951 The following table shows the possible cases. Here, "x->y" and, equivalently,
952 952 "y<-x", where x is an ancestor of y, means that some copy happened from x to y.
953 953
954 954 name | c1<-cta | cta<->ca | ca->c2
955 955 A.0 | | |
956 956 A.1 | X | |
957 957 A.2 | | X |
958 958 A.3 | | | X
959 959 A.4 | X | X |
960 960 A.5 | X | | X
961 961 A.6 | | X | X
962 962 A.7 | X | X | X
963 963
964 964 A.0 is trivial, and doesn't need copy tracking.
965 965 For A.1, a forward rename is recorded in the c1 pass, to be followed later.
966 966 In A.2, the rename is recorded in the c2 pass and followed backwards.
967 967 A.3 is recorded in the c2 pass as a forward rename to be duplicated on target.
968 968 In A.4, both passes of checkcopies record incomplete renames, which are
969 969 then joined in mergecopies to record a rename to be followed.
970 970 In A.5 and A.7, the c1 pass records an incomplete rename, while the c2 pass
971 971 records an incomplete divergence. The incomplete rename is then joined to the
972 972 appropriate side of the incomplete divergence, and the result is recorded as a
973 973 divergence. The code doesn't distinguish at all between these two cases, since
974 974 the end result of them is the same: an incomplete divergence joined with an
975 975 incomplete rename into a divergence.
976 976 Finally, A.6 records a divergence entirely in the c2 pass.
977 977
978 978 A.4 has a degenerate case a<-b<-a->a, where checkcopies isn't needed at all.
979 979 A.5 has a special case a<-b<-b->a, which is treated like a<-b->a in a merge.
980 980 A.5 has issue5343 as a special case.
981 981 A.6 has a special case a<-a<-b->a. Here, checkcopies will find a spurious
982 982 incomplete divergence, which is in fact complete. This is handled later in
983 983 mergecopies.
984 984 A.7 has 4 special cases: a<-b<-a->b (the "ping-pong" case), a<-b<-c->b,
985 985 a<-b<-a->c and a<-b<-c->a. Of these, only the "ping-pong" case is interesting,
986 986 the others are fairly trivial (a<-b<-c->b and a<-b<-a->c proceed like the base
987 987 case, a<-b<-c->a is treated the same as a<-b<-b->a).
988 988
989 989 f5a therefore tests the "ping-pong" rename case, where a file is renamed to the
990 990 same name on both branches, then the rename is backed out on one branch, and
991 991 the backout is grafted to the other branch. This creates a challenging rename
992 992 sequence of a<-b<-a->b in the graft target, topological CA, graft CA and graft
993 993 source, respectively. Since rename detection will run on the c1 side for such a
994 994 sequence (as for technical reasons, we split the c1 and c2 sides not at the
995 995 graft CA, but rather at the topological CA), it will pick up a false rename,
996 996 and cause a spurious merge conflict. This false rename is always exactly the
997 997 reverse of the true rename that would be detected on the c2 side, so we can
998 998 correct for it by detecting this condition and reversing as necessary.
999 999
1000 1000 First, set up the repository with commits to be grafted
1001 1001
1002 1002 $ hg init ../graftmove
1003 1003 $ cd ../graftmove
1004 1004 $ echo c1a > f1a
1005 1005 $ echo c2a > f2a
1006 1006 $ echo c3a > f3a
1007 1007 $ echo c4a > f4a
1008 1008 $ echo c5a > f5a
1009 1009 $ hg ci -qAm A0
1010 1010 $ hg mv f1a f1b
1011 1011 $ hg mv f3a f3b
1012 1012 $ hg mv f5a f5b
1013 1013 $ hg ci -qAm B0
1014 1014 $ echo c1c > f1b
1015 1015 $ hg mv f2a f2c
1016 1016 $ hg mv f5b f5a
1017 1017 $ echo c5c > f5a
1018 1018 $ hg ci -qAm C0
1019 1019 $ hg mv f3b f3d
1020 1020 $ echo c4d > f4a
1021 1021 $ hg ci -qAm D0
1022 1022 $ hg log -G
1023 1023 @ changeset: 3:b69f5839d2d9
1024 1024 | tag: tip
1025 1025 | user: test
1026 1026 | date: Thu Jan 01 00:00:00 1970 +0000
1027 1027 | summary: D0
1028 1028 |
1029 1029 o changeset: 2:f58c7e2b28fa
1030 1030 | user: test
1031 1031 | date: Thu Jan 01 00:00:00 1970 +0000
1032 1032 | summary: C0
1033 1033 |
1034 1034 o changeset: 1:3d7bba921b5d
1035 1035 | user: test
1036 1036 | date: Thu Jan 01 00:00:00 1970 +0000
1037 1037 | summary: B0
1038 1038 |
1039 1039 o changeset: 0:11f7a1b56675
1040 1040 user: test
1041 1041 date: Thu Jan 01 00:00:00 1970 +0000
1042 1042 summary: A0
1043 1043
1044 1044
1045 1045 Test the cases A.2 (f1x), A.3 (f2x) and a special case of A.6 (f5x) where the
1046 1046 two renames actually converge to the same name (thus no actual divergence).
1047 1047
1048 1048 $ hg up -q 'desc("A0")'
1049 1049 $ HGEDITOR="echo C1 >" hg graft -r 'desc("C0")' --edit
1050 1050 grafting 2:f58c7e2b28fa "C0"
1051 1051 merging f1a and f1b to f1a
1052 1052 merging f5a
1053 1053 warning: can't find ancestor for 'f5a' copied from 'f5b'!
1054 1054 $ hg status --change .
1055 1055 M f1a
1056 1056 M f5a
1057 1057 A f2c
1058 1058 R f2a
1059 1059 $ hg cat f1a
1060 1060 c1c
1061 1061 $ hg cat f1b
1062 1062 f1b: no such file in rev c9763722f9bd
1063 1063 [1]
1064 1064
1065 1065 Test the cases A.0 (f4x) and A.6 (f3x)
1066 1066
1067 1067 $ HGEDITOR="echo D1 >" hg graft -r 'desc("D0")' --edit
1068 1068 grafting 3:b69f5839d2d9 "D0"
1069 1069 note: possible conflict - f3b was renamed multiple times to:
1070 1070 f3a
1071 1071 f3d
1072 1072 warning: can't find ancestor for 'f3d' copied from 'f3b'!
1073 1073
1074 1074 Set up the repository for some further tests
1075 1075
1076 1076 $ hg up -q "min(desc("A0"))"
1077 1077 $ hg mv f1a f1e
1078 1078 $ echo c2e > f2a
1079 1079 $ hg mv f3a f3e
1080 1080 $ hg mv f4a f4e
1081 1081 $ hg mv f5a f5b
1082 1082 $ hg ci -qAm "E0"
1083 1083 $ hg up -q "min(desc("A0"))"
1084 1084 $ hg cp f1a f1f
1085 1085 $ hg ci -qAm "F0"
1086 1086 $ hg up -q "min(desc("A0"))"
1087 1087 $ hg cp f1a f1g
1088 1088 $ echo c1g > f1g
1089 1089 $ hg ci -qAm "G0"
1090 1090 $ hg log -G
1091 1091 @ changeset: 8:ba67f08fb15a
1092 1092 | tag: tip
1093 1093 | parent: 0:11f7a1b56675
1094 1094 | user: test
1095 1095 | date: Thu Jan 01 00:00:00 1970 +0000
1096 1096 | summary: G0
1097 1097 |
1098 1098 | o changeset: 7:d376ab0d7fda
1099 1099 |/ parent: 0:11f7a1b56675
1100 1100 | user: test
1101 1101 | date: Thu Jan 01 00:00:00 1970 +0000
1102 1102 | summary: F0
1103 1103 |
1104 1104 | o changeset: 6:6bd1736cab86
1105 1105 |/ parent: 0:11f7a1b56675
1106 1106 | user: test
1107 1107 | date: Thu Jan 01 00:00:00 1970 +0000
1108 1108 | summary: E0
1109 1109 |
1110 1110 | o changeset: 5:560daee679da
1111 1111 | | user: test
1112 1112 | | date: Thu Jan 01 00:00:00 1970 +0000
1113 1113 | | summary: D1
1114 1114 | |
1115 1115 | o changeset: 4:c9763722f9bd
1116 1116 |/ parent: 0:11f7a1b56675
1117 1117 | user: test
1118 1118 | date: Thu Jan 01 00:00:00 1970 +0000
1119 1119 | summary: C1
1120 1120 |
1121 1121 | o changeset: 3:b69f5839d2d9
1122 1122 | | user: test
1123 1123 | | date: Thu Jan 01 00:00:00 1970 +0000
1124 1124 | | summary: D0
1125 1125 | |
1126 1126 | o changeset: 2:f58c7e2b28fa
1127 1127 | | user: test
1128 1128 | | date: Thu Jan 01 00:00:00 1970 +0000
1129 1129 | | summary: C0
1130 1130 | |
1131 1131 | o changeset: 1:3d7bba921b5d
1132 1132 |/ user: test
1133 1133 | date: Thu Jan 01 00:00:00 1970 +0000
1134 1134 | summary: B0
1135 1135 |
1136 1136 o changeset: 0:11f7a1b56675
1137 1137 user: test
1138 1138 date: Thu Jan 01 00:00:00 1970 +0000
1139 1139 summary: A0
1140 1140
1141 1141
1142 1142 Test the cases A.4 (f1x), the "ping-pong" special case of A.7 (f5x),
1143 1143 and A.3 with a local content change to be preserved (f2x).
1144 1144
1145 1145 $ hg up -q "desc("E0")"
1146 1146 $ HGEDITOR="echo C2 >" hg graft -r 'desc("C0")' --edit
1147 1147 grafting 2:f58c7e2b28fa "C0"
1148 1148 merging f1e and f1b to f1e
1149 1149 merging f2a and f2c to f2c
1150 1150
1151 1151 Test the cases A.1 (f4x) and A.7 (f3x).
1152 1152
1153 1153 $ HGEDITOR="echo D2 >" hg graft -r 'desc("D0")' --edit
1154 1154 grafting 3:b69f5839d2d9 "D0"
1155 1155 note: possible conflict - f3b was renamed multiple times to:
1156 1156 f3d
1157 1157 f3e
1158 1158 merging f4e and f4a to f4e
1159 1159 warning: can't find ancestor for 'f3d' copied from 'f3b'!
1160 1160
1161 1161 $ hg cat f2c
1162 1162 c2e
1163 1163
1164 1164 Test the case A.5 (move case, f1x).
1165 1165
1166 1166 $ hg up -q "desc("C0")"
1167 1167 BROKEN: Shouldn't get the warning about missing ancestor
1168 1168 $ HGEDITOR="echo E1 >" hg graft -r 'desc("E0")' --edit
1169 1169 grafting 6:6bd1736cab86 "E0"
1170 1170 note: possible conflict - f1a was renamed multiple times to:
1171 1171 f1b
1172 1172 f1e
1173 1173 note: possible conflict - f3a was renamed multiple times to:
1174 1174 f3b
1175 1175 f3e
1176 1176 merging f2c and f2a to f2c
1177 1177 merging f5a and f5b to f5b
1178 1178 warning: can't find ancestor for 'f1e' copied from 'f1a'!
1179 1179 warning: can't find ancestor for 'f3e' copied from 'f3a'!
1180 1180 $ cat f1e
1181 1181 c1a
1182 1182
1183 1183 Test the case A.5 (copy case, f1x).
1184 1184
1185 1185 $ hg up -q "desc("C0")"
1186 1186 BROKEN: Shouldn't get the warning about missing ancestor
1187 1187 $ HGEDITOR="echo F1 >" hg graft -r 'desc("F0")' --edit
1188 1188 grafting 7:d376ab0d7fda "F0"
1189 1189 warning: can't find ancestor for 'f1f' copied from 'f1a'!
1190 1190 BROKEN: f1f should be marked a copy from f1b
1191 1191 $ hg st --copies --change .
1192 1192 A f1f
1193 1193 BROKEN: f1f should have the new content from f1b (i.e. "c1c")
1194 1194 $ cat f1f
1195 1195 c1a
1196 1196
1197 1197 Test the case A.5 (copy+modify case, f1x).
1198 1198
1199 1199 $ hg up -q "desc("C0")"
1200 1200 BROKEN: We should get a merge conflict from the 3-way merge between f1b in C0
1201 1201 (content "c1c") and f1g in G0 (content "c1g") with f1a in A0 as base (content
1202 1202 "c1a")
1203 1203 $ HGEDITOR="echo G1 >" hg graft -r 'desc("G0")' --edit
1204 1204 grafting 8:ba67f08fb15a "G0"
1205 1205 warning: can't find ancestor for 'f1g' copied from 'f1a'!
1206 1206
1207 1207 Check the results of the grafts tested
1208 1208
1209 1209 $ hg log -CGv --patch --git
1210 1210 @ changeset: 13:ef3adf6c20a4
1211 1211 | tag: tip
1212 1212 | parent: 2:f58c7e2b28fa
1213 1213 | user: test
1214 1214 | date: Thu Jan 01 00:00:00 1970 +0000
1215 1215 | files: f1g
1216 1216 | description:
1217 1217 | G1
1218 1218 |
1219 1219 |
1220 1220 | diff --git a/f1g b/f1g
1221 1221 | new file mode 100644
1222 1222 | --- /dev/null
1223 1223 | +++ b/f1g
1224 1224 | @@ -0,0 +1,1 @@
1225 1225 | +c1g
1226 1226 |
1227 1227 | o changeset: 12:b5542d755b54
1228 1228 |/ parent: 2:f58c7e2b28fa
1229 1229 | user: test
1230 1230 | date: Thu Jan 01 00:00:00 1970 +0000
1231 1231 | files: f1f
1232 1232 | description:
1233 1233 | F1
1234 1234 |
1235 1235 |
1236 1236 | diff --git a/f1f b/f1f
1237 1237 | new file mode 100644
1238 1238 | --- /dev/null
1239 1239 | +++ b/f1f
1240 1240 | @@ -0,0 +1,1 @@
1241 1241 | +c1a
1242 1242 |
1243 1243 | o changeset: 11:f8a162271246
1244 1244 |/ parent: 2:f58c7e2b28fa
1245 1245 | user: test
1246 1246 | date: Thu Jan 01 00:00:00 1970 +0000
1247 1247 | files: f1e f2c f3e f4a f4e f5a f5b
1248 1248 | copies: f4e (f4a) f5b (f5a)
1249 1249 | description:
1250 1250 | E1
1251 1251 |
1252 1252 |
1253 1253 | diff --git a/f1e b/f1e
1254 1254 | new file mode 100644
1255 1255 | --- /dev/null
1256 1256 | +++ b/f1e
1257 1257 | @@ -0,0 +1,1 @@
1258 1258 | +c1a
1259 1259 | diff --git a/f2c b/f2c
1260 1260 | --- a/f2c
1261 1261 | +++ b/f2c
1262 1262 | @@ -1,1 +1,1 @@
1263 1263 | -c2a
1264 1264 | +c2e
1265 1265 | diff --git a/f3e b/f3e
1266 1266 | new file mode 100644
1267 1267 | --- /dev/null
1268 1268 | +++ b/f3e
1269 1269 | @@ -0,0 +1,1 @@
1270 1270 | +c3a
1271 1271 | diff --git a/f4a b/f4e
1272 1272 | rename from f4a
1273 1273 | rename to f4e
1274 1274 | diff --git a/f5a b/f5b
1275 1275 | rename from f5a
1276 1276 | rename to f5b
1277 1277 |
1278 1278 | o changeset: 10:93ee502e8b0a
1279 1279 | | user: test
1280 1280 | | date: Thu Jan 01 00:00:00 1970 +0000
1281 1281 | | files: f3d f4e
1282 1282 | | description:
1283 1283 | | D2
1284 1284 | |
1285 1285 | |
1286 1286 | | diff --git a/f3d b/f3d
1287 1287 | | new file mode 100644
1288 1288 | | --- /dev/null
1289 1289 | | +++ b/f3d
1290 1290 | | @@ -0,0 +1,1 @@
1291 1291 | | +c3a
1292 1292 | | diff --git a/f4e b/f4e
1293 1293 | | --- a/f4e
1294 1294 | | +++ b/f4e
1295 1295 | | @@ -1,1 +1,1 @@
1296 1296 | | -c4a
1297 1297 | | +c4d
1298 1298 | |
1299 1299 | o changeset: 9:539cf145f496
1300 1300 | | parent: 6:6bd1736cab86
1301 1301 | | user: test
1302 1302 | | date: Thu Jan 01 00:00:00 1970 +0000
1303 1303 | | files: f1e f2a f2c f5a f5b
1304 1304 | | copies: f2c (f2a) f5a (f5b)
1305 1305 | | description:
1306 1306 | | C2
1307 1307 | |
1308 1308 | |
1309 1309 | | diff --git a/f1e b/f1e
1310 1310 | | --- a/f1e
1311 1311 | | +++ b/f1e
1312 1312 | | @@ -1,1 +1,1 @@
1313 1313 | | -c1a
1314 1314 | | +c1c
1315 1315 | | diff --git a/f2a b/f2c
1316 1316 | | rename from f2a
1317 1317 | | rename to f2c
1318 1318 | | diff --git a/f5b b/f5a
1319 1319 | | rename from f5b
1320 1320 | | rename to f5a
1321 1321 | | --- a/f5b
1322 1322 | | +++ b/f5a
1323 1323 | | @@ -1,1 +1,1 @@
1324 1324 | | -c5a
1325 1325 | | +c5c
1326 1326 | |
1327 1327 | | o changeset: 8:ba67f08fb15a
1328 1328 | | | parent: 0:11f7a1b56675
1329 1329 | | | user: test
1330 1330 | | | date: Thu Jan 01 00:00:00 1970 +0000
1331 1331 | | | files: f1g
1332 1332 | | | copies: f1g (f1a)
1333 1333 | | | description:
1334 1334 | | | G0
1335 1335 | | |
1336 1336 | | |
1337 1337 | | | diff --git a/f1a b/f1g
1338 1338 | | | copy from f1a
1339 1339 | | | copy to f1g
1340 1340 | | | --- a/f1a
1341 1341 | | | +++ b/f1g
1342 1342 | | | @@ -1,1 +1,1 @@
1343 1343 | | | -c1a
1344 1344 | | | +c1g
1345 1345 | | |
1346 1346 | | | o changeset: 7:d376ab0d7fda
1347 1347 | | |/ parent: 0:11f7a1b56675
1348 1348 | | | user: test
1349 1349 | | | date: Thu Jan 01 00:00:00 1970 +0000
1350 1350 | | | files: f1f
1351 1351 | | | copies: f1f (f1a)
1352 1352 | | | description:
1353 1353 | | | F0
1354 1354 | | |
1355 1355 | | |
1356 1356 | | | diff --git a/f1a b/f1f
1357 1357 | | | copy from f1a
1358 1358 | | | copy to f1f
1359 1359 | | |
1360 1360 | o | changeset: 6:6bd1736cab86
1361 1361 | |/ parent: 0:11f7a1b56675
1362 1362 | | user: test
1363 1363 | | date: Thu Jan 01 00:00:00 1970 +0000
1364 1364 | | files: f1a f1e f2a f3a f3e f4a f4e f5a f5b
1365 1365 | | copies: f1e (f1a) f3e (f3a) f4e (f4a) f5b (f5a)
1366 1366 | | description:
1367 1367 | | E0
1368 1368 | |
1369 1369 | |
1370 1370 | | diff --git a/f1a b/f1e
1371 1371 | | rename from f1a
1372 1372 | | rename to f1e
1373 1373 | | diff --git a/f2a b/f2a
1374 1374 | | --- a/f2a
1375 1375 | | +++ b/f2a
1376 1376 | | @@ -1,1 +1,1 @@
1377 1377 | | -c2a
1378 1378 | | +c2e
1379 1379 | | diff --git a/f3a b/f3e
1380 1380 | | rename from f3a
1381 1381 | | rename to f3e
1382 1382 | | diff --git a/f4a b/f4e
1383 1383 | | rename from f4a
1384 1384 | | rename to f4e
1385 1385 | | diff --git a/f5a b/f5b
1386 1386 | | rename from f5a
1387 1387 | | rename to f5b
1388 1388 | |
1389 1389 | | o changeset: 5:560daee679da
1390 1390 | | | user: test
1391 1391 | | | date: Thu Jan 01 00:00:00 1970 +0000
1392 1392 | | | files: f3d f4a
1393 1393 | | | description:
1394 1394 | | | D1
1395 1395 | | |
1396 1396 | | |
1397 1397 | | | diff --git a/f3d b/f3d
1398 1398 | | | new file mode 100644
1399 1399 | | | --- /dev/null
1400 1400 | | | +++ b/f3d
1401 1401 | | | @@ -0,0 +1,1 @@
1402 1402 | | | +c3a
1403 1403 | | | diff --git a/f4a b/f4a
1404 1404 | | | --- a/f4a
1405 1405 | | | +++ b/f4a
1406 1406 | | | @@ -1,1 +1,1 @@
1407 1407 | | | -c4a
1408 1408 | | | +c4d
1409 1409 | | |
1410 1410 | | o changeset: 4:c9763722f9bd
1411 1411 | |/ parent: 0:11f7a1b56675
1412 1412 | | user: test
1413 1413 | | date: Thu Jan 01 00:00:00 1970 +0000
1414 1414 | | files: f1a f2a f2c f5a
1415 1415 | | copies: f2c (f2a)
1416 1416 | | description:
1417 1417 | | C1
1418 1418 | |
1419 1419 | |
1420 1420 | | diff --git a/f1a b/f1a
1421 1421 | | --- a/f1a
1422 1422 | | +++ b/f1a
1423 1423 | | @@ -1,1 +1,1 @@
1424 1424 | | -c1a
1425 1425 | | +c1c
1426 1426 | | diff --git a/f2a b/f2c
1427 1427 | | rename from f2a
1428 1428 | | rename to f2c
1429 1429 | | diff --git a/f5a b/f5a
1430 1430 | | --- a/f5a
1431 1431 | | +++ b/f5a
1432 1432 | | @@ -1,1 +1,1 @@
1433 1433 | | -c5a
1434 1434 | | +c5c
1435 1435 | |
1436 1436 +---o changeset: 3:b69f5839d2d9
1437 1437 | | user: test
1438 1438 | | date: Thu Jan 01 00:00:00 1970 +0000
1439 1439 | | files: f3b f3d f4a
1440 1440 | | copies: f3d (f3b)
1441 1441 | | description:
1442 1442 | | D0
1443 1443 | |
1444 1444 | |
1445 1445 | | diff --git a/f3b b/f3d
1446 1446 | | rename from f3b
1447 1447 | | rename to f3d
1448 1448 | | diff --git a/f4a b/f4a
1449 1449 | | --- a/f4a
1450 1450 | | +++ b/f4a
1451 1451 | | @@ -1,1 +1,1 @@
1452 1452 | | -c4a
1453 1453 | | +c4d
1454 1454 | |
1455 1455 o | changeset: 2:f58c7e2b28fa
1456 1456 | | user: test
1457 1457 | | date: Thu Jan 01 00:00:00 1970 +0000
1458 1458 | | files: f1b f2a f2c f5a f5b
1459 1459 | | copies: f2c (f2a) f5a (f5b)
1460 1460 | | description:
1461 1461 | | C0
1462 1462 | |
1463 1463 | |
1464 1464 | | diff --git a/f1b b/f1b
1465 1465 | | --- a/f1b
1466 1466 | | +++ b/f1b
1467 1467 | | @@ -1,1 +1,1 @@
1468 1468 | | -c1a
1469 1469 | | +c1c
1470 1470 | | diff --git a/f2a b/f2c
1471 1471 | | rename from f2a
1472 1472 | | rename to f2c
1473 1473 | | diff --git a/f5b b/f5a
1474 1474 | | rename from f5b
1475 1475 | | rename to f5a
1476 1476 | | --- a/f5b
1477 1477 | | +++ b/f5a
1478 1478 | | @@ -1,1 +1,1 @@
1479 1479 | | -c5a
1480 1480 | | +c5c
1481 1481 | |
1482 1482 o | changeset: 1:3d7bba921b5d
1483 1483 |/ user: test
1484 1484 | date: Thu Jan 01 00:00:00 1970 +0000
1485 1485 | files: f1a f1b f3a f3b f5a f5b
1486 1486 | copies: f1b (f1a) f3b (f3a) f5b (f5a)
1487 1487 | description:
1488 1488 | B0
1489 1489 |
1490 1490 |
1491 1491 | diff --git a/f1a b/f1b
1492 1492 | rename from f1a
1493 1493 | rename to f1b
1494 1494 | diff --git a/f3a b/f3b
1495 1495 | rename from f3a
1496 1496 | rename to f3b
1497 1497 | diff --git a/f5a b/f5b
1498 1498 | rename from f5a
1499 1499 | rename to f5b
1500 1500 |
1501 1501 o changeset: 0:11f7a1b56675
1502 1502 user: test
1503 1503 date: Thu Jan 01 00:00:00 1970 +0000
1504 1504 files: f1a f2a f3a f4a f5a
1505 1505 description:
1506 1506 A0
1507 1507
1508 1508
1509 1509 diff --git a/f1a b/f1a
1510 1510 new file mode 100644
1511 1511 --- /dev/null
1512 1512 +++ b/f1a
1513 1513 @@ -0,0 +1,1 @@
1514 1514 +c1a
1515 1515 diff --git a/f2a b/f2a
1516 1516 new file mode 100644
1517 1517 --- /dev/null
1518 1518 +++ b/f2a
1519 1519 @@ -0,0 +1,1 @@
1520 1520 +c2a
1521 1521 diff --git a/f3a b/f3a
1522 1522 new file mode 100644
1523 1523 --- /dev/null
1524 1524 +++ b/f3a
1525 1525 @@ -0,0 +1,1 @@
1526 1526 +c3a
1527 1527 diff --git a/f4a b/f4a
1528 1528 new file mode 100644
1529 1529 --- /dev/null
1530 1530 +++ b/f4a
1531 1531 @@ -0,0 +1,1 @@
1532 1532 +c4a
1533 1533 diff --git a/f5a b/f5a
1534 1534 new file mode 100644
1535 1535 --- /dev/null
1536 1536 +++ b/f5a
1537 1537 @@ -0,0 +1,1 @@
1538 1538 +c5a
1539 1539
1540 1540 Check superfluous filemerge of files renamed in the past but untouched by graft
1541 1541
1542 1542 $ echo a > a
1543 1543 $ hg ci -qAma
1544 1544 $ hg mv a b
1545 1545 $ echo b > b
1546 1546 $ hg ci -qAmb
1547 1547 $ echo c > c
1548 1548 $ hg ci -qAmc
1549 1549 $ hg up -q .~2
1550 1550 $ hg graft tip -qt:fail
1551 1551
1552 1552 $ cd ..
1553 1553
1554 1554 Graft a change into a new file previously grafted into a renamed directory
1555 1555
1556 1556 $ hg init dirmovenewfile
1557 1557 $ cd dirmovenewfile
1558 1558 $ mkdir a
1559 1559 $ echo a > a/a
1560 1560 $ hg ci -qAma
1561 1561 $ echo x > a/x
1562 1562 $ hg ci -qAmx
1563 1563 $ hg up -q 0
1564 1564 $ hg mv -q a b
1565 1565 $ hg ci -qAmb
1566 1566 $ hg graft -q 1 # a/x grafted as b/x, but no copy information recorded
1567 1567 $ hg up -q 1
1568 1568 $ echo y > a/x
1569 1569 $ hg ci -qAmy
1570 1570 $ hg up -q 3
1571 1571 $ hg graft -q 4
1572 1572 $ hg status --change .
1573 1573 M b/x
1574 1574
1575 1575 Prepare for test of skipped changesets and how merges can influence it:
1576 1576
1577 1577 $ hg merge -q -r 1 --tool :local
1578 1578 $ hg ci -m m
1579 1579 $ echo xx >> b/x
1580 1580 $ hg ci -m xx
1581 1581
1582 1582 $ hg log -G -T '{rev} {desc|firstline}'
1583 1583 @ 7 xx
1584 1584 |
1585 1585 o 6 m
1586 1586 |\
1587 1587 | o 5 y
1588 1588 | |
1589 1589 +---o 4 y
1590 1590 | |
1591 1591 | o 3 x
1592 1592 | |
1593 1593 | o 2 b
1594 1594 | |
1595 1595 o | 1 x
1596 1596 |/
1597 1597 o 0 a
1598 1598
1599 1599 Grafting of plain changes correctly detects that 3 and 5 should be skipped:
1600 1600
1601 1601 $ hg up -qCr 4
1602 1602 $ hg graft --tool :local -r 2::5
1603 1603 skipping already grafted revision 3:ca093ca2f1d9 (was grafted from 1:13ec5badbf2a)
1604 1604 skipping already grafted revision 5:43e9eb70dab0 (was grafted from 4:6c9a1289e5f1)
1605 1605 grafting 2:42127f193bcd "b"
1606 1606
1607 1607 Extending the graft range to include a (skipped) merge of 3 will not prevent us from
1608 1608 also detecting that both 3 and 5 should be skipped:
1609 1609
1610 1610 $ hg up -qCr 4
1611 1611 $ hg graft --tool :local -r 2::7
1612 1612 skipping ungraftable merge revision 6
1613 1613 skipping already grafted revision 3:ca093ca2f1d9 (was grafted from 1:13ec5badbf2a)
1614 1614 skipping already grafted revision 5:43e9eb70dab0 (was grafted from 4:6c9a1289e5f1)
1615 1615 grafting 2:42127f193bcd "b"
1616 1616 grafting 7:d3c3f2b38ecc "xx"
1617 1617 note: graft of 7:d3c3f2b38ecc created no changes to commit
1618 1618
1619 1619 $ cd ..
1620 1620
1621 1621 Grafted revision should be warned and skipped only once. (issue6024)
1622 1622
1623 1623 $ mkdir issue6024
1624 1624 $ cd issue6024
1625 1625
1626 1626 $ hg init base
1627 1627 $ cd base
1628 1628 $ touch x
1629 1629 $ hg commit -qAminit
1630 1630 $ echo a > x
1631 1631 $ hg commit -mchange
1632 1632 $ hg update -q 0
1633 1633 $ hg graft -r 1
1634 1634 grafting 1:a0b923c546aa "change" (tip)
1635 1635 $ cd ..
1636 1636
1637 1637 $ hg clone -qr 2 base clone
1638 1638 $ cd clone
1639 1639 $ hg pull -q
1640 1640 $ hg merge -q 2
1641 1641 $ hg commit -mmerge
1642 1642 $ hg update -q 0
1643 1643 $ hg graft -r 1
1644 1644 grafting 1:04fc6d444368 "change"
1645 1645 $ hg update -q 3
1646 1646 $ hg log -G -T '{rev}:{node|shortest} <- {extras.source|shortest}\n'
1647 1647 o 4:4e16 <- a0b9
1648 1648 |
1649 1649 | @ 3:f0ac <-
1650 1650 | |\
1651 1651 +---o 2:a0b9 <-
1652 1652 | |
1653 1653 | o 1:04fc <- a0b9
1654 1654 |/
1655 1655 o 0:7848 <-
1656 1656
1657 1657
1658 1658 the source of rev 4 is an ancestor of the working parent, and was also
1659 1659 grafted as rev 1. it should be stripped from the target revisions only once.
1660 1660
1661 1661 $ hg graft -r 4
1662 1662 skipping already grafted revision 4:4e16bab40c9c (1:04fc6d444368 also has origin 2:a0b923c546aa)
1663 1663 [255]
1664 1664
1665 1665 $ cd ../..
1666 1666
1667 1667 Testing the reading of old format graftstate file with newer mercurial
1668 1668
1669 1669 $ hg init oldgraft
1670 1670 $ cd oldgraft
1671 1671 $ for ch in a b c; do echo foo > $ch; hg add $ch; hg ci -Aqm "added "$ch; done;
1672 1672 $ hg log -GT "{rev}:{node|short} {desc}\n"
1673 1673 @ 2:8be98ac1a569 added c
1674 1674 |
1675 1675 o 1:80e6d2c47cfe added b
1676 1676 |
1677 1677 o 0:f7ad41964313 added a
1678 1678
1679 1679 $ hg up 0
1680 1680 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
1681 1681 $ echo bar > b
1682 1682 $ hg add b
1683 1683 $ hg ci -m "bar to b"
1684 1684 created new head
1685 1685 $ hg graft -r 1 -r 2
1686 1686 grafting 1:80e6d2c47cfe "added b"
1687 1687 merging b
1688 1688 warning: conflicts while merging b! (edit, then use 'hg resolve --mark')
1689 1689 abort: unresolved conflicts, can't continue
1690 1690 (use 'hg resolve' and 'hg graft --continue')
1691 1691 [255]
1692 1692
1693 1693 Writing the nodes in old format to graftstate
1694 1694
1695 1695 $ hg log -r 1 -r 2 -T '{node}\n' > .hg/graftstate
1696 1696 $ echo foo > b
1697 1697 $ hg resolve -m
1698 1698 (no more unresolved files)
1699 1699 continue: hg graft --continue
1700 1700 $ hg graft --continue
1701 1701 grafting 1:80e6d2c47cfe "added b"
1702 1702 grafting 2:8be98ac1a569 "added c"
1703 1703
1704 1704 Testing that --user is preserved during conflicts and value is reused while
1705 1705 running `hg graft --continue`
1706 1706
1707 1707 $ hg log -G
1708 1708 @ changeset: 5:711e9fa999f1
1709 1709 | tag: tip
1710 1710 | user: test
1711 1711 | date: Thu Jan 01 00:00:00 1970 +0000
1712 1712 | summary: added c
1713 1713 |
1714 1714 o changeset: 4:e5ad7353b408
1715 1715 | user: test
1716 1716 | date: Thu Jan 01 00:00:00 1970 +0000
1717 1717 | summary: added b
1718 1718 |
1719 1719 o changeset: 3:9e887f7a939c
1720 1720 | parent: 0:f7ad41964313
1721 1721 | user: test
1722 1722 | date: Thu Jan 01 00:00:00 1970 +0000
1723 1723 | summary: bar to b
1724 1724 |
1725 1725 | o changeset: 2:8be98ac1a569
1726 1726 | | user: test
1727 1727 | | date: Thu Jan 01 00:00:00 1970 +0000
1728 1728 | | summary: added c
1729 1729 | |
1730 1730 | o changeset: 1:80e6d2c47cfe
1731 1731 |/ user: test
1732 1732 | date: Thu Jan 01 00:00:00 1970 +0000
1733 1733 | summary: added b
1734 1734 |
1735 1735 o changeset: 0:f7ad41964313
1736 1736 user: test
1737 1737 date: Thu Jan 01 00:00:00 1970 +0000
1738 1738 summary: added a
1739 1739
1740 1740
1741 1741 $ hg up '.^^'
1742 1742 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
1743 1743
1744 1744 $ hg graft -r 1 -r 2 --user batman
1745 1745 grafting 1:80e6d2c47cfe "added b"
1746 1746 merging b
1747 1747 warning: conflicts while merging b! (edit, then use 'hg resolve --mark')
1748 1748 abort: unresolved conflicts, can't continue
1749 1749 (use 'hg resolve' and 'hg graft --continue')
1750 1750 [255]
1751 1751
1752 1752 $ echo wat > b
1753 1753 $ hg resolve -m
1754 1754 (no more unresolved files)
1755 1755 continue: hg graft --continue
1756 1756
1757 1757 $ hg graft --continue
1758 1758 grafting 1:80e6d2c47cfe "added b"
1759 1759 grafting 2:8be98ac1a569 "added c"
1760 1760
1761 1761 $ hg log -Gr 3::
1762 1762 @ changeset: 7:11a36ffaacf2
1763 1763 | tag: tip
1764 1764 | user: batman
1765 1765 | date: Thu Jan 01 00:00:00 1970 +0000
1766 1766 | summary: added c
1767 1767 |
1768 1768 o changeset: 6:76803afc6511
1769 1769 | parent: 3:9e887f7a939c
1770 1770 | user: batman
1771 1771 | date: Thu Jan 01 00:00:00 1970 +0000
1772 1772 | summary: added b
1773 1773 |
1774 1774 | o changeset: 5:711e9fa999f1
1775 1775 | | user: test
1776 1776 | | date: Thu Jan 01 00:00:00 1970 +0000
1777 1777 | | summary: added c
1778 1778 | |
1779 1779 | o changeset: 4:e5ad7353b408
1780 1780 |/ user: test
1781 1781 | date: Thu Jan 01 00:00:00 1970 +0000
1782 1782 | summary: added b
1783 1783 |
1784 1784 o changeset: 3:9e887f7a939c
1785 1785 | parent: 0:f7ad41964313
1786 1786 ~ user: test
1787 1787 date: Thu Jan 01 00:00:00 1970 +0000
1788 1788 summary: bar to b
1789 1789
1790 1790 Test that --date is preserved and reused in `hg graft --continue`
1791 1791
1792 1792 $ hg up '.^^'
1793 1793 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
1794 1794 $ hg graft -r 1 -r 2 --date '1234560000 120'
1795 1795 grafting 1:80e6d2c47cfe "added b"
1796 1796 merging b
1797 1797 warning: conflicts while merging b! (edit, then use 'hg resolve --mark')
1798 1798 abort: unresolved conflicts, can't continue
1799 1799 (use 'hg resolve' and 'hg graft --continue')
1800 1800 [255]
1801 1801
1802 1802 $ echo foobar > b
1803 1803 $ hg resolve -m
1804 1804 (no more unresolved files)
1805 1805 continue: hg graft --continue
1806 1806 $ hg graft --continue
1807 1807 grafting 1:80e6d2c47cfe "added b"
1808 1808 grafting 2:8be98ac1a569 "added c"
1809 1809
1810 1810 $ hg log -Gr '.^^::.'
1811 1811 @ changeset: 9:1896b76e007a
1812 1812 | tag: tip
1813 1813 | user: test
1814 1814 | date: Fri Feb 13 21:18:00 2009 -0002
1815 1815 | summary: added c
1816 1816 |
1817 1817 o changeset: 8:ce2b4f1632af
1818 1818 | parent: 3:9e887f7a939c
1819 1819 | user: test
1820 1820 | date: Fri Feb 13 21:18:00 2009 -0002
1821 1821 | summary: added b
1822 1822 |
1823 1823 o changeset: 3:9e887f7a939c
1824 1824 | parent: 0:f7ad41964313
1825 1825 ~ user: test
1826 1826 date: Thu Jan 01 00:00:00 1970 +0000
1827 1827 summary: bar to b
1828 1828
1829 1829 Test that --log is preserved and reused in `hg graft --continue`
1830 1830
1831 1831 $ hg up '.^^'
1832 1832 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
1833 1833 $ hg graft -r 1 -r 2 --log
1834 1834 grafting 1:80e6d2c47cfe "added b"
1835 1835 merging b
1836 1836 warning: conflicts while merging b! (edit, then use 'hg resolve --mark')
1837 1837 abort: unresolved conflicts, can't continue
1838 1838 (use 'hg resolve' and 'hg graft --continue')
1839 1839 [255]
1840 1840
1841 1841 $ echo foobar > b
1842 1842 $ hg resolve -m
1843 1843 (no more unresolved files)
1844 1844 continue: hg graft --continue
1845 1845
1846 1846 $ hg graft --continue
1847 1847 grafting 1:80e6d2c47cfe "added b"
1848 1848 grafting 2:8be98ac1a569 "added c"
1849 1849
1850 1850 $ hg log -GT "{rev}:{node|short} {desc}" -r '.^^::.'
1851 1851 @ 11:30c1050a58b2 added c
1852 1852 | (grafted from 8be98ac1a56990c2d9ca6861041b8390af7bd6f3)
1853 1853 o 10:ec7eda2313e2 added b
1854 1854 | (grafted from 80e6d2c47cfe5b3185519568327a17a061c7efb6)
1855 1855 o 3:9e887f7a939c bar to b
1856 1856 |
1857 1857 ~
1858 1858
1859 1859 $ cd ..
1860 1860
1861 1861 Testing the --stop flag of `hg graft` which stops the interrupted graft
1862 1862
1863 1863 $ hg init stopgraft
1864 1864 $ cd stopgraft
1865 1865 $ for ch in a b c d; do echo $ch > $ch; hg add $ch; hg ci -Aqm "added "$ch; done;
1866 1866
1867 1867 $ hg log -G
1868 1868 @ changeset: 3:9150fe93bec6
1869 1869 | tag: tip
1870 1870 | user: test
1871 1871 | date: Thu Jan 01 00:00:00 1970 +0000
1872 1872 | summary: added d
1873 1873 |
1874 1874 o changeset: 2:155349b645be
1875 1875 | user: test
1876 1876 | date: Thu Jan 01 00:00:00 1970 +0000
1877 1877 | summary: added c
1878 1878 |
1879 1879 o changeset: 1:5f6d8a4bf34a
1880 1880 | user: test
1881 1881 | date: Thu Jan 01 00:00:00 1970 +0000
1882 1882 | summary: added b
1883 1883 |
1884 1884 o changeset: 0:9092f1db7931
1885 1885 user: test
1886 1886 date: Thu Jan 01 00:00:00 1970 +0000
1887 1887 summary: added a
1888 1888
1889 1889 $ hg up '.^^'
1890 1890 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
1891 1891
1892 1892 $ echo foo > d
1893 1893 $ hg ci -Aqm "added foo to d"
1894 1894
1895 1895 $ hg graft --stop
1896 1896 abort: no interrupted graft found
1897 1897 [255]
1898 1898
1899 1899 $ hg graft -r 3
1900 1900 grafting 3:9150fe93bec6 "added d"
1901 1901 merging d
1902 1902 warning: conflicts while merging d! (edit, then use 'hg resolve --mark')
1903 1903 abort: unresolved conflicts, can't continue
1904 1904 (use 'hg resolve' and 'hg graft --continue')
1905 1905 [255]
1906 1906
1907 1907 $ hg graft --stop --continue
1908 1908 abort: cannot use '--continue' and '--stop' together
1909 1909 [255]
1910 1910
1911 1911 $ hg graft --stop -U
1912 1912 abort: cannot specify any other flag with '--stop'
1913 1913 [255]
1914 1914 $ hg graft --stop --rev 4
1915 1915 abort: cannot specify any other flag with '--stop'
1916 1916 [255]
1917 1917 $ hg graft --stop --log
1918 1918 abort: cannot specify any other flag with '--stop'
1919 1919 [255]
1920 1920
1921 1921 $ hg graft --stop
1922 1922 stopped the interrupted graft
1923 1923 working directory is now at a0deacecd59d
1924 1924
1925 1925 $ hg diff
1926 1926
1927 1927 $ hg log -Gr '.'
1928 1928 @ changeset: 4:a0deacecd59d
1929 1929 | tag: tip
1930 1930 ~ parent: 1:5f6d8a4bf34a
1931 1931 user: test
1932 1932 date: Thu Jan 01 00:00:00 1970 +0000
1933 1933 summary: added foo to d
1934 1934
1935 1935 $ hg graft -r 2 -r 3
1936 1936 grafting 2:155349b645be "added c"
1937 1937 grafting 3:9150fe93bec6 "added d"
1938 1938 merging d
1939 1939 warning: conflicts while merging d! (edit, then use 'hg resolve --mark')
1940 1940 abort: unresolved conflicts, can't continue
1941 1941 (use 'hg resolve' and 'hg graft --continue')
1942 1942 [255]
1943 1943
1944 1944 $ hg graft --stop
1945 1945 stopped the interrupted graft
1946 1946 working directory is now at 75b447541a9e
1947 1947
1948 1948 $ hg diff
1949 1949
1950 1950 $ hg log -G -T "{rev}:{node|short} {desc}"
1951 1951 @ 5:75b447541a9e added c
1952 1952 |
1953 1953 o 4:a0deacecd59d added foo to d
1954 1954 |
1955 1955 | o 3:9150fe93bec6 added d
1956 1956 | |
1957 1957 | o 2:155349b645be added c
1958 1958 |/
1959 1959 o 1:5f6d8a4bf34a added b
1960 1960 |
1961 1961 o 0:9092f1db7931 added a
1962 1962
1963 1963 $ cd ..
1964 1964
1965 1965 Testing the --abort flag for `hg graft` which aborts and rollback to state
1966 1966 before the graft
1967 1967
1968 1968 $ hg init abortgraft
1969 1969 $ cd abortgraft
1970 1970 $ for ch in a b c d; do echo $ch > $ch; hg add $ch; hg ci -Aqm "added "$ch; done;
1971 1971
1972 1972 $ hg up '.^^'
1973 1973 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
1974 1974
1975 1975 $ echo x > x
1976 1976 $ hg ci -Aqm "added x"
1977 1977 $ hg up '.^'
1978 1978 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
1979 1979 $ echo foo > c
1980 1980 $ hg ci -Aqm "added foo to c"
1981 1981
1982 1982 $ hg log -GT "{rev}:{node|short} {desc}"
1983 1983 @ 5:36b793615f78 added foo to c
1984 1984 |
1985 1985 | o 4:863a25e1a9ea added x
1986 1986 |/
1987 1987 | o 3:9150fe93bec6 added d
1988 1988 | |
1989 1989 | o 2:155349b645be added c
1990 1990 |/
1991 1991 o 1:5f6d8a4bf34a added b
1992 1992 |
1993 1993 o 0:9092f1db7931 added a
1994 1994
1995 1995 $ hg up 9150fe93bec6
1996 1996 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
1997 1997
1998 1998 $ hg abort
1999 1999 abort: no interrupted graft to abort (abortflag !)
2000 2000 abort: no operation in progress (abortcommand !)
2001 2001 [255]
2002 2002
2003 2003 when stripping is required
2004 2004 $ hg graft -r 4 -r 5
2005 2005 grafting 4:863a25e1a9ea "added x"
2006 2006 grafting 5:36b793615f78 "added foo to c" (tip)
2007 2007 merging c
2008 2008 warning: conflicts while merging c! (edit, then use 'hg resolve --mark')
2009 2009 abort: unresolved conflicts, can't continue
2010 2010 (use 'hg resolve' and 'hg graft --continue')
2011 2011 [255]
2012 2012
2013 2013 $ hg graft --continue --abort
2014 2014 abort: cannot use '--continue' and '--abort' together
2015 2015 [255]
2016 2016
2017 2017 $ hg graft --abort --stop
2018 2018 abort: cannot use '--abort' and '--stop' together
2019 2019 [255]
2020 2020
2021 2021 $ hg graft --abort --currentuser
2022 2022 abort: cannot specify any other flag with '--abort'
2023 2023 [255]
2024 2024
2025 2025 $ hg graft --abort --edit
2026 2026 abort: cannot specify any other flag with '--abort'
2027 2027 [255]
2028 2028
2029 2029 #if abortcommand
2030 2030 when in dry-run mode
2031 2031 $ hg abort --dry-run
2032 2032 graft in progress, will be aborted
2033 2033 #endif
2034 2034
2035 2035 $ hg abort
2036 2036 graft aborted
2037 2037 working directory is now at 9150fe93bec6
2038 2038 $ hg log -GT "{rev}:{node|short} {desc}"
2039 2039 o 5:36b793615f78 added foo to c
2040 2040 |
2041 2041 | o 4:863a25e1a9ea added x
2042 2042 |/
2043 2043 | @ 3:9150fe93bec6 added d
2044 2044 | |
2045 2045 | o 2:155349b645be added c
2046 2046 |/
2047 2047 o 1:5f6d8a4bf34a added b
2048 2048 |
2049 2049 o 0:9092f1db7931 added a
2050 2050
2051 2051 when stripping is not required
2052 2052 $ hg graft -r 5
2053 2053 grafting 5:36b793615f78 "added foo to c" (tip)
2054 2054 merging c
2055 2055 warning: conflicts while merging c! (edit, then use 'hg resolve --mark')
2056 2056 abort: unresolved conflicts, can't continue
2057 2057 (use 'hg resolve' and 'hg graft --continue')
2058 2058 [255]
2059 2059
2060 2060 $ hg abort
2061 2061 graft aborted
2062 2062 working directory is now at 9150fe93bec6
2063 2063 $ hg log -GT "{rev}:{node|short} {desc}"
2064 2064 o 5:36b793615f78 added foo to c
2065 2065 |
2066 2066 | o 4:863a25e1a9ea added x
2067 2067 |/
2068 2068 | @ 3:9150fe93bec6 added d
2069 2069 | |
2070 2070 | o 2:155349b645be added c
2071 2071 |/
2072 2072 o 1:5f6d8a4bf34a added b
2073 2073 |
2074 2074 o 0:9092f1db7931 added a
2075 2075
2076 2076 when some of the changesets became public
2077 2077
2078 2078 $ hg graft -r 4 -r 5
2079 2079 grafting 4:863a25e1a9ea "added x"
2080 2080 grafting 5:36b793615f78 "added foo to c" (tip)
2081 2081 merging c
2082 2082 warning: conflicts while merging c! (edit, then use 'hg resolve --mark')
2083 2083 abort: unresolved conflicts, can't continue
2084 2084 (use 'hg resolve' and 'hg graft --continue')
2085 2085 [255]
2086 2086
2087 2087 $ hg log -GT "{rev}:{node|short} {desc}"
2088 2088 @ 6:6ec71c037d94 added x
2089 2089 |
2090 2090 | o 5:36b793615f78 added foo to c
2091 2091 | |
2092 2092 | | o 4:863a25e1a9ea added x
2093 2093 | |/
2094 2094 o | 3:9150fe93bec6 added d
2095 2095 | |
2096 2096 o | 2:155349b645be added c
2097 2097 |/
2098 2098 o 1:5f6d8a4bf34a added b
2099 2099 |
2100 2100 o 0:9092f1db7931 added a
2101 2101
2102 2102 $ hg phase -r 6 --public
2103 2103
2104 2104 $ hg abort
2105 2105 cannot clean up public changesets 6ec71c037d94
2106 2106 graft aborted
2107 2107 working directory is now at 6ec71c037d94
2108 2108
2109 2109 when we created new changesets on top of existing one
2110 2110
2111 2111 $ hg up '.^^'
2112 2112 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
2113 2113 $ echo y > y
2114 2114 $ hg ci -Aqm "added y"
2115 2115 $ echo z > z
2116 2116 $ hg ci -Aqm "added z"
2117 2117
2118 2118 $ hg up 3
2119 2119 1 files updated, 0 files merged, 3 files removed, 0 files unresolved
2120 2120 $ hg log -GT "{rev}:{node|short} {desc}"
2121 2121 o 8:637f9e9bbfd4 added z
2122 2122 |
2123 2123 o 7:123221671fd4 added y
2124 2124 |
2125 2125 | o 6:6ec71c037d94 added x
2126 2126 | |
2127 2127 | | o 5:36b793615f78 added foo to c
2128 2128 | | |
2129 2129 | | | o 4:863a25e1a9ea added x
2130 2130 | | |/
2131 2131 | @ | 3:9150fe93bec6 added d
2132 2132 |/ /
2133 2133 o / 2:155349b645be added c
2134 2134 |/
2135 2135 o 1:5f6d8a4bf34a added b
2136 2136 |
2137 2137 o 0:9092f1db7931 added a
2138 2138
2139 2139 $ hg graft -r 8 -r 7 -r 5
2140 2140 grafting 8:637f9e9bbfd4 "added z" (tip)
2141 2141 grafting 7:123221671fd4 "added y"
2142 2142 grafting 5:36b793615f78 "added foo to c"
2143 2143 merging c
2144 2144 warning: conflicts while merging c! (edit, then use 'hg resolve --mark')
2145 2145 abort: unresolved conflicts, can't continue
2146 2146 (use 'hg resolve' and 'hg graft --continue')
2147 2147 [255]
2148 2148
2149 2149 $ cd ..
2150 2150 $ hg init pullrepo
2151 2151 $ cd pullrepo
2152 2152 $ cat >> .hg/hgrc <<EOF
2153 2153 > [phases]
2154 2154 > publish=False
2155 2155 > EOF
2156 2156 $ hg pull ../abortgraft --config phases.publish=False
2157 2157 pulling from ../abortgraft
2158 2158 requesting all changes
2159 2159 adding changesets
2160 2160 adding manifests
2161 2161 adding file changes
2162 2162 added 11 changesets with 9 changes to 8 files (+4 heads)
2163 2163 new changesets 9092f1db7931:6b98ff0062dd (6 drafts)
2164 2164 (run 'hg heads' to see heads, 'hg merge' to merge)
2165 2165 $ hg up 9
2166 2166 5 files updated, 0 files merged, 0 files removed, 0 files unresolved
2167 2167 $ echo w > w
2168 2168 $ hg ci -Aqm "added w" --config phases.publish=False
2169 2169
2170 2170 $ cd ../abortgraft
2171 2171 $ hg pull ../pullrepo
2172 2172 pulling from ../pullrepo
2173 2173 searching for changes
2174 2174 adding changesets
2175 2175 adding manifests
2176 2176 adding file changes
2177 2177 added 1 changesets with 1 changes to 1 files (+1 heads)
2178 2178 new changesets 311dfc6cf3bf (1 drafts)
2179 2179 (run 'hg heads .' to see heads, 'hg merge' to merge)
2180 2180
2181 2181 $ hg abort
2182 2182 new changesets detected on destination branch, can't strip
2183 2183 graft aborted
2184 2184 working directory is now at 6b98ff0062dd
2185 2185
2186 2186 $ cd ..
2187 2187
2188 2188 ============================
2189 2189 Testing --no-commit option:|
2190 2190 ============================
2191 2191
2192 2192 $ hg init nocommit
2193 2193 $ cd nocommit
2194 2194 $ echo a > a
2195 2195 $ hg ci -qAma
2196 2196 $ echo b > b
2197 2197 $ hg ci -qAmb
2198 2198 $ hg up -q 0
2199 2199 $ echo c > c
2200 2200 $ hg ci -qAmc
2201 2201 $ hg log -GT "{rev}:{node|short} {desc}\n"
2202 2202 @ 2:d36c0562f908 c
2203 2203 |
2204 2204 | o 1:d2ae7f538514 b
2205 2205 |/
2206 2206 o 0:cb9a9f314b8b a
2207 2207
2208 2208
2209 2209 Check reporting when --no-commit used with non-applicable options:
2210 2210
2211 2211 $ hg graft 1 --no-commit -e
2212 2212 abort: cannot specify --no-commit and --edit together
2213 2213 [255]
2214 2214
2215 2215 $ hg graft 1 --no-commit --log
2216 2216 abort: cannot specify --no-commit and --log together
2217 2217 [255]
2218 2218
2219 2219 $ hg graft 1 --no-commit -D
2220 2220 abort: cannot specify --no-commit and --currentdate together
2221 2221 [255]
2222 2222
2223 2223 Test --no-commit is working:
2224 2224 $ hg graft 1 --no-commit
2225 2225 grafting 1:d2ae7f538514 "b"
2226 2226
2227 2227 $ hg log -GT "{rev}:{node|short} {desc}\n"
2228 2228 @ 2:d36c0562f908 c
2229 2229 |
2230 2230 | o 1:d2ae7f538514 b
2231 2231 |/
2232 2232 o 0:cb9a9f314b8b a
2233 2233
2234 2234
2235 2235 $ hg diff
2236 2236 diff -r d36c0562f908 b
2237 2237 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2238 2238 +++ b/b Thu Jan 01 00:00:00 1970 +0000
2239 2239 @@ -0,0 +1,1 @@
2240 2240 +b
2241 2241
2242 2242 Prepare wrdir to check --no-commit is resepected after --continue:
2243 2243
2244 2244 $ hg up -qC
2245 2245 $ echo A>a
2246 2246 $ hg ci -qm "A in file a"
2247 2247 $ hg up -q 1
2248 2248 $ echo B>a
2249 2249 $ hg ci -qm "B in file a"
2250 2250 $ hg log -GT "{rev}:{node|short} {desc}\n"
2251 2251 @ 4:2aa9ad1006ff B in file a
2252 2252 |
2253 2253 | o 3:09e253b87e17 A in file a
2254 2254 | |
2255 2255 | o 2:d36c0562f908 c
2256 2256 | |
2257 2257 o | 1:d2ae7f538514 b
2258 2258 |/
2259 2259 o 0:cb9a9f314b8b a
2260 2260
2261 2261
2262 2262 $ hg graft 3 --no-commit
2263 2263 grafting 3:09e253b87e17 "A in file a"
2264 2264 merging a
2265 2265 warning: conflicts while merging a! (edit, then use 'hg resolve --mark')
2266 2266 abort: unresolved conflicts, can't continue
2267 2267 (use 'hg resolve' and 'hg graft --continue')
2268 2268 [255]
2269 2269
2270 2270 Resolve conflict:
2271 2271 $ echo A>a
2272 2272 $ hg resolve --mark
2273 2273 (no more unresolved files)
2274 2274 continue: hg graft --continue
2275 2275
2276 2276 $ hg graft --continue
2277 2277 grafting 3:09e253b87e17 "A in file a"
2278 2278 $ hg log -GT "{rev}:{node|short} {desc}\n"
2279 2279 @ 4:2aa9ad1006ff B in file a
2280 2280 |
2281 2281 | o 3:09e253b87e17 A in file a
2282 2282 | |
2283 2283 | o 2:d36c0562f908 c
2284 2284 | |
2285 2285 o | 1:d2ae7f538514 b
2286 2286 |/
2287 2287 o 0:cb9a9f314b8b a
2288 2288
2289 2289 $ hg diff
2290 2290 diff -r 2aa9ad1006ff a
2291 2291 --- a/a Thu Jan 01 00:00:00 1970 +0000
2292 2292 +++ b/a Thu Jan 01 00:00:00 1970 +0000
2293 2293 @@ -1,1 +1,1 @@
2294 2294 -B
2295 2295 +A
2296 2296
2297 2297 $ hg up -qC
2298 2298
2299 2299 Check --no-commit is resepected when passed with --continue:
2300 2300
2301 2301 $ hg graft 3
2302 2302 grafting 3:09e253b87e17 "A in file a"
2303 2303 merging a
2304 2304 warning: conflicts while merging a! (edit, then use 'hg resolve --mark')
2305 2305 abort: unresolved conflicts, can't continue
2306 2306 (use 'hg resolve' and 'hg graft --continue')
2307 2307 [255]
2308 2308
2309 2309 Resolve conflict:
2310 2310 $ echo A>a
2311 2311 $ hg resolve --mark
2312 2312 (no more unresolved files)
2313 2313 continue: hg graft --continue
2314 2314
2315 2315 $ hg graft --continue --no-commit
2316 2316 grafting 3:09e253b87e17 "A in file a"
2317 2317 $ hg diff
2318 2318 diff -r 2aa9ad1006ff a
2319 2319 --- a/a Thu Jan 01 00:00:00 1970 +0000
2320 2320 +++ b/a Thu Jan 01 00:00:00 1970 +0000
2321 2321 @@ -1,1 +1,1 @@
2322 2322 -B
2323 2323 +A
2324 2324
2325 2325 $ hg log -GT "{rev}:{node|short} {desc}\n"
2326 2326 @ 4:2aa9ad1006ff B in file a
2327 2327 |
2328 2328 | o 3:09e253b87e17 A in file a
2329 2329 | |
2330 2330 | o 2:d36c0562f908 c
2331 2331 | |
2332 2332 o | 1:d2ae7f538514 b
2333 2333 |/
2334 2334 o 0:cb9a9f314b8b a
2335 2335
2336 2336 $ hg up -qC
2337 2337
2338 2338 Test --no-commit when graft multiple revisions:
2339 2339 When there is conflict:
2340 2340 $ hg graft -r "2::3" --no-commit
2341 2341 grafting 2:d36c0562f908 "c"
2342 2342 grafting 3:09e253b87e17 "A in file a"
2343 2343 merging a
2344 2344 warning: conflicts while merging a! (edit, then use 'hg resolve --mark')
2345 2345 abort: unresolved conflicts, can't continue
2346 2346 (use 'hg resolve' and 'hg graft --continue')
2347 2347 [255]
2348 2348
2349 2349 $ echo A>a
2350 2350 $ hg resolve --mark
2351 2351 (no more unresolved files)
2352 2352 continue: hg graft --continue
2353 2353 $ hg graft --continue
2354 2354 grafting 3:09e253b87e17 "A in file a"
2355 2355 $ hg diff
2356 2356 diff -r 2aa9ad1006ff a
2357 2357 --- a/a Thu Jan 01 00:00:00 1970 +0000
2358 2358 +++ b/a Thu Jan 01 00:00:00 1970 +0000
2359 2359 @@ -1,1 +1,1 @@
2360 2360 -B
2361 2361 +A
2362 2362 diff -r 2aa9ad1006ff c
2363 2363 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2364 2364 +++ b/c Thu Jan 01 00:00:00 1970 +0000
2365 2365 @@ -0,0 +1,1 @@
2366 2366 +c
2367 2367
2368 2368 $ hg log -GT "{rev}:{node|short} {desc}\n"
2369 2369 @ 4:2aa9ad1006ff B in file a
2370 2370 |
2371 2371 | o 3:09e253b87e17 A in file a
2372 2372 | |
2373 2373 | o 2:d36c0562f908 c
2374 2374 | |
2375 2375 o | 1:d2ae7f538514 b
2376 2376 |/
2377 2377 o 0:cb9a9f314b8b a
2378 2378
2379 2379 $ hg up -qC
2380 2380
2381 2381 When there is no conflict:
2382 2382 $ echo d>d
2383 2383 $ hg add d -q
2384 2384 $ hg ci -qmd
2385 2385 $ hg up 3 -q
2386 2386 $ hg log -GT "{rev}:{node|short} {desc}\n"
2387 2387 o 5:baefa8927fc0 d
2388 2388 |
2389 2389 o 4:2aa9ad1006ff B in file a
2390 2390 |
2391 2391 | @ 3:09e253b87e17 A in file a
2392 2392 | |
2393 2393 | o 2:d36c0562f908 c
2394 2394 | |
2395 2395 o | 1:d2ae7f538514 b
2396 2396 |/
2397 2397 o 0:cb9a9f314b8b a
2398 2398
2399 2399
2400 2400 $ hg graft -r 1 -r 5 --no-commit
2401 2401 grafting 1:d2ae7f538514 "b"
2402 2402 grafting 5:baefa8927fc0 "d" (tip)
2403 2403 $ hg diff
2404 2404 diff -r 09e253b87e17 b
2405 2405 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2406 2406 +++ b/b Thu Jan 01 00:00:00 1970 +0000
2407 2407 @@ -0,0 +1,1 @@
2408 2408 +b
2409 2409 diff -r 09e253b87e17 d
2410 2410 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2411 2411 +++ b/d Thu Jan 01 00:00:00 1970 +0000
2412 2412 @@ -0,0 +1,1 @@
2413 2413 +d
2414 2414 $ hg log -GT "{rev}:{node|short} {desc}\n"
2415 2415 o 5:baefa8927fc0 d
2416 2416 |
2417 2417 o 4:2aa9ad1006ff B in file a
2418 2418 |
2419 2419 | @ 3:09e253b87e17 A in file a
2420 2420 | |
2421 2421 | o 2:d36c0562f908 c
2422 2422 | |
2423 2423 o | 1:d2ae7f538514 b
2424 2424 |/
2425 2425 o 0:cb9a9f314b8b a
2426 2426
2427 2427 $ cd ..
@@ -1,595 +1,595 b''
1 1 Test uncommit - set up the config
2 2
3 3 $ cat >> $HGRCPATH <<EOF
4 4 > [experimental]
5 5 > evolution.createmarkers=True
6 6 > evolution.allowunstable=True
7 7 > [extensions]
8 8 > uncommit =
9 9 > drawdag=$TESTDIR/drawdag.py
10 10 > EOF
11 11
12 12 Build up a repo
13 13
14 14 $ hg init repo
15 15 $ cd repo
16 16 $ hg bookmark foo
17 17
18 18 Help for uncommit
19 19
20 20 $ hg help uncommit
21 21 hg uncommit [OPTION]... [FILE]...
22 22
23 23 uncommit part or all of a local changeset
24 24
25 25 This command undoes the effect of a local commit, returning the affected
26 26 files to their uncommitted state. This means that files modified or
27 27 deleted in the changeset will be left unchanged, and so will remain
28 28 modified in the working directory.
29 29
30 30 If no files are specified, the commit will be pruned, unless --keep is
31 31 given.
32 32
33 33 (use 'hg help -e uncommit' to show help for the uncommit extension)
34 34
35 35 options ([+] can be repeated):
36 36
37 37 --keep allow an empty commit after uncommitting
38 38 --allow-dirty-working-copy allow uncommit with outstanding changes
39 39 -n --note TEXT store a note on uncommit
40 40 -I --include PATTERN [+] include names matching the given patterns
41 41 -X --exclude PATTERN [+] exclude names matching the given patterns
42 42 -m --message TEXT use text as commit message
43 43 -l --logfile FILE read commit message from file
44 44 -d --date DATE record the specified date as commit date
45 45 -u --user USER record the specified user as committer
46 46 -D --currentdate record the current date as commit date
47 47 -U --currentuser record the current user as committer
48 48
49 49 (some details hidden, use --verbose to show complete help)
50 50
51 51 Uncommit with no commits should fail
52 52
53 53 $ hg uncommit
54 54 abort: cannot uncommit null changeset
55 55 (no changeset checked out)
56 56 [255]
57 57
58 58 Create some commits
59 59
60 60 $ touch files
61 61 $ hg add files
62 62 $ for i in a ab abc abcd abcde; do echo $i > files; echo $i > file-$i; hg add file-$i; hg commit -m "added file-$i"; done
63 63 $ ls
64 64 file-a
65 65 file-ab
66 66 file-abc
67 67 file-abcd
68 68 file-abcde
69 69 files
70 70
71 71 $ hg log -G -T '{rev}:{node} {desc}' --hidden
72 72 @ 4:6c4fd43ed714e7fcd8adbaa7b16c953c2e985b60 added file-abcde
73 73 |
74 74 o 3:6db330d65db434145c0b59d291853e9a84719b24 added file-abcd
75 75 |
76 76 o 2:abf2df566fc193b3ac34d946e63c1583e4d4732b added file-abc
77 77 |
78 78 o 1:69a232e754b08d568c4899475faf2eb44b857802 added file-ab
79 79 |
80 80 o 0:3004d2d9b50883c1538fc754a3aeb55f1b4084f6 added file-a
81 81
82 82 Simple uncommit off the top, also moves bookmark
83 83
84 84 $ hg bookmark
85 85 * foo 4:6c4fd43ed714
86 86 $ hg uncommit
87 87 $ hg status
88 88 M files
89 89 A file-abcde
90 90 $ hg bookmark
91 91 * foo 3:6db330d65db4
92 92
93 93 $ hg log -G -T '{rev}:{node} {desc}' --hidden
94 94 x 4:6c4fd43ed714e7fcd8adbaa7b16c953c2e985b60 added file-abcde
95 95 |
96 96 @ 3:6db330d65db434145c0b59d291853e9a84719b24 added file-abcd
97 97 |
98 98 o 2:abf2df566fc193b3ac34d946e63c1583e4d4732b added file-abc
99 99 |
100 100 o 1:69a232e754b08d568c4899475faf2eb44b857802 added file-ab
101 101 |
102 102 o 0:3004d2d9b50883c1538fc754a3aeb55f1b4084f6 added file-a
103 103
104 104
105 105 Recommit
106 106
107 107 $ hg commit -m 'new change abcde'
108 108 $ hg status
109 109 $ hg heads -T '{rev}:{node} {desc}'
110 110 5:0c07a3ccda771b25f1cb1edbd02e683723344ef1 new change abcde (no-eol)
111 111
112 112 Uncommit of non-existent and unchanged files aborts
113 113 $ hg uncommit nothinghere
114 114 abort: cannot uncommit "nothinghere"
115 115 (file does not exist)
116 116 [255]
117 117 $ hg status
118 118 $ hg uncommit file-abc
119 119 abort: cannot uncommit "file-abc"
120 120 (file was not changed in working directory parent)
121 121 [255]
122 122 $ hg status
123 123
124 124 Try partial uncommit, also moves bookmark
125 125
126 126 $ hg bookmark
127 127 * foo 5:0c07a3ccda77
128 128 $ hg uncommit files
129 129 $ hg status
130 130 M files
131 131 $ hg bookmark
132 132 * foo 6:3727deee06f7
133 133 $ hg heads -T '{rev}:{node} {desc}'
134 134 6:3727deee06f72f5ffa8db792ee299cf39e3e190b new change abcde (no-eol)
135 135 $ hg log -r . -p -T '{rev}:{node} {desc}'
136 136 6:3727deee06f72f5ffa8db792ee299cf39e3e190b new change abcdediff -r 6db330d65db4 -r 3727deee06f7 file-abcde
137 137 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
138 138 +++ b/file-abcde Thu Jan 01 00:00:00 1970 +0000
139 139 @@ -0,0 +1,1 @@
140 140 +abcde
141 141
142 142 $ hg log -G -T '{rev}:{node} {desc}' --hidden
143 143 @ 6:3727deee06f72f5ffa8db792ee299cf39e3e190b new change abcde
144 144 |
145 145 | x 5:0c07a3ccda771b25f1cb1edbd02e683723344ef1 new change abcde
146 146 |/
147 147 | x 4:6c4fd43ed714e7fcd8adbaa7b16c953c2e985b60 added file-abcde
148 148 |/
149 149 o 3:6db330d65db434145c0b59d291853e9a84719b24 added file-abcd
150 150 |
151 151 o 2:abf2df566fc193b3ac34d946e63c1583e4d4732b added file-abc
152 152 |
153 153 o 1:69a232e754b08d568c4899475faf2eb44b857802 added file-ab
154 154 |
155 155 o 0:3004d2d9b50883c1538fc754a3aeb55f1b4084f6 added file-a
156 156
157 157 $ hg commit -m 'update files for abcde'
158 158
159 159 Uncommit with dirty state
160 160
161 161 $ echo "foo" >> files
162 162 $ cat files
163 163 abcde
164 164 foo
165 165 $ hg status
166 166 M files
167 167 $ hg uncommit
168 168 abort: uncommitted changes
169 169 (requires --allow-dirty-working-copy to uncommit)
170 170 [255]
171 171 $ hg uncommit files
172 172 abort: uncommitted changes
173 173 (requires --allow-dirty-working-copy to uncommit)
174 174 [255]
175 175 $ cat files
176 176 abcde
177 177 foo
178 178 $ hg commit --amend -m "files abcde + foo"
179 179
180 180 Testing the 'experimental.uncommitondirtywdir' config
181 181
182 182 $ echo "bar" >> files
183 183 $ hg uncommit
184 184 abort: uncommitted changes
185 185 (requires --allow-dirty-working-copy to uncommit)
186 186 [255]
187 187 $ hg uncommit --config experimental.uncommitondirtywdir=True
188 188 $ hg commit -m "files abcde + foo"
189 189
190 190 Uncommit in the middle of a stack, does not move bookmark
191 191
192 192 $ hg checkout '.^^^'
193 193 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
194 194 (leaving bookmark foo)
195 195 $ hg log -r . -p -T '{rev}:{node} {desc}'
196 196 2:abf2df566fc193b3ac34d946e63c1583e4d4732b added file-abcdiff -r 69a232e754b0 -r abf2df566fc1 file-abc
197 197 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
198 198 +++ b/file-abc Thu Jan 01 00:00:00 1970 +0000
199 199 @@ -0,0 +1,1 @@
200 200 +abc
201 201 diff -r 69a232e754b0 -r abf2df566fc1 files
202 202 --- a/files Thu Jan 01 00:00:00 1970 +0000
203 203 +++ b/files Thu Jan 01 00:00:00 1970 +0000
204 204 @@ -1,1 +1,1 @@
205 205 -ab
206 206 +abc
207 207
208 208 $ hg bookmark
209 209 foo 9:48e5bd7cd583
210 210 $ hg uncommit
211 211 3 new orphan changesets
212 212 $ hg status
213 213 M files
214 214 A file-abc
215 215 $ hg heads -T '{rev}:{node} {desc}'
216 216 9:48e5bd7cd583eb24164ef8b89185819c84c96ed7 files abcde + foo (no-eol)
217 217 $ hg bookmark
218 218 foo 9:48e5bd7cd583
219 219 $ hg commit -m 'new abc'
220 220 created new head
221 221
222 222 Partial uncommit in the middle, does not move bookmark
223 223
224 224 $ hg checkout '.^'
225 225 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
226 226 $ hg log -r . -p -T '{rev}:{node} {desc}'
227 227 1:69a232e754b08d568c4899475faf2eb44b857802 added file-abdiff -r 3004d2d9b508 -r 69a232e754b0 file-ab
228 228 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
229 229 +++ b/file-ab Thu Jan 01 00:00:00 1970 +0000
230 230 @@ -0,0 +1,1 @@
231 231 +ab
232 232 diff -r 3004d2d9b508 -r 69a232e754b0 files
233 233 --- a/files Thu Jan 01 00:00:00 1970 +0000
234 234 +++ b/files Thu Jan 01 00:00:00 1970 +0000
235 235 @@ -1,1 +1,1 @@
236 236 -a
237 237 +ab
238 238
239 239 $ hg bookmark
240 240 foo 9:48e5bd7cd583
241 241 $ hg uncommit file-ab
242 242 1 new orphan changesets
243 243 $ hg status
244 244 A file-ab
245 245
246 246 $ hg heads -T '{rev}:{node} {desc}\n'
247 247 11:8eb87968f2edb7f27f27fe676316e179de65fff6 added file-ab
248 248 10:5dc89ca4486f8a88716c5797fa9f498d13d7c2e1 new abc
249 249 9:48e5bd7cd583eb24164ef8b89185819c84c96ed7 files abcde + foo
250 250
251 251 $ hg bookmark
252 252 foo 9:48e5bd7cd583
253 253 $ hg commit -m 'update ab'
254 254 $ hg status
255 255 $ hg heads -T '{rev}:{node} {desc}\n'
256 256 12:f21039c59242b085491bb58f591afc4ed1c04c09 update ab
257 257 10:5dc89ca4486f8a88716c5797fa9f498d13d7c2e1 new abc
258 258 9:48e5bd7cd583eb24164ef8b89185819c84c96ed7 files abcde + foo
259 259
260 260 $ hg log -G -T '{rev}:{node} {desc}' --hidden
261 261 @ 12:f21039c59242b085491bb58f591afc4ed1c04c09 update ab
262 262 |
263 263 o 11:8eb87968f2edb7f27f27fe676316e179de65fff6 added file-ab
264 264 |
265 265 | * 10:5dc89ca4486f8a88716c5797fa9f498d13d7c2e1 new abc
266 266 | |
267 267 | | * 9:48e5bd7cd583eb24164ef8b89185819c84c96ed7 files abcde + foo
268 268 | | |
269 269 | | | x 8:84beeba0ac30e19521c036e4d2dd3a5fa02586ff files abcde + foo
270 270 | | |/
271 271 | | | x 7:0977fa602c2fd7d8427ed4e7ee15ea13b84c9173 update files for abcde
272 272 | | |/
273 273 | | * 6:3727deee06f72f5ffa8db792ee299cf39e3e190b new change abcde
274 274 | | |
275 275 | | | x 5:0c07a3ccda771b25f1cb1edbd02e683723344ef1 new change abcde
276 276 | | |/
277 277 | | | x 4:6c4fd43ed714e7fcd8adbaa7b16c953c2e985b60 added file-abcde
278 278 | | |/
279 279 | | * 3:6db330d65db434145c0b59d291853e9a84719b24 added file-abcd
280 280 | | |
281 281 | | x 2:abf2df566fc193b3ac34d946e63c1583e4d4732b added file-abc
282 282 | |/
283 283 | x 1:69a232e754b08d568c4899475faf2eb44b857802 added file-ab
284 284 |/
285 285 o 0:3004d2d9b50883c1538fc754a3aeb55f1b4084f6 added file-a
286 286
287 287 Uncommit with draft parent
288 288
289 289 $ hg uncommit
290 290 $ hg phase -r .
291 291 11: draft
292 292 $ hg commit -m 'update ab again'
293 293
294 294 Phase is preserved
295 295
296 296 $ hg uncommit --keep --config phases.new-commit=secret
297 297 note: keeping empty commit
298 298 $ hg phase -r .
299 299 14: draft
300 300 $ hg commit --amend -m 'update ab again'
301 301
302 302 Uncommit with public parent
303 303
304 304 $ hg phase -p "::.^"
305 305 $ hg uncommit
306 306 $ hg phase -r .
307 307 11: public
308 308
309 309 Partial uncommit with public parent
310 310
311 311 $ echo xyz > xyz
312 312 $ hg add xyz
313 313 $ hg commit -m "update ab and add xyz"
314 314 $ hg uncommit xyz
315 315 $ hg status
316 316 A xyz
317 317 $ hg phase -r .
318 318 17: draft
319 319 $ hg phase -r ".^"
320 320 11: public
321 321
322 322 Uncommit with --keep or experimental.uncommit.keep leaves an empty changeset
323 323
324 324 $ cd $TESTTMP
325 325 $ hg init repo1
326 326 $ cd repo1
327 327 $ hg debugdrawdag <<'EOS'
328 328 > Q
329 329 > |
330 330 > P
331 331 > EOS
332 332 $ hg up Q -q
333 333 $ hg uncommit --keep
334 334 note: keeping empty commit
335 335 $ hg log -G -T '{desc} FILES: {files}'
336 336 @ Q FILES:
337 337 |
338 338 | x Q FILES: Q
339 339 |/
340 340 o P FILES: P
341 341
342 342 $ cat >> .hg/hgrc <<EOF
343 343 > [experimental]
344 344 > uncommit.keep=True
345 345 > EOF
346 346 $ hg ci --amend
347 347 $ hg uncommit
348 348 note: keeping empty commit
349 349 $ hg log -G -T '{desc} FILES: {files}'
350 350 @ Q FILES:
351 351 |
352 352 | x Q FILES: Q
353 353 |/
354 354 o P FILES: P
355 355
356 356 $ hg status
357 357 A Q
358 358 $ hg ci --amend
359 359 $ hg uncommit --no-keep
360 360 $ hg log -G -T '{desc} FILES: {files}'
361 361 x Q FILES: Q
362 362 |
363 363 @ P FILES: P
364 364
365 365 $ hg status
366 366 A Q
367 367 $ cd ..
368 368 $ rm -rf repo1
369 369
370 370 Testing uncommit while merge
371 371
372 372 $ hg init repo2
373 373 $ cd repo2
374 374
375 375 Create some history
376 376
377 377 $ touch a
378 378 $ hg add a
379 379 $ for i in 1 2 3; do echo $i > a; hg commit -m "a $i"; done
380 380 $ hg checkout 0
381 381 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
382 382 $ touch b
383 383 $ hg add b
384 384 $ for i in 1 2 3; do echo $i > b; hg commit -m "b $i"; done
385 385 created new head
386 386 $ hg log -G -T '{rev}:{node} {desc}' --hidden
387 387 @ 5:2cd56cdde163ded2fbb16ba2f918c96046ab0bf2 b 3
388 388 |
389 389 o 4:c3a0d5bb3b15834ffd2ef9ef603e93ec65cf2037 b 2
390 390 |
391 391 o 3:49bb009ca26078726b8870f1edb29fae8f7618f5 b 1
392 392 |
393 393 | o 2:990982b7384266e691f1bc08ca36177adcd1c8a9 a 3
394 394 | |
395 395 | o 1:24d38e3cf160c7b6f5ffe82179332229886a6d34 a 2
396 396 |/
397 397 o 0:ea4e33293d4d274a2ba73150733c2612231f398c a 1
398 398
399 399
400 400 Add and expect uncommit to fail on both merge working dir and merge changeset
401 401
402 402 $ hg merge 2
403 403 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
404 404 (branch merge, don't forget to commit)
405 405
406 406 $ hg uncommit
407 407 abort: outstanding uncommitted merge
408 408 (requires --allow-dirty-working-copy to uncommit)
409 409 [255]
410 410
411 411 $ hg uncommit --config experimental.uncommitondirtywdir=True
412 412 abort: cannot uncommit while merging
413 413 [255]
414 414
415 415 $ hg status
416 416 M a
417 417 $ hg commit -m 'merge a and b'
418 418
419 419 $ hg uncommit
420 420 abort: cannot uncommit merge changeset
421 421 [255]
422 422
423 423 $ hg status
424 424 $ hg log -G -T '{rev}:{node} {desc}' --hidden
425 425 @ 6:c03b9c37bc67bf504d4912061cfb527b47a63c6e merge a and b
426 426 |\
427 427 | o 5:2cd56cdde163ded2fbb16ba2f918c96046ab0bf2 b 3
428 428 | |
429 429 | o 4:c3a0d5bb3b15834ffd2ef9ef603e93ec65cf2037 b 2
430 430 | |
431 431 | o 3:49bb009ca26078726b8870f1edb29fae8f7618f5 b 1
432 432 | |
433 433 o | 2:990982b7384266e691f1bc08ca36177adcd1c8a9 a 3
434 434 | |
435 435 o | 1:24d38e3cf160c7b6f5ffe82179332229886a6d34 a 2
436 436 |/
437 437 o 0:ea4e33293d4d274a2ba73150733c2612231f398c a 1
438 438
439 439
440 440 Rename a->b, then remove b in working copy. Result should remove a.
441 441
442 442 $ hg co -q 0
443 443 $ hg mv a b
444 444 $ hg ci -qm 'move a to b'
445 445 $ hg rm b
446 446 $ hg uncommit --config experimental.uncommitondirtywdir=True
447 447 $ hg st --copies
448 448 R a
449 449 $ hg revert a
450 450
451 451 Rename a->b, then rename b->c in working copy. Result should rename a->c.
452 452
453 453 $ hg co -q 0
454 454 $ hg mv a b
455 455 $ hg ci -qm 'move a to b'
456 456 $ hg mv b c
457 457 $ hg uncommit --config experimental.uncommitondirtywdir=True
458 458 $ hg st --copies
459 459 A c
460 460 a
461 461 R a
462 462 $ hg revert a
463 463 $ hg forget c
464 464 $ rm c
465 465
466 466 Copy a->b1 and a->b2, then rename b1->c in working copy. Result should copy a->b2 and a->c.
467 467
468 468 $ hg co -q 0
469 469 $ hg cp a b1
470 470 $ hg cp a b2
471 471 $ hg ci -qm 'move a to b1 and b2'
472 472 $ hg mv b1 c
473 473 $ hg uncommit --config experimental.uncommitondirtywdir=True
474 474 $ hg st --copies
475 475 A b2
476 476 a
477 477 A c
478 478 a
479 479 $ cd ..
480 480
481 481 --allow-dirty-working-copy should also work on a dirty PATH
482 482
483 483 $ hg init issue5977
484 484 $ cd issue5977
485 485 $ echo 'super critical info!' > a
486 486 $ hg ci -Am 'add a'
487 487 adding a
488 488 $ echo 'foo' > b
489 489 $ hg add b
490 490 $ hg status
491 491 A b
492 492 $ hg unc a
493 493 note: keeping empty commit
494 494 $ cat a
495 495 super critical info!
496 496 $ hg log
497 497 changeset: 1:656ba143d384
498 498 tag: tip
499 499 parent: -1:000000000000
500 500 user: test
501 501 date: Thu Jan 01 00:00:00 1970 +0000
502 502 summary: add a
503 503
504 504 $ hg ci -Am 'add b'
505 505 $ echo 'foo bar' > b
506 506 $ hg unc b
507 507 abort: uncommitted changes
508 508 (requires --allow-dirty-working-copy to uncommit)
509 509 [255]
510 510 $ hg unc --allow-dirty-working-copy b
511 511 $ hg log
512 512 changeset: 3:30fa958635b2
513 513 tag: tip
514 514 parent: 1:656ba143d384
515 515 user: test
516 516 date: Thu Jan 01 00:00:00 1970 +0000
517 517 summary: add b
518 518
519 519 changeset: 1:656ba143d384
520 520 parent: -1:000000000000
521 521 user: test
522 522 date: Thu Jan 01 00:00:00 1970 +0000
523 523 summary: add a
524 524
525 525 Removes can be uncommitted
526 526
527 527 $ hg ci -m 'modified b'
528 528 $ hg rm b
529 529 $ hg ci -m 'remove b'
530 530 $ hg uncommit b
531 531 note: keeping empty commit
532 532 $ hg status
533 533 R b
534 534
535 535 Uncommitting a directory won't run afoul of the checks that an explicit file
536 536 can be uncommitted.
537 537
538 538 $ mkdir dir
539 539 $ echo 1 > dir/file.txt
540 540 $ hg ci -Aqm 'add file in directory'
541 541 $ hg uncommit dir -m 'uncommit with message' -u 'different user' \
542 542 > -d 'Jun 30 12:12:12 1980 +0000'
543 543 $ hg status
544 544 A dir/file.txt
545 545 $ hg log -r .
546 546 changeset: 8:b4dd26dc42e0
547 547 tag: tip
548 548 parent: 6:2278a4c24330
549 549 user: different user
550 550 date: Mon Jun 30 12:12:12 1980 +0000
551 551 summary: uncommit with message
552 552
553 553 Bad option combinations
554 554
555 555 $ hg rollback -q --config ui.rollback=True
556 556 $ hg uncommit -U --user 'user'
557 abort: --user and --currentuser are mutually exclusive
557 abort: cannot specify both --user and --currentuser
558 558 [255]
559 559 $ hg uncommit -D --date today
560 abort: --date and --currentdate are mutually exclusive
560 abort: cannot specify both --date and --currentdate
561 561 [255]
562 562
563 563 `uncommit <dir>` and `cd <dir> && uncommit .` behave the same...
564 564
565 565 $ echo 2 > dir/file2.txt
566 566 $ hg ci -Aqm 'add file2 in directory'
567 567 $ hg uncommit dir
568 568 note: keeping empty commit
569 569 $ hg status
570 570 A dir/file2.txt
571 571
572 572 $ hg rollback -q --config ui.rollback=True
573 573 $ cd dir
574 574 $ hg uncommit . -n 'this is a note'
575 575 note: keeping empty commit
576 576 $ hg status
577 577 A dir/file2.txt
578 578 $ cd ..
579 579
580 580 ... and errors out the same way when nothing can be uncommitted
581 581
582 582 $ hg rollback -q --config ui.rollback=True
583 583 $ mkdir emptydir
584 584 $ hg uncommit emptydir
585 585 abort: cannot uncommit "emptydir"
586 586 (file was untracked in working directory parent)
587 587 [255]
588 588
589 589 $ cd emptydir
590 590 $ hg uncommit .
591 591 abort: cannot uncommit "emptydir"
592 592 (file was untracked in working directory parent)
593 593 [255]
594 594 $ hg status
595 595 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now