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