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