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