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