Show More
@@ -8,6 +8,7 b'' | |||
|
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 | |
@@ -440,6 +441,227 b' def recordfilter(ui, originalhunks, matc' | |||
|
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 | ): |
@@ -455,222 +677,15 b' def dorecord(' | |||
|
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: |
General Comments 0
You need to be logged in to leave comments.
Login now