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