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