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