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