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