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