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