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