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