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