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