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