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