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