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