comparison mercurial/cmdutil.py @ 50085:28dfb2df4ab9

commit: use `dirstate.change_files` to scope the associated `addremove` This was significantly more complicated than I expected, because multiple extensions get in the way. I introduced a context that lazily open the transaction and associated context to work around these complication. See the inline documentation for details. Introducing the wrapping transaction remove the need for dirstate-guard (one of the ultimate goal of all this), and slightly affect the result of a `hg rollback` after a `hg commit --addremove`. That last part is deemed fine. It aligns the behavior with what happens after a failed `hg commit --addremove` and nobody should be using `hg rollback` anyway. The small output change in the test come from the different transaction timing and fact the transaction now backup the dirstate before the addremove, which might mean "no file to backup" when the repository starts from an empty state.
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Wed, 15 Feb 2023 11:51:58 +0100
parents a46dfc2b58a3
children 6cdcab3ae3fa
comparison
equal deleted inserted replaced
50084:a46dfc2b58a3 50085:28dfb2df4ab9
27 from . import ( 27 from . import (
28 bookmarks, 28 bookmarks,
29 changelog, 29 changelog,
30 copies, 30 copies,
31 crecord as crecordmod, 31 crecord as crecordmod,
32 dirstateguard,
33 encoding, 32 encoding,
34 error, 33 error,
35 formatter, 34 formatter,
36 logcmdutil, 35 logcmdutil,
37 match as matchmod, 36 match as matchmod,
2787 ) 2786 )
2788 2787
2789 return err 2788 return err
2790 2789
2791 2790
2791 class _AddRemoveContext:
2792 """a small (hacky) context to deal with lazy opening of context
2793
2794 This is to be used in the `commit` function right below. This deals with
2795 lazily open a `changing_files` context inside a `transaction` that span the
2796 full commit operation.
2797
2798 We need :
2799 - a `changing_files` context to wrap the dirstate change within the
2800 "addremove" operation,
2801 - a transaction to make sure these change are not written right after the
2802 addremove, but when the commit operation succeed.
2803
2804 However it get complicated because:
2805 - opening a transaction "this early" shuffle hooks order, especially the
2806 `precommit` one happening after the `pretxtopen` one which I am not too
2807 enthusiastic about.
2808 - the `mq` extensions + the `record` extension stacks many layers of call
2809 to implement `qrefresh --interactive` and this result with `mq` calling a
2810 `strip` in the middle of this function. Which prevent the existence of
2811 transaction wrapping all of its function code. (however, `qrefresh` never
2812 call the `addremove` bits.
2813 - the largefile extensions (and maybe other extensions?) wraps `addremove`
2814 so slicing `addremove` in smaller bits is a complex endeavour.
2815
2816 So I eventually took a this shortcut that open the transaction if we
2817 actually needs it, not disturbing much of the rest of the code.
2818
2819 It will result in some hooks order change for `hg commit --addremove`,
2820 however it seems a corner case enough to ignore that for now (hopefully).
2821
2822 Notes that None of the above problems seems insurmountable, however I have
2823 been fighting with this specific piece of code for a couple of day already
2824 and I need a solution to keep moving forward on the bigger work around
2825 `changing_files` context that is being introduced at the same time as this
2826 hack.
2827
2828 Each problem seems to have a solution:
2829 - the hook order issue could be solved by refactoring the many-layer stack
2830 that currently composes a commit and calling them earlier,
2831 - the mq issue could be solved by refactoring `mq` so that the final strip
2832 is done after transaction closure. Be warned that the mq code is quite
2833 antic however.
2834 - large-file could be reworked in parallel of the `addremove` to be
2835 friendlier to this.
2836
2837 However each of these tasks are too much a diversion right now. In addition
2838 they will be much easier to undertake when the `changing_files` dust has
2839 settled."""
2840
2841 def __init__(self, repo):
2842 self._repo = repo
2843 self._transaction = None
2844 self._dirstate_context = None
2845 self._state = None
2846
2847 def __enter__(self):
2848 assert self._state is None
2849 self._state = True
2850 return self
2851
2852 def open_transaction(self):
2853 """open a `transaction` and `changing_files` context
2854
2855 Call this when you know that change to the dirstate will be needed and
2856 we need to open the transaction early
2857
2858 This will also open the dirstate `changing_files` context, so you should
2859 call `close_dirstate_context` when the distate changes are done.
2860 """
2861 assert self._state is not None
2862 if self._transaction is None:
2863 self._transaction = self._repo.transaction(b'commit')
2864 self._transaction.__enter__()
2865 if self._dirstate_context is None:
2866 self._dirstate_context = self._repo.dirstate.changing_files(
2867 self._repo
2868 )
2869 self._dirstate_context.__enter__()
2870
2871 def close_dirstate_context(self):
2872 """close the change_files if any
2873
2874 Call this after the (potential) `open_transaction` call to close the
2875 (potential) changing_files context.
2876 """
2877 if self._dirstate_context is not None:
2878 self._dirstate_context.__exit__(None, None, None)
2879 self._dirstate_context = None
2880
2881 def __exit__(self, *args):
2882 if self._dirstate_context is not None:
2883 self._dirstate_context.__exit__(*args)
2884 if self._transaction is not None:
2885 self._transaction.__exit__(*args)
2886
2887
2792 def commit(ui, repo, commitfunc, pats, opts): 2888 def commit(ui, repo, commitfunc, pats, opts):
2793 '''commit the specified files or all outstanding changes''' 2889 '''commit the specified files or all outstanding changes'''
2794 date = opts.get(b'date') 2890 date = opts.get(b'date')
2795 if date: 2891 if date:
2796 opts[b'date'] = dateutil.parsedate(date) 2892 opts[b'date'] = dateutil.parsedate(date)
2797 2893
2798 dsguard = None 2894 with repo.wlock(), repo.lock():
2799 # extract addremove carefully -- this function can be called from a command
2800 # that doesn't support addremove
2801 if opts.get(b'addremove'):
2802 dsguard = dirstateguard.dirstateguard(repo, b'commit')
2803 with dsguard or util.nullcontextmanager():
2804 message = logmessage(ui, opts) 2895 message = logmessage(ui, opts)
2805 matcher = scmutil.match(repo[None], pats, opts) 2896 matcher = scmutil.match(repo[None], pats, opts)
2806 if True: 2897
2898 with _AddRemoveContext(repo) as c:
2807 # extract addremove carefully -- this function can be called from a 2899 # extract addremove carefully -- this function can be called from a
2808 # command that doesn't support addremove 2900 # command that doesn't support addremove
2809 if opts.get(b'addremove'): 2901 if opts.get(b'addremove'):
2810 relative = scmutil.anypats(pats, opts) 2902 relative = scmutil.anypats(pats, opts)
2811 uipathfn = scmutil.getuipathfn( 2903 uipathfn = scmutil.getuipathfn(
2816 repo, 2908 repo,
2817 matcher, 2909 matcher,
2818 b"", 2910 b"",
2819 uipathfn, 2911 uipathfn,
2820 opts, 2912 opts,
2913 open_tr=c.open_transaction,
2821 ) 2914 )
2822 m = _(b"failed to mark all new/missing files as added/removed") 2915 m = _(b"failed to mark all new/missing files as added/removed")
2823 if r != 0: 2916 if r != 0:
2824 raise error.Abort(m) 2917 raise error.Abort(m)
2825 2918 c.close_dirstate_context()
2826 return commitfunc(ui, repo, message, matcher, opts) 2919 return commitfunc(ui, repo, message, matcher, opts)
2827 2920
2828 2921
2829 def samefile(f, ctx1, ctx2): 2922 def samefile(f, ctx1, ctx2):
2830 if f in ctx1.manifest(): 2923 if f in ctx1.manifest():