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