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