##// END OF EJS Templates
record: extract a closure to the module level...
marmoute -
r51098:ee7a7155 default
parent child Browse files
Show More
@@ -8,6 +8,7 b''
8
8
9 import copy as copymod
9 import copy as copymod
10 import errno
10 import errno
11 import functools
11 import os
12 import os
12 import re
13 import re
13
14
@@ -440,6 +441,227 b' def recordfilter(ui, originalhunks, matc'
440 return newchunks, newopts
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 def dorecord(
665 def dorecord(
444 ui, repo, commitfunc, cmdsuggest, backupall, filterfn, *pats, **opts
666 ui, repo, commitfunc, cmdsuggest, backupall, filterfn, *pats, **opts
445 ):
667 ):
@@ -455,222 +677,15 b' def dorecord('
455 if not opts.get(b'user'):
677 if not opts.get(b'user'):
456 ui.username() # raise exception, username not provided
678 ui.username() # raise exception, username not provided
457
679
458 def recordfunc(ui, repo, message, match, opts):
680 func = functools.partial(
459 """This is generic record driver.
681 _record,
460
682 commitfunc=commitfunc,
461 Its job is to interactively filter local changes, and
683 backupall=backupall,
462 accordingly prepare working directory into a state in which the
684 filterfn=filterfn,
463 job can be delegated to a non-interactive commit command such as
685 pats=pats,
464 'commit' or 'qrefresh'.
686 )
465
687
466 After the actual job is done by non-interactive command, the
688 return commit(ui, repo, func, pats, opts)
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)
674
689
675
690
676 class dirnode:
691 class dirnode:
General Comments 0
You need to be logged in to leave comments. Login now