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