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