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