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