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