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