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