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