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