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