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