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