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