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