##// END OF EJS Templates
purge: add a --confirm option...
marmoute -
r47078:135056e8 default
parent child Browse files
Show More
@@ -1,136 +1,139 b''
1 1 # Copyright (C) 2006 - Marco Barisione <marco@barisione.org>
2 2 #
3 3 # This is a small extension for Mercurial (https://mercurial-scm.org/)
4 4 # that removes files not known to mercurial
5 5 #
6 6 # This program was inspired by the "cvspurge" script contained in CVS
7 7 # utilities (http://www.red-bean.com/cvsutils/).
8 8 #
9 9 # For help on the usage of "hg purge" use:
10 10 # hg help purge
11 11 #
12 12 # This program is free software; you can redistribute it and/or modify
13 13 # it under the terms of the GNU General Public License as published by
14 14 # the Free Software Foundation; either version 2 of the License, or
15 15 # (at your option) any later version.
16 16 #
17 17 # This program is distributed in the hope that it will be useful,
18 18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 20 # GNU General Public License for more details.
21 21 #
22 22 # You should have received a copy of the GNU General Public License
23 23 # along with this program; if not, see <http://www.gnu.org/licenses/>.
24 24
25 25 '''command to delete untracked files from the working directory'''
26 26 from __future__ import absolute_import
27 27
28 28 from mercurial.i18n import _
29 29 from mercurial import (
30 30 cmdutil,
31 31 merge as mergemod,
32 32 pycompat,
33 33 registrar,
34 34 scmutil,
35 35 )
36 36
37 37 cmdtable = {}
38 38 command = registrar.command(cmdtable)
39 39 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
40 40 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
41 41 # be specifying the version(s) of Mercurial they are tested with, or
42 42 # leave the attribute unspecified.
43 43 testedwith = b'ships-with-hg-core'
44 44
45 45
46 46 @command(
47 47 b'purge|clean',
48 48 [
49 49 (b'a', b'abort-on-err', None, _(b'abort if an error occurs')),
50 50 (b'', b'all', None, _(b'purge ignored files too')),
51 51 (b'i', b'ignored', None, _(b'purge only ignored files')),
52 52 (b'', b'dirs', None, _(b'purge empty directories')),
53 53 (b'', b'files', None, _(b'purge files')),
54 54 (b'p', b'print', None, _(b'print filenames instead of deleting them')),
55 55 (
56 56 b'0',
57 57 b'print0',
58 58 None,
59 59 _(
60 60 b'end filenames with NUL, for use with xargs'
61 61 b' (implies -p/--print)'
62 62 ),
63 63 ),
64 (b'', b'confirm', None, _(b'ask before permanently deleting files')),
64 65 ]
65 66 + cmdutil.walkopts,
66 67 _(b'hg purge [OPTION]... [DIR]...'),
67 68 helpcategory=command.CATEGORY_WORKING_DIRECTORY,
68 69 )
69 70 def purge(ui, repo, *dirs, **opts):
70 71 """removes files not tracked by Mercurial
71 72
72 73 Delete files not known to Mercurial. This is useful to test local
73 74 and uncommitted changes in an otherwise-clean source tree.
74 75
75 76 This means that purge will delete the following by default:
76 77
77 78 - Unknown files: files marked with "?" by :hg:`status`
78 79 - Empty directories: in fact Mercurial ignores directories unless
79 80 they contain files under source control management
80 81
81 82 But it will leave untouched:
82 83
83 84 - Modified and unmodified tracked files
84 85 - Ignored files (unless -i or --all is specified)
85 86 - New files added to the repository (with :hg:`add`)
86 87
87 88 The --files and --dirs options can be used to direct purge to delete
88 89 only files, only directories, or both. If neither option is given,
89 90 both will be deleted.
90 91
91 92 If directories are given on the command line, only files in these
92 93 directories are considered.
93 94
94 95 Be careful with purge, as you could irreversibly delete some files
95 96 you forgot to add to the repository. If you only want to print the
96 97 list of files that this program would delete, use the --print
97 98 option.
98 99 """
99 100 opts = pycompat.byteskwargs(opts)
100 101 cmdutil.check_at_most_one_arg(opts, b'all', b'ignored')
101 102
102 103 act = not opts.get(b'print')
103 104 eol = b'\n'
104 105 if opts.get(b'print0'):
105 106 eol = b'\0'
106 107 act = False # --print0 implies --print
107 108 if opts.get(b'all', False):
108 109 ignored = True
109 110 unknown = True
110 111 else:
111 112 ignored = opts.get(b'ignored', False)
112 113 unknown = not ignored
113 114
114 115 removefiles = opts.get(b'files')
115 116 removedirs = opts.get(b'dirs')
117 confirm = opts.get(b'confirm')
116 118
117 119 if not removefiles and not removedirs:
118 120 removefiles = True
119 121 removedirs = True
120 122
121 123 match = scmutil.match(repo[None], dirs, opts)
122 124
123 125 paths = mergemod.purge(
124 126 repo,
125 127 match,
126 128 unknown=unknown,
127 129 ignored=ignored,
128 130 removeemptydirs=removedirs,
129 131 removefiles=removefiles,
130 132 abortonerror=opts.get(b'abort_on_err'),
131 133 noop=not act,
134 confirm=confirm,
132 135 )
133 136
134 137 for path in paths:
135 138 if not act:
136 139 ui.write(b'%s%s' % (path, eol))
@@ -1,2392 +1,2414 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 confirm=False,
2327 2328 ):
2328 2329 """Purge the working directory of untracked files.
2329 2330
2330 2331 ``matcher`` is a matcher configured to scan the working directory -
2331 2332 potentially a subset.
2332 2333
2333 2334 ``unknown`` controls whether unknown files should be purged.
2334 2335
2335 2336 ``ignored`` controls whether ignored files should be purged.
2336 2337
2337 2338 ``removeemptydirs`` controls whether empty directories should be removed.
2338 2339
2339 2340 ``removefiles`` controls whether files are removed.
2340 2341
2341 2342 ``abortonerror`` causes an exception to be raised if an error occurs
2342 2343 deleting a file or directory.
2343 2344
2344 2345 ``noop`` controls whether to actually remove files. If not defined, actions
2345 2346 will be taken.
2346 2347
2348 ``confirm`` ask confirmation before actually removing anything.
2349
2347 2350 Returns an iterable of relative paths in the working directory that were
2348 2351 or would be removed.
2349 2352 """
2350 2353
2351 2354 def remove(removefn, path):
2352 2355 try:
2353 2356 removefn(path)
2354 2357 except OSError:
2355 2358 m = _(b'%s cannot be removed') % path
2356 2359 if abortonerror:
2357 2360 raise error.Abort(m)
2358 2361 else:
2359 2362 repo.ui.warn(_(b'warning: %s\n') % m)
2360 2363
2361 2364 # There's no API to copy a matcher. So mutate the passed matcher and
2362 2365 # restore it when we're done.
2363 2366 oldtraversedir = matcher.traversedir
2364 2367
2365 2368 res = []
2366 2369
2367 2370 try:
2368 2371 if removeemptydirs:
2369 2372 directories = []
2370 2373 matcher.traversedir = directories.append
2371 2374
2372 2375 status = repo.status(match=matcher, ignored=ignored, unknown=unknown)
2373 2376
2377 if confirm:
2378 nb_ignored = len(status.ignored)
2379 nb_unkown = len(status.unknown)
2380 if nb_unkown and nb_ignored:
2381 msg = _(b"permanently delete %d unkown and %d ignored files?")
2382 msg %= (nb_unkown, nb_ignored)
2383 elif nb_unkown:
2384 msg = _(b"permanently delete %d unkown files?")
2385 msg %= nb_unkown
2386 elif nb_ignored:
2387 msg = _(b"permanently delete %d ignored files?")
2388 msg %= nb_ignored
2389 else:
2390 # XXX we might be missing directory there
2391 return res
2392 msg += b" (yN)$$ &Yes $$ &No"
2393 if repo.ui.promptchoice(msg, default=1) == 1:
2394 raise error.CanceledError(_(b'removal cancelled'))
2395
2374 2396 if removefiles:
2375 2397 for f in sorted(status.unknown + status.ignored):
2376 2398 if not noop:
2377 2399 repo.ui.note(_(b'removing file %s\n') % f)
2378 2400 remove(repo.wvfs.unlink, f)
2379 2401 res.append(f)
2380 2402
2381 2403 if removeemptydirs:
2382 2404 for f in sorted(directories, reverse=True):
2383 2405 if matcher(f) and not repo.wvfs.listdir(f):
2384 2406 if not noop:
2385 2407 repo.ui.note(_(b'removing directory %s\n') % f)
2386 2408 remove(repo.wvfs.rmdir, f)
2387 2409 res.append(f)
2388 2410
2389 2411 return res
2390 2412
2391 2413 finally:
2392 2414 matcher.traversedir = oldtraversedir
@@ -1,306 +1,323 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 26 $ hg purge -v
27 27 removing directory empty_dir
28 28 $ ls -A
29 29 .hg
30 30 .hgignore
31 31 directory
32 32 r1
33 33
34 34 delete an untracked directory
35 35
36 36 $ mkdir untracked_dir
37 37 $ touch untracked_dir/untracked_file1
38 38 $ touch untracked_dir/untracked_file2
39 39 $ hg purge -p
40 40 untracked_dir/untracked_file1
41 41 untracked_dir/untracked_file2
42 42 $ hg purge -v
43 43 removing file untracked_dir/untracked_file1
44 44 removing file untracked_dir/untracked_file2
45 45 removing directory untracked_dir
46 46 $ ls -A
47 47 .hg
48 48 .hgignore
49 49 directory
50 50 r1
51 51
52 52 delete an untracked file
53 53
54 54 $ touch untracked_file
55 55 $ touch untracked_file_readonly
56 56 $ "$PYTHON" <<EOF
57 57 > import os
58 58 > import stat
59 59 > f = 'untracked_file_readonly'
60 60 > os.chmod(f, stat.S_IMODE(os.stat(f).st_mode) & ~stat.S_IWRITE)
61 61 > EOF
62 62 $ hg purge -p
63 63 untracked_file
64 64 untracked_file_readonly
65 $ hg purge --confirm
66 permanently delete 2 unkown files? (yN) n
67 abort: removal cancelled
68 [250]
65 69 $ hg purge -v
66 70 removing file untracked_file
67 71 removing file untracked_file_readonly
68 72 $ ls -A
69 73 .hg
70 74 .hgignore
71 75 directory
72 76 r1
73 77
74 78 delete an untracked file in a tracked directory
75 79
76 80 $ touch directory/untracked_file
77 81 $ hg purge -p
78 82 directory/untracked_file
79 83 $ hg purge -v
80 84 removing file directory/untracked_file
81 85 $ ls -A
82 86 .hg
83 87 .hgignore
84 88 directory
85 89 r1
86 90
87 91 delete nested directories
88 92
89 93 $ mkdir -p untracked_directory/nested_directory
90 94 $ hg purge -p
91 95 untracked_directory/nested_directory
92 96 $ hg purge -v
93 97 removing directory untracked_directory/nested_directory
94 98 removing directory untracked_directory
95 99 $ ls -A
96 100 .hg
97 101 .hgignore
98 102 directory
99 103 r1
100 104
101 105 delete nested directories from a subdir
102 106
103 107 $ mkdir -p untracked_directory/nested_directory
104 108 $ cd directory
105 109 $ hg purge -p
106 110 untracked_directory/nested_directory
107 111 $ hg purge -v
108 112 removing directory untracked_directory/nested_directory
109 113 removing directory untracked_directory
110 114 $ cd ..
111 115 $ ls -A
112 116 .hg
113 117 .hgignore
114 118 directory
115 119 r1
116 120
117 121 delete only part of the tree
118 122
119 123 $ mkdir -p untracked_directory/nested_directory
120 124 $ touch directory/untracked_file
121 125 $ cd directory
122 126 $ hg purge -p ../untracked_directory
123 127 untracked_directory/nested_directory
128 $ hg purge --confirm
129 permanently delete 1 unkown files? (yN) n
130 abort: removal cancelled
131 [250]
124 132 $ hg purge -v ../untracked_directory
125 133 removing directory untracked_directory/nested_directory
126 134 removing directory untracked_directory
127 135 $ cd ..
128 136 $ ls -A
129 137 .hg
130 138 .hgignore
131 139 directory
132 140 r1
133 141 $ ls directory/untracked_file
134 142 directory/untracked_file
135 143 $ rm directory/untracked_file
136 144
137 145 skip ignored files if -i or --all not specified
138 146
139 147 $ touch ignored
140 148 $ hg purge -p
149 $ hg purge --confirm
141 150 $ hg purge -v
142 151 $ touch untracked_file
143 152 $ ls
144 153 directory
145 154 ignored
146 155 r1
147 156 untracked_file
148 157 $ hg purge -p -i
149 158 ignored
159 $ hg purge --confirm -i
160 permanently delete 1 ignored files? (yN) n
161 abort: removal cancelled
162 [250]
150 163 $ hg purge -v -i
151 164 removing file ignored
152 165 $ ls -A
153 166 .hg
154 167 .hgignore
155 168 directory
156 169 r1
157 170 untracked_file
158 171 $ touch ignored
159 172 $ hg purge -p --all
160 173 ignored
161 174 untracked_file
175 $ hg purge --confirm --all
176 permanently delete 1 unkown and 1 ignored files? (yN) n
177 abort: removal cancelled
178 [250]
162 179 $ hg purge -v --all
163 180 removing file ignored
164 181 removing file untracked_file
165 182 $ ls
166 183 directory
167 184 r1
168 185
169 186 abort with missing files until we support name mangling filesystems
170 187
171 188 $ touch untracked_file
172 189 $ rm r1
173 190
174 191 hide error messages to avoid changing the output when the text changes
175 192
176 193 $ hg purge -p 2> /dev/null
177 194 untracked_file
178 195 $ hg st
179 196 ! r1
180 197 ? untracked_file
181 198
182 199 $ hg purge -p
183 200 untracked_file
184 201 $ hg purge -v 2> /dev/null
185 202 removing file untracked_file
186 203 $ hg st
187 204 ! r1
188 205
189 206 $ hg purge -v
190 207 $ hg revert --all --quiet
191 208 $ hg st -a
192 209
193 210 tracked file in ignored directory (issue621)
194 211
195 212 $ echo directory >> .hgignore
196 213 $ hg ci -m 'ignore directory'
197 214 $ touch untracked_file
198 215 $ hg purge -p
199 216 untracked_file
200 217 $ hg purge -v
201 218 removing file untracked_file
202 219
203 220 skip excluded files
204 221
205 222 $ touch excluded_file
206 223 $ hg purge -p -X excluded_file
207 224 $ hg purge -v -X excluded_file
208 225 $ ls -A
209 226 .hg
210 227 .hgignore
211 228 directory
212 229 excluded_file
213 230 r1
214 231 $ rm excluded_file
215 232
216 233 skip files in excluded dirs
217 234
218 235 $ mkdir excluded_dir
219 236 $ touch excluded_dir/file
220 237 $ hg purge -p -X excluded_dir
221 238 $ hg purge -v -X excluded_dir
222 239 $ ls -A
223 240 .hg
224 241 .hgignore
225 242 directory
226 243 excluded_dir
227 244 r1
228 245 $ ls excluded_dir
229 246 file
230 247 $ rm -R excluded_dir
231 248
232 249 skip excluded empty dirs
233 250
234 251 $ mkdir excluded_dir
235 252 $ hg purge -p -X excluded_dir
236 253 $ hg purge -v -X excluded_dir
237 254 $ ls -A
238 255 .hg
239 256 .hgignore
240 257 directory
241 258 excluded_dir
242 259 r1
243 260 $ rmdir excluded_dir
244 261
245 262 skip patterns
246 263
247 264 $ mkdir .svn
248 265 $ touch .svn/foo
249 266 $ mkdir directory/.svn
250 267 $ touch directory/.svn/foo
251 268 $ hg purge -p -X .svn -X '*/.svn'
252 269 $ hg purge -p -X re:.*.svn
253 270
254 271 $ rm -R .svn directory r1
255 272
256 273 only remove files
257 274
258 275 $ mkdir -p empty_dir dir
259 276 $ touch untracked_file dir/untracked_file
260 277 $ hg purge -p --files
261 278 dir/untracked_file
262 279 untracked_file
263 280 $ hg purge -v --files
264 281 removing file dir/untracked_file
265 282 removing file untracked_file
266 283 $ ls -A
267 284 .hg
268 285 .hgignore
269 286 dir
270 287 empty_dir
271 288 $ ls dir
272 289
273 290 only remove dirs
274 291
275 292 $ mkdir -p empty_dir dir
276 293 $ touch untracked_file dir/untracked_file
277 294 $ hg purge -p --dirs
278 295 empty_dir
279 296 $ hg purge -v --dirs
280 297 removing directory empty_dir
281 298 $ ls -A
282 299 .hg
283 300 .hgignore
284 301 dir
285 302 untracked_file
286 303 $ ls dir
287 304 untracked_file
288 305
289 306 remove both files and dirs
290 307
291 308 $ mkdir -p empty_dir dir
292 309 $ touch untracked_file dir/untracked_file
293 310 $ hg purge -p --files --dirs
294 311 dir/untracked_file
295 312 untracked_file
296 313 empty_dir
297 314 $ hg purge -v --files --dirs
298 315 removing file dir/untracked_file
299 316 removing file untracked_file
300 317 removing directory empty_dir
301 318 removing directory dir
302 319 $ ls -A
303 320 .hg
304 321 .hgignore
305 322
306 323 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now