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