##// END OF EJS Templates
purge: add a --confirm option...
marmoute -
r47078:135056e8 default
parent child Browse files
Show More
@@ -1,136 +1,139 b''
1 # Copyright (C) 2006 - Marco Barisione <marco@barisione.org>
1 # Copyright (C) 2006 - Marco Barisione <marco@barisione.org>
2 #
2 #
3 # This is a small extension for Mercurial (https://mercurial-scm.org/)
3 # This is a small extension for Mercurial (https://mercurial-scm.org/)
4 # that removes files not known to mercurial
4 # that removes files not known to mercurial
5 #
5 #
6 # This program was inspired by the "cvspurge" script contained in CVS
6 # This program was inspired by the "cvspurge" script contained in CVS
7 # utilities (http://www.red-bean.com/cvsutils/).
7 # utilities (http://www.red-bean.com/cvsutils/).
8 #
8 #
9 # For help on the usage of "hg purge" use:
9 # For help on the usage of "hg purge" use:
10 # hg help purge
10 # hg help purge
11 #
11 #
12 # This program is free software; you can redistribute it and/or modify
12 # This program is free software; you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation; either version 2 of the License, or
14 # the Free Software Foundation; either version 2 of the License, or
15 # (at your option) any later version.
15 # (at your option) any later version.
16 #
16 #
17 # This program is distributed in the hope that it will be useful,
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
20 # GNU General Public License for more details.
21 #
21 #
22 # You should have received a copy of the GNU General Public License
22 # You should have received a copy of the GNU General Public License
23 # along with this program; if not, see <http://www.gnu.org/licenses/>.
23 # along with this program; if not, see <http://www.gnu.org/licenses/>.
24
24
25 '''command to delete untracked files from the working directory'''
25 '''command to delete untracked files from the working directory'''
26 from __future__ import absolute_import
26 from __future__ import absolute_import
27
27
28 from mercurial.i18n import _
28 from mercurial.i18n import _
29 from mercurial import (
29 from mercurial import (
30 cmdutil,
30 cmdutil,
31 merge as mergemod,
31 merge as mergemod,
32 pycompat,
32 pycompat,
33 registrar,
33 registrar,
34 scmutil,
34 scmutil,
35 )
35 )
36
36
37 cmdtable = {}
37 cmdtable = {}
38 command = registrar.command(cmdtable)
38 command = registrar.command(cmdtable)
39 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
39 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
40 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
40 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
41 # be specifying the version(s) of Mercurial they are tested with, or
41 # be specifying the version(s) of Mercurial they are tested with, or
42 # leave the attribute unspecified.
42 # leave the attribute unspecified.
43 testedwith = b'ships-with-hg-core'
43 testedwith = b'ships-with-hg-core'
44
44
45
45
46 @command(
46 @command(
47 b'purge|clean',
47 b'purge|clean',
48 [
48 [
49 (b'a', b'abort-on-err', None, _(b'abort if an error occurs')),
49 (b'a', b'abort-on-err', None, _(b'abort if an error occurs')),
50 (b'', b'all', None, _(b'purge ignored files too')),
50 (b'', b'all', None, _(b'purge ignored files too')),
51 (b'i', b'ignored', None, _(b'purge only ignored files')),
51 (b'i', b'ignored', None, _(b'purge only ignored files')),
52 (b'', b'dirs', None, _(b'purge empty directories')),
52 (b'', b'dirs', None, _(b'purge empty directories')),
53 (b'', b'files', None, _(b'purge files')),
53 (b'', b'files', None, _(b'purge files')),
54 (b'p', b'print', None, _(b'print filenames instead of deleting them')),
54 (b'p', b'print', None, _(b'print filenames instead of deleting them')),
55 (
55 (
56 b'0',
56 b'0',
57 b'print0',
57 b'print0',
58 None,
58 None,
59 _(
59 _(
60 b'end filenames with NUL, for use with xargs'
60 b'end filenames with NUL, for use with xargs'
61 b' (implies -p/--print)'
61 b' (implies -p/--print)'
62 ),
62 ),
63 ),
63 ),
64 (b'', b'confirm', None, _(b'ask before permanently deleting files')),
64 ]
65 ]
65 + cmdutil.walkopts,
66 + cmdutil.walkopts,
66 _(b'hg purge [OPTION]... [DIR]...'),
67 _(b'hg purge [OPTION]... [DIR]...'),
67 helpcategory=command.CATEGORY_WORKING_DIRECTORY,
68 helpcategory=command.CATEGORY_WORKING_DIRECTORY,
68 )
69 )
69 def purge(ui, repo, *dirs, **opts):
70 def purge(ui, repo, *dirs, **opts):
70 """removes files not tracked by Mercurial
71 """removes files not tracked by Mercurial
71
72
72 Delete files not known to Mercurial. This is useful to test local
73 Delete files not known to Mercurial. This is useful to test local
73 and uncommitted changes in an otherwise-clean source tree.
74 and uncommitted changes in an otherwise-clean source tree.
74
75
75 This means that purge will delete the following by default:
76 This means that purge will delete the following by default:
76
77
77 - Unknown files: files marked with "?" by :hg:`status`
78 - Unknown files: files marked with "?" by :hg:`status`
78 - Empty directories: in fact Mercurial ignores directories unless
79 - Empty directories: in fact Mercurial ignores directories unless
79 they contain files under source control management
80 they contain files under source control management
80
81
81 But it will leave untouched:
82 But it will leave untouched:
82
83
83 - Modified and unmodified tracked files
84 - Modified and unmodified tracked files
84 - Ignored files (unless -i or --all is specified)
85 - Ignored files (unless -i or --all is specified)
85 - New files added to the repository (with :hg:`add`)
86 - New files added to the repository (with :hg:`add`)
86
87
87 The --files and --dirs options can be used to direct purge to delete
88 The --files and --dirs options can be used to direct purge to delete
88 only files, only directories, or both. If neither option is given,
89 only files, only directories, or both. If neither option is given,
89 both will be deleted.
90 both will be deleted.
90
91
91 If directories are given on the command line, only files in these
92 If directories are given on the command line, only files in these
92 directories are considered.
93 directories are considered.
93
94
94 Be careful with purge, as you could irreversibly delete some files
95 Be careful with purge, as you could irreversibly delete some files
95 you forgot to add to the repository. If you only want to print the
96 you forgot to add to the repository. If you only want to print the
96 list of files that this program would delete, use the --print
97 list of files that this program would delete, use the --print
97 option.
98 option.
98 """
99 """
99 opts = pycompat.byteskwargs(opts)
100 opts = pycompat.byteskwargs(opts)
100 cmdutil.check_at_most_one_arg(opts, b'all', b'ignored')
101 cmdutil.check_at_most_one_arg(opts, b'all', b'ignored')
101
102
102 act = not opts.get(b'print')
103 act = not opts.get(b'print')
103 eol = b'\n'
104 eol = b'\n'
104 if opts.get(b'print0'):
105 if opts.get(b'print0'):
105 eol = b'\0'
106 eol = b'\0'
106 act = False # --print0 implies --print
107 act = False # --print0 implies --print
107 if opts.get(b'all', False):
108 if opts.get(b'all', False):
108 ignored = True
109 ignored = True
109 unknown = True
110 unknown = True
110 else:
111 else:
111 ignored = opts.get(b'ignored', False)
112 ignored = opts.get(b'ignored', False)
112 unknown = not ignored
113 unknown = not ignored
113
114
114 removefiles = opts.get(b'files')
115 removefiles = opts.get(b'files')
115 removedirs = opts.get(b'dirs')
116 removedirs = opts.get(b'dirs')
117 confirm = opts.get(b'confirm')
116
118
117 if not removefiles and not removedirs:
119 if not removefiles and not removedirs:
118 removefiles = True
120 removefiles = True
119 removedirs = True
121 removedirs = True
120
122
121 match = scmutil.match(repo[None], dirs, opts)
123 match = scmutil.match(repo[None], dirs, opts)
122
124
123 paths = mergemod.purge(
125 paths = mergemod.purge(
124 repo,
126 repo,
125 match,
127 match,
126 unknown=unknown,
128 unknown=unknown,
127 ignored=ignored,
129 ignored=ignored,
128 removeemptydirs=removedirs,
130 removeemptydirs=removedirs,
129 removefiles=removefiles,
131 removefiles=removefiles,
130 abortonerror=opts.get(b'abort_on_err'),
132 abortonerror=opts.get(b'abort_on_err'),
131 noop=not act,
133 noop=not act,
134 confirm=confirm,
132 )
135 )
133
136
134 for path in paths:
137 for path in paths:
135 if not act:
138 if not act:
136 ui.write(b'%s%s' % (path, eol))
139 ui.write(b'%s%s' % (path, eol))
@@ -1,2392 +1,2414 b''
1 # merge.py - directory-level update/merge handling for Mercurial
1 # merge.py - directory-level update/merge handling for Mercurial
2 #
2 #
3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import collections
10 import collections
11 import errno
11 import errno
12 import stat
12 import stat
13 import struct
13 import struct
14
14
15 from .i18n import _
15 from .i18n import _
16 from .node import (
16 from .node import (
17 addednodeid,
17 addednodeid,
18 modifiednodeid,
18 modifiednodeid,
19 nullid,
19 nullid,
20 nullrev,
20 nullrev,
21 )
21 )
22 from .thirdparty import attr
22 from .thirdparty import attr
23 from . import (
23 from . import (
24 copies,
24 copies,
25 encoding,
25 encoding,
26 error,
26 error,
27 filemerge,
27 filemerge,
28 match as matchmod,
28 match as matchmod,
29 mergestate as mergestatemod,
29 mergestate as mergestatemod,
30 obsutil,
30 obsutil,
31 pathutil,
31 pathutil,
32 pycompat,
32 pycompat,
33 scmutil,
33 scmutil,
34 subrepoutil,
34 subrepoutil,
35 util,
35 util,
36 worker,
36 worker,
37 )
37 )
38
38
39 _pack = struct.pack
39 _pack = struct.pack
40 _unpack = struct.unpack
40 _unpack = struct.unpack
41
41
42
42
43 def _getcheckunknownconfig(repo, section, name):
43 def _getcheckunknownconfig(repo, section, name):
44 config = repo.ui.config(section, name)
44 config = repo.ui.config(section, name)
45 valid = [b'abort', b'ignore', b'warn']
45 valid = [b'abort', b'ignore', b'warn']
46 if config not in valid:
46 if config not in valid:
47 validstr = b', '.join([b"'" + v + b"'" for v in valid])
47 validstr = b', '.join([b"'" + v + b"'" for v in valid])
48 raise error.ConfigError(
48 raise error.ConfigError(
49 _(b"%s.%s not valid ('%s' is none of %s)")
49 _(b"%s.%s not valid ('%s' is none of %s)")
50 % (section, name, config, validstr)
50 % (section, name, config, validstr)
51 )
51 )
52 return config
52 return config
53
53
54
54
55 def _checkunknownfile(repo, wctx, mctx, f, f2=None):
55 def _checkunknownfile(repo, wctx, mctx, f, f2=None):
56 if wctx.isinmemory():
56 if wctx.isinmemory():
57 # Nothing to do in IMM because nothing in the "working copy" can be an
57 # Nothing to do in IMM because nothing in the "working copy" can be an
58 # unknown file.
58 # unknown file.
59 #
59 #
60 # Note that we should bail out here, not in ``_checkunknownfiles()``,
60 # Note that we should bail out here, not in ``_checkunknownfiles()``,
61 # because that function does other useful work.
61 # because that function does other useful work.
62 return False
62 return False
63
63
64 if f2 is None:
64 if f2 is None:
65 f2 = f
65 f2 = f
66 return (
66 return (
67 repo.wvfs.audit.check(f)
67 repo.wvfs.audit.check(f)
68 and repo.wvfs.isfileorlink(f)
68 and repo.wvfs.isfileorlink(f)
69 and repo.dirstate.normalize(f) not in repo.dirstate
69 and repo.dirstate.normalize(f) not in repo.dirstate
70 and mctx[f2].cmp(wctx[f])
70 and mctx[f2].cmp(wctx[f])
71 )
71 )
72
72
73
73
74 class _unknowndirschecker(object):
74 class _unknowndirschecker(object):
75 """
75 """
76 Look for any unknown files or directories that may have a path conflict
76 Look for any unknown files or directories that may have a path conflict
77 with a file. If any path prefix of the file exists as a file or link,
77 with a file. If any path prefix of the file exists as a file or link,
78 then it conflicts. If the file itself is a directory that contains any
78 then it conflicts. If the file itself is a directory that contains any
79 file that is not tracked, then it conflicts.
79 file that is not tracked, then it conflicts.
80
80
81 Returns the shortest path at which a conflict occurs, or None if there is
81 Returns the shortest path at which a conflict occurs, or None if there is
82 no conflict.
82 no conflict.
83 """
83 """
84
84
85 def __init__(self):
85 def __init__(self):
86 # A set of paths known to be good. This prevents repeated checking of
86 # A set of paths known to be good. This prevents repeated checking of
87 # dirs. It will be updated with any new dirs that are checked and found
87 # dirs. It will be updated with any new dirs that are checked and found
88 # to be safe.
88 # to be safe.
89 self._unknowndircache = set()
89 self._unknowndircache = set()
90
90
91 # A set of paths that are known to be absent. This prevents repeated
91 # A set of paths that are known to be absent. This prevents repeated
92 # checking of subdirectories that are known not to exist. It will be
92 # checking of subdirectories that are known not to exist. It will be
93 # updated with any new dirs that are checked and found to be absent.
93 # updated with any new dirs that are checked and found to be absent.
94 self._missingdircache = set()
94 self._missingdircache = set()
95
95
96 def __call__(self, repo, wctx, f):
96 def __call__(self, repo, wctx, f):
97 if wctx.isinmemory():
97 if wctx.isinmemory():
98 # Nothing to do in IMM for the same reason as ``_checkunknownfile``.
98 # Nothing to do in IMM for the same reason as ``_checkunknownfile``.
99 return False
99 return False
100
100
101 # Check for path prefixes that exist as unknown files.
101 # Check for path prefixes that exist as unknown files.
102 for p in reversed(list(pathutil.finddirs(f))):
102 for p in reversed(list(pathutil.finddirs(f))):
103 if p in self._missingdircache:
103 if p in self._missingdircache:
104 return
104 return
105 if p in self._unknowndircache:
105 if p in self._unknowndircache:
106 continue
106 continue
107 if repo.wvfs.audit.check(p):
107 if repo.wvfs.audit.check(p):
108 if (
108 if (
109 repo.wvfs.isfileorlink(p)
109 repo.wvfs.isfileorlink(p)
110 and repo.dirstate.normalize(p) not in repo.dirstate
110 and repo.dirstate.normalize(p) not in repo.dirstate
111 ):
111 ):
112 return p
112 return p
113 if not repo.wvfs.lexists(p):
113 if not repo.wvfs.lexists(p):
114 self._missingdircache.add(p)
114 self._missingdircache.add(p)
115 return
115 return
116 self._unknowndircache.add(p)
116 self._unknowndircache.add(p)
117
117
118 # Check if the file conflicts with a directory containing unknown files.
118 # Check if the file conflicts with a directory containing unknown files.
119 if repo.wvfs.audit.check(f) and repo.wvfs.isdir(f):
119 if repo.wvfs.audit.check(f) and repo.wvfs.isdir(f):
120 # Does the directory contain any files that are not in the dirstate?
120 # Does the directory contain any files that are not in the dirstate?
121 for p, dirs, files in repo.wvfs.walk(f):
121 for p, dirs, files in repo.wvfs.walk(f):
122 for fn in files:
122 for fn in files:
123 relf = util.pconvert(repo.wvfs.reljoin(p, fn))
123 relf = util.pconvert(repo.wvfs.reljoin(p, fn))
124 relf = repo.dirstate.normalize(relf, isknown=True)
124 relf = repo.dirstate.normalize(relf, isknown=True)
125 if relf not in repo.dirstate:
125 if relf not in repo.dirstate:
126 return f
126 return f
127 return None
127 return None
128
128
129
129
130 def _checkunknownfiles(repo, wctx, mctx, force, mresult, mergeforce):
130 def _checkunknownfiles(repo, wctx, mctx, force, mresult, mergeforce):
131 """
131 """
132 Considers any actions that care about the presence of conflicting unknown
132 Considers any actions that care about the presence of conflicting unknown
133 files. For some actions, the result is to abort; for others, it is to
133 files. For some actions, the result is to abort; for others, it is to
134 choose a different action.
134 choose a different action.
135 """
135 """
136 fileconflicts = set()
136 fileconflicts = set()
137 pathconflicts = set()
137 pathconflicts = set()
138 warnconflicts = set()
138 warnconflicts = set()
139 abortconflicts = set()
139 abortconflicts = set()
140 unknownconfig = _getcheckunknownconfig(repo, b'merge', b'checkunknown')
140 unknownconfig = _getcheckunknownconfig(repo, b'merge', b'checkunknown')
141 ignoredconfig = _getcheckunknownconfig(repo, b'merge', b'checkignored')
141 ignoredconfig = _getcheckunknownconfig(repo, b'merge', b'checkignored')
142 pathconfig = repo.ui.configbool(
142 pathconfig = repo.ui.configbool(
143 b'experimental', b'merge.checkpathconflicts'
143 b'experimental', b'merge.checkpathconflicts'
144 )
144 )
145 if not force:
145 if not force:
146
146
147 def collectconflicts(conflicts, config):
147 def collectconflicts(conflicts, config):
148 if config == b'abort':
148 if config == b'abort':
149 abortconflicts.update(conflicts)
149 abortconflicts.update(conflicts)
150 elif config == b'warn':
150 elif config == b'warn':
151 warnconflicts.update(conflicts)
151 warnconflicts.update(conflicts)
152
152
153 checkunknowndirs = _unknowndirschecker()
153 checkunknowndirs = _unknowndirschecker()
154 for f in mresult.files(
154 for f in mresult.files(
155 (
155 (
156 mergestatemod.ACTION_CREATED,
156 mergestatemod.ACTION_CREATED,
157 mergestatemod.ACTION_DELETED_CHANGED,
157 mergestatemod.ACTION_DELETED_CHANGED,
158 )
158 )
159 ):
159 ):
160 if _checkunknownfile(repo, wctx, mctx, f):
160 if _checkunknownfile(repo, wctx, mctx, f):
161 fileconflicts.add(f)
161 fileconflicts.add(f)
162 elif pathconfig and f not in wctx:
162 elif pathconfig and f not in wctx:
163 path = checkunknowndirs(repo, wctx, f)
163 path = checkunknowndirs(repo, wctx, f)
164 if path is not None:
164 if path is not None:
165 pathconflicts.add(path)
165 pathconflicts.add(path)
166 for f, args, msg in mresult.getactions(
166 for f, args, msg in mresult.getactions(
167 [mergestatemod.ACTION_LOCAL_DIR_RENAME_GET]
167 [mergestatemod.ACTION_LOCAL_DIR_RENAME_GET]
168 ):
168 ):
169 if _checkunknownfile(repo, wctx, mctx, f, args[0]):
169 if _checkunknownfile(repo, wctx, mctx, f, args[0]):
170 fileconflicts.add(f)
170 fileconflicts.add(f)
171
171
172 allconflicts = fileconflicts | pathconflicts
172 allconflicts = fileconflicts | pathconflicts
173 ignoredconflicts = {c for c in allconflicts if repo.dirstate._ignore(c)}
173 ignoredconflicts = {c for c in allconflicts if repo.dirstate._ignore(c)}
174 unknownconflicts = allconflicts - ignoredconflicts
174 unknownconflicts = allconflicts - ignoredconflicts
175 collectconflicts(ignoredconflicts, ignoredconfig)
175 collectconflicts(ignoredconflicts, ignoredconfig)
176 collectconflicts(unknownconflicts, unknownconfig)
176 collectconflicts(unknownconflicts, unknownconfig)
177 else:
177 else:
178 for f, args, msg in list(
178 for f, args, msg in list(
179 mresult.getactions([mergestatemod.ACTION_CREATED_MERGE])
179 mresult.getactions([mergestatemod.ACTION_CREATED_MERGE])
180 ):
180 ):
181 fl2, anc = args
181 fl2, anc = args
182 different = _checkunknownfile(repo, wctx, mctx, f)
182 different = _checkunknownfile(repo, wctx, mctx, f)
183 if repo.dirstate._ignore(f):
183 if repo.dirstate._ignore(f):
184 config = ignoredconfig
184 config = ignoredconfig
185 else:
185 else:
186 config = unknownconfig
186 config = unknownconfig
187
187
188 # The behavior when force is True is described by this table:
188 # The behavior when force is True is described by this table:
189 # config different mergeforce | action backup
189 # config different mergeforce | action backup
190 # * n * | get n
190 # * n * | get n
191 # * y y | merge -
191 # * y y | merge -
192 # abort y n | merge - (1)
192 # abort y n | merge - (1)
193 # warn y n | warn + get y
193 # warn y n | warn + get y
194 # ignore y n | get y
194 # ignore y n | get y
195 #
195 #
196 # (1) this is probably the wrong behavior here -- we should
196 # (1) this is probably the wrong behavior here -- we should
197 # probably abort, but some actions like rebases currently
197 # probably abort, but some actions like rebases currently
198 # don't like an abort happening in the middle of
198 # don't like an abort happening in the middle of
199 # merge.update.
199 # merge.update.
200 if not different:
200 if not different:
201 mresult.addfile(
201 mresult.addfile(
202 f,
202 f,
203 mergestatemod.ACTION_GET,
203 mergestatemod.ACTION_GET,
204 (fl2, False),
204 (fl2, False),
205 b'remote created',
205 b'remote created',
206 )
206 )
207 elif mergeforce or config == b'abort':
207 elif mergeforce or config == b'abort':
208 mresult.addfile(
208 mresult.addfile(
209 f,
209 f,
210 mergestatemod.ACTION_MERGE,
210 mergestatemod.ACTION_MERGE,
211 (f, f, None, False, anc),
211 (f, f, None, False, anc),
212 b'remote differs from untracked local',
212 b'remote differs from untracked local',
213 )
213 )
214 elif config == b'abort':
214 elif config == b'abort':
215 abortconflicts.add(f)
215 abortconflicts.add(f)
216 else:
216 else:
217 if config == b'warn':
217 if config == b'warn':
218 warnconflicts.add(f)
218 warnconflicts.add(f)
219 mresult.addfile(
219 mresult.addfile(
220 f,
220 f,
221 mergestatemod.ACTION_GET,
221 mergestatemod.ACTION_GET,
222 (fl2, True),
222 (fl2, True),
223 b'remote created',
223 b'remote created',
224 )
224 )
225
225
226 for f in sorted(abortconflicts):
226 for f in sorted(abortconflicts):
227 warn = repo.ui.warn
227 warn = repo.ui.warn
228 if f in pathconflicts:
228 if f in pathconflicts:
229 if repo.wvfs.isfileorlink(f):
229 if repo.wvfs.isfileorlink(f):
230 warn(_(b"%s: untracked file conflicts with directory\n") % f)
230 warn(_(b"%s: untracked file conflicts with directory\n") % f)
231 else:
231 else:
232 warn(_(b"%s: untracked directory conflicts with file\n") % f)
232 warn(_(b"%s: untracked directory conflicts with file\n") % f)
233 else:
233 else:
234 warn(_(b"%s: untracked file differs\n") % f)
234 warn(_(b"%s: untracked file differs\n") % f)
235 if abortconflicts:
235 if abortconflicts:
236 raise error.Abort(
236 raise error.Abort(
237 _(
237 _(
238 b"untracked files in working directory "
238 b"untracked files in working directory "
239 b"differ from files in requested revision"
239 b"differ from files in requested revision"
240 )
240 )
241 )
241 )
242
242
243 for f in sorted(warnconflicts):
243 for f in sorted(warnconflicts):
244 if repo.wvfs.isfileorlink(f):
244 if repo.wvfs.isfileorlink(f):
245 repo.ui.warn(_(b"%s: replacing untracked file\n") % f)
245 repo.ui.warn(_(b"%s: replacing untracked file\n") % f)
246 else:
246 else:
247 repo.ui.warn(_(b"%s: replacing untracked files in directory\n") % f)
247 repo.ui.warn(_(b"%s: replacing untracked files in directory\n") % f)
248
248
249 for f, args, msg in list(
249 for f, args, msg in list(
250 mresult.getactions([mergestatemod.ACTION_CREATED])
250 mresult.getactions([mergestatemod.ACTION_CREATED])
251 ):
251 ):
252 backup = (
252 backup = (
253 f in fileconflicts
253 f in fileconflicts
254 or f in pathconflicts
254 or f in pathconflicts
255 or any(p in pathconflicts for p in pathutil.finddirs(f))
255 or any(p in pathconflicts for p in pathutil.finddirs(f))
256 )
256 )
257 (flags,) = args
257 (flags,) = args
258 mresult.addfile(f, mergestatemod.ACTION_GET, (flags, backup), msg)
258 mresult.addfile(f, mergestatemod.ACTION_GET, (flags, backup), msg)
259
259
260
260
261 def _forgetremoved(wctx, mctx, branchmerge, mresult):
261 def _forgetremoved(wctx, mctx, branchmerge, mresult):
262 """
262 """
263 Forget removed files
263 Forget removed files
264
264
265 If we're jumping between revisions (as opposed to merging), and if
265 If we're jumping between revisions (as opposed to merging), and if
266 neither the working directory nor the target rev has the file,
266 neither the working directory nor the target rev has the file,
267 then we need to remove it from the dirstate, to prevent the
267 then we need to remove it from the dirstate, to prevent the
268 dirstate from listing the file when it is no longer in the
268 dirstate from listing the file when it is no longer in the
269 manifest.
269 manifest.
270
270
271 If we're merging, and the other revision has removed a file
271 If we're merging, and the other revision has removed a file
272 that is not present in the working directory, we need to mark it
272 that is not present in the working directory, we need to mark it
273 as removed.
273 as removed.
274 """
274 """
275
275
276 m = mergestatemod.ACTION_FORGET
276 m = mergestatemod.ACTION_FORGET
277 if branchmerge:
277 if branchmerge:
278 m = mergestatemod.ACTION_REMOVE
278 m = mergestatemod.ACTION_REMOVE
279 for f in wctx.deleted():
279 for f in wctx.deleted():
280 if f not in mctx:
280 if f not in mctx:
281 mresult.addfile(f, m, None, b"forget deleted")
281 mresult.addfile(f, m, None, b"forget deleted")
282
282
283 if not branchmerge:
283 if not branchmerge:
284 for f in wctx.removed():
284 for f in wctx.removed():
285 if f not in mctx:
285 if f not in mctx:
286 mresult.addfile(
286 mresult.addfile(
287 f,
287 f,
288 mergestatemod.ACTION_FORGET,
288 mergestatemod.ACTION_FORGET,
289 None,
289 None,
290 b"forget removed",
290 b"forget removed",
291 )
291 )
292
292
293
293
294 def _checkcollision(repo, wmf, mresult):
294 def _checkcollision(repo, wmf, mresult):
295 """
295 """
296 Check for case-folding collisions.
296 Check for case-folding collisions.
297 """
297 """
298 # If the repo is narrowed, filter out files outside the narrowspec.
298 # If the repo is narrowed, filter out files outside the narrowspec.
299 narrowmatch = repo.narrowmatch()
299 narrowmatch = repo.narrowmatch()
300 if not narrowmatch.always():
300 if not narrowmatch.always():
301 pmmf = set(wmf.walk(narrowmatch))
301 pmmf = set(wmf.walk(narrowmatch))
302 if mresult:
302 if mresult:
303 for f in list(mresult.files()):
303 for f in list(mresult.files()):
304 if not narrowmatch(f):
304 if not narrowmatch(f):
305 mresult.removefile(f)
305 mresult.removefile(f)
306 else:
306 else:
307 # build provisional merged manifest up
307 # build provisional merged manifest up
308 pmmf = set(wmf)
308 pmmf = set(wmf)
309
309
310 if mresult:
310 if mresult:
311 # KEEP and EXEC are no-op
311 # KEEP and EXEC are no-op
312 for f in mresult.files(
312 for f in mresult.files(
313 (
313 (
314 mergestatemod.ACTION_ADD,
314 mergestatemod.ACTION_ADD,
315 mergestatemod.ACTION_ADD_MODIFIED,
315 mergestatemod.ACTION_ADD_MODIFIED,
316 mergestatemod.ACTION_FORGET,
316 mergestatemod.ACTION_FORGET,
317 mergestatemod.ACTION_GET,
317 mergestatemod.ACTION_GET,
318 mergestatemod.ACTION_CHANGED_DELETED,
318 mergestatemod.ACTION_CHANGED_DELETED,
319 mergestatemod.ACTION_DELETED_CHANGED,
319 mergestatemod.ACTION_DELETED_CHANGED,
320 )
320 )
321 ):
321 ):
322 pmmf.add(f)
322 pmmf.add(f)
323 for f in mresult.files((mergestatemod.ACTION_REMOVE,)):
323 for f in mresult.files((mergestatemod.ACTION_REMOVE,)):
324 pmmf.discard(f)
324 pmmf.discard(f)
325 for f, args, msg in mresult.getactions(
325 for f, args, msg in mresult.getactions(
326 [mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL]
326 [mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL]
327 ):
327 ):
328 f2, flags = args
328 f2, flags = args
329 pmmf.discard(f2)
329 pmmf.discard(f2)
330 pmmf.add(f)
330 pmmf.add(f)
331 for f in mresult.files((mergestatemod.ACTION_LOCAL_DIR_RENAME_GET,)):
331 for f in mresult.files((mergestatemod.ACTION_LOCAL_DIR_RENAME_GET,)):
332 pmmf.add(f)
332 pmmf.add(f)
333 for f, args, msg in mresult.getactions([mergestatemod.ACTION_MERGE]):
333 for f, args, msg in mresult.getactions([mergestatemod.ACTION_MERGE]):
334 f1, f2, fa, move, anc = args
334 f1, f2, fa, move, anc = args
335 if move:
335 if move:
336 pmmf.discard(f1)
336 pmmf.discard(f1)
337 pmmf.add(f)
337 pmmf.add(f)
338
338
339 # check case-folding collision in provisional merged manifest
339 # check case-folding collision in provisional merged manifest
340 foldmap = {}
340 foldmap = {}
341 for f in pmmf:
341 for f in pmmf:
342 fold = util.normcase(f)
342 fold = util.normcase(f)
343 if fold in foldmap:
343 if fold in foldmap:
344 raise error.Abort(
344 raise error.Abort(
345 _(b"case-folding collision between %s and %s")
345 _(b"case-folding collision between %s and %s")
346 % (f, foldmap[fold])
346 % (f, foldmap[fold])
347 )
347 )
348 foldmap[fold] = f
348 foldmap[fold] = f
349
349
350 # check case-folding of directories
350 # check case-folding of directories
351 foldprefix = unfoldprefix = lastfull = b''
351 foldprefix = unfoldprefix = lastfull = b''
352 for fold, f in sorted(foldmap.items()):
352 for fold, f in sorted(foldmap.items()):
353 if fold.startswith(foldprefix) and not f.startswith(unfoldprefix):
353 if fold.startswith(foldprefix) and not f.startswith(unfoldprefix):
354 # the folded prefix matches but actual casing is different
354 # the folded prefix matches but actual casing is different
355 raise error.Abort(
355 raise error.Abort(
356 _(b"case-folding collision between %s and directory of %s")
356 _(b"case-folding collision between %s and directory of %s")
357 % (lastfull, f)
357 % (lastfull, f)
358 )
358 )
359 foldprefix = fold + b'/'
359 foldprefix = fold + b'/'
360 unfoldprefix = f + b'/'
360 unfoldprefix = f + b'/'
361 lastfull = f
361 lastfull = f
362
362
363
363
364 def _filesindirs(repo, manifest, dirs):
364 def _filesindirs(repo, manifest, dirs):
365 """
365 """
366 Generator that yields pairs of all the files in the manifest that are found
366 Generator that yields pairs of all the files in the manifest that are found
367 inside the directories listed in dirs, and which directory they are found
367 inside the directories listed in dirs, and which directory they are found
368 in.
368 in.
369 """
369 """
370 for f in manifest:
370 for f in manifest:
371 for p in pathutil.finddirs(f):
371 for p in pathutil.finddirs(f):
372 if p in dirs:
372 if p in dirs:
373 yield f, p
373 yield f, p
374 break
374 break
375
375
376
376
377 def checkpathconflicts(repo, wctx, mctx, mresult):
377 def checkpathconflicts(repo, wctx, mctx, mresult):
378 """
378 """
379 Check if any actions introduce path conflicts in the repository, updating
379 Check if any actions introduce path conflicts in the repository, updating
380 actions to record or handle the path conflict accordingly.
380 actions to record or handle the path conflict accordingly.
381 """
381 """
382 mf = wctx.manifest()
382 mf = wctx.manifest()
383
383
384 # The set of local files that conflict with a remote directory.
384 # The set of local files that conflict with a remote directory.
385 localconflicts = set()
385 localconflicts = set()
386
386
387 # The set of directories that conflict with a remote file, and so may cause
387 # The set of directories that conflict with a remote file, and so may cause
388 # conflicts if they still contain any files after the merge.
388 # conflicts if they still contain any files after the merge.
389 remoteconflicts = set()
389 remoteconflicts = set()
390
390
391 # The set of directories that appear as both a file and a directory in the
391 # The set of directories that appear as both a file and a directory in the
392 # remote manifest. These indicate an invalid remote manifest, which
392 # remote manifest. These indicate an invalid remote manifest, which
393 # can't be updated to cleanly.
393 # can't be updated to cleanly.
394 invalidconflicts = set()
394 invalidconflicts = set()
395
395
396 # The set of directories that contain files that are being created.
396 # The set of directories that contain files that are being created.
397 createdfiledirs = set()
397 createdfiledirs = set()
398
398
399 # The set of files deleted by all the actions.
399 # The set of files deleted by all the actions.
400 deletedfiles = set()
400 deletedfiles = set()
401
401
402 for f in mresult.files(
402 for f in mresult.files(
403 (
403 (
404 mergestatemod.ACTION_CREATED,
404 mergestatemod.ACTION_CREATED,
405 mergestatemod.ACTION_DELETED_CHANGED,
405 mergestatemod.ACTION_DELETED_CHANGED,
406 mergestatemod.ACTION_MERGE,
406 mergestatemod.ACTION_MERGE,
407 mergestatemod.ACTION_CREATED_MERGE,
407 mergestatemod.ACTION_CREATED_MERGE,
408 )
408 )
409 ):
409 ):
410 # This action may create a new local file.
410 # This action may create a new local file.
411 createdfiledirs.update(pathutil.finddirs(f))
411 createdfiledirs.update(pathutil.finddirs(f))
412 if mf.hasdir(f):
412 if mf.hasdir(f):
413 # The file aliases a local directory. This might be ok if all
413 # The file aliases a local directory. This might be ok if all
414 # the files in the local directory are being deleted. This
414 # the files in the local directory are being deleted. This
415 # will be checked once we know what all the deleted files are.
415 # will be checked once we know what all the deleted files are.
416 remoteconflicts.add(f)
416 remoteconflicts.add(f)
417 # Track the names of all deleted files.
417 # Track the names of all deleted files.
418 for f in mresult.files((mergestatemod.ACTION_REMOVE,)):
418 for f in mresult.files((mergestatemod.ACTION_REMOVE,)):
419 deletedfiles.add(f)
419 deletedfiles.add(f)
420 for (f, args, msg) in mresult.getactions((mergestatemod.ACTION_MERGE,)):
420 for (f, args, msg) in mresult.getactions((mergestatemod.ACTION_MERGE,)):
421 f1, f2, fa, move, anc = args
421 f1, f2, fa, move, anc = args
422 if move:
422 if move:
423 deletedfiles.add(f1)
423 deletedfiles.add(f1)
424 for (f, args, msg) in mresult.getactions(
424 for (f, args, msg) in mresult.getactions(
425 (mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL,)
425 (mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL,)
426 ):
426 ):
427 f2, flags = args
427 f2, flags = args
428 deletedfiles.add(f2)
428 deletedfiles.add(f2)
429
429
430 # Check all directories that contain created files for path conflicts.
430 # Check all directories that contain created files for path conflicts.
431 for p in createdfiledirs:
431 for p in createdfiledirs:
432 if p in mf:
432 if p in mf:
433 if p in mctx:
433 if p in mctx:
434 # A file is in a directory which aliases both a local
434 # A file is in a directory which aliases both a local
435 # and a remote file. This is an internal inconsistency
435 # and a remote file. This is an internal inconsistency
436 # within the remote manifest.
436 # within the remote manifest.
437 invalidconflicts.add(p)
437 invalidconflicts.add(p)
438 else:
438 else:
439 # A file is in a directory which aliases a local file.
439 # A file is in a directory which aliases a local file.
440 # We will need to rename the local file.
440 # We will need to rename the local file.
441 localconflicts.add(p)
441 localconflicts.add(p)
442 pd = mresult.getfile(p)
442 pd = mresult.getfile(p)
443 if pd and pd[0] in (
443 if pd and pd[0] in (
444 mergestatemod.ACTION_CREATED,
444 mergestatemod.ACTION_CREATED,
445 mergestatemod.ACTION_DELETED_CHANGED,
445 mergestatemod.ACTION_DELETED_CHANGED,
446 mergestatemod.ACTION_MERGE,
446 mergestatemod.ACTION_MERGE,
447 mergestatemod.ACTION_CREATED_MERGE,
447 mergestatemod.ACTION_CREATED_MERGE,
448 ):
448 ):
449 # The file is in a directory which aliases a remote file.
449 # The file is in a directory which aliases a remote file.
450 # This is an internal inconsistency within the remote
450 # This is an internal inconsistency within the remote
451 # manifest.
451 # manifest.
452 invalidconflicts.add(p)
452 invalidconflicts.add(p)
453
453
454 # Rename all local conflicting files that have not been deleted.
454 # Rename all local conflicting files that have not been deleted.
455 for p in localconflicts:
455 for p in localconflicts:
456 if p not in deletedfiles:
456 if p not in deletedfiles:
457 ctxname = bytes(wctx).rstrip(b'+')
457 ctxname = bytes(wctx).rstrip(b'+')
458 pnew = util.safename(p, ctxname, wctx, set(mresult.files()))
458 pnew = util.safename(p, ctxname, wctx, set(mresult.files()))
459 porig = wctx[p].copysource() or p
459 porig = wctx[p].copysource() or p
460 mresult.addfile(
460 mresult.addfile(
461 pnew,
461 pnew,
462 mergestatemod.ACTION_PATH_CONFLICT_RESOLVE,
462 mergestatemod.ACTION_PATH_CONFLICT_RESOLVE,
463 (p, porig),
463 (p, porig),
464 b'local path conflict',
464 b'local path conflict',
465 )
465 )
466 mresult.addfile(
466 mresult.addfile(
467 p,
467 p,
468 mergestatemod.ACTION_PATH_CONFLICT,
468 mergestatemod.ACTION_PATH_CONFLICT,
469 (pnew, b'l'),
469 (pnew, b'l'),
470 b'path conflict',
470 b'path conflict',
471 )
471 )
472
472
473 if remoteconflicts:
473 if remoteconflicts:
474 # Check if all files in the conflicting directories have been removed.
474 # Check if all files in the conflicting directories have been removed.
475 ctxname = bytes(mctx).rstrip(b'+')
475 ctxname = bytes(mctx).rstrip(b'+')
476 for f, p in _filesindirs(repo, mf, remoteconflicts):
476 for f, p in _filesindirs(repo, mf, remoteconflicts):
477 if f not in deletedfiles:
477 if f not in deletedfiles:
478 m, args, msg = mresult.getfile(p)
478 m, args, msg = mresult.getfile(p)
479 pnew = util.safename(p, ctxname, wctx, set(mresult.files()))
479 pnew = util.safename(p, ctxname, wctx, set(mresult.files()))
480 if m in (
480 if m in (
481 mergestatemod.ACTION_DELETED_CHANGED,
481 mergestatemod.ACTION_DELETED_CHANGED,
482 mergestatemod.ACTION_MERGE,
482 mergestatemod.ACTION_MERGE,
483 ):
483 ):
484 # Action was merge, just update target.
484 # Action was merge, just update target.
485 mresult.addfile(pnew, m, args, msg)
485 mresult.addfile(pnew, m, args, msg)
486 else:
486 else:
487 # Action was create, change to renamed get action.
487 # Action was create, change to renamed get action.
488 fl = args[0]
488 fl = args[0]
489 mresult.addfile(
489 mresult.addfile(
490 pnew,
490 pnew,
491 mergestatemod.ACTION_LOCAL_DIR_RENAME_GET,
491 mergestatemod.ACTION_LOCAL_DIR_RENAME_GET,
492 (p, fl),
492 (p, fl),
493 b'remote path conflict',
493 b'remote path conflict',
494 )
494 )
495 mresult.addfile(
495 mresult.addfile(
496 p,
496 p,
497 mergestatemod.ACTION_PATH_CONFLICT,
497 mergestatemod.ACTION_PATH_CONFLICT,
498 (pnew, mergestatemod.ACTION_REMOVE),
498 (pnew, mergestatemod.ACTION_REMOVE),
499 b'path conflict',
499 b'path conflict',
500 )
500 )
501 remoteconflicts.remove(p)
501 remoteconflicts.remove(p)
502 break
502 break
503
503
504 if invalidconflicts:
504 if invalidconflicts:
505 for p in invalidconflicts:
505 for p in invalidconflicts:
506 repo.ui.warn(_(b"%s: is both a file and a directory\n") % p)
506 repo.ui.warn(_(b"%s: is both a file and a directory\n") % p)
507 raise error.Abort(_(b"destination manifest contains path conflicts"))
507 raise error.Abort(_(b"destination manifest contains path conflicts"))
508
508
509
509
510 def _filternarrowactions(narrowmatch, branchmerge, mresult):
510 def _filternarrowactions(narrowmatch, branchmerge, mresult):
511 """
511 """
512 Filters out actions that can ignored because the repo is narrowed.
512 Filters out actions that can ignored because the repo is narrowed.
513
513
514 Raise an exception if the merge cannot be completed because the repo is
514 Raise an exception if the merge cannot be completed because the repo is
515 narrowed.
515 narrowed.
516 """
516 """
517 # TODO: handle with nonconflicttypes
517 # TODO: handle with nonconflicttypes
518 nonconflicttypes = {
518 nonconflicttypes = {
519 mergestatemod.ACTION_ADD,
519 mergestatemod.ACTION_ADD,
520 mergestatemod.ACTION_ADD_MODIFIED,
520 mergestatemod.ACTION_ADD_MODIFIED,
521 mergestatemod.ACTION_CREATED,
521 mergestatemod.ACTION_CREATED,
522 mergestatemod.ACTION_CREATED_MERGE,
522 mergestatemod.ACTION_CREATED_MERGE,
523 mergestatemod.ACTION_FORGET,
523 mergestatemod.ACTION_FORGET,
524 mergestatemod.ACTION_GET,
524 mergestatemod.ACTION_GET,
525 mergestatemod.ACTION_REMOVE,
525 mergestatemod.ACTION_REMOVE,
526 mergestatemod.ACTION_EXEC,
526 mergestatemod.ACTION_EXEC,
527 }
527 }
528 # We mutate the items in the dict during iteration, so iterate
528 # We mutate the items in the dict during iteration, so iterate
529 # over a copy.
529 # over a copy.
530 for f, action in mresult.filemap():
530 for f, action in mresult.filemap():
531 if narrowmatch(f):
531 if narrowmatch(f):
532 pass
532 pass
533 elif not branchmerge:
533 elif not branchmerge:
534 mresult.removefile(f) # just updating, ignore changes outside clone
534 mresult.removefile(f) # just updating, ignore changes outside clone
535 elif action[0] in mergestatemod.NO_OP_ACTIONS:
535 elif action[0] in mergestatemod.NO_OP_ACTIONS:
536 mresult.removefile(f) # merge does not affect file
536 mresult.removefile(f) # merge does not affect file
537 elif action[0] in nonconflicttypes:
537 elif action[0] in nonconflicttypes:
538 raise error.Abort(
538 raise error.Abort(
539 _(
539 _(
540 b'merge affects file \'%s\' outside narrow, '
540 b'merge affects file \'%s\' outside narrow, '
541 b'which is not yet supported'
541 b'which is not yet supported'
542 )
542 )
543 % f,
543 % f,
544 hint=_(b'merging in the other direction may work'),
544 hint=_(b'merging in the other direction may work'),
545 )
545 )
546 else:
546 else:
547 raise error.Abort(
547 raise error.Abort(
548 _(b'conflict in file \'%s\' is outside narrow clone') % f
548 _(b'conflict in file \'%s\' is outside narrow clone') % f
549 )
549 )
550
550
551
551
552 class mergeresult(object):
552 class mergeresult(object):
553 """An object representing result of merging manifests.
553 """An object representing result of merging manifests.
554
554
555 It has information about what actions need to be performed on dirstate
555 It has information about what actions need to be performed on dirstate
556 mapping of divergent renames and other such cases."""
556 mapping of divergent renames and other such cases."""
557
557
558 def __init__(self):
558 def __init__(self):
559 """
559 """
560 filemapping: dict of filename as keys and action related info as values
560 filemapping: dict of filename as keys and action related info as values
561 diverge: mapping of source name -> list of dest name for
561 diverge: mapping of source name -> list of dest name for
562 divergent renames
562 divergent renames
563 renamedelete: mapping of source name -> list of destinations for files
563 renamedelete: mapping of source name -> list of destinations for files
564 deleted on one side and renamed on other.
564 deleted on one side and renamed on other.
565 commitinfo: dict containing data which should be used on commit
565 commitinfo: dict containing data which should be used on commit
566 contains a filename -> info mapping
566 contains a filename -> info mapping
567 actionmapping: dict of action names as keys and values are dict of
567 actionmapping: dict of action names as keys and values are dict of
568 filename as key and related data as values
568 filename as key and related data as values
569 """
569 """
570 self._filemapping = {}
570 self._filemapping = {}
571 self._diverge = {}
571 self._diverge = {}
572 self._renamedelete = {}
572 self._renamedelete = {}
573 self._commitinfo = collections.defaultdict(dict)
573 self._commitinfo = collections.defaultdict(dict)
574 self._actionmapping = collections.defaultdict(dict)
574 self._actionmapping = collections.defaultdict(dict)
575
575
576 def updatevalues(self, diverge, renamedelete):
576 def updatevalues(self, diverge, renamedelete):
577 self._diverge = diverge
577 self._diverge = diverge
578 self._renamedelete = renamedelete
578 self._renamedelete = renamedelete
579
579
580 def addfile(self, filename, action, data, message):
580 def addfile(self, filename, action, data, message):
581 """adds a new file to the mergeresult object
581 """adds a new file to the mergeresult object
582
582
583 filename: file which we are adding
583 filename: file which we are adding
584 action: one of mergestatemod.ACTION_*
584 action: one of mergestatemod.ACTION_*
585 data: a tuple of information like fctx and ctx related to this merge
585 data: a tuple of information like fctx and ctx related to this merge
586 message: a message about the merge
586 message: a message about the merge
587 """
587 """
588 # if the file already existed, we need to delete it's old
588 # if the file already existed, we need to delete it's old
589 # entry form _actionmapping too
589 # entry form _actionmapping too
590 if filename in self._filemapping:
590 if filename in self._filemapping:
591 a, d, m = self._filemapping[filename]
591 a, d, m = self._filemapping[filename]
592 del self._actionmapping[a][filename]
592 del self._actionmapping[a][filename]
593
593
594 self._filemapping[filename] = (action, data, message)
594 self._filemapping[filename] = (action, data, message)
595 self._actionmapping[action][filename] = (data, message)
595 self._actionmapping[action][filename] = (data, message)
596
596
597 def getfile(self, filename, default_return=None):
597 def getfile(self, filename, default_return=None):
598 """returns (action, args, msg) about this file
598 """returns (action, args, msg) about this file
599
599
600 returns default_return if the file is not present"""
600 returns default_return if the file is not present"""
601 if filename in self._filemapping:
601 if filename in self._filemapping:
602 return self._filemapping[filename]
602 return self._filemapping[filename]
603 return default_return
603 return default_return
604
604
605 def files(self, actions=None):
605 def files(self, actions=None):
606 """returns files on which provided action needs to perfromed
606 """returns files on which provided action needs to perfromed
607
607
608 If actions is None, all files are returned
608 If actions is None, all files are returned
609 """
609 """
610 # TODO: think whether we should return renamedelete and
610 # TODO: think whether we should return renamedelete and
611 # diverge filenames also
611 # diverge filenames also
612 if actions is None:
612 if actions is None:
613 for f in self._filemapping:
613 for f in self._filemapping:
614 yield f
614 yield f
615
615
616 else:
616 else:
617 for a in actions:
617 for a in actions:
618 for f in self._actionmapping[a]:
618 for f in self._actionmapping[a]:
619 yield f
619 yield f
620
620
621 def removefile(self, filename):
621 def removefile(self, filename):
622 """removes a file from the mergeresult object as the file might
622 """removes a file from the mergeresult object as the file might
623 not merging anymore"""
623 not merging anymore"""
624 action, data, message = self._filemapping[filename]
624 action, data, message = self._filemapping[filename]
625 del self._filemapping[filename]
625 del self._filemapping[filename]
626 del self._actionmapping[action][filename]
626 del self._actionmapping[action][filename]
627
627
628 def getactions(self, actions, sort=False):
628 def getactions(self, actions, sort=False):
629 """get list of files which are marked with these actions
629 """get list of files which are marked with these actions
630 if sort is true, files for each action is sorted and then added
630 if sort is true, files for each action is sorted and then added
631
631
632 Returns a list of tuple of form (filename, data, message)
632 Returns a list of tuple of form (filename, data, message)
633 """
633 """
634 for a in actions:
634 for a in actions:
635 if sort:
635 if sort:
636 for f in sorted(self._actionmapping[a]):
636 for f in sorted(self._actionmapping[a]):
637 args, msg = self._actionmapping[a][f]
637 args, msg = self._actionmapping[a][f]
638 yield f, args, msg
638 yield f, args, msg
639 else:
639 else:
640 for f, (args, msg) in pycompat.iteritems(
640 for f, (args, msg) in pycompat.iteritems(
641 self._actionmapping[a]
641 self._actionmapping[a]
642 ):
642 ):
643 yield f, args, msg
643 yield f, args, msg
644
644
645 def len(self, actions=None):
645 def len(self, actions=None):
646 """returns number of files which needs actions
646 """returns number of files which needs actions
647
647
648 if actions is passed, total of number of files in that action
648 if actions is passed, total of number of files in that action
649 only is returned"""
649 only is returned"""
650
650
651 if actions is None:
651 if actions is None:
652 return len(self._filemapping)
652 return len(self._filemapping)
653
653
654 return sum(len(self._actionmapping[a]) for a in actions)
654 return sum(len(self._actionmapping[a]) for a in actions)
655
655
656 def filemap(self, sort=False):
656 def filemap(self, sort=False):
657 if sorted:
657 if sorted:
658 for key, val in sorted(pycompat.iteritems(self._filemapping)):
658 for key, val in sorted(pycompat.iteritems(self._filemapping)):
659 yield key, val
659 yield key, val
660 else:
660 else:
661 for key, val in pycompat.iteritems(self._filemapping):
661 for key, val in pycompat.iteritems(self._filemapping):
662 yield key, val
662 yield key, val
663
663
664 def addcommitinfo(self, filename, key, value):
664 def addcommitinfo(self, filename, key, value):
665 """adds key-value information about filename which will be required
665 """adds key-value information about filename which will be required
666 while committing this merge"""
666 while committing this merge"""
667 self._commitinfo[filename][key] = value
667 self._commitinfo[filename][key] = value
668
668
669 @property
669 @property
670 def diverge(self):
670 def diverge(self):
671 return self._diverge
671 return self._diverge
672
672
673 @property
673 @property
674 def renamedelete(self):
674 def renamedelete(self):
675 return self._renamedelete
675 return self._renamedelete
676
676
677 @property
677 @property
678 def commitinfo(self):
678 def commitinfo(self):
679 return self._commitinfo
679 return self._commitinfo
680
680
681 @property
681 @property
682 def actionsdict(self):
682 def actionsdict(self):
683 """returns a dictionary of actions to be perfomed with action as key
683 """returns a dictionary of actions to be perfomed with action as key
684 and a list of files and related arguments as values"""
684 and a list of files and related arguments as values"""
685 res = collections.defaultdict(list)
685 res = collections.defaultdict(list)
686 for a, d in pycompat.iteritems(self._actionmapping):
686 for a, d in pycompat.iteritems(self._actionmapping):
687 for f, (args, msg) in pycompat.iteritems(d):
687 for f, (args, msg) in pycompat.iteritems(d):
688 res[a].append((f, args, msg))
688 res[a].append((f, args, msg))
689 return res
689 return res
690
690
691 def setactions(self, actions):
691 def setactions(self, actions):
692 self._filemapping = actions
692 self._filemapping = actions
693 self._actionmapping = collections.defaultdict(dict)
693 self._actionmapping = collections.defaultdict(dict)
694 for f, (act, data, msg) in pycompat.iteritems(self._filemapping):
694 for f, (act, data, msg) in pycompat.iteritems(self._filemapping):
695 self._actionmapping[act][f] = data, msg
695 self._actionmapping[act][f] = data, msg
696
696
697 def hasconflicts(self):
697 def hasconflicts(self):
698 """tells whether this merge resulted in some actions which can
698 """tells whether this merge resulted in some actions which can
699 result in conflicts or not"""
699 result in conflicts or not"""
700 for a in self._actionmapping.keys():
700 for a in self._actionmapping.keys():
701 if (
701 if (
702 a
702 a
703 not in (
703 not in (
704 mergestatemod.ACTION_GET,
704 mergestatemod.ACTION_GET,
705 mergestatemod.ACTION_EXEC,
705 mergestatemod.ACTION_EXEC,
706 mergestatemod.ACTION_REMOVE,
706 mergestatemod.ACTION_REMOVE,
707 mergestatemod.ACTION_PATH_CONFLICT_RESOLVE,
707 mergestatemod.ACTION_PATH_CONFLICT_RESOLVE,
708 )
708 )
709 and self._actionmapping[a]
709 and self._actionmapping[a]
710 and a not in mergestatemod.NO_OP_ACTIONS
710 and a not in mergestatemod.NO_OP_ACTIONS
711 ):
711 ):
712 return True
712 return True
713
713
714 return False
714 return False
715
715
716
716
717 def manifestmerge(
717 def manifestmerge(
718 repo,
718 repo,
719 wctx,
719 wctx,
720 p2,
720 p2,
721 pa,
721 pa,
722 branchmerge,
722 branchmerge,
723 force,
723 force,
724 matcher,
724 matcher,
725 acceptremote,
725 acceptremote,
726 followcopies,
726 followcopies,
727 forcefulldiff=False,
727 forcefulldiff=False,
728 ):
728 ):
729 """
729 """
730 Merge wctx and p2 with ancestor pa and generate merge action list
730 Merge wctx and p2 with ancestor pa and generate merge action list
731
731
732 branchmerge and force are as passed in to update
732 branchmerge and force are as passed in to update
733 matcher = matcher to filter file lists
733 matcher = matcher to filter file lists
734 acceptremote = accept the incoming changes without prompting
734 acceptremote = accept the incoming changes without prompting
735
735
736 Returns an object of mergeresult class
736 Returns an object of mergeresult class
737 """
737 """
738 mresult = mergeresult()
738 mresult = mergeresult()
739 if matcher is not None and matcher.always():
739 if matcher is not None and matcher.always():
740 matcher = None
740 matcher = None
741
741
742 # manifests fetched in order are going to be faster, so prime the caches
742 # manifests fetched in order are going to be faster, so prime the caches
743 [
743 [
744 x.manifest()
744 x.manifest()
745 for x in sorted(wctx.parents() + [p2, pa], key=scmutil.intrev)
745 for x in sorted(wctx.parents() + [p2, pa], key=scmutil.intrev)
746 ]
746 ]
747
747
748 branch_copies1 = copies.branch_copies()
748 branch_copies1 = copies.branch_copies()
749 branch_copies2 = copies.branch_copies()
749 branch_copies2 = copies.branch_copies()
750 diverge = {}
750 diverge = {}
751 # information from merge which is needed at commit time
751 # information from merge which is needed at commit time
752 # for example choosing filelog of which parent to commit
752 # for example choosing filelog of which parent to commit
753 # TODO: use specific constants in future for this mapping
753 # TODO: use specific constants in future for this mapping
754 if followcopies:
754 if followcopies:
755 branch_copies1, branch_copies2, diverge = copies.mergecopies(
755 branch_copies1, branch_copies2, diverge = copies.mergecopies(
756 repo, wctx, p2, pa
756 repo, wctx, p2, pa
757 )
757 )
758
758
759 boolbm = pycompat.bytestr(bool(branchmerge))
759 boolbm = pycompat.bytestr(bool(branchmerge))
760 boolf = pycompat.bytestr(bool(force))
760 boolf = pycompat.bytestr(bool(force))
761 boolm = pycompat.bytestr(bool(matcher))
761 boolm = pycompat.bytestr(bool(matcher))
762 repo.ui.note(_(b"resolving manifests\n"))
762 repo.ui.note(_(b"resolving manifests\n"))
763 repo.ui.debug(
763 repo.ui.debug(
764 b" branchmerge: %s, force: %s, partial: %s\n" % (boolbm, boolf, boolm)
764 b" branchmerge: %s, force: %s, partial: %s\n" % (boolbm, boolf, boolm)
765 )
765 )
766 repo.ui.debug(b" ancestor: %s, local: %s, remote: %s\n" % (pa, wctx, p2))
766 repo.ui.debug(b" ancestor: %s, local: %s, remote: %s\n" % (pa, wctx, p2))
767
767
768 m1, m2, ma = wctx.manifest(), p2.manifest(), pa.manifest()
768 m1, m2, ma = wctx.manifest(), p2.manifest(), pa.manifest()
769 copied1 = set(branch_copies1.copy.values())
769 copied1 = set(branch_copies1.copy.values())
770 copied1.update(branch_copies1.movewithdir.values())
770 copied1.update(branch_copies1.movewithdir.values())
771 copied2 = set(branch_copies2.copy.values())
771 copied2 = set(branch_copies2.copy.values())
772 copied2.update(branch_copies2.movewithdir.values())
772 copied2.update(branch_copies2.movewithdir.values())
773
773
774 if b'.hgsubstate' in m1 and wctx.rev() is None:
774 if b'.hgsubstate' in m1 and wctx.rev() is None:
775 # Check whether sub state is modified, and overwrite the manifest
775 # Check whether sub state is modified, and overwrite the manifest
776 # to flag the change. If wctx is a committed revision, we shouldn't
776 # to flag the change. If wctx is a committed revision, we shouldn't
777 # care for the dirty state of the working directory.
777 # care for the dirty state of the working directory.
778 if any(wctx.sub(s).dirty() for s in wctx.substate):
778 if any(wctx.sub(s).dirty() for s in wctx.substate):
779 m1[b'.hgsubstate'] = modifiednodeid
779 m1[b'.hgsubstate'] = modifiednodeid
780
780
781 # Don't use m2-vs-ma optimization if:
781 # Don't use m2-vs-ma optimization if:
782 # - ma is the same as m1 or m2, which we're just going to diff again later
782 # - ma is the same as m1 or m2, which we're just going to diff again later
783 # - The caller specifically asks for a full diff, which is useful during bid
783 # - The caller specifically asks for a full diff, which is useful during bid
784 # merge.
784 # merge.
785 # - we are tracking salvaged files specifically hence should process all
785 # - we are tracking salvaged files specifically hence should process all
786 # files
786 # files
787 if (
787 if (
788 pa not in ([wctx, p2] + wctx.parents())
788 pa not in ([wctx, p2] + wctx.parents())
789 and not forcefulldiff
789 and not forcefulldiff
790 and not (
790 and not (
791 repo.ui.configbool(b'experimental', b'merge-track-salvaged')
791 repo.ui.configbool(b'experimental', b'merge-track-salvaged')
792 or repo.filecopiesmode == b'changeset-sidedata'
792 or repo.filecopiesmode == b'changeset-sidedata'
793 )
793 )
794 ):
794 ):
795 # Identify which files are relevant to the merge, so we can limit the
795 # Identify which files are relevant to the merge, so we can limit the
796 # total m1-vs-m2 diff to just those files. This has significant
796 # total m1-vs-m2 diff to just those files. This has significant
797 # performance benefits in large repositories.
797 # performance benefits in large repositories.
798 relevantfiles = set(ma.diff(m2).keys())
798 relevantfiles = set(ma.diff(m2).keys())
799
799
800 # For copied and moved files, we need to add the source file too.
800 # For copied and moved files, we need to add the source file too.
801 for copykey, copyvalue in pycompat.iteritems(branch_copies1.copy):
801 for copykey, copyvalue in pycompat.iteritems(branch_copies1.copy):
802 if copyvalue in relevantfiles:
802 if copyvalue in relevantfiles:
803 relevantfiles.add(copykey)
803 relevantfiles.add(copykey)
804 for movedirkey in branch_copies1.movewithdir:
804 for movedirkey in branch_copies1.movewithdir:
805 relevantfiles.add(movedirkey)
805 relevantfiles.add(movedirkey)
806 filesmatcher = scmutil.matchfiles(repo, relevantfiles)
806 filesmatcher = scmutil.matchfiles(repo, relevantfiles)
807 matcher = matchmod.intersectmatchers(matcher, filesmatcher)
807 matcher = matchmod.intersectmatchers(matcher, filesmatcher)
808
808
809 diff = m1.diff(m2, match=matcher)
809 diff = m1.diff(m2, match=matcher)
810
810
811 for f, ((n1, fl1), (n2, fl2)) in pycompat.iteritems(diff):
811 for f, ((n1, fl1), (n2, fl2)) in pycompat.iteritems(diff):
812 if n1 and n2: # file exists on both local and remote side
812 if n1 and n2: # file exists on both local and remote side
813 if f not in ma:
813 if f not in ma:
814 # TODO: what if they're renamed from different sources?
814 # TODO: what if they're renamed from different sources?
815 fa = branch_copies1.copy.get(
815 fa = branch_copies1.copy.get(
816 f, None
816 f, None
817 ) or branch_copies2.copy.get(f, None)
817 ) or branch_copies2.copy.get(f, None)
818 args, msg = None, None
818 args, msg = None, None
819 if fa is not None:
819 if fa is not None:
820 args = (f, f, fa, False, pa.node())
820 args = (f, f, fa, False, pa.node())
821 msg = b'both renamed from %s' % fa
821 msg = b'both renamed from %s' % fa
822 else:
822 else:
823 args = (f, f, None, False, pa.node())
823 args = (f, f, None, False, pa.node())
824 msg = b'both created'
824 msg = b'both created'
825 mresult.addfile(f, mergestatemod.ACTION_MERGE, args, msg)
825 mresult.addfile(f, mergestatemod.ACTION_MERGE, args, msg)
826 elif f in branch_copies1.copy:
826 elif f in branch_copies1.copy:
827 fa = branch_copies1.copy[f]
827 fa = branch_copies1.copy[f]
828 mresult.addfile(
828 mresult.addfile(
829 f,
829 f,
830 mergestatemod.ACTION_MERGE,
830 mergestatemod.ACTION_MERGE,
831 (f, fa, fa, False, pa.node()),
831 (f, fa, fa, False, pa.node()),
832 b'local replaced from %s' % fa,
832 b'local replaced from %s' % fa,
833 )
833 )
834 elif f in branch_copies2.copy:
834 elif f in branch_copies2.copy:
835 fa = branch_copies2.copy[f]
835 fa = branch_copies2.copy[f]
836 mresult.addfile(
836 mresult.addfile(
837 f,
837 f,
838 mergestatemod.ACTION_MERGE,
838 mergestatemod.ACTION_MERGE,
839 (fa, f, fa, False, pa.node()),
839 (fa, f, fa, False, pa.node()),
840 b'other replaced from %s' % fa,
840 b'other replaced from %s' % fa,
841 )
841 )
842 else:
842 else:
843 a = ma[f]
843 a = ma[f]
844 fla = ma.flags(f)
844 fla = ma.flags(f)
845 nol = b'l' not in fl1 + fl2 + fla
845 nol = b'l' not in fl1 + fl2 + fla
846 if n2 == a and fl2 == fla:
846 if n2 == a and fl2 == fla:
847 mresult.addfile(
847 mresult.addfile(
848 f,
848 f,
849 mergestatemod.ACTION_KEEP,
849 mergestatemod.ACTION_KEEP,
850 (),
850 (),
851 b'remote unchanged',
851 b'remote unchanged',
852 )
852 )
853 elif n1 == a and fl1 == fla: # local unchanged - use remote
853 elif n1 == a and fl1 == fla: # local unchanged - use remote
854 if n1 == n2: # optimization: keep local content
854 if n1 == n2: # optimization: keep local content
855 mresult.addfile(
855 mresult.addfile(
856 f,
856 f,
857 mergestatemod.ACTION_EXEC,
857 mergestatemod.ACTION_EXEC,
858 (fl2,),
858 (fl2,),
859 b'update permissions',
859 b'update permissions',
860 )
860 )
861 else:
861 else:
862 mresult.addfile(
862 mresult.addfile(
863 f,
863 f,
864 mergestatemod.ACTION_GET,
864 mergestatemod.ACTION_GET,
865 (fl2, False),
865 (fl2, False),
866 b'remote is newer',
866 b'remote is newer',
867 )
867 )
868 if branchmerge:
868 if branchmerge:
869 mresult.addcommitinfo(
869 mresult.addcommitinfo(
870 f, b'filenode-source', b'other'
870 f, b'filenode-source', b'other'
871 )
871 )
872 elif nol and n2 == a: # remote only changed 'x'
872 elif nol and n2 == a: # remote only changed 'x'
873 mresult.addfile(
873 mresult.addfile(
874 f,
874 f,
875 mergestatemod.ACTION_EXEC,
875 mergestatemod.ACTION_EXEC,
876 (fl2,),
876 (fl2,),
877 b'update permissions',
877 b'update permissions',
878 )
878 )
879 elif nol and n1 == a: # local only changed 'x'
879 elif nol and n1 == a: # local only changed 'x'
880 mresult.addfile(
880 mresult.addfile(
881 f,
881 f,
882 mergestatemod.ACTION_GET,
882 mergestatemod.ACTION_GET,
883 (fl1, False),
883 (fl1, False),
884 b'remote is newer',
884 b'remote is newer',
885 )
885 )
886 if branchmerge:
886 if branchmerge:
887 mresult.addcommitinfo(f, b'filenode-source', b'other')
887 mresult.addcommitinfo(f, b'filenode-source', b'other')
888 else: # both changed something
888 else: # both changed something
889 mresult.addfile(
889 mresult.addfile(
890 f,
890 f,
891 mergestatemod.ACTION_MERGE,
891 mergestatemod.ACTION_MERGE,
892 (f, f, f, False, pa.node()),
892 (f, f, f, False, pa.node()),
893 b'versions differ',
893 b'versions differ',
894 )
894 )
895 elif n1: # file exists only on local side
895 elif n1: # file exists only on local side
896 if f in copied2:
896 if f in copied2:
897 pass # we'll deal with it on m2 side
897 pass # we'll deal with it on m2 side
898 elif (
898 elif (
899 f in branch_copies1.movewithdir
899 f in branch_copies1.movewithdir
900 ): # directory rename, move local
900 ): # directory rename, move local
901 f2 = branch_copies1.movewithdir[f]
901 f2 = branch_copies1.movewithdir[f]
902 if f2 in m2:
902 if f2 in m2:
903 mresult.addfile(
903 mresult.addfile(
904 f2,
904 f2,
905 mergestatemod.ACTION_MERGE,
905 mergestatemod.ACTION_MERGE,
906 (f, f2, None, True, pa.node()),
906 (f, f2, None, True, pa.node()),
907 b'remote directory rename, both created',
907 b'remote directory rename, both created',
908 )
908 )
909 else:
909 else:
910 mresult.addfile(
910 mresult.addfile(
911 f2,
911 f2,
912 mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL,
912 mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL,
913 (f, fl1),
913 (f, fl1),
914 b'remote directory rename - move from %s' % f,
914 b'remote directory rename - move from %s' % f,
915 )
915 )
916 elif f in branch_copies1.copy:
916 elif f in branch_copies1.copy:
917 f2 = branch_copies1.copy[f]
917 f2 = branch_copies1.copy[f]
918 mresult.addfile(
918 mresult.addfile(
919 f,
919 f,
920 mergestatemod.ACTION_MERGE,
920 mergestatemod.ACTION_MERGE,
921 (f, f2, f2, False, pa.node()),
921 (f, f2, f2, False, pa.node()),
922 b'local copied/moved from %s' % f2,
922 b'local copied/moved from %s' % f2,
923 )
923 )
924 elif f in ma: # clean, a different, no remote
924 elif f in ma: # clean, a different, no remote
925 if n1 != ma[f]:
925 if n1 != ma[f]:
926 if acceptremote:
926 if acceptremote:
927 mresult.addfile(
927 mresult.addfile(
928 f,
928 f,
929 mergestatemod.ACTION_REMOVE,
929 mergestatemod.ACTION_REMOVE,
930 None,
930 None,
931 b'remote delete',
931 b'remote delete',
932 )
932 )
933 else:
933 else:
934 mresult.addfile(
934 mresult.addfile(
935 f,
935 f,
936 mergestatemod.ACTION_CHANGED_DELETED,
936 mergestatemod.ACTION_CHANGED_DELETED,
937 (f, None, f, False, pa.node()),
937 (f, None, f, False, pa.node()),
938 b'prompt changed/deleted',
938 b'prompt changed/deleted',
939 )
939 )
940 if branchmerge:
940 if branchmerge:
941 mresult.addcommitinfo(
941 mresult.addcommitinfo(
942 f, b'merge-removal-candidate', b'yes'
942 f, b'merge-removal-candidate', b'yes'
943 )
943 )
944 elif n1 == addednodeid:
944 elif n1 == addednodeid:
945 # This file was locally added. We should forget it instead of
945 # This file was locally added. We should forget it instead of
946 # deleting it.
946 # deleting it.
947 mresult.addfile(
947 mresult.addfile(
948 f,
948 f,
949 mergestatemod.ACTION_FORGET,
949 mergestatemod.ACTION_FORGET,
950 None,
950 None,
951 b'remote deleted',
951 b'remote deleted',
952 )
952 )
953 else:
953 else:
954 mresult.addfile(
954 mresult.addfile(
955 f,
955 f,
956 mergestatemod.ACTION_REMOVE,
956 mergestatemod.ACTION_REMOVE,
957 None,
957 None,
958 b'other deleted',
958 b'other deleted',
959 )
959 )
960 if branchmerge:
960 if branchmerge:
961 # the file must be absent after merging,
961 # the file must be absent after merging,
962 # howeber the user might make
962 # howeber the user might make
963 # the file reappear using revert and if they does,
963 # the file reappear using revert and if they does,
964 # we force create a new node
964 # we force create a new node
965 mresult.addcommitinfo(
965 mresult.addcommitinfo(
966 f, b'merge-removal-candidate', b'yes'
966 f, b'merge-removal-candidate', b'yes'
967 )
967 )
968
968
969 else: # file not in ancestor, not in remote
969 else: # file not in ancestor, not in remote
970 mresult.addfile(
970 mresult.addfile(
971 f,
971 f,
972 mergestatemod.ACTION_KEEP_NEW,
972 mergestatemod.ACTION_KEEP_NEW,
973 None,
973 None,
974 b'ancestor missing, remote missing',
974 b'ancestor missing, remote missing',
975 )
975 )
976
976
977 elif n2: # file exists only on remote side
977 elif n2: # file exists only on remote side
978 if f in copied1:
978 if f in copied1:
979 pass # we'll deal with it on m1 side
979 pass # we'll deal with it on m1 side
980 elif f in branch_copies2.movewithdir:
980 elif f in branch_copies2.movewithdir:
981 f2 = branch_copies2.movewithdir[f]
981 f2 = branch_copies2.movewithdir[f]
982 if f2 in m1:
982 if f2 in m1:
983 mresult.addfile(
983 mresult.addfile(
984 f2,
984 f2,
985 mergestatemod.ACTION_MERGE,
985 mergestatemod.ACTION_MERGE,
986 (f2, f, None, False, pa.node()),
986 (f2, f, None, False, pa.node()),
987 b'local directory rename, both created',
987 b'local directory rename, both created',
988 )
988 )
989 else:
989 else:
990 mresult.addfile(
990 mresult.addfile(
991 f2,
991 f2,
992 mergestatemod.ACTION_LOCAL_DIR_RENAME_GET,
992 mergestatemod.ACTION_LOCAL_DIR_RENAME_GET,
993 (f, fl2),
993 (f, fl2),
994 b'local directory rename - get from %s' % f,
994 b'local directory rename - get from %s' % f,
995 )
995 )
996 elif f in branch_copies2.copy:
996 elif f in branch_copies2.copy:
997 f2 = branch_copies2.copy[f]
997 f2 = branch_copies2.copy[f]
998 msg, args = None, None
998 msg, args = None, None
999 if f2 in m2:
999 if f2 in m2:
1000 args = (f2, f, f2, False, pa.node())
1000 args = (f2, f, f2, False, pa.node())
1001 msg = b'remote copied from %s' % f2
1001 msg = b'remote copied from %s' % f2
1002 else:
1002 else:
1003 args = (f2, f, f2, True, pa.node())
1003 args = (f2, f, f2, True, pa.node())
1004 msg = b'remote moved from %s' % f2
1004 msg = b'remote moved from %s' % f2
1005 mresult.addfile(f, mergestatemod.ACTION_MERGE, args, msg)
1005 mresult.addfile(f, mergestatemod.ACTION_MERGE, args, msg)
1006 elif f not in ma:
1006 elif f not in ma:
1007 # local unknown, remote created: the logic is described by the
1007 # local unknown, remote created: the logic is described by the
1008 # following table:
1008 # following table:
1009 #
1009 #
1010 # force branchmerge different | action
1010 # force branchmerge different | action
1011 # n * * | create
1011 # n * * | create
1012 # y n * | create
1012 # y n * | create
1013 # y y n | create
1013 # y y n | create
1014 # y y y | merge
1014 # y y y | merge
1015 #
1015 #
1016 # Checking whether the files are different is expensive, so we
1016 # Checking whether the files are different is expensive, so we
1017 # don't do that when we can avoid it.
1017 # don't do that when we can avoid it.
1018 if not force:
1018 if not force:
1019 mresult.addfile(
1019 mresult.addfile(
1020 f,
1020 f,
1021 mergestatemod.ACTION_CREATED,
1021 mergestatemod.ACTION_CREATED,
1022 (fl2,),
1022 (fl2,),
1023 b'remote created',
1023 b'remote created',
1024 )
1024 )
1025 elif not branchmerge:
1025 elif not branchmerge:
1026 mresult.addfile(
1026 mresult.addfile(
1027 f,
1027 f,
1028 mergestatemod.ACTION_CREATED,
1028 mergestatemod.ACTION_CREATED,
1029 (fl2,),
1029 (fl2,),
1030 b'remote created',
1030 b'remote created',
1031 )
1031 )
1032 else:
1032 else:
1033 mresult.addfile(
1033 mresult.addfile(
1034 f,
1034 f,
1035 mergestatemod.ACTION_CREATED_MERGE,
1035 mergestatemod.ACTION_CREATED_MERGE,
1036 (fl2, pa.node()),
1036 (fl2, pa.node()),
1037 b'remote created, get or merge',
1037 b'remote created, get or merge',
1038 )
1038 )
1039 elif n2 != ma[f]:
1039 elif n2 != ma[f]:
1040 df = None
1040 df = None
1041 for d in branch_copies1.dirmove:
1041 for d in branch_copies1.dirmove:
1042 if f.startswith(d):
1042 if f.startswith(d):
1043 # new file added in a directory that was moved
1043 # new file added in a directory that was moved
1044 df = branch_copies1.dirmove[d] + f[len(d) :]
1044 df = branch_copies1.dirmove[d] + f[len(d) :]
1045 break
1045 break
1046 if df is not None and df in m1:
1046 if df is not None and df in m1:
1047 mresult.addfile(
1047 mresult.addfile(
1048 df,
1048 df,
1049 mergestatemod.ACTION_MERGE,
1049 mergestatemod.ACTION_MERGE,
1050 (df, f, f, False, pa.node()),
1050 (df, f, f, False, pa.node()),
1051 b'local directory rename - respect move '
1051 b'local directory rename - respect move '
1052 b'from %s' % f,
1052 b'from %s' % f,
1053 )
1053 )
1054 elif acceptremote:
1054 elif acceptremote:
1055 mresult.addfile(
1055 mresult.addfile(
1056 f,
1056 f,
1057 mergestatemod.ACTION_CREATED,
1057 mergestatemod.ACTION_CREATED,
1058 (fl2,),
1058 (fl2,),
1059 b'remote recreating',
1059 b'remote recreating',
1060 )
1060 )
1061 else:
1061 else:
1062 mresult.addfile(
1062 mresult.addfile(
1063 f,
1063 f,
1064 mergestatemod.ACTION_DELETED_CHANGED,
1064 mergestatemod.ACTION_DELETED_CHANGED,
1065 (None, f, f, False, pa.node()),
1065 (None, f, f, False, pa.node()),
1066 b'prompt deleted/changed',
1066 b'prompt deleted/changed',
1067 )
1067 )
1068 if branchmerge:
1068 if branchmerge:
1069 mresult.addcommitinfo(
1069 mresult.addcommitinfo(
1070 f, b'merge-removal-candidate', b'yes'
1070 f, b'merge-removal-candidate', b'yes'
1071 )
1071 )
1072 else:
1072 else:
1073 mresult.addfile(
1073 mresult.addfile(
1074 f,
1074 f,
1075 mergestatemod.ACTION_KEEP_ABSENT,
1075 mergestatemod.ACTION_KEEP_ABSENT,
1076 None,
1076 None,
1077 b'local not present, remote unchanged',
1077 b'local not present, remote unchanged',
1078 )
1078 )
1079 if branchmerge:
1079 if branchmerge:
1080 # the file must be absent after merging
1080 # the file must be absent after merging
1081 # however the user might make
1081 # however the user might make
1082 # the file reappear using revert and if they does,
1082 # the file reappear using revert and if they does,
1083 # we force create a new node
1083 # we force create a new node
1084 mresult.addcommitinfo(f, b'merge-removal-candidate', b'yes')
1084 mresult.addcommitinfo(f, b'merge-removal-candidate', b'yes')
1085
1085
1086 if repo.ui.configbool(b'experimental', b'merge.checkpathconflicts'):
1086 if repo.ui.configbool(b'experimental', b'merge.checkpathconflicts'):
1087 # If we are merging, look for path conflicts.
1087 # If we are merging, look for path conflicts.
1088 checkpathconflicts(repo, wctx, p2, mresult)
1088 checkpathconflicts(repo, wctx, p2, mresult)
1089
1089
1090 narrowmatch = repo.narrowmatch()
1090 narrowmatch = repo.narrowmatch()
1091 if not narrowmatch.always():
1091 if not narrowmatch.always():
1092 # Updates "actions" in place
1092 # Updates "actions" in place
1093 _filternarrowactions(narrowmatch, branchmerge, mresult)
1093 _filternarrowactions(narrowmatch, branchmerge, mresult)
1094
1094
1095 renamedelete = branch_copies1.renamedelete
1095 renamedelete = branch_copies1.renamedelete
1096 renamedelete.update(branch_copies2.renamedelete)
1096 renamedelete.update(branch_copies2.renamedelete)
1097
1097
1098 mresult.updatevalues(diverge, renamedelete)
1098 mresult.updatevalues(diverge, renamedelete)
1099 return mresult
1099 return mresult
1100
1100
1101
1101
1102 def _resolvetrivial(repo, wctx, mctx, ancestor, mresult):
1102 def _resolvetrivial(repo, wctx, mctx, ancestor, mresult):
1103 """Resolves false conflicts where the nodeid changed but the content
1103 """Resolves false conflicts where the nodeid changed but the content
1104 remained the same."""
1104 remained the same."""
1105 # We force a copy of actions.items() because we're going to mutate
1105 # We force a copy of actions.items() because we're going to mutate
1106 # actions as we resolve trivial conflicts.
1106 # actions as we resolve trivial conflicts.
1107 for f in list(mresult.files((mergestatemod.ACTION_CHANGED_DELETED,))):
1107 for f in list(mresult.files((mergestatemod.ACTION_CHANGED_DELETED,))):
1108 if f in ancestor and not wctx[f].cmp(ancestor[f]):
1108 if f in ancestor and not wctx[f].cmp(ancestor[f]):
1109 # local did change but ended up with same content
1109 # local did change but ended up with same content
1110 mresult.addfile(
1110 mresult.addfile(
1111 f, mergestatemod.ACTION_REMOVE, None, b'prompt same'
1111 f, mergestatemod.ACTION_REMOVE, None, b'prompt same'
1112 )
1112 )
1113
1113
1114 for f in list(mresult.files((mergestatemod.ACTION_DELETED_CHANGED,))):
1114 for f in list(mresult.files((mergestatemod.ACTION_DELETED_CHANGED,))):
1115 if f in ancestor and not mctx[f].cmp(ancestor[f]):
1115 if f in ancestor and not mctx[f].cmp(ancestor[f]):
1116 # remote did change but ended up with same content
1116 # remote did change but ended up with same content
1117 mresult.removefile(f) # don't get = keep local deleted
1117 mresult.removefile(f) # don't get = keep local deleted
1118
1118
1119
1119
1120 def calculateupdates(
1120 def calculateupdates(
1121 repo,
1121 repo,
1122 wctx,
1122 wctx,
1123 mctx,
1123 mctx,
1124 ancestors,
1124 ancestors,
1125 branchmerge,
1125 branchmerge,
1126 force,
1126 force,
1127 acceptremote,
1127 acceptremote,
1128 followcopies,
1128 followcopies,
1129 matcher=None,
1129 matcher=None,
1130 mergeforce=False,
1130 mergeforce=False,
1131 ):
1131 ):
1132 """
1132 """
1133 Calculate the actions needed to merge mctx into wctx using ancestors
1133 Calculate the actions needed to merge mctx into wctx using ancestors
1134
1134
1135 Uses manifestmerge() to merge manifest and get list of actions required to
1135 Uses manifestmerge() to merge manifest and get list of actions required to
1136 perform for merging two manifests. If there are multiple ancestors, uses bid
1136 perform for merging two manifests. If there are multiple ancestors, uses bid
1137 merge if enabled.
1137 merge if enabled.
1138
1138
1139 Also filters out actions which are unrequired if repository is sparse.
1139 Also filters out actions which are unrequired if repository is sparse.
1140
1140
1141 Returns mergeresult object same as manifestmerge().
1141 Returns mergeresult object same as manifestmerge().
1142 """
1142 """
1143 # Avoid cycle.
1143 # Avoid cycle.
1144 from . import sparse
1144 from . import sparse
1145
1145
1146 mresult = None
1146 mresult = None
1147 if len(ancestors) == 1: # default
1147 if len(ancestors) == 1: # default
1148 mresult = manifestmerge(
1148 mresult = manifestmerge(
1149 repo,
1149 repo,
1150 wctx,
1150 wctx,
1151 mctx,
1151 mctx,
1152 ancestors[0],
1152 ancestors[0],
1153 branchmerge,
1153 branchmerge,
1154 force,
1154 force,
1155 matcher,
1155 matcher,
1156 acceptremote,
1156 acceptremote,
1157 followcopies,
1157 followcopies,
1158 )
1158 )
1159 _checkunknownfiles(repo, wctx, mctx, force, mresult, mergeforce)
1159 _checkunknownfiles(repo, wctx, mctx, force, mresult, mergeforce)
1160
1160
1161 else: # only when merge.preferancestor=* - the default
1161 else: # only when merge.preferancestor=* - the default
1162 repo.ui.note(
1162 repo.ui.note(
1163 _(b"note: merging %s and %s using bids from ancestors %s\n")
1163 _(b"note: merging %s and %s using bids from ancestors %s\n")
1164 % (
1164 % (
1165 wctx,
1165 wctx,
1166 mctx,
1166 mctx,
1167 _(b' and ').join(pycompat.bytestr(anc) for anc in ancestors),
1167 _(b' and ').join(pycompat.bytestr(anc) for anc in ancestors),
1168 )
1168 )
1169 )
1169 )
1170
1170
1171 # mapping filename to bids (action method to list af actions)
1171 # mapping filename to bids (action method to list af actions)
1172 # {FILENAME1 : BID1, FILENAME2 : BID2}
1172 # {FILENAME1 : BID1, FILENAME2 : BID2}
1173 # BID is another dictionary which contains
1173 # BID is another dictionary which contains
1174 # mapping of following form:
1174 # mapping of following form:
1175 # {ACTION_X : [info, ..], ACTION_Y : [info, ..]}
1175 # {ACTION_X : [info, ..], ACTION_Y : [info, ..]}
1176 fbids = {}
1176 fbids = {}
1177 mresult = mergeresult()
1177 mresult = mergeresult()
1178 diverge, renamedelete = None, None
1178 diverge, renamedelete = None, None
1179 for ancestor in ancestors:
1179 for ancestor in ancestors:
1180 repo.ui.note(_(b'\ncalculating bids for ancestor %s\n') % ancestor)
1180 repo.ui.note(_(b'\ncalculating bids for ancestor %s\n') % ancestor)
1181 mresult1 = manifestmerge(
1181 mresult1 = manifestmerge(
1182 repo,
1182 repo,
1183 wctx,
1183 wctx,
1184 mctx,
1184 mctx,
1185 ancestor,
1185 ancestor,
1186 branchmerge,
1186 branchmerge,
1187 force,
1187 force,
1188 matcher,
1188 matcher,
1189 acceptremote,
1189 acceptremote,
1190 followcopies,
1190 followcopies,
1191 forcefulldiff=True,
1191 forcefulldiff=True,
1192 )
1192 )
1193 _checkunknownfiles(repo, wctx, mctx, force, mresult1, mergeforce)
1193 _checkunknownfiles(repo, wctx, mctx, force, mresult1, mergeforce)
1194
1194
1195 # Track the shortest set of warning on the theory that bid
1195 # Track the shortest set of warning on the theory that bid
1196 # merge will correctly incorporate more information
1196 # merge will correctly incorporate more information
1197 if diverge is None or len(mresult1.diverge) < len(diverge):
1197 if diverge is None or len(mresult1.diverge) < len(diverge):
1198 diverge = mresult1.diverge
1198 diverge = mresult1.diverge
1199 if renamedelete is None or len(renamedelete) < len(
1199 if renamedelete is None or len(renamedelete) < len(
1200 mresult1.renamedelete
1200 mresult1.renamedelete
1201 ):
1201 ):
1202 renamedelete = mresult1.renamedelete
1202 renamedelete = mresult1.renamedelete
1203
1203
1204 # blindly update final mergeresult commitinfo with what we get
1204 # blindly update final mergeresult commitinfo with what we get
1205 # from mergeresult object for each ancestor
1205 # from mergeresult object for each ancestor
1206 # TODO: some commitinfo depends on what bid merge choose and hence
1206 # TODO: some commitinfo depends on what bid merge choose and hence
1207 # we will need to make commitinfo also depend on bid merge logic
1207 # we will need to make commitinfo also depend on bid merge logic
1208 mresult._commitinfo.update(mresult1._commitinfo)
1208 mresult._commitinfo.update(mresult1._commitinfo)
1209
1209
1210 for f, a in mresult1.filemap(sort=True):
1210 for f, a in mresult1.filemap(sort=True):
1211 m, args, msg = a
1211 m, args, msg = a
1212 repo.ui.debug(b' %s: %s -> %s\n' % (f, msg, m))
1212 repo.ui.debug(b' %s: %s -> %s\n' % (f, msg, m))
1213 if f in fbids:
1213 if f in fbids:
1214 d = fbids[f]
1214 d = fbids[f]
1215 if m in d:
1215 if m in d:
1216 d[m].append(a)
1216 d[m].append(a)
1217 else:
1217 else:
1218 d[m] = [a]
1218 d[m] = [a]
1219 else:
1219 else:
1220 fbids[f] = {m: [a]}
1220 fbids[f] = {m: [a]}
1221
1221
1222 # Call for bids
1222 # Call for bids
1223 # Pick the best bid for each file
1223 # Pick the best bid for each file
1224 repo.ui.note(
1224 repo.ui.note(
1225 _(b'\nauction for merging merge bids (%d ancestors)\n')
1225 _(b'\nauction for merging merge bids (%d ancestors)\n')
1226 % len(ancestors)
1226 % len(ancestors)
1227 )
1227 )
1228 for f, bids in sorted(fbids.items()):
1228 for f, bids in sorted(fbids.items()):
1229 if repo.ui.debugflag:
1229 if repo.ui.debugflag:
1230 repo.ui.debug(b" list of bids for %s:\n" % f)
1230 repo.ui.debug(b" list of bids for %s:\n" % f)
1231 for m, l in sorted(bids.items()):
1231 for m, l in sorted(bids.items()):
1232 for _f, args, msg in l:
1232 for _f, args, msg in l:
1233 repo.ui.debug(b' %s -> %s\n' % (msg, m))
1233 repo.ui.debug(b' %s -> %s\n' % (msg, m))
1234 # bids is a mapping from action method to list af actions
1234 # bids is a mapping from action method to list af actions
1235 # Consensus?
1235 # Consensus?
1236 if len(bids) == 1: # all bids are the same kind of method
1236 if len(bids) == 1: # all bids are the same kind of method
1237 m, l = list(bids.items())[0]
1237 m, l = list(bids.items())[0]
1238 if all(a == l[0] for a in l[1:]): # len(bids) is > 1
1238 if all(a == l[0] for a in l[1:]): # len(bids) is > 1
1239 repo.ui.note(_(b" %s: consensus for %s\n") % (f, m))
1239 repo.ui.note(_(b" %s: consensus for %s\n") % (f, m))
1240 mresult.addfile(f, *l[0])
1240 mresult.addfile(f, *l[0])
1241 continue
1241 continue
1242 # If keep is an option, just do it.
1242 # If keep is an option, just do it.
1243 if mergestatemod.ACTION_KEEP in bids:
1243 if mergestatemod.ACTION_KEEP in bids:
1244 repo.ui.note(_(b" %s: picking 'keep' action\n") % f)
1244 repo.ui.note(_(b" %s: picking 'keep' action\n") % f)
1245 mresult.addfile(f, *bids[mergestatemod.ACTION_KEEP][0])
1245 mresult.addfile(f, *bids[mergestatemod.ACTION_KEEP][0])
1246 continue
1246 continue
1247 # If keep absent is an option, just do that
1247 # If keep absent is an option, just do that
1248 if mergestatemod.ACTION_KEEP_ABSENT in bids:
1248 if mergestatemod.ACTION_KEEP_ABSENT in bids:
1249 repo.ui.note(_(b" %s: picking 'keep absent' action\n") % f)
1249 repo.ui.note(_(b" %s: picking 'keep absent' action\n") % f)
1250 mresult.addfile(f, *bids[mergestatemod.ACTION_KEEP_ABSENT][0])
1250 mresult.addfile(f, *bids[mergestatemod.ACTION_KEEP_ABSENT][0])
1251 continue
1251 continue
1252 # ACTION_KEEP_NEW and ACTION_CHANGED_DELETED are conflicting actions
1252 # ACTION_KEEP_NEW and ACTION_CHANGED_DELETED are conflicting actions
1253 # as one say that file is new while other says that file was present
1253 # as one say that file is new while other says that file was present
1254 # earlier too and has a change delete conflict
1254 # earlier too and has a change delete conflict
1255 # Let's fall back to conflicting ACTION_CHANGED_DELETED and let user
1255 # Let's fall back to conflicting ACTION_CHANGED_DELETED and let user
1256 # do the right thing
1256 # do the right thing
1257 if (
1257 if (
1258 mergestatemod.ACTION_CHANGED_DELETED in bids
1258 mergestatemod.ACTION_CHANGED_DELETED in bids
1259 and mergestatemod.ACTION_KEEP_NEW in bids
1259 and mergestatemod.ACTION_KEEP_NEW in bids
1260 ):
1260 ):
1261 repo.ui.note(_(b" %s: picking 'changed/deleted' action\n") % f)
1261 repo.ui.note(_(b" %s: picking 'changed/deleted' action\n") % f)
1262 mresult.addfile(
1262 mresult.addfile(
1263 f, *bids[mergestatemod.ACTION_CHANGED_DELETED][0]
1263 f, *bids[mergestatemod.ACTION_CHANGED_DELETED][0]
1264 )
1264 )
1265 continue
1265 continue
1266 # If keep new is an option, let's just do that
1266 # If keep new is an option, let's just do that
1267 if mergestatemod.ACTION_KEEP_NEW in bids:
1267 if mergestatemod.ACTION_KEEP_NEW in bids:
1268 repo.ui.note(_(b" %s: picking 'keep new' action\n") % f)
1268 repo.ui.note(_(b" %s: picking 'keep new' action\n") % f)
1269 mresult.addfile(f, *bids[mergestatemod.ACTION_KEEP_NEW][0])
1269 mresult.addfile(f, *bids[mergestatemod.ACTION_KEEP_NEW][0])
1270 continue
1270 continue
1271 # ACTION_GET and ACTION_DELETE_CHANGED are conflicting actions as
1271 # ACTION_GET and ACTION_DELETE_CHANGED are conflicting actions as
1272 # one action states the file is newer/created on remote side and
1272 # one action states the file is newer/created on remote side and
1273 # other states that file is deleted locally and changed on remote
1273 # other states that file is deleted locally and changed on remote
1274 # side. Let's fallback and rely on a conflicting action to let user
1274 # side. Let's fallback and rely on a conflicting action to let user
1275 # do the right thing
1275 # do the right thing
1276 if (
1276 if (
1277 mergestatemod.ACTION_DELETED_CHANGED in bids
1277 mergestatemod.ACTION_DELETED_CHANGED in bids
1278 and mergestatemod.ACTION_GET in bids
1278 and mergestatemod.ACTION_GET in bids
1279 ):
1279 ):
1280 repo.ui.note(_(b" %s: picking 'delete/changed' action\n") % f)
1280 repo.ui.note(_(b" %s: picking 'delete/changed' action\n") % f)
1281 mresult.addfile(
1281 mresult.addfile(
1282 f, *bids[mergestatemod.ACTION_DELETED_CHANGED][0]
1282 f, *bids[mergestatemod.ACTION_DELETED_CHANGED][0]
1283 )
1283 )
1284 continue
1284 continue
1285 # If there are gets and they all agree [how could they not?], do it.
1285 # If there are gets and they all agree [how could they not?], do it.
1286 if mergestatemod.ACTION_GET in bids:
1286 if mergestatemod.ACTION_GET in bids:
1287 ga0 = bids[mergestatemod.ACTION_GET][0]
1287 ga0 = bids[mergestatemod.ACTION_GET][0]
1288 if all(a == ga0 for a in bids[mergestatemod.ACTION_GET][1:]):
1288 if all(a == ga0 for a in bids[mergestatemod.ACTION_GET][1:]):
1289 repo.ui.note(_(b" %s: picking 'get' action\n") % f)
1289 repo.ui.note(_(b" %s: picking 'get' action\n") % f)
1290 mresult.addfile(f, *ga0)
1290 mresult.addfile(f, *ga0)
1291 continue
1291 continue
1292 # TODO: Consider other simple actions such as mode changes
1292 # TODO: Consider other simple actions such as mode changes
1293 # Handle inefficient democrazy.
1293 # Handle inefficient democrazy.
1294 repo.ui.note(_(b' %s: multiple bids for merge action:\n') % f)
1294 repo.ui.note(_(b' %s: multiple bids for merge action:\n') % f)
1295 for m, l in sorted(bids.items()):
1295 for m, l in sorted(bids.items()):
1296 for _f, args, msg in l:
1296 for _f, args, msg in l:
1297 repo.ui.note(b' %s -> %s\n' % (msg, m))
1297 repo.ui.note(b' %s -> %s\n' % (msg, m))
1298 # Pick random action. TODO: Instead, prompt user when resolving
1298 # Pick random action. TODO: Instead, prompt user when resolving
1299 m, l = list(bids.items())[0]
1299 m, l = list(bids.items())[0]
1300 repo.ui.warn(
1300 repo.ui.warn(
1301 _(b' %s: ambiguous merge - picked %s action\n') % (f, m)
1301 _(b' %s: ambiguous merge - picked %s action\n') % (f, m)
1302 )
1302 )
1303 mresult.addfile(f, *l[0])
1303 mresult.addfile(f, *l[0])
1304 continue
1304 continue
1305 repo.ui.note(_(b'end of auction\n\n'))
1305 repo.ui.note(_(b'end of auction\n\n'))
1306 mresult.updatevalues(diverge, renamedelete)
1306 mresult.updatevalues(diverge, renamedelete)
1307
1307
1308 if wctx.rev() is None:
1308 if wctx.rev() is None:
1309 _forgetremoved(wctx, mctx, branchmerge, mresult)
1309 _forgetremoved(wctx, mctx, branchmerge, mresult)
1310
1310
1311 sparse.filterupdatesactions(repo, wctx, mctx, branchmerge, mresult)
1311 sparse.filterupdatesactions(repo, wctx, mctx, branchmerge, mresult)
1312 _resolvetrivial(repo, wctx, mctx, ancestors[0], mresult)
1312 _resolvetrivial(repo, wctx, mctx, ancestors[0], mresult)
1313
1313
1314 return mresult
1314 return mresult
1315
1315
1316
1316
1317 def _getcwd():
1317 def _getcwd():
1318 try:
1318 try:
1319 return encoding.getcwd()
1319 return encoding.getcwd()
1320 except OSError as err:
1320 except OSError as err:
1321 if err.errno == errno.ENOENT:
1321 if err.errno == errno.ENOENT:
1322 return None
1322 return None
1323 raise
1323 raise
1324
1324
1325
1325
1326 def batchremove(repo, wctx, actions):
1326 def batchremove(repo, wctx, actions):
1327 """apply removes to the working directory
1327 """apply removes to the working directory
1328
1328
1329 yields tuples for progress updates
1329 yields tuples for progress updates
1330 """
1330 """
1331 verbose = repo.ui.verbose
1331 verbose = repo.ui.verbose
1332 cwd = _getcwd()
1332 cwd = _getcwd()
1333 i = 0
1333 i = 0
1334 for f, args, msg in actions:
1334 for f, args, msg in actions:
1335 repo.ui.debug(b" %s: %s -> r\n" % (f, msg))
1335 repo.ui.debug(b" %s: %s -> r\n" % (f, msg))
1336 if verbose:
1336 if verbose:
1337 repo.ui.note(_(b"removing %s\n") % f)
1337 repo.ui.note(_(b"removing %s\n") % f)
1338 wctx[f].audit()
1338 wctx[f].audit()
1339 try:
1339 try:
1340 wctx[f].remove(ignoremissing=True)
1340 wctx[f].remove(ignoremissing=True)
1341 except OSError as inst:
1341 except OSError as inst:
1342 repo.ui.warn(
1342 repo.ui.warn(
1343 _(b"update failed to remove %s: %s!\n")
1343 _(b"update failed to remove %s: %s!\n")
1344 % (f, pycompat.bytestr(inst.strerror))
1344 % (f, pycompat.bytestr(inst.strerror))
1345 )
1345 )
1346 if i == 100:
1346 if i == 100:
1347 yield i, f
1347 yield i, f
1348 i = 0
1348 i = 0
1349 i += 1
1349 i += 1
1350 if i > 0:
1350 if i > 0:
1351 yield i, f
1351 yield i, f
1352
1352
1353 if cwd and not _getcwd():
1353 if cwd and not _getcwd():
1354 # cwd was removed in the course of removing files; print a helpful
1354 # cwd was removed in the course of removing files; print a helpful
1355 # warning.
1355 # warning.
1356 repo.ui.warn(
1356 repo.ui.warn(
1357 _(
1357 _(
1358 b"current directory was removed\n"
1358 b"current directory was removed\n"
1359 b"(consider changing to repo root: %s)\n"
1359 b"(consider changing to repo root: %s)\n"
1360 )
1360 )
1361 % repo.root
1361 % repo.root
1362 )
1362 )
1363
1363
1364
1364
1365 def batchget(repo, mctx, wctx, wantfiledata, actions):
1365 def batchget(repo, mctx, wctx, wantfiledata, actions):
1366 """apply gets to the working directory
1366 """apply gets to the working directory
1367
1367
1368 mctx is the context to get from
1368 mctx is the context to get from
1369
1369
1370 Yields arbitrarily many (False, tuple) for progress updates, followed by
1370 Yields arbitrarily many (False, tuple) for progress updates, followed by
1371 exactly one (True, filedata). When wantfiledata is false, filedata is an
1371 exactly one (True, filedata). When wantfiledata is false, filedata is an
1372 empty dict. When wantfiledata is true, filedata[f] is a triple (mode, size,
1372 empty dict. When wantfiledata is true, filedata[f] is a triple (mode, size,
1373 mtime) of the file f written for each action.
1373 mtime) of the file f written for each action.
1374 """
1374 """
1375 filedata = {}
1375 filedata = {}
1376 verbose = repo.ui.verbose
1376 verbose = repo.ui.verbose
1377 fctx = mctx.filectx
1377 fctx = mctx.filectx
1378 ui = repo.ui
1378 ui = repo.ui
1379 i = 0
1379 i = 0
1380 with repo.wvfs.backgroundclosing(ui, expectedcount=len(actions)):
1380 with repo.wvfs.backgroundclosing(ui, expectedcount=len(actions)):
1381 for f, (flags, backup), msg in actions:
1381 for f, (flags, backup), msg in actions:
1382 repo.ui.debug(b" %s: %s -> g\n" % (f, msg))
1382 repo.ui.debug(b" %s: %s -> g\n" % (f, msg))
1383 if verbose:
1383 if verbose:
1384 repo.ui.note(_(b"getting %s\n") % f)
1384 repo.ui.note(_(b"getting %s\n") % f)
1385
1385
1386 if backup:
1386 if backup:
1387 # If a file or directory exists with the same name, back that
1387 # If a file or directory exists with the same name, back that
1388 # up. Otherwise, look to see if there is a file that conflicts
1388 # up. Otherwise, look to see if there is a file that conflicts
1389 # with a directory this file is in, and if so, back that up.
1389 # with a directory this file is in, and if so, back that up.
1390 conflicting = f
1390 conflicting = f
1391 if not repo.wvfs.lexists(f):
1391 if not repo.wvfs.lexists(f):
1392 for p in pathutil.finddirs(f):
1392 for p in pathutil.finddirs(f):
1393 if repo.wvfs.isfileorlink(p):
1393 if repo.wvfs.isfileorlink(p):
1394 conflicting = p
1394 conflicting = p
1395 break
1395 break
1396 if repo.wvfs.lexists(conflicting):
1396 if repo.wvfs.lexists(conflicting):
1397 orig = scmutil.backuppath(ui, repo, conflicting)
1397 orig = scmutil.backuppath(ui, repo, conflicting)
1398 util.rename(repo.wjoin(conflicting), orig)
1398 util.rename(repo.wjoin(conflicting), orig)
1399 wfctx = wctx[f]
1399 wfctx = wctx[f]
1400 wfctx.clearunknown()
1400 wfctx.clearunknown()
1401 atomictemp = ui.configbool(b"experimental", b"update.atomic-file")
1401 atomictemp = ui.configbool(b"experimental", b"update.atomic-file")
1402 size = wfctx.write(
1402 size = wfctx.write(
1403 fctx(f).data(),
1403 fctx(f).data(),
1404 flags,
1404 flags,
1405 backgroundclose=True,
1405 backgroundclose=True,
1406 atomictemp=atomictemp,
1406 atomictemp=atomictemp,
1407 )
1407 )
1408 if wantfiledata:
1408 if wantfiledata:
1409 s = wfctx.lstat()
1409 s = wfctx.lstat()
1410 mode = s.st_mode
1410 mode = s.st_mode
1411 mtime = s[stat.ST_MTIME]
1411 mtime = s[stat.ST_MTIME]
1412 filedata[f] = (mode, size, mtime) # for dirstate.normal
1412 filedata[f] = (mode, size, mtime) # for dirstate.normal
1413 if i == 100:
1413 if i == 100:
1414 yield False, (i, f)
1414 yield False, (i, f)
1415 i = 0
1415 i = 0
1416 i += 1
1416 i += 1
1417 if i > 0:
1417 if i > 0:
1418 yield False, (i, f)
1418 yield False, (i, f)
1419 yield True, filedata
1419 yield True, filedata
1420
1420
1421
1421
1422 def _prefetchfiles(repo, ctx, mresult):
1422 def _prefetchfiles(repo, ctx, mresult):
1423 """Invoke ``scmutil.prefetchfiles()`` for the files relevant to the dict
1423 """Invoke ``scmutil.prefetchfiles()`` for the files relevant to the dict
1424 of merge actions. ``ctx`` is the context being merged in."""
1424 of merge actions. ``ctx`` is the context being merged in."""
1425
1425
1426 # Skipping 'a', 'am', 'f', 'r', 'dm', 'e', 'k', 'p' and 'pr', because they
1426 # Skipping 'a', 'am', 'f', 'r', 'dm', 'e', 'k', 'p' and 'pr', because they
1427 # don't touch the context to be merged in. 'cd' is skipped, because
1427 # don't touch the context to be merged in. 'cd' is skipped, because
1428 # changed/deleted never resolves to something from the remote side.
1428 # changed/deleted never resolves to something from the remote side.
1429 files = mresult.files(
1429 files = mresult.files(
1430 [
1430 [
1431 mergestatemod.ACTION_GET,
1431 mergestatemod.ACTION_GET,
1432 mergestatemod.ACTION_DELETED_CHANGED,
1432 mergestatemod.ACTION_DELETED_CHANGED,
1433 mergestatemod.ACTION_LOCAL_DIR_RENAME_GET,
1433 mergestatemod.ACTION_LOCAL_DIR_RENAME_GET,
1434 mergestatemod.ACTION_MERGE,
1434 mergestatemod.ACTION_MERGE,
1435 ]
1435 ]
1436 )
1436 )
1437
1437
1438 prefetch = scmutil.prefetchfiles
1438 prefetch = scmutil.prefetchfiles
1439 matchfiles = scmutil.matchfiles
1439 matchfiles = scmutil.matchfiles
1440 prefetch(
1440 prefetch(
1441 repo,
1441 repo,
1442 [
1442 [
1443 (
1443 (
1444 ctx.rev(),
1444 ctx.rev(),
1445 matchfiles(repo, files),
1445 matchfiles(repo, files),
1446 )
1446 )
1447 ],
1447 ],
1448 )
1448 )
1449
1449
1450
1450
1451 @attr.s(frozen=True)
1451 @attr.s(frozen=True)
1452 class updateresult(object):
1452 class updateresult(object):
1453 updatedcount = attr.ib()
1453 updatedcount = attr.ib()
1454 mergedcount = attr.ib()
1454 mergedcount = attr.ib()
1455 removedcount = attr.ib()
1455 removedcount = attr.ib()
1456 unresolvedcount = attr.ib()
1456 unresolvedcount = attr.ib()
1457
1457
1458 def isempty(self):
1458 def isempty(self):
1459 return not (
1459 return not (
1460 self.updatedcount
1460 self.updatedcount
1461 or self.mergedcount
1461 or self.mergedcount
1462 or self.removedcount
1462 or self.removedcount
1463 or self.unresolvedcount
1463 or self.unresolvedcount
1464 )
1464 )
1465
1465
1466
1466
1467 def applyupdates(
1467 def applyupdates(
1468 repo,
1468 repo,
1469 mresult,
1469 mresult,
1470 wctx,
1470 wctx,
1471 mctx,
1471 mctx,
1472 overwrite,
1472 overwrite,
1473 wantfiledata,
1473 wantfiledata,
1474 labels=None,
1474 labels=None,
1475 ):
1475 ):
1476 """apply the merge action list to the working directory
1476 """apply the merge action list to the working directory
1477
1477
1478 mresult is a mergeresult object representing result of the merge
1478 mresult is a mergeresult object representing result of the merge
1479 wctx is the working copy context
1479 wctx is the working copy context
1480 mctx is the context to be merged into the working copy
1480 mctx is the context to be merged into the working copy
1481
1481
1482 Return a tuple of (counts, filedata), where counts is a tuple
1482 Return a tuple of (counts, filedata), where counts is a tuple
1483 (updated, merged, removed, unresolved) that describes how many
1483 (updated, merged, removed, unresolved) that describes how many
1484 files were affected by the update, and filedata is as described in
1484 files were affected by the update, and filedata is as described in
1485 batchget.
1485 batchget.
1486 """
1486 """
1487
1487
1488 _prefetchfiles(repo, mctx, mresult)
1488 _prefetchfiles(repo, mctx, mresult)
1489
1489
1490 updated, merged, removed = 0, 0, 0
1490 updated, merged, removed = 0, 0, 0
1491 ms = wctx.mergestate(clean=True)
1491 ms = wctx.mergestate(clean=True)
1492 ms.start(wctx.p1().node(), mctx.node(), labels)
1492 ms.start(wctx.p1().node(), mctx.node(), labels)
1493
1493
1494 for f, op in pycompat.iteritems(mresult.commitinfo):
1494 for f, op in pycompat.iteritems(mresult.commitinfo):
1495 # the other side of filenode was choosen while merging, store this in
1495 # the other side of filenode was choosen while merging, store this in
1496 # mergestate so that it can be reused on commit
1496 # mergestate so that it can be reused on commit
1497 ms.addcommitinfo(f, op)
1497 ms.addcommitinfo(f, op)
1498
1498
1499 numupdates = mresult.len() - mresult.len(mergestatemod.NO_OP_ACTIONS)
1499 numupdates = mresult.len() - mresult.len(mergestatemod.NO_OP_ACTIONS)
1500 progress = repo.ui.makeprogress(
1500 progress = repo.ui.makeprogress(
1501 _(b'updating'), unit=_(b'files'), total=numupdates
1501 _(b'updating'), unit=_(b'files'), total=numupdates
1502 )
1502 )
1503
1503
1504 if b'.hgsubstate' in mresult._actionmapping[mergestatemod.ACTION_REMOVE]:
1504 if b'.hgsubstate' in mresult._actionmapping[mergestatemod.ACTION_REMOVE]:
1505 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1505 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1506
1506
1507 # record path conflicts
1507 # record path conflicts
1508 for f, args, msg in mresult.getactions(
1508 for f, args, msg in mresult.getactions(
1509 [mergestatemod.ACTION_PATH_CONFLICT], sort=True
1509 [mergestatemod.ACTION_PATH_CONFLICT], sort=True
1510 ):
1510 ):
1511 f1, fo = args
1511 f1, fo = args
1512 s = repo.ui.status
1512 s = repo.ui.status
1513 s(
1513 s(
1514 _(
1514 _(
1515 b"%s: path conflict - a file or link has the same name as a "
1515 b"%s: path conflict - a file or link has the same name as a "
1516 b"directory\n"
1516 b"directory\n"
1517 )
1517 )
1518 % f
1518 % f
1519 )
1519 )
1520 if fo == b'l':
1520 if fo == b'l':
1521 s(_(b"the local file has been renamed to %s\n") % f1)
1521 s(_(b"the local file has been renamed to %s\n") % f1)
1522 else:
1522 else:
1523 s(_(b"the remote file has been renamed to %s\n") % f1)
1523 s(_(b"the remote file has been renamed to %s\n") % f1)
1524 s(_(b"resolve manually then use 'hg resolve --mark %s'\n") % f)
1524 s(_(b"resolve manually then use 'hg resolve --mark %s'\n") % f)
1525 ms.addpathconflict(f, f1, fo)
1525 ms.addpathconflict(f, f1, fo)
1526 progress.increment(item=f)
1526 progress.increment(item=f)
1527
1527
1528 # When merging in-memory, we can't support worker processes, so set the
1528 # When merging in-memory, we can't support worker processes, so set the
1529 # per-item cost at 0 in that case.
1529 # per-item cost at 0 in that case.
1530 cost = 0 if wctx.isinmemory() else 0.001
1530 cost = 0 if wctx.isinmemory() else 0.001
1531
1531
1532 # remove in parallel (must come before resolving path conflicts and getting)
1532 # remove in parallel (must come before resolving path conflicts and getting)
1533 prog = worker.worker(
1533 prog = worker.worker(
1534 repo.ui,
1534 repo.ui,
1535 cost,
1535 cost,
1536 batchremove,
1536 batchremove,
1537 (repo, wctx),
1537 (repo, wctx),
1538 list(mresult.getactions([mergestatemod.ACTION_REMOVE], sort=True)),
1538 list(mresult.getactions([mergestatemod.ACTION_REMOVE], sort=True)),
1539 )
1539 )
1540 for i, item in prog:
1540 for i, item in prog:
1541 progress.increment(step=i, item=item)
1541 progress.increment(step=i, item=item)
1542 removed = mresult.len((mergestatemod.ACTION_REMOVE,))
1542 removed = mresult.len((mergestatemod.ACTION_REMOVE,))
1543
1543
1544 # resolve path conflicts (must come before getting)
1544 # resolve path conflicts (must come before getting)
1545 for f, args, msg in mresult.getactions(
1545 for f, args, msg in mresult.getactions(
1546 [mergestatemod.ACTION_PATH_CONFLICT_RESOLVE], sort=True
1546 [mergestatemod.ACTION_PATH_CONFLICT_RESOLVE], sort=True
1547 ):
1547 ):
1548 repo.ui.debug(b" %s: %s -> pr\n" % (f, msg))
1548 repo.ui.debug(b" %s: %s -> pr\n" % (f, msg))
1549 (f0, origf0) = args
1549 (f0, origf0) = args
1550 if wctx[f0].lexists():
1550 if wctx[f0].lexists():
1551 repo.ui.note(_(b"moving %s to %s\n") % (f0, f))
1551 repo.ui.note(_(b"moving %s to %s\n") % (f0, f))
1552 wctx[f].audit()
1552 wctx[f].audit()
1553 wctx[f].write(wctx.filectx(f0).data(), wctx.filectx(f0).flags())
1553 wctx[f].write(wctx.filectx(f0).data(), wctx.filectx(f0).flags())
1554 wctx[f0].remove()
1554 wctx[f0].remove()
1555 progress.increment(item=f)
1555 progress.increment(item=f)
1556
1556
1557 # get in parallel.
1557 # get in parallel.
1558 threadsafe = repo.ui.configbool(
1558 threadsafe = repo.ui.configbool(
1559 b'experimental', b'worker.wdir-get-thread-safe'
1559 b'experimental', b'worker.wdir-get-thread-safe'
1560 )
1560 )
1561 prog = worker.worker(
1561 prog = worker.worker(
1562 repo.ui,
1562 repo.ui,
1563 cost,
1563 cost,
1564 batchget,
1564 batchget,
1565 (repo, mctx, wctx, wantfiledata),
1565 (repo, mctx, wctx, wantfiledata),
1566 list(mresult.getactions([mergestatemod.ACTION_GET], sort=True)),
1566 list(mresult.getactions([mergestatemod.ACTION_GET], sort=True)),
1567 threadsafe=threadsafe,
1567 threadsafe=threadsafe,
1568 hasretval=True,
1568 hasretval=True,
1569 )
1569 )
1570 getfiledata = {}
1570 getfiledata = {}
1571 for final, res in prog:
1571 for final, res in prog:
1572 if final:
1572 if final:
1573 getfiledata = res
1573 getfiledata = res
1574 else:
1574 else:
1575 i, item = res
1575 i, item = res
1576 progress.increment(step=i, item=item)
1576 progress.increment(step=i, item=item)
1577
1577
1578 if b'.hgsubstate' in mresult._actionmapping[mergestatemod.ACTION_GET]:
1578 if b'.hgsubstate' in mresult._actionmapping[mergestatemod.ACTION_GET]:
1579 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1579 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1580
1580
1581 # forget (manifest only, just log it) (must come first)
1581 # forget (manifest only, just log it) (must come first)
1582 for f, args, msg in mresult.getactions(
1582 for f, args, msg in mresult.getactions(
1583 (mergestatemod.ACTION_FORGET,), sort=True
1583 (mergestatemod.ACTION_FORGET,), sort=True
1584 ):
1584 ):
1585 repo.ui.debug(b" %s: %s -> f\n" % (f, msg))
1585 repo.ui.debug(b" %s: %s -> f\n" % (f, msg))
1586 progress.increment(item=f)
1586 progress.increment(item=f)
1587
1587
1588 # re-add (manifest only, just log it)
1588 # re-add (manifest only, just log it)
1589 for f, args, msg in mresult.getactions(
1589 for f, args, msg in mresult.getactions(
1590 (mergestatemod.ACTION_ADD,), sort=True
1590 (mergestatemod.ACTION_ADD,), sort=True
1591 ):
1591 ):
1592 repo.ui.debug(b" %s: %s -> a\n" % (f, msg))
1592 repo.ui.debug(b" %s: %s -> a\n" % (f, msg))
1593 progress.increment(item=f)
1593 progress.increment(item=f)
1594
1594
1595 # re-add/mark as modified (manifest only, just log it)
1595 # re-add/mark as modified (manifest only, just log it)
1596 for f, args, msg in mresult.getactions(
1596 for f, args, msg in mresult.getactions(
1597 (mergestatemod.ACTION_ADD_MODIFIED,), sort=True
1597 (mergestatemod.ACTION_ADD_MODIFIED,), sort=True
1598 ):
1598 ):
1599 repo.ui.debug(b" %s: %s -> am\n" % (f, msg))
1599 repo.ui.debug(b" %s: %s -> am\n" % (f, msg))
1600 progress.increment(item=f)
1600 progress.increment(item=f)
1601
1601
1602 # keep (noop, just log it)
1602 # keep (noop, just log it)
1603 for a in mergestatemod.NO_OP_ACTIONS:
1603 for a in mergestatemod.NO_OP_ACTIONS:
1604 for f, args, msg in mresult.getactions((a,), sort=True):
1604 for f, args, msg in mresult.getactions((a,), sort=True):
1605 repo.ui.debug(b" %s: %s -> %s\n" % (f, msg, a))
1605 repo.ui.debug(b" %s: %s -> %s\n" % (f, msg, a))
1606 # no progress
1606 # no progress
1607
1607
1608 # directory rename, move local
1608 # directory rename, move local
1609 for f, args, msg in mresult.getactions(
1609 for f, args, msg in mresult.getactions(
1610 (mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL,), sort=True
1610 (mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL,), sort=True
1611 ):
1611 ):
1612 repo.ui.debug(b" %s: %s -> dm\n" % (f, msg))
1612 repo.ui.debug(b" %s: %s -> dm\n" % (f, msg))
1613 progress.increment(item=f)
1613 progress.increment(item=f)
1614 f0, flags = args
1614 f0, flags = args
1615 repo.ui.note(_(b"moving %s to %s\n") % (f0, f))
1615 repo.ui.note(_(b"moving %s to %s\n") % (f0, f))
1616 wctx[f].audit()
1616 wctx[f].audit()
1617 wctx[f].write(wctx.filectx(f0).data(), flags)
1617 wctx[f].write(wctx.filectx(f0).data(), flags)
1618 wctx[f0].remove()
1618 wctx[f0].remove()
1619
1619
1620 # local directory rename, get
1620 # local directory rename, get
1621 for f, args, msg in mresult.getactions(
1621 for f, args, msg in mresult.getactions(
1622 (mergestatemod.ACTION_LOCAL_DIR_RENAME_GET,), sort=True
1622 (mergestatemod.ACTION_LOCAL_DIR_RENAME_GET,), sort=True
1623 ):
1623 ):
1624 repo.ui.debug(b" %s: %s -> dg\n" % (f, msg))
1624 repo.ui.debug(b" %s: %s -> dg\n" % (f, msg))
1625 progress.increment(item=f)
1625 progress.increment(item=f)
1626 f0, flags = args
1626 f0, flags = args
1627 repo.ui.note(_(b"getting %s to %s\n") % (f0, f))
1627 repo.ui.note(_(b"getting %s to %s\n") % (f0, f))
1628 wctx[f].write(mctx.filectx(f0).data(), flags)
1628 wctx[f].write(mctx.filectx(f0).data(), flags)
1629
1629
1630 # exec
1630 # exec
1631 for f, args, msg in mresult.getactions(
1631 for f, args, msg in mresult.getactions(
1632 (mergestatemod.ACTION_EXEC,), sort=True
1632 (mergestatemod.ACTION_EXEC,), sort=True
1633 ):
1633 ):
1634 repo.ui.debug(b" %s: %s -> e\n" % (f, msg))
1634 repo.ui.debug(b" %s: %s -> e\n" % (f, msg))
1635 progress.increment(item=f)
1635 progress.increment(item=f)
1636 (flags,) = args
1636 (flags,) = args
1637 wctx[f].audit()
1637 wctx[f].audit()
1638 wctx[f].setflags(b'l' in flags, b'x' in flags)
1638 wctx[f].setflags(b'l' in flags, b'x' in flags)
1639
1639
1640 moves = []
1640 moves = []
1641
1641
1642 # 'cd' and 'dc' actions are treated like other merge conflicts
1642 # 'cd' and 'dc' actions are treated like other merge conflicts
1643 mergeactions = list(
1643 mergeactions = list(
1644 mresult.getactions(
1644 mresult.getactions(
1645 [
1645 [
1646 mergestatemod.ACTION_CHANGED_DELETED,
1646 mergestatemod.ACTION_CHANGED_DELETED,
1647 mergestatemod.ACTION_DELETED_CHANGED,
1647 mergestatemod.ACTION_DELETED_CHANGED,
1648 mergestatemod.ACTION_MERGE,
1648 mergestatemod.ACTION_MERGE,
1649 ],
1649 ],
1650 sort=True,
1650 sort=True,
1651 )
1651 )
1652 )
1652 )
1653 for f, args, msg in mergeactions:
1653 for f, args, msg in mergeactions:
1654 f1, f2, fa, move, anc = args
1654 f1, f2, fa, move, anc = args
1655 if f == b'.hgsubstate': # merged internally
1655 if f == b'.hgsubstate': # merged internally
1656 continue
1656 continue
1657 if f1 is None:
1657 if f1 is None:
1658 fcl = filemerge.absentfilectx(wctx, fa)
1658 fcl = filemerge.absentfilectx(wctx, fa)
1659 else:
1659 else:
1660 repo.ui.debug(b" preserving %s for resolve of %s\n" % (f1, f))
1660 repo.ui.debug(b" preserving %s for resolve of %s\n" % (f1, f))
1661 fcl = wctx[f1]
1661 fcl = wctx[f1]
1662 if f2 is None:
1662 if f2 is None:
1663 fco = filemerge.absentfilectx(mctx, fa)
1663 fco = filemerge.absentfilectx(mctx, fa)
1664 else:
1664 else:
1665 fco = mctx[f2]
1665 fco = mctx[f2]
1666 actx = repo[anc]
1666 actx = repo[anc]
1667 if fa in actx:
1667 if fa in actx:
1668 fca = actx[fa]
1668 fca = actx[fa]
1669 else:
1669 else:
1670 # TODO: move to absentfilectx
1670 # TODO: move to absentfilectx
1671 fca = repo.filectx(f1, fileid=nullrev)
1671 fca = repo.filectx(f1, fileid=nullrev)
1672 ms.add(fcl, fco, fca, f)
1672 ms.add(fcl, fco, fca, f)
1673 if f1 != f and move:
1673 if f1 != f and move:
1674 moves.append(f1)
1674 moves.append(f1)
1675
1675
1676 # remove renamed files after safely stored
1676 # remove renamed files after safely stored
1677 for f in moves:
1677 for f in moves:
1678 if wctx[f].lexists():
1678 if wctx[f].lexists():
1679 repo.ui.debug(b"removing %s\n" % f)
1679 repo.ui.debug(b"removing %s\n" % f)
1680 wctx[f].audit()
1680 wctx[f].audit()
1681 wctx[f].remove()
1681 wctx[f].remove()
1682
1682
1683 # these actions updates the file
1683 # these actions updates the file
1684 updated = mresult.len(
1684 updated = mresult.len(
1685 (
1685 (
1686 mergestatemod.ACTION_GET,
1686 mergestatemod.ACTION_GET,
1687 mergestatemod.ACTION_EXEC,
1687 mergestatemod.ACTION_EXEC,
1688 mergestatemod.ACTION_LOCAL_DIR_RENAME_GET,
1688 mergestatemod.ACTION_LOCAL_DIR_RENAME_GET,
1689 mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL,
1689 mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL,
1690 )
1690 )
1691 )
1691 )
1692
1692
1693 try:
1693 try:
1694 # premerge
1694 # premerge
1695 tocomplete = []
1695 tocomplete = []
1696 for f, args, msg in mergeactions:
1696 for f, args, msg in mergeactions:
1697 repo.ui.debug(b" %s: %s -> m (premerge)\n" % (f, msg))
1697 repo.ui.debug(b" %s: %s -> m (premerge)\n" % (f, msg))
1698 progress.increment(item=f)
1698 progress.increment(item=f)
1699 if f == b'.hgsubstate': # subrepo states need updating
1699 if f == b'.hgsubstate': # subrepo states need updating
1700 subrepoutil.submerge(
1700 subrepoutil.submerge(
1701 repo, wctx, mctx, wctx.ancestor(mctx), overwrite, labels
1701 repo, wctx, mctx, wctx.ancestor(mctx), overwrite, labels
1702 )
1702 )
1703 continue
1703 continue
1704 wctx[f].audit()
1704 wctx[f].audit()
1705 complete, r = ms.preresolve(f, wctx)
1705 complete, r = ms.preresolve(f, wctx)
1706 if not complete:
1706 if not complete:
1707 numupdates += 1
1707 numupdates += 1
1708 tocomplete.append((f, args, msg))
1708 tocomplete.append((f, args, msg))
1709
1709
1710 # merge
1710 # merge
1711 for f, args, msg in tocomplete:
1711 for f, args, msg in tocomplete:
1712 repo.ui.debug(b" %s: %s -> m (merge)\n" % (f, msg))
1712 repo.ui.debug(b" %s: %s -> m (merge)\n" % (f, msg))
1713 progress.increment(item=f, total=numupdates)
1713 progress.increment(item=f, total=numupdates)
1714 ms.resolve(f, wctx)
1714 ms.resolve(f, wctx)
1715
1715
1716 finally:
1716 finally:
1717 ms.commit()
1717 ms.commit()
1718
1718
1719 unresolved = ms.unresolvedcount()
1719 unresolved = ms.unresolvedcount()
1720
1720
1721 msupdated, msmerged, msremoved = ms.counts()
1721 msupdated, msmerged, msremoved = ms.counts()
1722 updated += msupdated
1722 updated += msupdated
1723 merged += msmerged
1723 merged += msmerged
1724 removed += msremoved
1724 removed += msremoved
1725
1725
1726 extraactions = ms.actions()
1726 extraactions = ms.actions()
1727 if extraactions:
1727 if extraactions:
1728 for k, acts in pycompat.iteritems(extraactions):
1728 for k, acts in pycompat.iteritems(extraactions):
1729 for a in acts:
1729 for a in acts:
1730 mresult.addfile(a[0], k, *a[1:])
1730 mresult.addfile(a[0], k, *a[1:])
1731 if k == mergestatemod.ACTION_GET and wantfiledata:
1731 if k == mergestatemod.ACTION_GET and wantfiledata:
1732 # no filedata until mergestate is updated to provide it
1732 # no filedata until mergestate is updated to provide it
1733 for a in acts:
1733 for a in acts:
1734 getfiledata[a[0]] = None
1734 getfiledata[a[0]] = None
1735
1735
1736 progress.complete()
1736 progress.complete()
1737 assert len(getfiledata) == (
1737 assert len(getfiledata) == (
1738 mresult.len((mergestatemod.ACTION_GET,)) if wantfiledata else 0
1738 mresult.len((mergestatemod.ACTION_GET,)) if wantfiledata else 0
1739 )
1739 )
1740 return updateresult(updated, merged, removed, unresolved), getfiledata
1740 return updateresult(updated, merged, removed, unresolved), getfiledata
1741
1741
1742
1742
1743 def _advertisefsmonitor(repo, num_gets, p1node):
1743 def _advertisefsmonitor(repo, num_gets, p1node):
1744 # Advertise fsmonitor when its presence could be useful.
1744 # Advertise fsmonitor when its presence could be useful.
1745 #
1745 #
1746 # We only advertise when performing an update from an empty working
1746 # We only advertise when performing an update from an empty working
1747 # directory. This typically only occurs during initial clone.
1747 # directory. This typically only occurs during initial clone.
1748 #
1748 #
1749 # We give users a mechanism to disable the warning in case it is
1749 # We give users a mechanism to disable the warning in case it is
1750 # annoying.
1750 # annoying.
1751 #
1751 #
1752 # We only allow on Linux and MacOS because that's where fsmonitor is
1752 # We only allow on Linux and MacOS because that's where fsmonitor is
1753 # considered stable.
1753 # considered stable.
1754 fsmonitorwarning = repo.ui.configbool(b'fsmonitor', b'warn_when_unused')
1754 fsmonitorwarning = repo.ui.configbool(b'fsmonitor', b'warn_when_unused')
1755 fsmonitorthreshold = repo.ui.configint(
1755 fsmonitorthreshold = repo.ui.configint(
1756 b'fsmonitor', b'warn_update_file_count'
1756 b'fsmonitor', b'warn_update_file_count'
1757 )
1757 )
1758 # avoid cycle dirstate -> sparse -> merge -> dirstate
1758 # avoid cycle dirstate -> sparse -> merge -> dirstate
1759 from . import dirstate
1759 from . import dirstate
1760
1760
1761 if dirstate.rustmod is not None:
1761 if dirstate.rustmod is not None:
1762 # When using rust status, fsmonitor becomes necessary at higher sizes
1762 # When using rust status, fsmonitor becomes necessary at higher sizes
1763 fsmonitorthreshold = repo.ui.configint(
1763 fsmonitorthreshold = repo.ui.configint(
1764 b'fsmonitor',
1764 b'fsmonitor',
1765 b'warn_update_file_count_rust',
1765 b'warn_update_file_count_rust',
1766 )
1766 )
1767
1767
1768 try:
1768 try:
1769 # avoid cycle: extensions -> cmdutil -> merge
1769 # avoid cycle: extensions -> cmdutil -> merge
1770 from . import extensions
1770 from . import extensions
1771
1771
1772 extensions.find(b'fsmonitor')
1772 extensions.find(b'fsmonitor')
1773 fsmonitorenabled = repo.ui.config(b'fsmonitor', b'mode') != b'off'
1773 fsmonitorenabled = repo.ui.config(b'fsmonitor', b'mode') != b'off'
1774 # We intentionally don't look at whether fsmonitor has disabled
1774 # We intentionally don't look at whether fsmonitor has disabled
1775 # itself because a) fsmonitor may have already printed a warning
1775 # itself because a) fsmonitor may have already printed a warning
1776 # b) we only care about the config state here.
1776 # b) we only care about the config state here.
1777 except KeyError:
1777 except KeyError:
1778 fsmonitorenabled = False
1778 fsmonitorenabled = False
1779
1779
1780 if (
1780 if (
1781 fsmonitorwarning
1781 fsmonitorwarning
1782 and not fsmonitorenabled
1782 and not fsmonitorenabled
1783 and p1node == nullid
1783 and p1node == nullid
1784 and num_gets >= fsmonitorthreshold
1784 and num_gets >= fsmonitorthreshold
1785 and pycompat.sysplatform.startswith((b'linux', b'darwin'))
1785 and pycompat.sysplatform.startswith((b'linux', b'darwin'))
1786 ):
1786 ):
1787 repo.ui.warn(
1787 repo.ui.warn(
1788 _(
1788 _(
1789 b'(warning: large working directory being used without '
1789 b'(warning: large working directory being used without '
1790 b'fsmonitor enabled; enable fsmonitor to improve performance; '
1790 b'fsmonitor enabled; enable fsmonitor to improve performance; '
1791 b'see "hg help -e fsmonitor")\n'
1791 b'see "hg help -e fsmonitor")\n'
1792 )
1792 )
1793 )
1793 )
1794
1794
1795
1795
1796 UPDATECHECK_ABORT = b'abort' # handled at higher layers
1796 UPDATECHECK_ABORT = b'abort' # handled at higher layers
1797 UPDATECHECK_NONE = b'none'
1797 UPDATECHECK_NONE = b'none'
1798 UPDATECHECK_LINEAR = b'linear'
1798 UPDATECHECK_LINEAR = b'linear'
1799 UPDATECHECK_NO_CONFLICT = b'noconflict'
1799 UPDATECHECK_NO_CONFLICT = b'noconflict'
1800
1800
1801
1801
1802 def _update(
1802 def _update(
1803 repo,
1803 repo,
1804 node,
1804 node,
1805 branchmerge,
1805 branchmerge,
1806 force,
1806 force,
1807 ancestor=None,
1807 ancestor=None,
1808 mergeancestor=False,
1808 mergeancestor=False,
1809 labels=None,
1809 labels=None,
1810 matcher=None,
1810 matcher=None,
1811 mergeforce=False,
1811 mergeforce=False,
1812 updatedirstate=True,
1812 updatedirstate=True,
1813 updatecheck=None,
1813 updatecheck=None,
1814 wc=None,
1814 wc=None,
1815 ):
1815 ):
1816 """
1816 """
1817 Perform a merge between the working directory and the given node
1817 Perform a merge between the working directory and the given node
1818
1818
1819 node = the node to update to
1819 node = the node to update to
1820 branchmerge = whether to merge between branches
1820 branchmerge = whether to merge between branches
1821 force = whether to force branch merging or file overwriting
1821 force = whether to force branch merging or file overwriting
1822 matcher = a matcher to filter file lists (dirstate not updated)
1822 matcher = a matcher to filter file lists (dirstate not updated)
1823 mergeancestor = whether it is merging with an ancestor. If true,
1823 mergeancestor = whether it is merging with an ancestor. If true,
1824 we should accept the incoming changes for any prompts that occur.
1824 we should accept the incoming changes for any prompts that occur.
1825 If false, merging with an ancestor (fast-forward) is only allowed
1825 If false, merging with an ancestor (fast-forward) is only allowed
1826 between different named branches. This flag is used by rebase extension
1826 between different named branches. This flag is used by rebase extension
1827 as a temporary fix and should be avoided in general.
1827 as a temporary fix and should be avoided in general.
1828 labels = labels to use for base, local and other
1828 labels = labels to use for base, local and other
1829 mergeforce = whether the merge was run with 'merge --force' (deprecated): if
1829 mergeforce = whether the merge was run with 'merge --force' (deprecated): if
1830 this is True, then 'force' should be True as well.
1830 this is True, then 'force' should be True as well.
1831
1831
1832 The table below shows all the behaviors of the update command given the
1832 The table below shows all the behaviors of the update command given the
1833 -c/--check and -C/--clean or no options, whether the working directory is
1833 -c/--check and -C/--clean or no options, whether the working directory is
1834 dirty, whether a revision is specified, and the relationship of the parent
1834 dirty, whether a revision is specified, and the relationship of the parent
1835 rev to the target rev (linear or not). Match from top first. The -n
1835 rev to the target rev (linear or not). Match from top first. The -n
1836 option doesn't exist on the command line, but represents the
1836 option doesn't exist on the command line, but represents the
1837 experimental.updatecheck=noconflict option.
1837 experimental.updatecheck=noconflict option.
1838
1838
1839 This logic is tested by test-update-branches.t.
1839 This logic is tested by test-update-branches.t.
1840
1840
1841 -c -C -n -m dirty rev linear | result
1841 -c -C -n -m dirty rev linear | result
1842 y y * * * * * | (1)
1842 y y * * * * * | (1)
1843 y * y * * * * | (1)
1843 y * y * * * * | (1)
1844 y * * y * * * | (1)
1844 y * * y * * * | (1)
1845 * y y * * * * | (1)
1845 * y y * * * * | (1)
1846 * y * y * * * | (1)
1846 * y * y * * * | (1)
1847 * * y y * * * | (1)
1847 * * y y * * * | (1)
1848 * * * * * n n | x
1848 * * * * * n n | x
1849 * * * * n * * | ok
1849 * * * * n * * | ok
1850 n n n n y * y | merge
1850 n n n n y * y | merge
1851 n n n n y y n | (2)
1851 n n n n y y n | (2)
1852 n n n y y * * | merge
1852 n n n y y * * | merge
1853 n n y n y * * | merge if no conflict
1853 n n y n y * * | merge if no conflict
1854 n y n n y * * | discard
1854 n y n n y * * | discard
1855 y n n n y * * | (3)
1855 y n n n y * * | (3)
1856
1856
1857 x = can't happen
1857 x = can't happen
1858 * = don't-care
1858 * = don't-care
1859 1 = incompatible options (checked in commands.py)
1859 1 = incompatible options (checked in commands.py)
1860 2 = abort: uncommitted changes (commit or update --clean to discard changes)
1860 2 = abort: uncommitted changes (commit or update --clean to discard changes)
1861 3 = abort: uncommitted changes (checked in commands.py)
1861 3 = abort: uncommitted changes (checked in commands.py)
1862
1862
1863 The merge is performed inside ``wc``, a workingctx-like objects. It defaults
1863 The merge is performed inside ``wc``, a workingctx-like objects. It defaults
1864 to repo[None] if None is passed.
1864 to repo[None] if None is passed.
1865
1865
1866 Return the same tuple as applyupdates().
1866 Return the same tuple as applyupdates().
1867 """
1867 """
1868 # Avoid cycle.
1868 # Avoid cycle.
1869 from . import sparse
1869 from . import sparse
1870
1870
1871 # This function used to find the default destination if node was None, but
1871 # This function used to find the default destination if node was None, but
1872 # that's now in destutil.py.
1872 # that's now in destutil.py.
1873 assert node is not None
1873 assert node is not None
1874 if not branchmerge and not force:
1874 if not branchmerge and not force:
1875 # TODO: remove the default once all callers that pass branchmerge=False
1875 # TODO: remove the default once all callers that pass branchmerge=False
1876 # and force=False pass a value for updatecheck. We may want to allow
1876 # and force=False pass a value for updatecheck. We may want to allow
1877 # updatecheck='abort' to better suppport some of these callers.
1877 # updatecheck='abort' to better suppport some of these callers.
1878 if updatecheck is None:
1878 if updatecheck is None:
1879 updatecheck = UPDATECHECK_LINEAR
1879 updatecheck = UPDATECHECK_LINEAR
1880 if updatecheck not in (
1880 if updatecheck not in (
1881 UPDATECHECK_NONE,
1881 UPDATECHECK_NONE,
1882 UPDATECHECK_LINEAR,
1882 UPDATECHECK_LINEAR,
1883 UPDATECHECK_NO_CONFLICT,
1883 UPDATECHECK_NO_CONFLICT,
1884 ):
1884 ):
1885 raise ValueError(
1885 raise ValueError(
1886 r'Invalid updatecheck %r (can accept %r)'
1886 r'Invalid updatecheck %r (can accept %r)'
1887 % (
1887 % (
1888 updatecheck,
1888 updatecheck,
1889 (
1889 (
1890 UPDATECHECK_NONE,
1890 UPDATECHECK_NONE,
1891 UPDATECHECK_LINEAR,
1891 UPDATECHECK_LINEAR,
1892 UPDATECHECK_NO_CONFLICT,
1892 UPDATECHECK_NO_CONFLICT,
1893 ),
1893 ),
1894 )
1894 )
1895 )
1895 )
1896 if wc is not None and wc.isinmemory():
1896 if wc is not None and wc.isinmemory():
1897 maybe_wlock = util.nullcontextmanager()
1897 maybe_wlock = util.nullcontextmanager()
1898 else:
1898 else:
1899 maybe_wlock = repo.wlock()
1899 maybe_wlock = repo.wlock()
1900 with maybe_wlock:
1900 with maybe_wlock:
1901 if wc is None:
1901 if wc is None:
1902 wc = repo[None]
1902 wc = repo[None]
1903 pl = wc.parents()
1903 pl = wc.parents()
1904 p1 = pl[0]
1904 p1 = pl[0]
1905 p2 = repo[node]
1905 p2 = repo[node]
1906 if ancestor is not None:
1906 if ancestor is not None:
1907 pas = [repo[ancestor]]
1907 pas = [repo[ancestor]]
1908 else:
1908 else:
1909 if repo.ui.configlist(b'merge', b'preferancestor') == [b'*']:
1909 if repo.ui.configlist(b'merge', b'preferancestor') == [b'*']:
1910 cahs = repo.changelog.commonancestorsheads(p1.node(), p2.node())
1910 cahs = repo.changelog.commonancestorsheads(p1.node(), p2.node())
1911 pas = [repo[anc] for anc in (sorted(cahs) or [nullid])]
1911 pas = [repo[anc] for anc in (sorted(cahs) or [nullid])]
1912 else:
1912 else:
1913 pas = [p1.ancestor(p2, warn=branchmerge)]
1913 pas = [p1.ancestor(p2, warn=branchmerge)]
1914
1914
1915 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), bytes(p1), bytes(p2)
1915 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), bytes(p1), bytes(p2)
1916
1916
1917 overwrite = force and not branchmerge
1917 overwrite = force and not branchmerge
1918 ### check phase
1918 ### check phase
1919 if not overwrite:
1919 if not overwrite:
1920 if len(pl) > 1:
1920 if len(pl) > 1:
1921 raise error.Abort(_(b"outstanding uncommitted merge"))
1921 raise error.Abort(_(b"outstanding uncommitted merge"))
1922 ms = wc.mergestate()
1922 ms = wc.mergestate()
1923 if ms.unresolvedcount():
1923 if ms.unresolvedcount():
1924 raise error.Abort(
1924 raise error.Abort(
1925 _(b"outstanding merge conflicts"),
1925 _(b"outstanding merge conflicts"),
1926 hint=_(b"use 'hg resolve' to resolve"),
1926 hint=_(b"use 'hg resolve' to resolve"),
1927 )
1927 )
1928 if branchmerge:
1928 if branchmerge:
1929 if pas == [p2]:
1929 if pas == [p2]:
1930 raise error.Abort(
1930 raise error.Abort(
1931 _(
1931 _(
1932 b"merging with a working directory ancestor"
1932 b"merging with a working directory ancestor"
1933 b" has no effect"
1933 b" has no effect"
1934 )
1934 )
1935 )
1935 )
1936 elif pas == [p1]:
1936 elif pas == [p1]:
1937 if not mergeancestor and wc.branch() == p2.branch():
1937 if not mergeancestor and wc.branch() == p2.branch():
1938 raise error.Abort(
1938 raise error.Abort(
1939 _(b"nothing to merge"),
1939 _(b"nothing to merge"),
1940 hint=_(b"use 'hg update' or check 'hg heads'"),
1940 hint=_(b"use 'hg update' or check 'hg heads'"),
1941 )
1941 )
1942 if not force and (wc.files() or wc.deleted()):
1942 if not force and (wc.files() or wc.deleted()):
1943 raise error.StateError(
1943 raise error.StateError(
1944 _(b"uncommitted changes"),
1944 _(b"uncommitted changes"),
1945 hint=_(b"use 'hg status' to list changes"),
1945 hint=_(b"use 'hg status' to list changes"),
1946 )
1946 )
1947 if not wc.isinmemory():
1947 if not wc.isinmemory():
1948 for s in sorted(wc.substate):
1948 for s in sorted(wc.substate):
1949 wc.sub(s).bailifchanged()
1949 wc.sub(s).bailifchanged()
1950
1950
1951 elif not overwrite:
1951 elif not overwrite:
1952 if p1 == p2: # no-op update
1952 if p1 == p2: # no-op update
1953 # call the hooks and exit early
1953 # call the hooks and exit early
1954 repo.hook(b'preupdate', throw=True, parent1=xp2, parent2=b'')
1954 repo.hook(b'preupdate', throw=True, parent1=xp2, parent2=b'')
1955 repo.hook(b'update', parent1=xp2, parent2=b'', error=0)
1955 repo.hook(b'update', parent1=xp2, parent2=b'', error=0)
1956 return updateresult(0, 0, 0, 0)
1956 return updateresult(0, 0, 0, 0)
1957
1957
1958 if updatecheck == UPDATECHECK_LINEAR and pas not in (
1958 if updatecheck == UPDATECHECK_LINEAR and pas not in (
1959 [p1],
1959 [p1],
1960 [p2],
1960 [p2],
1961 ): # nonlinear
1961 ): # nonlinear
1962 dirty = wc.dirty(missing=True)
1962 dirty = wc.dirty(missing=True)
1963 if dirty:
1963 if dirty:
1964 # Branching is a bit strange to ensure we do the minimal
1964 # Branching is a bit strange to ensure we do the minimal
1965 # amount of call to obsutil.foreground.
1965 # amount of call to obsutil.foreground.
1966 foreground = obsutil.foreground(repo, [p1.node()])
1966 foreground = obsutil.foreground(repo, [p1.node()])
1967 # note: the <node> variable contains a random identifier
1967 # note: the <node> variable contains a random identifier
1968 if repo[node].node() in foreground:
1968 if repo[node].node() in foreground:
1969 pass # allow updating to successors
1969 pass # allow updating to successors
1970 else:
1970 else:
1971 msg = _(b"uncommitted changes")
1971 msg = _(b"uncommitted changes")
1972 hint = _(b"commit or update --clean to discard changes")
1972 hint = _(b"commit or update --clean to discard changes")
1973 raise error.UpdateAbort(msg, hint=hint)
1973 raise error.UpdateAbort(msg, hint=hint)
1974 else:
1974 else:
1975 # Allow jumping branches if clean and specific rev given
1975 # Allow jumping branches if clean and specific rev given
1976 pass
1976 pass
1977
1977
1978 if overwrite:
1978 if overwrite:
1979 pas = [wc]
1979 pas = [wc]
1980 elif not branchmerge:
1980 elif not branchmerge:
1981 pas = [p1]
1981 pas = [p1]
1982
1982
1983 # deprecated config: merge.followcopies
1983 # deprecated config: merge.followcopies
1984 followcopies = repo.ui.configbool(b'merge', b'followcopies')
1984 followcopies = repo.ui.configbool(b'merge', b'followcopies')
1985 if overwrite:
1985 if overwrite:
1986 followcopies = False
1986 followcopies = False
1987 elif not pas[0]:
1987 elif not pas[0]:
1988 followcopies = False
1988 followcopies = False
1989 if not branchmerge and not wc.dirty(missing=True):
1989 if not branchmerge and not wc.dirty(missing=True):
1990 followcopies = False
1990 followcopies = False
1991
1991
1992 ### calculate phase
1992 ### calculate phase
1993 mresult = calculateupdates(
1993 mresult = calculateupdates(
1994 repo,
1994 repo,
1995 wc,
1995 wc,
1996 p2,
1996 p2,
1997 pas,
1997 pas,
1998 branchmerge,
1998 branchmerge,
1999 force,
1999 force,
2000 mergeancestor,
2000 mergeancestor,
2001 followcopies,
2001 followcopies,
2002 matcher=matcher,
2002 matcher=matcher,
2003 mergeforce=mergeforce,
2003 mergeforce=mergeforce,
2004 )
2004 )
2005
2005
2006 if updatecheck == UPDATECHECK_NO_CONFLICT:
2006 if updatecheck == UPDATECHECK_NO_CONFLICT:
2007 if mresult.hasconflicts():
2007 if mresult.hasconflicts():
2008 msg = _(b"conflicting changes")
2008 msg = _(b"conflicting changes")
2009 hint = _(b"commit or update --clean to discard changes")
2009 hint = _(b"commit or update --clean to discard changes")
2010 raise error.Abort(msg, hint=hint)
2010 raise error.Abort(msg, hint=hint)
2011
2011
2012 # Prompt and create actions. Most of this is in the resolve phase
2012 # Prompt and create actions. Most of this is in the resolve phase
2013 # already, but we can't handle .hgsubstate in filemerge or
2013 # already, but we can't handle .hgsubstate in filemerge or
2014 # subrepoutil.submerge yet so we have to keep prompting for it.
2014 # subrepoutil.submerge yet so we have to keep prompting for it.
2015 vals = mresult.getfile(b'.hgsubstate')
2015 vals = mresult.getfile(b'.hgsubstate')
2016 if vals:
2016 if vals:
2017 f = b'.hgsubstate'
2017 f = b'.hgsubstate'
2018 m, args, msg = vals
2018 m, args, msg = vals
2019 prompts = filemerge.partextras(labels)
2019 prompts = filemerge.partextras(labels)
2020 prompts[b'f'] = f
2020 prompts[b'f'] = f
2021 if m == mergestatemod.ACTION_CHANGED_DELETED:
2021 if m == mergestatemod.ACTION_CHANGED_DELETED:
2022 if repo.ui.promptchoice(
2022 if repo.ui.promptchoice(
2023 _(
2023 _(
2024 b"local%(l)s changed %(f)s which other%(o)s deleted\n"
2024 b"local%(l)s changed %(f)s which other%(o)s deleted\n"
2025 b"use (c)hanged version or (d)elete?"
2025 b"use (c)hanged version or (d)elete?"
2026 b"$$ &Changed $$ &Delete"
2026 b"$$ &Changed $$ &Delete"
2027 )
2027 )
2028 % prompts,
2028 % prompts,
2029 0,
2029 0,
2030 ):
2030 ):
2031 mresult.addfile(
2031 mresult.addfile(
2032 f,
2032 f,
2033 mergestatemod.ACTION_REMOVE,
2033 mergestatemod.ACTION_REMOVE,
2034 None,
2034 None,
2035 b'prompt delete',
2035 b'prompt delete',
2036 )
2036 )
2037 elif f in p1:
2037 elif f in p1:
2038 mresult.addfile(
2038 mresult.addfile(
2039 f,
2039 f,
2040 mergestatemod.ACTION_ADD_MODIFIED,
2040 mergestatemod.ACTION_ADD_MODIFIED,
2041 None,
2041 None,
2042 b'prompt keep',
2042 b'prompt keep',
2043 )
2043 )
2044 else:
2044 else:
2045 mresult.addfile(
2045 mresult.addfile(
2046 f,
2046 f,
2047 mergestatemod.ACTION_ADD,
2047 mergestatemod.ACTION_ADD,
2048 None,
2048 None,
2049 b'prompt keep',
2049 b'prompt keep',
2050 )
2050 )
2051 elif m == mergestatemod.ACTION_DELETED_CHANGED:
2051 elif m == mergestatemod.ACTION_DELETED_CHANGED:
2052 f1, f2, fa, move, anc = args
2052 f1, f2, fa, move, anc = args
2053 flags = p2[f2].flags()
2053 flags = p2[f2].flags()
2054 if (
2054 if (
2055 repo.ui.promptchoice(
2055 repo.ui.promptchoice(
2056 _(
2056 _(
2057 b"other%(o)s changed %(f)s which local%(l)s deleted\n"
2057 b"other%(o)s changed %(f)s which local%(l)s deleted\n"
2058 b"use (c)hanged version or leave (d)eleted?"
2058 b"use (c)hanged version or leave (d)eleted?"
2059 b"$$ &Changed $$ &Deleted"
2059 b"$$ &Changed $$ &Deleted"
2060 )
2060 )
2061 % prompts,
2061 % prompts,
2062 0,
2062 0,
2063 )
2063 )
2064 == 0
2064 == 0
2065 ):
2065 ):
2066 mresult.addfile(
2066 mresult.addfile(
2067 f,
2067 f,
2068 mergestatemod.ACTION_GET,
2068 mergestatemod.ACTION_GET,
2069 (flags, False),
2069 (flags, False),
2070 b'prompt recreating',
2070 b'prompt recreating',
2071 )
2071 )
2072 else:
2072 else:
2073 mresult.removefile(f)
2073 mresult.removefile(f)
2074
2074
2075 if not util.fscasesensitive(repo.path):
2075 if not util.fscasesensitive(repo.path):
2076 # check collision between files only in p2 for clean update
2076 # check collision between files only in p2 for clean update
2077 if not branchmerge and (
2077 if not branchmerge and (
2078 force or not wc.dirty(missing=True, branch=False)
2078 force or not wc.dirty(missing=True, branch=False)
2079 ):
2079 ):
2080 _checkcollision(repo, p2.manifest(), None)
2080 _checkcollision(repo, p2.manifest(), None)
2081 else:
2081 else:
2082 _checkcollision(repo, wc.manifest(), mresult)
2082 _checkcollision(repo, wc.manifest(), mresult)
2083
2083
2084 # divergent renames
2084 # divergent renames
2085 for f, fl in sorted(pycompat.iteritems(mresult.diverge)):
2085 for f, fl in sorted(pycompat.iteritems(mresult.diverge)):
2086 repo.ui.warn(
2086 repo.ui.warn(
2087 _(
2087 _(
2088 b"note: possible conflict - %s was renamed "
2088 b"note: possible conflict - %s was renamed "
2089 b"multiple times to:\n"
2089 b"multiple times to:\n"
2090 )
2090 )
2091 % f
2091 % f
2092 )
2092 )
2093 for nf in sorted(fl):
2093 for nf in sorted(fl):
2094 repo.ui.warn(b" %s\n" % nf)
2094 repo.ui.warn(b" %s\n" % nf)
2095
2095
2096 # rename and delete
2096 # rename and delete
2097 for f, fl in sorted(pycompat.iteritems(mresult.renamedelete)):
2097 for f, fl in sorted(pycompat.iteritems(mresult.renamedelete)):
2098 repo.ui.warn(
2098 repo.ui.warn(
2099 _(
2099 _(
2100 b"note: possible conflict - %s was deleted "
2100 b"note: possible conflict - %s was deleted "
2101 b"and renamed to:\n"
2101 b"and renamed to:\n"
2102 )
2102 )
2103 % f
2103 % f
2104 )
2104 )
2105 for nf in sorted(fl):
2105 for nf in sorted(fl):
2106 repo.ui.warn(b" %s\n" % nf)
2106 repo.ui.warn(b" %s\n" % nf)
2107
2107
2108 ### apply phase
2108 ### apply phase
2109 if not branchmerge: # just jump to the new rev
2109 if not branchmerge: # just jump to the new rev
2110 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, b''
2110 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, b''
2111 # If we're doing a partial update, we need to skip updating
2111 # If we're doing a partial update, we need to skip updating
2112 # the dirstate.
2112 # the dirstate.
2113 always = matcher is None or matcher.always()
2113 always = matcher is None or matcher.always()
2114 updatedirstate = updatedirstate and always and not wc.isinmemory()
2114 updatedirstate = updatedirstate and always and not wc.isinmemory()
2115 if updatedirstate:
2115 if updatedirstate:
2116 repo.hook(b'preupdate', throw=True, parent1=xp1, parent2=xp2)
2116 repo.hook(b'preupdate', throw=True, parent1=xp1, parent2=xp2)
2117 # note that we're in the middle of an update
2117 # note that we're in the middle of an update
2118 repo.vfs.write(b'updatestate', p2.hex())
2118 repo.vfs.write(b'updatestate', p2.hex())
2119
2119
2120 _advertisefsmonitor(
2120 _advertisefsmonitor(
2121 repo, mresult.len((mergestatemod.ACTION_GET,)), p1.node()
2121 repo, mresult.len((mergestatemod.ACTION_GET,)), p1.node()
2122 )
2122 )
2123
2123
2124 wantfiledata = updatedirstate and not branchmerge
2124 wantfiledata = updatedirstate and not branchmerge
2125 stats, getfiledata = applyupdates(
2125 stats, getfiledata = applyupdates(
2126 repo,
2126 repo,
2127 mresult,
2127 mresult,
2128 wc,
2128 wc,
2129 p2,
2129 p2,
2130 overwrite,
2130 overwrite,
2131 wantfiledata,
2131 wantfiledata,
2132 labels=labels,
2132 labels=labels,
2133 )
2133 )
2134
2134
2135 if updatedirstate:
2135 if updatedirstate:
2136 with repo.dirstate.parentchange():
2136 with repo.dirstate.parentchange():
2137 repo.setparents(fp1, fp2)
2137 repo.setparents(fp1, fp2)
2138 mergestatemod.recordupdates(
2138 mergestatemod.recordupdates(
2139 repo, mresult.actionsdict, branchmerge, getfiledata
2139 repo, mresult.actionsdict, branchmerge, getfiledata
2140 )
2140 )
2141 # update completed, clear state
2141 # update completed, clear state
2142 util.unlink(repo.vfs.join(b'updatestate'))
2142 util.unlink(repo.vfs.join(b'updatestate'))
2143
2143
2144 if not branchmerge:
2144 if not branchmerge:
2145 repo.dirstate.setbranch(p2.branch())
2145 repo.dirstate.setbranch(p2.branch())
2146
2146
2147 # If we're updating to a location, clean up any stale temporary includes
2147 # If we're updating to a location, clean up any stale temporary includes
2148 # (ex: this happens during hg rebase --abort).
2148 # (ex: this happens during hg rebase --abort).
2149 if not branchmerge:
2149 if not branchmerge:
2150 sparse.prunetemporaryincludes(repo)
2150 sparse.prunetemporaryincludes(repo)
2151
2151
2152 if updatedirstate:
2152 if updatedirstate:
2153 repo.hook(
2153 repo.hook(
2154 b'update', parent1=xp1, parent2=xp2, error=stats.unresolvedcount
2154 b'update', parent1=xp1, parent2=xp2, error=stats.unresolvedcount
2155 )
2155 )
2156 return stats
2156 return stats
2157
2157
2158
2158
2159 def merge(ctx, labels=None, force=False, wc=None):
2159 def merge(ctx, labels=None, force=False, wc=None):
2160 """Merge another topological branch into the working copy.
2160 """Merge another topological branch into the working copy.
2161
2161
2162 force = whether the merge was run with 'merge --force' (deprecated)
2162 force = whether the merge was run with 'merge --force' (deprecated)
2163 """
2163 """
2164
2164
2165 return _update(
2165 return _update(
2166 ctx.repo(),
2166 ctx.repo(),
2167 ctx.rev(),
2167 ctx.rev(),
2168 labels=labels,
2168 labels=labels,
2169 branchmerge=True,
2169 branchmerge=True,
2170 force=force,
2170 force=force,
2171 mergeforce=force,
2171 mergeforce=force,
2172 wc=wc,
2172 wc=wc,
2173 )
2173 )
2174
2174
2175
2175
2176 def update(ctx, updatecheck=None, wc=None):
2176 def update(ctx, updatecheck=None, wc=None):
2177 """Do a regular update to the given commit, aborting if there are conflicts.
2177 """Do a regular update to the given commit, aborting if there are conflicts.
2178
2178
2179 The 'updatecheck' argument can be used to control what to do in case of
2179 The 'updatecheck' argument can be used to control what to do in case of
2180 conflicts.
2180 conflicts.
2181
2181
2182 Note: This is a new, higher-level update() than the one that used to exist
2182 Note: This is a new, higher-level update() than the one that used to exist
2183 in this module. That function is now called _update(). You can hopefully
2183 in this module. That function is now called _update(). You can hopefully
2184 replace your callers to use this new update(), or clean_update(), merge(),
2184 replace your callers to use this new update(), or clean_update(), merge(),
2185 revert_to(), or graft().
2185 revert_to(), or graft().
2186 """
2186 """
2187 return _update(
2187 return _update(
2188 ctx.repo(),
2188 ctx.repo(),
2189 ctx.rev(),
2189 ctx.rev(),
2190 branchmerge=False,
2190 branchmerge=False,
2191 force=False,
2191 force=False,
2192 labels=[b'working copy', b'destination'],
2192 labels=[b'working copy', b'destination'],
2193 updatecheck=updatecheck,
2193 updatecheck=updatecheck,
2194 wc=wc,
2194 wc=wc,
2195 )
2195 )
2196
2196
2197
2197
2198 def clean_update(ctx, wc=None):
2198 def clean_update(ctx, wc=None):
2199 """Do a clean update to the given commit.
2199 """Do a clean update to the given commit.
2200
2200
2201 This involves updating to the commit and discarding any changes in the
2201 This involves updating to the commit and discarding any changes in the
2202 working copy.
2202 working copy.
2203 """
2203 """
2204 return _update(ctx.repo(), ctx.rev(), branchmerge=False, force=True, wc=wc)
2204 return _update(ctx.repo(), ctx.rev(), branchmerge=False, force=True, wc=wc)
2205
2205
2206
2206
2207 def revert_to(ctx, matcher=None, wc=None):
2207 def revert_to(ctx, matcher=None, wc=None):
2208 """Revert the working copy to the given commit.
2208 """Revert the working copy to the given commit.
2209
2209
2210 The working copy will keep its current parent(s) but its content will
2210 The working copy will keep its current parent(s) but its content will
2211 be the same as in the given commit.
2211 be the same as in the given commit.
2212 """
2212 """
2213
2213
2214 return _update(
2214 return _update(
2215 ctx.repo(),
2215 ctx.repo(),
2216 ctx.rev(),
2216 ctx.rev(),
2217 branchmerge=False,
2217 branchmerge=False,
2218 force=True,
2218 force=True,
2219 updatedirstate=False,
2219 updatedirstate=False,
2220 matcher=matcher,
2220 matcher=matcher,
2221 wc=wc,
2221 wc=wc,
2222 )
2222 )
2223
2223
2224
2224
2225 def graft(
2225 def graft(
2226 repo,
2226 repo,
2227 ctx,
2227 ctx,
2228 base=None,
2228 base=None,
2229 labels=None,
2229 labels=None,
2230 keepparent=False,
2230 keepparent=False,
2231 keepconflictparent=False,
2231 keepconflictparent=False,
2232 wctx=None,
2232 wctx=None,
2233 ):
2233 ):
2234 """Do a graft-like merge.
2234 """Do a graft-like merge.
2235
2235
2236 This is a merge where the merge ancestor is chosen such that one
2236 This is a merge where the merge ancestor is chosen such that one
2237 or more changesets are grafted onto the current changeset. In
2237 or more changesets are grafted onto the current changeset. In
2238 addition to the merge, this fixes up the dirstate to include only
2238 addition to the merge, this fixes up the dirstate to include only
2239 a single parent (if keepparent is False) and tries to duplicate any
2239 a single parent (if keepparent is False) and tries to duplicate any
2240 renames/copies appropriately.
2240 renames/copies appropriately.
2241
2241
2242 ctx - changeset to rebase
2242 ctx - changeset to rebase
2243 base - merge base, or ctx.p1() if not specified
2243 base - merge base, or ctx.p1() if not specified
2244 labels - merge labels eg ['local', 'graft']
2244 labels - merge labels eg ['local', 'graft']
2245 keepparent - keep second parent if any
2245 keepparent - keep second parent if any
2246 keepconflictparent - if unresolved, keep parent used for the merge
2246 keepconflictparent - if unresolved, keep parent used for the merge
2247
2247
2248 """
2248 """
2249 # If we're grafting a descendant onto an ancestor, be sure to pass
2249 # If we're grafting a descendant onto an ancestor, be sure to pass
2250 # mergeancestor=True to update. This does two things: 1) allows the merge if
2250 # mergeancestor=True to update. This does two things: 1) allows the merge if
2251 # the destination is the same as the parent of the ctx (so we can use graft
2251 # the destination is the same as the parent of the ctx (so we can use graft
2252 # to copy commits), and 2) informs update that the incoming changes are
2252 # to copy commits), and 2) informs update that the incoming changes are
2253 # newer than the destination so it doesn't prompt about "remote changed foo
2253 # newer than the destination so it doesn't prompt about "remote changed foo
2254 # which local deleted".
2254 # which local deleted".
2255 # We also pass mergeancestor=True when base is the same revision as p1. 2)
2255 # We also pass mergeancestor=True when base is the same revision as p1. 2)
2256 # doesn't matter as there can't possibly be conflicts, but 1) is necessary.
2256 # doesn't matter as there can't possibly be conflicts, but 1) is necessary.
2257 wctx = wctx or repo[None]
2257 wctx = wctx or repo[None]
2258 pctx = wctx.p1()
2258 pctx = wctx.p1()
2259 base = base or ctx.p1()
2259 base = base or ctx.p1()
2260 mergeancestor = (
2260 mergeancestor = (
2261 repo.changelog.isancestor(pctx.node(), ctx.node())
2261 repo.changelog.isancestor(pctx.node(), ctx.node())
2262 or pctx.rev() == base.rev()
2262 or pctx.rev() == base.rev()
2263 )
2263 )
2264
2264
2265 stats = _update(
2265 stats = _update(
2266 repo,
2266 repo,
2267 ctx.node(),
2267 ctx.node(),
2268 True,
2268 True,
2269 True,
2269 True,
2270 base.node(),
2270 base.node(),
2271 mergeancestor=mergeancestor,
2271 mergeancestor=mergeancestor,
2272 labels=labels,
2272 labels=labels,
2273 wc=wctx,
2273 wc=wctx,
2274 )
2274 )
2275
2275
2276 if keepconflictparent and stats.unresolvedcount:
2276 if keepconflictparent and stats.unresolvedcount:
2277 pother = ctx.node()
2277 pother = ctx.node()
2278 else:
2278 else:
2279 pother = nullid
2279 pother = nullid
2280 parents = ctx.parents()
2280 parents = ctx.parents()
2281 if keepparent and len(parents) == 2 and base in parents:
2281 if keepparent and len(parents) == 2 and base in parents:
2282 parents.remove(base)
2282 parents.remove(base)
2283 pother = parents[0].node()
2283 pother = parents[0].node()
2284 # Never set both parents equal to each other
2284 # Never set both parents equal to each other
2285 if pother == pctx.node():
2285 if pother == pctx.node():
2286 pother = nullid
2286 pother = nullid
2287
2287
2288 if wctx.isinmemory():
2288 if wctx.isinmemory():
2289 wctx.setparents(pctx.node(), pother)
2289 wctx.setparents(pctx.node(), pother)
2290 # fix up dirstate for copies and renames
2290 # fix up dirstate for copies and renames
2291 copies.graftcopies(wctx, ctx, base)
2291 copies.graftcopies(wctx, ctx, base)
2292 else:
2292 else:
2293 with repo.dirstate.parentchange():
2293 with repo.dirstate.parentchange():
2294 repo.setparents(pctx.node(), pother)
2294 repo.setparents(pctx.node(), pother)
2295 repo.dirstate.write(repo.currenttransaction())
2295 repo.dirstate.write(repo.currenttransaction())
2296 # fix up dirstate for copies and renames
2296 # fix up dirstate for copies and renames
2297 copies.graftcopies(wctx, ctx, base)
2297 copies.graftcopies(wctx, ctx, base)
2298 return stats
2298 return stats
2299
2299
2300
2300
2301 def back_out(ctx, parent=None, wc=None):
2301 def back_out(ctx, parent=None, wc=None):
2302 if parent is None:
2302 if parent is None:
2303 if ctx.p2() is not None:
2303 if ctx.p2() is not None:
2304 raise error.ProgrammingError(
2304 raise error.ProgrammingError(
2305 b"must specify parent of merge commit to back out"
2305 b"must specify parent of merge commit to back out"
2306 )
2306 )
2307 parent = ctx.p1()
2307 parent = ctx.p1()
2308 return _update(
2308 return _update(
2309 ctx.repo(),
2309 ctx.repo(),
2310 parent,
2310 parent,
2311 branchmerge=True,
2311 branchmerge=True,
2312 force=True,
2312 force=True,
2313 ancestor=ctx.node(),
2313 ancestor=ctx.node(),
2314 mergeancestor=False,
2314 mergeancestor=False,
2315 )
2315 )
2316
2316
2317
2317
2318 def purge(
2318 def purge(
2319 repo,
2319 repo,
2320 matcher,
2320 matcher,
2321 unknown=True,
2321 unknown=True,
2322 ignored=False,
2322 ignored=False,
2323 removeemptydirs=True,
2323 removeemptydirs=True,
2324 removefiles=True,
2324 removefiles=True,
2325 abortonerror=False,
2325 abortonerror=False,
2326 noop=False,
2326 noop=False,
2327 confirm=False,
2327 ):
2328 ):
2328 """Purge the working directory of untracked files.
2329 """Purge the working directory of untracked files.
2329
2330
2330 ``matcher`` is a matcher configured to scan the working directory -
2331 ``matcher`` is a matcher configured to scan the working directory -
2331 potentially a subset.
2332 potentially a subset.
2332
2333
2333 ``unknown`` controls whether unknown files should be purged.
2334 ``unknown`` controls whether unknown files should be purged.
2334
2335
2335 ``ignored`` controls whether ignored files should be purged.
2336 ``ignored`` controls whether ignored files should be purged.
2336
2337
2337 ``removeemptydirs`` controls whether empty directories should be removed.
2338 ``removeemptydirs`` controls whether empty directories should be removed.
2338
2339
2339 ``removefiles`` controls whether files are removed.
2340 ``removefiles`` controls whether files are removed.
2340
2341
2341 ``abortonerror`` causes an exception to be raised if an error occurs
2342 ``abortonerror`` causes an exception to be raised if an error occurs
2342 deleting a file or directory.
2343 deleting a file or directory.
2343
2344
2344 ``noop`` controls whether to actually remove files. If not defined, actions
2345 ``noop`` controls whether to actually remove files. If not defined, actions
2345 will be taken.
2346 will be taken.
2346
2347
2348 ``confirm`` ask confirmation before actually removing anything.
2349
2347 Returns an iterable of relative paths in the working directory that were
2350 Returns an iterable of relative paths in the working directory that were
2348 or would be removed.
2351 or would be removed.
2349 """
2352 """
2350
2353
2351 def remove(removefn, path):
2354 def remove(removefn, path):
2352 try:
2355 try:
2353 removefn(path)
2356 removefn(path)
2354 except OSError:
2357 except OSError:
2355 m = _(b'%s cannot be removed') % path
2358 m = _(b'%s cannot be removed') % path
2356 if abortonerror:
2359 if abortonerror:
2357 raise error.Abort(m)
2360 raise error.Abort(m)
2358 else:
2361 else:
2359 repo.ui.warn(_(b'warning: %s\n') % m)
2362 repo.ui.warn(_(b'warning: %s\n') % m)
2360
2363
2361 # There's no API to copy a matcher. So mutate the passed matcher and
2364 # There's no API to copy a matcher. So mutate the passed matcher and
2362 # restore it when we're done.
2365 # restore it when we're done.
2363 oldtraversedir = matcher.traversedir
2366 oldtraversedir = matcher.traversedir
2364
2367
2365 res = []
2368 res = []
2366
2369
2367 try:
2370 try:
2368 if removeemptydirs:
2371 if removeemptydirs:
2369 directories = []
2372 directories = []
2370 matcher.traversedir = directories.append
2373 matcher.traversedir = directories.append
2371
2374
2372 status = repo.status(match=matcher, ignored=ignored, unknown=unknown)
2375 status = repo.status(match=matcher, ignored=ignored, unknown=unknown)
2373
2376
2377 if confirm:
2378 nb_ignored = len(status.ignored)
2379 nb_unkown = len(status.unknown)
2380 if nb_unkown and nb_ignored:
2381 msg = _(b"permanently delete %d unkown and %d ignored files?")
2382 msg %= (nb_unkown, nb_ignored)
2383 elif nb_unkown:
2384 msg = _(b"permanently delete %d unkown files?")
2385 msg %= nb_unkown
2386 elif nb_ignored:
2387 msg = _(b"permanently delete %d ignored files?")
2388 msg %= nb_ignored
2389 else:
2390 # XXX we might be missing directory there
2391 return res
2392 msg += b" (yN)$$ &Yes $$ &No"
2393 if repo.ui.promptchoice(msg, default=1) == 1:
2394 raise error.CanceledError(_(b'removal cancelled'))
2395
2374 if removefiles:
2396 if removefiles:
2375 for f in sorted(status.unknown + status.ignored):
2397 for f in sorted(status.unknown + status.ignored):
2376 if not noop:
2398 if not noop:
2377 repo.ui.note(_(b'removing file %s\n') % f)
2399 repo.ui.note(_(b'removing file %s\n') % f)
2378 remove(repo.wvfs.unlink, f)
2400 remove(repo.wvfs.unlink, f)
2379 res.append(f)
2401 res.append(f)
2380
2402
2381 if removeemptydirs:
2403 if removeemptydirs:
2382 for f in sorted(directories, reverse=True):
2404 for f in sorted(directories, reverse=True):
2383 if matcher(f) and not repo.wvfs.listdir(f):
2405 if matcher(f) and not repo.wvfs.listdir(f):
2384 if not noop:
2406 if not noop:
2385 repo.ui.note(_(b'removing directory %s\n') % f)
2407 repo.ui.note(_(b'removing directory %s\n') % f)
2386 remove(repo.wvfs.rmdir, f)
2408 remove(repo.wvfs.rmdir, f)
2387 res.append(f)
2409 res.append(f)
2388
2410
2389 return res
2411 return res
2390
2412
2391 finally:
2413 finally:
2392 matcher.traversedir = oldtraversedir
2414 matcher.traversedir = oldtraversedir
@@ -1,306 +1,323 b''
1 $ cat <<EOF >> $HGRCPATH
1 $ cat <<EOF >> $HGRCPATH
2 > [extensions]
2 > [extensions]
3 > purge =
3 > purge =
4 > EOF
4 > EOF
5
5
6 init
6 init
7
7
8 $ hg init t
8 $ hg init t
9 $ cd t
9 $ cd t
10
10
11 setup
11 setup
12
12
13 $ echo r1 > r1
13 $ echo r1 > r1
14 $ hg ci -qAmr1 -d'0 0'
14 $ hg ci -qAmr1 -d'0 0'
15 $ mkdir directory
15 $ mkdir directory
16 $ echo r2 > directory/r2
16 $ echo r2 > directory/r2
17 $ hg ci -qAmr2 -d'1 0'
17 $ hg ci -qAmr2 -d'1 0'
18 $ echo 'ignored' > .hgignore
18 $ echo 'ignored' > .hgignore
19 $ hg ci -qAmr3 -d'2 0'
19 $ hg ci -qAmr3 -d'2 0'
20
20
21 delete an empty directory
21 delete an empty directory
22
22
23 $ mkdir empty_dir
23 $ mkdir empty_dir
24 $ hg purge -p -v
24 $ hg purge -p -v
25 empty_dir
25 empty_dir
26 $ hg purge -v
26 $ hg purge -v
27 removing directory empty_dir
27 removing directory empty_dir
28 $ ls -A
28 $ ls -A
29 .hg
29 .hg
30 .hgignore
30 .hgignore
31 directory
31 directory
32 r1
32 r1
33
33
34 delete an untracked directory
34 delete an untracked directory
35
35
36 $ mkdir untracked_dir
36 $ mkdir untracked_dir
37 $ touch untracked_dir/untracked_file1
37 $ touch untracked_dir/untracked_file1
38 $ touch untracked_dir/untracked_file2
38 $ touch untracked_dir/untracked_file2
39 $ hg purge -p
39 $ hg purge -p
40 untracked_dir/untracked_file1
40 untracked_dir/untracked_file1
41 untracked_dir/untracked_file2
41 untracked_dir/untracked_file2
42 $ hg purge -v
42 $ hg purge -v
43 removing file untracked_dir/untracked_file1
43 removing file untracked_dir/untracked_file1
44 removing file untracked_dir/untracked_file2
44 removing file untracked_dir/untracked_file2
45 removing directory untracked_dir
45 removing directory untracked_dir
46 $ ls -A
46 $ ls -A
47 .hg
47 .hg
48 .hgignore
48 .hgignore
49 directory
49 directory
50 r1
50 r1
51
51
52 delete an untracked file
52 delete an untracked file
53
53
54 $ touch untracked_file
54 $ touch untracked_file
55 $ touch untracked_file_readonly
55 $ touch untracked_file_readonly
56 $ "$PYTHON" <<EOF
56 $ "$PYTHON" <<EOF
57 > import os
57 > import os
58 > import stat
58 > import stat
59 > f = 'untracked_file_readonly'
59 > f = 'untracked_file_readonly'
60 > os.chmod(f, stat.S_IMODE(os.stat(f).st_mode) & ~stat.S_IWRITE)
60 > os.chmod(f, stat.S_IMODE(os.stat(f).st_mode) & ~stat.S_IWRITE)
61 > EOF
61 > EOF
62 $ hg purge -p
62 $ hg purge -p
63 untracked_file
63 untracked_file
64 untracked_file_readonly
64 untracked_file_readonly
65 $ hg purge --confirm
66 permanently delete 2 unkown files? (yN) n
67 abort: removal cancelled
68 [250]
65 $ hg purge -v
69 $ hg purge -v
66 removing file untracked_file
70 removing file untracked_file
67 removing file untracked_file_readonly
71 removing file untracked_file_readonly
68 $ ls -A
72 $ ls -A
69 .hg
73 .hg
70 .hgignore
74 .hgignore
71 directory
75 directory
72 r1
76 r1
73
77
74 delete an untracked file in a tracked directory
78 delete an untracked file in a tracked directory
75
79
76 $ touch directory/untracked_file
80 $ touch directory/untracked_file
77 $ hg purge -p
81 $ hg purge -p
78 directory/untracked_file
82 directory/untracked_file
79 $ hg purge -v
83 $ hg purge -v
80 removing file directory/untracked_file
84 removing file directory/untracked_file
81 $ ls -A
85 $ ls -A
82 .hg
86 .hg
83 .hgignore
87 .hgignore
84 directory
88 directory
85 r1
89 r1
86
90
87 delete nested directories
91 delete nested directories
88
92
89 $ mkdir -p untracked_directory/nested_directory
93 $ mkdir -p untracked_directory/nested_directory
90 $ hg purge -p
94 $ hg purge -p
91 untracked_directory/nested_directory
95 untracked_directory/nested_directory
92 $ hg purge -v
96 $ hg purge -v
93 removing directory untracked_directory/nested_directory
97 removing directory untracked_directory/nested_directory
94 removing directory untracked_directory
98 removing directory untracked_directory
95 $ ls -A
99 $ ls -A
96 .hg
100 .hg
97 .hgignore
101 .hgignore
98 directory
102 directory
99 r1
103 r1
100
104
101 delete nested directories from a subdir
105 delete nested directories from a subdir
102
106
103 $ mkdir -p untracked_directory/nested_directory
107 $ mkdir -p untracked_directory/nested_directory
104 $ cd directory
108 $ cd directory
105 $ hg purge -p
109 $ hg purge -p
106 untracked_directory/nested_directory
110 untracked_directory/nested_directory
107 $ hg purge -v
111 $ hg purge -v
108 removing directory untracked_directory/nested_directory
112 removing directory untracked_directory/nested_directory
109 removing directory untracked_directory
113 removing directory untracked_directory
110 $ cd ..
114 $ cd ..
111 $ ls -A
115 $ ls -A
112 .hg
116 .hg
113 .hgignore
117 .hgignore
114 directory
118 directory
115 r1
119 r1
116
120
117 delete only part of the tree
121 delete only part of the tree
118
122
119 $ mkdir -p untracked_directory/nested_directory
123 $ mkdir -p untracked_directory/nested_directory
120 $ touch directory/untracked_file
124 $ touch directory/untracked_file
121 $ cd directory
125 $ cd directory
122 $ hg purge -p ../untracked_directory
126 $ hg purge -p ../untracked_directory
123 untracked_directory/nested_directory
127 untracked_directory/nested_directory
128 $ hg purge --confirm
129 permanently delete 1 unkown files? (yN) n
130 abort: removal cancelled
131 [250]
124 $ hg purge -v ../untracked_directory
132 $ hg purge -v ../untracked_directory
125 removing directory untracked_directory/nested_directory
133 removing directory untracked_directory/nested_directory
126 removing directory untracked_directory
134 removing directory untracked_directory
127 $ cd ..
135 $ cd ..
128 $ ls -A
136 $ ls -A
129 .hg
137 .hg
130 .hgignore
138 .hgignore
131 directory
139 directory
132 r1
140 r1
133 $ ls directory/untracked_file
141 $ ls directory/untracked_file
134 directory/untracked_file
142 directory/untracked_file
135 $ rm directory/untracked_file
143 $ rm directory/untracked_file
136
144
137 skip ignored files if -i or --all not specified
145 skip ignored files if -i or --all not specified
138
146
139 $ touch ignored
147 $ touch ignored
140 $ hg purge -p
148 $ hg purge -p
149 $ hg purge --confirm
141 $ hg purge -v
150 $ hg purge -v
142 $ touch untracked_file
151 $ touch untracked_file
143 $ ls
152 $ ls
144 directory
153 directory
145 ignored
154 ignored
146 r1
155 r1
147 untracked_file
156 untracked_file
148 $ hg purge -p -i
157 $ hg purge -p -i
149 ignored
158 ignored
159 $ hg purge --confirm -i
160 permanently delete 1 ignored files? (yN) n
161 abort: removal cancelled
162 [250]
150 $ hg purge -v -i
163 $ hg purge -v -i
151 removing file ignored
164 removing file ignored
152 $ ls -A
165 $ ls -A
153 .hg
166 .hg
154 .hgignore
167 .hgignore
155 directory
168 directory
156 r1
169 r1
157 untracked_file
170 untracked_file
158 $ touch ignored
171 $ touch ignored
159 $ hg purge -p --all
172 $ hg purge -p --all
160 ignored
173 ignored
161 untracked_file
174 untracked_file
175 $ hg purge --confirm --all
176 permanently delete 1 unkown and 1 ignored files? (yN) n
177 abort: removal cancelled
178 [250]
162 $ hg purge -v --all
179 $ hg purge -v --all
163 removing file ignored
180 removing file ignored
164 removing file untracked_file
181 removing file untracked_file
165 $ ls
182 $ ls
166 directory
183 directory
167 r1
184 r1
168
185
169 abort with missing files until we support name mangling filesystems
186 abort with missing files until we support name mangling filesystems
170
187
171 $ touch untracked_file
188 $ touch untracked_file
172 $ rm r1
189 $ rm r1
173
190
174 hide error messages to avoid changing the output when the text changes
191 hide error messages to avoid changing the output when the text changes
175
192
176 $ hg purge -p 2> /dev/null
193 $ hg purge -p 2> /dev/null
177 untracked_file
194 untracked_file
178 $ hg st
195 $ hg st
179 ! r1
196 ! r1
180 ? untracked_file
197 ? untracked_file
181
198
182 $ hg purge -p
199 $ hg purge -p
183 untracked_file
200 untracked_file
184 $ hg purge -v 2> /dev/null
201 $ hg purge -v 2> /dev/null
185 removing file untracked_file
202 removing file untracked_file
186 $ hg st
203 $ hg st
187 ! r1
204 ! r1
188
205
189 $ hg purge -v
206 $ hg purge -v
190 $ hg revert --all --quiet
207 $ hg revert --all --quiet
191 $ hg st -a
208 $ hg st -a
192
209
193 tracked file in ignored directory (issue621)
210 tracked file in ignored directory (issue621)
194
211
195 $ echo directory >> .hgignore
212 $ echo directory >> .hgignore
196 $ hg ci -m 'ignore directory'
213 $ hg ci -m 'ignore directory'
197 $ touch untracked_file
214 $ touch untracked_file
198 $ hg purge -p
215 $ hg purge -p
199 untracked_file
216 untracked_file
200 $ hg purge -v
217 $ hg purge -v
201 removing file untracked_file
218 removing file untracked_file
202
219
203 skip excluded files
220 skip excluded files
204
221
205 $ touch excluded_file
222 $ touch excluded_file
206 $ hg purge -p -X excluded_file
223 $ hg purge -p -X excluded_file
207 $ hg purge -v -X excluded_file
224 $ hg purge -v -X excluded_file
208 $ ls -A
225 $ ls -A
209 .hg
226 .hg
210 .hgignore
227 .hgignore
211 directory
228 directory
212 excluded_file
229 excluded_file
213 r1
230 r1
214 $ rm excluded_file
231 $ rm excluded_file
215
232
216 skip files in excluded dirs
233 skip files in excluded dirs
217
234
218 $ mkdir excluded_dir
235 $ mkdir excluded_dir
219 $ touch excluded_dir/file
236 $ touch excluded_dir/file
220 $ hg purge -p -X excluded_dir
237 $ hg purge -p -X excluded_dir
221 $ hg purge -v -X excluded_dir
238 $ hg purge -v -X excluded_dir
222 $ ls -A
239 $ ls -A
223 .hg
240 .hg
224 .hgignore
241 .hgignore
225 directory
242 directory
226 excluded_dir
243 excluded_dir
227 r1
244 r1
228 $ ls excluded_dir
245 $ ls excluded_dir
229 file
246 file
230 $ rm -R excluded_dir
247 $ rm -R excluded_dir
231
248
232 skip excluded empty dirs
249 skip excluded empty dirs
233
250
234 $ mkdir excluded_dir
251 $ mkdir excluded_dir
235 $ hg purge -p -X excluded_dir
252 $ hg purge -p -X excluded_dir
236 $ hg purge -v -X excluded_dir
253 $ hg purge -v -X excluded_dir
237 $ ls -A
254 $ ls -A
238 .hg
255 .hg
239 .hgignore
256 .hgignore
240 directory
257 directory
241 excluded_dir
258 excluded_dir
242 r1
259 r1
243 $ rmdir excluded_dir
260 $ rmdir excluded_dir
244
261
245 skip patterns
262 skip patterns
246
263
247 $ mkdir .svn
264 $ mkdir .svn
248 $ touch .svn/foo
265 $ touch .svn/foo
249 $ mkdir directory/.svn
266 $ mkdir directory/.svn
250 $ touch directory/.svn/foo
267 $ touch directory/.svn/foo
251 $ hg purge -p -X .svn -X '*/.svn'
268 $ hg purge -p -X .svn -X '*/.svn'
252 $ hg purge -p -X re:.*.svn
269 $ hg purge -p -X re:.*.svn
253
270
254 $ rm -R .svn directory r1
271 $ rm -R .svn directory r1
255
272
256 only remove files
273 only remove files
257
274
258 $ mkdir -p empty_dir dir
275 $ mkdir -p empty_dir dir
259 $ touch untracked_file dir/untracked_file
276 $ touch untracked_file dir/untracked_file
260 $ hg purge -p --files
277 $ hg purge -p --files
261 dir/untracked_file
278 dir/untracked_file
262 untracked_file
279 untracked_file
263 $ hg purge -v --files
280 $ hg purge -v --files
264 removing file dir/untracked_file
281 removing file dir/untracked_file
265 removing file untracked_file
282 removing file untracked_file
266 $ ls -A
283 $ ls -A
267 .hg
284 .hg
268 .hgignore
285 .hgignore
269 dir
286 dir
270 empty_dir
287 empty_dir
271 $ ls dir
288 $ ls dir
272
289
273 only remove dirs
290 only remove dirs
274
291
275 $ mkdir -p empty_dir dir
292 $ mkdir -p empty_dir dir
276 $ touch untracked_file dir/untracked_file
293 $ touch untracked_file dir/untracked_file
277 $ hg purge -p --dirs
294 $ hg purge -p --dirs
278 empty_dir
295 empty_dir
279 $ hg purge -v --dirs
296 $ hg purge -v --dirs
280 removing directory empty_dir
297 removing directory empty_dir
281 $ ls -A
298 $ ls -A
282 .hg
299 .hg
283 .hgignore
300 .hgignore
284 dir
301 dir
285 untracked_file
302 untracked_file
286 $ ls dir
303 $ ls dir
287 untracked_file
304 untracked_file
288
305
289 remove both files and dirs
306 remove both files and dirs
290
307
291 $ mkdir -p empty_dir dir
308 $ mkdir -p empty_dir dir
292 $ touch untracked_file dir/untracked_file
309 $ touch untracked_file dir/untracked_file
293 $ hg purge -p --files --dirs
310 $ hg purge -p --files --dirs
294 dir/untracked_file
311 dir/untracked_file
295 untracked_file
312 untracked_file
296 empty_dir
313 empty_dir
297 $ hg purge -v --files --dirs
314 $ hg purge -v --files --dirs
298 removing file dir/untracked_file
315 removing file dir/untracked_file
299 removing file untracked_file
316 removing file untracked_file
300 removing directory empty_dir
317 removing directory empty_dir
301 removing directory dir
318 removing directory dir
302 $ ls -A
319 $ ls -A
303 .hg
320 .hg
304 .hgignore
321 .hgignore
305
322
306 $ cd ..
323 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now