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