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