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