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