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