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