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