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