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