##// END OF EJS Templates
merge: break up a not-so-one-liner for readability...
marmoute -
r49554:bca57b01 default
parent child Browse files
Show More
@@ -1,2493 +1,2492 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, mergestatemod.ACTION_REMOVE),
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 raise error.Abort(
1930 msg = _(b"nothing to merge")
1931 _(b"nothing to merge"),
1931 hint = _(b"use 'hg update' or check 'hg heads'")
1932 hint=_(b"use 'hg update' or check 'hg heads'"),
1932 raise error.Abort(msg, hint=hint)
1933 )
1934 if not force and (wc.files() or wc.deleted()):
1933 if not force and (wc.files() or wc.deleted()):
1935 raise error.StateError(
1934 raise error.StateError(
1936 _(b"uncommitted changes"),
1935 _(b"uncommitted changes"),
1937 hint=_(b"use 'hg status' to list changes"),
1936 hint=_(b"use 'hg status' to list changes"),
1938 )
1937 )
1939 if not wc.isinmemory():
1938 if not wc.isinmemory():
1940 for s in sorted(wc.substate):
1939 for s in sorted(wc.substate):
1941 wc.sub(s).bailifchanged()
1940 wc.sub(s).bailifchanged()
1942
1941
1943 elif not overwrite:
1942 elif not overwrite:
1944 if p1 == p2: # no-op update
1943 if p1 == p2: # no-op update
1945 # call the hooks and exit early
1944 # call the hooks and exit early
1946 repo.hook(b'preupdate', throw=True, parent1=xp2, parent2=b'')
1945 repo.hook(b'preupdate', throw=True, parent1=xp2, parent2=b'')
1947 repo.hook(b'update', parent1=xp2, parent2=b'', error=0)
1946 repo.hook(b'update', parent1=xp2, parent2=b'', error=0)
1948 return updateresult(0, 0, 0, 0)
1947 return updateresult(0, 0, 0, 0)
1949
1948
1950 if updatecheck == UPDATECHECK_LINEAR and pas not in (
1949 if updatecheck == UPDATECHECK_LINEAR and pas not in (
1951 [p1],
1950 [p1],
1952 [p2],
1951 [p2],
1953 ): # nonlinear
1952 ): # nonlinear
1954 dirty = wc.dirty(missing=True)
1953 dirty = wc.dirty(missing=True)
1955 if dirty:
1954 if dirty:
1956 # Branching is a bit strange to ensure we do the minimal
1955 # Branching is a bit strange to ensure we do the minimal
1957 # amount of call to obsutil.foreground.
1956 # amount of call to obsutil.foreground.
1958 foreground = obsutil.foreground(repo, [p1.node()])
1957 foreground = obsutil.foreground(repo, [p1.node()])
1959 # note: the <node> variable contains a random identifier
1958 # note: the <node> variable contains a random identifier
1960 if repo[node].node() in foreground:
1959 if repo[node].node() in foreground:
1961 pass # allow updating to successors
1960 pass # allow updating to successors
1962 else:
1961 else:
1963 msg = _(b"uncommitted changes")
1962 msg = _(b"uncommitted changes")
1964 hint = _(b"commit or update --clean to discard changes")
1963 hint = _(b"commit or update --clean to discard changes")
1965 raise error.UpdateAbort(msg, hint=hint)
1964 raise error.UpdateAbort(msg, hint=hint)
1966 else:
1965 else:
1967 # Allow jumping branches if clean and specific rev given
1966 # Allow jumping branches if clean and specific rev given
1968 pass
1967 pass
1969
1968
1970 if overwrite:
1969 if overwrite:
1971 pas = [wc]
1970 pas = [wc]
1972 elif not branchmerge:
1971 elif not branchmerge:
1973 pas = [p1]
1972 pas = [p1]
1974
1973
1975 # deprecated config: merge.followcopies
1974 # deprecated config: merge.followcopies
1976 followcopies = repo.ui.configbool(b'merge', b'followcopies')
1975 followcopies = repo.ui.configbool(b'merge', b'followcopies')
1977 if overwrite:
1976 if overwrite:
1978 followcopies = False
1977 followcopies = False
1979 elif not pas[0]:
1978 elif not pas[0]:
1980 followcopies = False
1979 followcopies = False
1981 if not branchmerge and not wc.dirty(missing=True):
1980 if not branchmerge and not wc.dirty(missing=True):
1982 followcopies = False
1981 followcopies = False
1983
1982
1984 ### calculate phase
1983 ### calculate phase
1985 mresult = calculateupdates(
1984 mresult = calculateupdates(
1986 repo,
1985 repo,
1987 wc,
1986 wc,
1988 p2,
1987 p2,
1989 pas,
1988 pas,
1990 branchmerge,
1989 branchmerge,
1991 force,
1990 force,
1992 mergeancestor,
1991 mergeancestor,
1993 followcopies,
1992 followcopies,
1994 matcher=matcher,
1993 matcher=matcher,
1995 mergeforce=mergeforce,
1994 mergeforce=mergeforce,
1996 )
1995 )
1997
1996
1998 if updatecheck == UPDATECHECK_NO_CONFLICT:
1997 if updatecheck == UPDATECHECK_NO_CONFLICT:
1999 if mresult.hasconflicts():
1998 if mresult.hasconflicts():
2000 msg = _(b"conflicting changes")
1999 msg = _(b"conflicting changes")
2001 hint = _(b"commit or update --clean to discard changes")
2000 hint = _(b"commit or update --clean to discard changes")
2002 raise error.StateError(msg, hint=hint)
2001 raise error.StateError(msg, hint=hint)
2003
2002
2004 # Prompt and create actions. Most of this is in the resolve phase
2003 # Prompt and create actions. Most of this is in the resolve phase
2005 # already, but we can't handle .hgsubstate in filemerge or
2004 # already, but we can't handle .hgsubstate in filemerge or
2006 # subrepoutil.submerge yet so we have to keep prompting for it.
2005 # subrepoutil.submerge yet so we have to keep prompting for it.
2007 vals = mresult.getfile(b'.hgsubstate')
2006 vals = mresult.getfile(b'.hgsubstate')
2008 if vals:
2007 if vals:
2009 f = b'.hgsubstate'
2008 f = b'.hgsubstate'
2010 m, args, msg = vals
2009 m, args, msg = vals
2011 prompts = filemerge.partextras(labels)
2010 prompts = filemerge.partextras(labels)
2012 prompts[b'f'] = f
2011 prompts[b'f'] = f
2013 if m == mergestatemod.ACTION_CHANGED_DELETED:
2012 if m == mergestatemod.ACTION_CHANGED_DELETED:
2014 if repo.ui.promptchoice(
2013 if repo.ui.promptchoice(
2015 _(
2014 _(
2016 b"local%(l)s changed %(f)s which other%(o)s deleted\n"
2015 b"local%(l)s changed %(f)s which other%(o)s deleted\n"
2017 b"use (c)hanged version or (d)elete?"
2016 b"use (c)hanged version or (d)elete?"
2018 b"$$ &Changed $$ &Delete"
2017 b"$$ &Changed $$ &Delete"
2019 )
2018 )
2020 % prompts,
2019 % prompts,
2021 0,
2020 0,
2022 ):
2021 ):
2023 mresult.addfile(
2022 mresult.addfile(
2024 f,
2023 f,
2025 mergestatemod.ACTION_REMOVE,
2024 mergestatemod.ACTION_REMOVE,
2026 None,
2025 None,
2027 b'prompt delete',
2026 b'prompt delete',
2028 )
2027 )
2029 elif f in p1:
2028 elif f in p1:
2030 mresult.addfile(
2029 mresult.addfile(
2031 f,
2030 f,
2032 mergestatemod.ACTION_ADD_MODIFIED,
2031 mergestatemod.ACTION_ADD_MODIFIED,
2033 None,
2032 None,
2034 b'prompt keep',
2033 b'prompt keep',
2035 )
2034 )
2036 else:
2035 else:
2037 mresult.addfile(
2036 mresult.addfile(
2038 f,
2037 f,
2039 mergestatemod.ACTION_ADD,
2038 mergestatemod.ACTION_ADD,
2040 None,
2039 None,
2041 b'prompt keep',
2040 b'prompt keep',
2042 )
2041 )
2043 elif m == mergestatemod.ACTION_DELETED_CHANGED:
2042 elif m == mergestatemod.ACTION_DELETED_CHANGED:
2044 f1, f2, fa, move, anc = args
2043 f1, f2, fa, move, anc = args
2045 flags = p2[f2].flags()
2044 flags = p2[f2].flags()
2046 if (
2045 if (
2047 repo.ui.promptchoice(
2046 repo.ui.promptchoice(
2048 _(
2047 _(
2049 b"other%(o)s changed %(f)s which local%(l)s deleted\n"
2048 b"other%(o)s changed %(f)s which local%(l)s deleted\n"
2050 b"use (c)hanged version or leave (d)eleted?"
2049 b"use (c)hanged version or leave (d)eleted?"
2051 b"$$ &Changed $$ &Deleted"
2050 b"$$ &Changed $$ &Deleted"
2052 )
2051 )
2053 % prompts,
2052 % prompts,
2054 0,
2053 0,
2055 )
2054 )
2056 == 0
2055 == 0
2057 ):
2056 ):
2058 mresult.addfile(
2057 mresult.addfile(
2059 f,
2058 f,
2060 mergestatemod.ACTION_GET,
2059 mergestatemod.ACTION_GET,
2061 (flags, False),
2060 (flags, False),
2062 b'prompt recreating',
2061 b'prompt recreating',
2063 )
2062 )
2064 else:
2063 else:
2065 mresult.removefile(f)
2064 mresult.removefile(f)
2066
2065
2067 if not util.fscasesensitive(repo.path):
2066 if not util.fscasesensitive(repo.path):
2068 # check collision between files only in p2 for clean update
2067 # check collision between files only in p2 for clean update
2069 if not branchmerge and (
2068 if not branchmerge and (
2070 force or not wc.dirty(missing=True, branch=False)
2069 force or not wc.dirty(missing=True, branch=False)
2071 ):
2070 ):
2072 _checkcollision(repo, p2.manifest(), None)
2071 _checkcollision(repo, p2.manifest(), None)
2073 else:
2072 else:
2074 _checkcollision(repo, wc.manifest(), mresult)
2073 _checkcollision(repo, wc.manifest(), mresult)
2075
2074
2076 # divergent renames
2075 # divergent renames
2077 for f, fl in sorted(pycompat.iteritems(mresult.diverge)):
2076 for f, fl in sorted(pycompat.iteritems(mresult.diverge)):
2078 repo.ui.warn(
2077 repo.ui.warn(
2079 _(
2078 _(
2080 b"note: possible conflict - %s was renamed "
2079 b"note: possible conflict - %s was renamed "
2081 b"multiple times to:\n"
2080 b"multiple times to:\n"
2082 )
2081 )
2083 % f
2082 % f
2084 )
2083 )
2085 for nf in sorted(fl):
2084 for nf in sorted(fl):
2086 repo.ui.warn(b" %s\n" % nf)
2085 repo.ui.warn(b" %s\n" % nf)
2087
2086
2088 # rename and delete
2087 # rename and delete
2089 for f, fl in sorted(pycompat.iteritems(mresult.renamedelete)):
2088 for f, fl in sorted(pycompat.iteritems(mresult.renamedelete)):
2090 repo.ui.warn(
2089 repo.ui.warn(
2091 _(
2090 _(
2092 b"note: possible conflict - %s was deleted "
2091 b"note: possible conflict - %s was deleted "
2093 b"and renamed to:\n"
2092 b"and renamed to:\n"
2094 )
2093 )
2095 % f
2094 % f
2096 )
2095 )
2097 for nf in sorted(fl):
2096 for nf in sorted(fl):
2098 repo.ui.warn(b" %s\n" % nf)
2097 repo.ui.warn(b" %s\n" % nf)
2099
2098
2100 ### apply phase
2099 ### apply phase
2101 if not branchmerge: # just jump to the new rev
2100 if not branchmerge: # just jump to the new rev
2102 fp1, fp2, xp1, xp2 = fp2, repo.nullid, xp2, b''
2101 fp1, fp2, xp1, xp2 = fp2, repo.nullid, xp2, b''
2103 # If we're doing a partial update, we need to skip updating
2102 # If we're doing a partial update, we need to skip updating
2104 # the dirstate.
2103 # the dirstate.
2105 always = matcher is None or matcher.always()
2104 always = matcher is None or matcher.always()
2106 updatedirstate = updatedirstate and always and not wc.isinmemory()
2105 updatedirstate = updatedirstate and always and not wc.isinmemory()
2107 if updatedirstate:
2106 if updatedirstate:
2108 repo.hook(b'preupdate', throw=True, parent1=xp1, parent2=xp2)
2107 repo.hook(b'preupdate', throw=True, parent1=xp1, parent2=xp2)
2109 # note that we're in the middle of an update
2108 # note that we're in the middle of an update
2110 repo.vfs.write(b'updatestate', p2.hex())
2109 repo.vfs.write(b'updatestate', p2.hex())
2111
2110
2112 _advertisefsmonitor(
2111 _advertisefsmonitor(
2113 repo, mresult.len((mergestatemod.ACTION_GET,)), p1.node()
2112 repo, mresult.len((mergestatemod.ACTION_GET,)), p1.node()
2114 )
2113 )
2115
2114
2116 wantfiledata = updatedirstate and not branchmerge
2115 wantfiledata = updatedirstate and not branchmerge
2117 stats, getfiledata, extraactions = applyupdates(
2116 stats, getfiledata, extraactions = applyupdates(
2118 repo,
2117 repo,
2119 mresult,
2118 mresult,
2120 wc,
2119 wc,
2121 p2,
2120 p2,
2122 overwrite,
2121 overwrite,
2123 wantfiledata,
2122 wantfiledata,
2124 labels=labels,
2123 labels=labels,
2125 )
2124 )
2126
2125
2127 if updatedirstate:
2126 if updatedirstate:
2128 if extraactions:
2127 if extraactions:
2129 for k, acts in pycompat.iteritems(extraactions):
2128 for k, acts in pycompat.iteritems(extraactions):
2130 for a in acts:
2129 for a in acts:
2131 mresult.addfile(a[0], k, *a[1:])
2130 mresult.addfile(a[0], k, *a[1:])
2132 if k == mergestatemod.ACTION_GET and wantfiledata:
2131 if k == mergestatemod.ACTION_GET and wantfiledata:
2133 # no filedata until mergestate is updated to provide it
2132 # no filedata until mergestate is updated to provide it
2134 for a in acts:
2133 for a in acts:
2135 getfiledata[a[0]] = None
2134 getfiledata[a[0]] = None
2136
2135
2137 assert len(getfiledata) == (
2136 assert len(getfiledata) == (
2138 mresult.len((mergestatemod.ACTION_GET,)) if wantfiledata else 0
2137 mresult.len((mergestatemod.ACTION_GET,)) if wantfiledata else 0
2139 )
2138 )
2140 with repo.dirstate.parentchange():
2139 with repo.dirstate.parentchange():
2141 ### Filter Filedata
2140 ### Filter Filedata
2142 #
2141 #
2143 # We gathered "cache" information for the clean file while
2142 # We gathered "cache" information for the clean file while
2144 # updating them: mtime, size and mode.
2143 # updating them: mtime, size and mode.
2145 #
2144 #
2146 # At the time this comment is written, they are various issues
2145 # At the time this comment is written, they are various issues
2147 # with how we gather the `mode` and `mtime` information (see
2146 # with how we gather the `mode` and `mtime` information (see
2148 # the comment in `batchget`).
2147 # the comment in `batchget`).
2149 #
2148 #
2150 # We are going to smooth one of this issue here : mtime ambiguity.
2149 # We are going to smooth one of this issue here : mtime ambiguity.
2151 #
2150 #
2152 # i.e. even if the mtime gathered during `batchget` was
2151 # i.e. even if the mtime gathered during `batchget` was
2153 # correct[1] a change happening right after it could change the
2152 # correct[1] a change happening right after it could change the
2154 # content while keeping the same mtime[2].
2153 # content while keeping the same mtime[2].
2155 #
2154 #
2156 # When we reach the current code, the "on disk" part of the
2155 # When we reach the current code, the "on disk" part of the
2157 # update operation is finished. We still assume that no other
2156 # update operation is finished. We still assume that no other
2158 # process raced that "on disk" part, but we want to at least
2157 # process raced that "on disk" part, but we want to at least
2159 # prevent later file change to alter the content of the file
2158 # prevent later file change to alter the content of the file
2160 # right after the update operation. So quickly that the same
2159 # right after the update operation. So quickly that the same
2161 # mtime is record for the operation.
2160 # mtime is record for the operation.
2162 # To prevent such ambiguity to happens, we will only keep the
2161 # To prevent such ambiguity to happens, we will only keep the
2163 # "file data" for files with mtime that are stricly in the past,
2162 # "file data" for files with mtime that are stricly in the past,
2164 # i.e. whose mtime is strictly lower than the current time.
2163 # i.e. whose mtime is strictly lower than the current time.
2165 #
2164 #
2166 # This protect us from race conditions from operation that could
2165 # This protect us from race conditions from operation that could
2167 # run right after this one, especially other Mercurial
2166 # run right after this one, especially other Mercurial
2168 # operation that could be waiting for the wlock to touch files
2167 # operation that could be waiting for the wlock to touch files
2169 # content and the dirstate.
2168 # content and the dirstate.
2170 #
2169 #
2171 # In an ideal world, we could only get reliable information in
2170 # In an ideal world, we could only get reliable information in
2172 # `getfiledata` (from `getbatch`), however the current approach
2171 # `getfiledata` (from `getbatch`), however the current approach
2173 # have been a successful compromise since many years.
2172 # have been a successful compromise since many years.
2174 #
2173 #
2175 # At the time this comment is written, not using any "cache"
2174 # At the time this comment is written, not using any "cache"
2176 # file data at all here would not be viable. As it would result is
2175 # file data at all here would not be viable. As it would result is
2177 # a very large amount of work (equivalent to the previous `hg
2176 # a very large amount of work (equivalent to the previous `hg
2178 # update` during the next status after an update).
2177 # update` during the next status after an update).
2179 #
2178 #
2180 # [1] the current code cannot grantee that the `mtime` and
2179 # [1] the current code cannot grantee that the `mtime` and
2181 # `mode` are correct, but the result is "okay in practice".
2180 # `mode` are correct, but the result is "okay in practice".
2182 # (see the comment in `batchget`). #
2181 # (see the comment in `batchget`). #
2183 #
2182 #
2184 # [2] using nano-second precision can greatly help here because
2183 # [2] using nano-second precision can greatly help here because
2185 # it makes the "different write with same mtime" issue
2184 # it makes the "different write with same mtime" issue
2186 # virtually vanish. However, dirstate v1 cannot store such
2185 # virtually vanish. However, dirstate v1 cannot store such
2187 # precision and a bunch of python-runtime, operating-system and
2186 # precision and a bunch of python-runtime, operating-system and
2188 # filesystem does not provide use with such precision, so we
2187 # filesystem does not provide use with such precision, so we
2189 # have to operate as if it wasn't available.
2188 # have to operate as if it wasn't available.
2190 if getfiledata:
2189 if getfiledata:
2191 ambiguous_mtime = {}
2190 ambiguous_mtime = {}
2192 now = timestamp.get_fs_now(repo.vfs)
2191 now = timestamp.get_fs_now(repo.vfs)
2193 if now is None:
2192 if now is None:
2194 # we can't write to the FS, so we won't actually update
2193 # we can't write to the FS, so we won't actually update
2195 # the dirstate content anyway, no need to put cache
2194 # the dirstate content anyway, no need to put cache
2196 # information.
2195 # information.
2197 getfiledata = None
2196 getfiledata = None
2198 else:
2197 else:
2199 now_sec = now[0]
2198 now_sec = now[0]
2200 for f, m in pycompat.iteritems(getfiledata):
2199 for f, m in pycompat.iteritems(getfiledata):
2201 if m is not None and m[2][0] >= now_sec:
2200 if m is not None and m[2][0] >= now_sec:
2202 ambiguous_mtime[f] = (m[0], m[1], None)
2201 ambiguous_mtime[f] = (m[0], m[1], None)
2203 for f, m in pycompat.iteritems(ambiguous_mtime):
2202 for f, m in pycompat.iteritems(ambiguous_mtime):
2204 getfiledata[f] = m
2203 getfiledata[f] = m
2205
2204
2206 repo.setparents(fp1, fp2)
2205 repo.setparents(fp1, fp2)
2207 mergestatemod.recordupdates(
2206 mergestatemod.recordupdates(
2208 repo, mresult.actionsdict, branchmerge, getfiledata
2207 repo, mresult.actionsdict, branchmerge, getfiledata
2209 )
2208 )
2210 # update completed, clear state
2209 # update completed, clear state
2211 util.unlink(repo.vfs.join(b'updatestate'))
2210 util.unlink(repo.vfs.join(b'updatestate'))
2212
2211
2213 if not branchmerge:
2212 if not branchmerge:
2214 repo.dirstate.setbranch(p2.branch())
2213 repo.dirstate.setbranch(p2.branch())
2215
2214
2216 # If we're updating to a location, clean up any stale temporary includes
2215 # If we're updating to a location, clean up any stale temporary includes
2217 # (ex: this happens during hg rebase --abort).
2216 # (ex: this happens during hg rebase --abort).
2218 if not branchmerge:
2217 if not branchmerge:
2219 sparse.prunetemporaryincludes(repo)
2218 sparse.prunetemporaryincludes(repo)
2220
2219
2221 if updatedirstate:
2220 if updatedirstate:
2222 repo.hook(
2221 repo.hook(
2223 b'update', parent1=xp1, parent2=xp2, error=stats.unresolvedcount
2222 b'update', parent1=xp1, parent2=xp2, error=stats.unresolvedcount
2224 )
2223 )
2225 return stats
2224 return stats
2226
2225
2227
2226
2228 def merge(ctx, labels=None, force=False, wc=None):
2227 def merge(ctx, labels=None, force=False, wc=None):
2229 """Merge another topological branch into the working copy.
2228 """Merge another topological branch into the working copy.
2230
2229
2231 force = whether the merge was run with 'merge --force' (deprecated)
2230 force = whether the merge was run with 'merge --force' (deprecated)
2232 """
2231 """
2233
2232
2234 return _update(
2233 return _update(
2235 ctx.repo(),
2234 ctx.repo(),
2236 ctx.rev(),
2235 ctx.rev(),
2237 labels=labels,
2236 labels=labels,
2238 branchmerge=True,
2237 branchmerge=True,
2239 force=force,
2238 force=force,
2240 mergeforce=force,
2239 mergeforce=force,
2241 wc=wc,
2240 wc=wc,
2242 )
2241 )
2243
2242
2244
2243
2245 def update(ctx, updatecheck=None, wc=None):
2244 def update(ctx, updatecheck=None, wc=None):
2246 """Do a regular update to the given commit, aborting if there are conflicts.
2245 """Do a regular update to the given commit, aborting if there are conflicts.
2247
2246
2248 The 'updatecheck' argument can be used to control what to do in case of
2247 The 'updatecheck' argument can be used to control what to do in case of
2249 conflicts.
2248 conflicts.
2250
2249
2251 Note: This is a new, higher-level update() than the one that used to exist
2250 Note: This is a new, higher-level update() than the one that used to exist
2252 in this module. That function is now called _update(). You can hopefully
2251 in this module. That function is now called _update(). You can hopefully
2253 replace your callers to use this new update(), or clean_update(), merge(),
2252 replace your callers to use this new update(), or clean_update(), merge(),
2254 revert_to(), or graft().
2253 revert_to(), or graft().
2255 """
2254 """
2256 return _update(
2255 return _update(
2257 ctx.repo(),
2256 ctx.repo(),
2258 ctx.rev(),
2257 ctx.rev(),
2259 branchmerge=False,
2258 branchmerge=False,
2260 force=False,
2259 force=False,
2261 labels=[b'working copy', b'destination', b'working copy parent'],
2260 labels=[b'working copy', b'destination', b'working copy parent'],
2262 updatecheck=updatecheck,
2261 updatecheck=updatecheck,
2263 wc=wc,
2262 wc=wc,
2264 )
2263 )
2265
2264
2266
2265
2267 def clean_update(ctx, wc=None):
2266 def clean_update(ctx, wc=None):
2268 """Do a clean update to the given commit.
2267 """Do a clean update to the given commit.
2269
2268
2270 This involves updating to the commit and discarding any changes in the
2269 This involves updating to the commit and discarding any changes in the
2271 working copy.
2270 working copy.
2272 """
2271 """
2273 return _update(ctx.repo(), ctx.rev(), branchmerge=False, force=True, wc=wc)
2272 return _update(ctx.repo(), ctx.rev(), branchmerge=False, force=True, wc=wc)
2274
2273
2275
2274
2276 def revert_to(ctx, matcher=None, wc=None):
2275 def revert_to(ctx, matcher=None, wc=None):
2277 """Revert the working copy to the given commit.
2276 """Revert the working copy to the given commit.
2278
2277
2279 The working copy will keep its current parent(s) but its content will
2278 The working copy will keep its current parent(s) but its content will
2280 be the same as in the given commit.
2279 be the same as in the given commit.
2281 """
2280 """
2282
2281
2283 return _update(
2282 return _update(
2284 ctx.repo(),
2283 ctx.repo(),
2285 ctx.rev(),
2284 ctx.rev(),
2286 branchmerge=False,
2285 branchmerge=False,
2287 force=True,
2286 force=True,
2288 updatedirstate=False,
2287 updatedirstate=False,
2289 matcher=matcher,
2288 matcher=matcher,
2290 wc=wc,
2289 wc=wc,
2291 )
2290 )
2292
2291
2293
2292
2294 def graft(
2293 def graft(
2295 repo,
2294 repo,
2296 ctx,
2295 ctx,
2297 base=None,
2296 base=None,
2298 labels=None,
2297 labels=None,
2299 keepparent=False,
2298 keepparent=False,
2300 keepconflictparent=False,
2299 keepconflictparent=False,
2301 wctx=None,
2300 wctx=None,
2302 ):
2301 ):
2303 """Do a graft-like merge.
2302 """Do a graft-like merge.
2304
2303
2305 This is a merge where the merge ancestor is chosen such that one
2304 This is a merge where the merge ancestor is chosen such that one
2306 or more changesets are grafted onto the current changeset. In
2305 or more changesets are grafted onto the current changeset. In
2307 addition to the merge, this fixes up the dirstate to include only
2306 addition to the merge, this fixes up the dirstate to include only
2308 a single parent (if keepparent is False) and tries to duplicate any
2307 a single parent (if keepparent is False) and tries to duplicate any
2309 renames/copies appropriately.
2308 renames/copies appropriately.
2310
2309
2311 ctx - changeset to rebase
2310 ctx - changeset to rebase
2312 base - merge base, or ctx.p1() if not specified
2311 base - merge base, or ctx.p1() if not specified
2313 labels - merge labels eg ['local', 'graft']
2312 labels - merge labels eg ['local', 'graft']
2314 keepparent - keep second parent if any
2313 keepparent - keep second parent if any
2315 keepconflictparent - if unresolved, keep parent used for the merge
2314 keepconflictparent - if unresolved, keep parent used for the merge
2316
2315
2317 """
2316 """
2318 # If we're grafting a descendant onto an ancestor, be sure to pass
2317 # If we're grafting a descendant onto an ancestor, be sure to pass
2319 # mergeancestor=True to update. This does two things: 1) allows the merge if
2318 # mergeancestor=True to update. This does two things: 1) allows the merge if
2320 # the destination is the same as the parent of the ctx (so we can use graft
2319 # the destination is the same as the parent of the ctx (so we can use graft
2321 # to copy commits), and 2) informs update that the incoming changes are
2320 # to copy commits), and 2) informs update that the incoming changes are
2322 # newer than the destination so it doesn't prompt about "remote changed foo
2321 # newer than the destination so it doesn't prompt about "remote changed foo
2323 # which local deleted".
2322 # which local deleted".
2324 # We also pass mergeancestor=True when base is the same revision as p1. 2)
2323 # We also pass mergeancestor=True when base is the same revision as p1. 2)
2325 # doesn't matter as there can't possibly be conflicts, but 1) is necessary.
2324 # doesn't matter as there can't possibly be conflicts, but 1) is necessary.
2326 wctx = wctx or repo[None]
2325 wctx = wctx or repo[None]
2327 pctx = wctx.p1()
2326 pctx = wctx.p1()
2328 base = base or ctx.p1()
2327 base = base or ctx.p1()
2329 mergeancestor = (
2328 mergeancestor = (
2330 repo.changelog.isancestor(pctx.node(), ctx.node())
2329 repo.changelog.isancestor(pctx.node(), ctx.node())
2331 or pctx.rev() == base.rev()
2330 or pctx.rev() == base.rev()
2332 )
2331 )
2333
2332
2334 stats = _update(
2333 stats = _update(
2335 repo,
2334 repo,
2336 ctx.node(),
2335 ctx.node(),
2337 True,
2336 True,
2338 True,
2337 True,
2339 base.node(),
2338 base.node(),
2340 mergeancestor=mergeancestor,
2339 mergeancestor=mergeancestor,
2341 labels=labels,
2340 labels=labels,
2342 wc=wctx,
2341 wc=wctx,
2343 )
2342 )
2344
2343
2345 if keepconflictparent and stats.unresolvedcount:
2344 if keepconflictparent and stats.unresolvedcount:
2346 pother = ctx.node()
2345 pother = ctx.node()
2347 else:
2346 else:
2348 pother = repo.nullid
2347 pother = repo.nullid
2349 parents = ctx.parents()
2348 parents = ctx.parents()
2350 if keepparent and len(parents) == 2 and base in parents:
2349 if keepparent and len(parents) == 2 and base in parents:
2351 parents.remove(base)
2350 parents.remove(base)
2352 pother = parents[0].node()
2351 pother = parents[0].node()
2353 # Never set both parents equal to each other
2352 # Never set both parents equal to each other
2354 if pother == pctx.node():
2353 if pother == pctx.node():
2355 pother = repo.nullid
2354 pother = repo.nullid
2356
2355
2357 if wctx.isinmemory():
2356 if wctx.isinmemory():
2358 wctx.setparents(pctx.node(), pother)
2357 wctx.setparents(pctx.node(), pother)
2359 # fix up dirstate for copies and renames
2358 # fix up dirstate for copies and renames
2360 copies.graftcopies(wctx, ctx, base)
2359 copies.graftcopies(wctx, ctx, base)
2361 else:
2360 else:
2362 with repo.dirstate.parentchange():
2361 with repo.dirstate.parentchange():
2363 repo.setparents(pctx.node(), pother)
2362 repo.setparents(pctx.node(), pother)
2364 repo.dirstate.write(repo.currenttransaction())
2363 repo.dirstate.write(repo.currenttransaction())
2365 # fix up dirstate for copies and renames
2364 # fix up dirstate for copies and renames
2366 copies.graftcopies(wctx, ctx, base)
2365 copies.graftcopies(wctx, ctx, base)
2367 return stats
2366 return stats
2368
2367
2369
2368
2370 def back_out(ctx, parent=None, wc=None):
2369 def back_out(ctx, parent=None, wc=None):
2371 if parent is None:
2370 if parent is None:
2372 if ctx.p2() is not None:
2371 if ctx.p2() is not None:
2373 raise error.ProgrammingError(
2372 raise error.ProgrammingError(
2374 b"must specify parent of merge commit to back out"
2373 b"must specify parent of merge commit to back out"
2375 )
2374 )
2376 parent = ctx.p1()
2375 parent = ctx.p1()
2377 return _update(
2376 return _update(
2378 ctx.repo(),
2377 ctx.repo(),
2379 parent,
2378 parent,
2380 branchmerge=True,
2379 branchmerge=True,
2381 force=True,
2380 force=True,
2382 ancestor=ctx.node(),
2381 ancestor=ctx.node(),
2383 mergeancestor=False,
2382 mergeancestor=False,
2384 )
2383 )
2385
2384
2386
2385
2387 def purge(
2386 def purge(
2388 repo,
2387 repo,
2389 matcher,
2388 matcher,
2390 unknown=True,
2389 unknown=True,
2391 ignored=False,
2390 ignored=False,
2392 removeemptydirs=True,
2391 removeemptydirs=True,
2393 removefiles=True,
2392 removefiles=True,
2394 abortonerror=False,
2393 abortonerror=False,
2395 noop=False,
2394 noop=False,
2396 confirm=False,
2395 confirm=False,
2397 ):
2396 ):
2398 """Purge the working directory of untracked files.
2397 """Purge the working directory of untracked files.
2399
2398
2400 ``matcher`` is a matcher configured to scan the working directory -
2399 ``matcher`` is a matcher configured to scan the working directory -
2401 potentially a subset.
2400 potentially a subset.
2402
2401
2403 ``unknown`` controls whether unknown files should be purged.
2402 ``unknown`` controls whether unknown files should be purged.
2404
2403
2405 ``ignored`` controls whether ignored files should be purged.
2404 ``ignored`` controls whether ignored files should be purged.
2406
2405
2407 ``removeemptydirs`` controls whether empty directories should be removed.
2406 ``removeemptydirs`` controls whether empty directories should be removed.
2408
2407
2409 ``removefiles`` controls whether files are removed.
2408 ``removefiles`` controls whether files are removed.
2410
2409
2411 ``abortonerror`` causes an exception to be raised if an error occurs
2410 ``abortonerror`` causes an exception to be raised if an error occurs
2412 deleting a file or directory.
2411 deleting a file or directory.
2413
2412
2414 ``noop`` controls whether to actually remove files. If not defined, actions
2413 ``noop`` controls whether to actually remove files. If not defined, actions
2415 will be taken.
2414 will be taken.
2416
2415
2417 ``confirm`` ask confirmation before actually removing anything.
2416 ``confirm`` ask confirmation before actually removing anything.
2418
2417
2419 Returns an iterable of relative paths in the working directory that were
2418 Returns an iterable of relative paths in the working directory that were
2420 or would be removed.
2419 or would be removed.
2421 """
2420 """
2422
2421
2423 def remove(removefn, path):
2422 def remove(removefn, path):
2424 try:
2423 try:
2425 removefn(path)
2424 removefn(path)
2426 except OSError:
2425 except OSError:
2427 m = _(b'%s cannot be removed') % path
2426 m = _(b'%s cannot be removed') % path
2428 if abortonerror:
2427 if abortonerror:
2429 raise error.Abort(m)
2428 raise error.Abort(m)
2430 else:
2429 else:
2431 repo.ui.warn(_(b'warning: %s\n') % m)
2430 repo.ui.warn(_(b'warning: %s\n') % m)
2432
2431
2433 # There's no API to copy a matcher. So mutate the passed matcher and
2432 # There's no API to copy a matcher. So mutate the passed matcher and
2434 # restore it when we're done.
2433 # restore it when we're done.
2435 oldtraversedir = matcher.traversedir
2434 oldtraversedir = matcher.traversedir
2436
2435
2437 res = []
2436 res = []
2438
2437
2439 try:
2438 try:
2440 if removeemptydirs:
2439 if removeemptydirs:
2441 directories = []
2440 directories = []
2442 matcher.traversedir = directories.append
2441 matcher.traversedir = directories.append
2443
2442
2444 status = repo.status(match=matcher, ignored=ignored, unknown=unknown)
2443 status = repo.status(match=matcher, ignored=ignored, unknown=unknown)
2445
2444
2446 if confirm:
2445 if confirm:
2447 nb_ignored = len(status.ignored)
2446 nb_ignored = len(status.ignored)
2448 nb_unknown = len(status.unknown)
2447 nb_unknown = len(status.unknown)
2449 if nb_unknown and nb_ignored:
2448 if nb_unknown and nb_ignored:
2450 msg = _(b"permanently delete %d unknown and %d ignored files?")
2449 msg = _(b"permanently delete %d unknown and %d ignored files?")
2451 msg %= (nb_unknown, nb_ignored)
2450 msg %= (nb_unknown, nb_ignored)
2452 elif nb_unknown:
2451 elif nb_unknown:
2453 msg = _(b"permanently delete %d unknown files?")
2452 msg = _(b"permanently delete %d unknown files?")
2454 msg %= nb_unknown
2453 msg %= nb_unknown
2455 elif nb_ignored:
2454 elif nb_ignored:
2456 msg = _(b"permanently delete %d ignored files?")
2455 msg = _(b"permanently delete %d ignored files?")
2457 msg %= nb_ignored
2456 msg %= nb_ignored
2458 elif removeemptydirs:
2457 elif removeemptydirs:
2459 dir_count = 0
2458 dir_count = 0
2460 for f in directories:
2459 for f in directories:
2461 if matcher(f) and not repo.wvfs.listdir(f):
2460 if matcher(f) and not repo.wvfs.listdir(f):
2462 dir_count += 1
2461 dir_count += 1
2463 if dir_count:
2462 if dir_count:
2464 msg = _(
2463 msg = _(
2465 b"permanently delete at least %d empty directories?"
2464 b"permanently delete at least %d empty directories?"
2466 )
2465 )
2467 msg %= dir_count
2466 msg %= dir_count
2468 else:
2467 else:
2469 # XXX we might be missing directory there
2468 # XXX we might be missing directory there
2470 return res
2469 return res
2471 msg += b" (yN)$$ &Yes $$ &No"
2470 msg += b" (yN)$$ &Yes $$ &No"
2472 if repo.ui.promptchoice(msg, default=1) == 1:
2471 if repo.ui.promptchoice(msg, default=1) == 1:
2473 raise error.CanceledError(_(b'removal cancelled'))
2472 raise error.CanceledError(_(b'removal cancelled'))
2474
2473
2475 if removefiles:
2474 if removefiles:
2476 for f in sorted(status.unknown + status.ignored):
2475 for f in sorted(status.unknown + status.ignored):
2477 if not noop:
2476 if not noop:
2478 repo.ui.note(_(b'removing file %s\n') % f)
2477 repo.ui.note(_(b'removing file %s\n') % f)
2479 remove(repo.wvfs.unlink, f)
2478 remove(repo.wvfs.unlink, f)
2480 res.append(f)
2479 res.append(f)
2481
2480
2482 if removeemptydirs:
2481 if removeemptydirs:
2483 for f in sorted(directories, reverse=True):
2482 for f in sorted(directories, reverse=True):
2484 if matcher(f) and not repo.wvfs.listdir(f):
2483 if matcher(f) and not repo.wvfs.listdir(f):
2485 if not noop:
2484 if not noop:
2486 repo.ui.note(_(b'removing directory %s\n') % f)
2485 repo.ui.note(_(b'removing directory %s\n') % f)
2487 remove(repo.wvfs.rmdir, f)
2486 remove(repo.wvfs.rmdir, f)
2488 res.append(f)
2487 res.append(f)
2489
2488
2490 return res
2489 return res
2491
2490
2492 finally:
2491 finally:
2493 matcher.traversedir = oldtraversedir
2492 matcher.traversedir = oldtraversedir
General Comments 0
You need to be logged in to leave comments. Login now