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