##// END OF EJS Templates
purge: add -i flag to delete ignored files instead of untracked files...
Valentin Gatien-Baron -
r44771:9f8eddd2 default
parent child Browse files
Show More
@@ -1,127 +1,136 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 (b'i', b'ignored', None, _(b'purge only ignored files')),
51 52 (b'', b'dirs', None, _(b'purge empty directories')),
52 53 (b'', b'files', None, _(b'purge files')),
53 54 (b'p', b'print', None, _(b'print filenames instead of deleting them')),
54 55 (
55 56 b'0',
56 57 b'print0',
57 58 None,
58 59 _(
59 60 b'end filenames with NUL, for use with xargs'
60 61 b' (implies -p/--print)'
61 62 ),
62 63 ),
63 64 ]
64 65 + cmdutil.walkopts,
65 66 _(b'hg purge [OPTION]... [DIR]...'),
66 67 helpcategory=command.CATEGORY_MAINTENANCE,
67 68 )
68 69 def purge(ui, repo, *dirs, **opts):
69 70 '''removes files not tracked by Mercurial
70 71
71 72 Delete files not known to Mercurial. This is useful to test local
72 73 and uncommitted changes in an otherwise-clean source tree.
73 74
74 75 This means that purge will delete the following by default:
75 76
76 77 - Unknown files: files marked with "?" by :hg:`status`
77 78 - Empty directories: in fact Mercurial ignores directories unless
78 79 they contain files under source control management
79 80
80 81 But it will leave untouched:
81 82
82 83 - Modified and unmodified tracked files
83 - Ignored files (unless --all is specified)
84 - Ignored files (unless -i or --all is specified)
84 85 - New files added to the repository (with :hg:`add`)
85 86
86 87 The --files and --dirs options can be used to direct purge to delete
87 88 only files, only directories, or both. If neither option is given,
88 89 both will be deleted.
89 90
90 91 If directories are given on the command line, only files in these
91 92 directories are considered.
92 93
93 94 Be careful with purge, as you could irreversibly delete some files
94 95 you forgot to add to the repository. If you only want to print the
95 96 list of files that this program would delete, use the --print
96 97 option.
97 98 '''
98 99 opts = pycompat.byteskwargs(opts)
100 cmdutil.check_at_most_one_arg(opts, b'all', b'ignored')
99 101
100 102 act = not opts.get(b'print')
101 103 eol = b'\n'
102 104 if opts.get(b'print0'):
103 105 eol = b'\0'
104 106 act = False # --print0 implies --print
107 if opts.get(b'all', False):
108 ignored = True
109 unknown = True
110 else:
111 ignored = opts.get(b'ignored', False)
112 unknown = not ignored
105 113
106 114 removefiles = opts.get(b'files')
107 115 removedirs = opts.get(b'dirs')
108 116
109 117 if not removefiles and not removedirs:
110 118 removefiles = True
111 119 removedirs = True
112 120
113 121 match = scmutil.match(repo[None], dirs, opts)
114 122
115 123 paths = mergemod.purge(
116 124 repo,
117 125 match,
118 ignored=opts.get(b'all', False),
126 unknown=unknown,
127 ignored=ignored,
119 128 removeemptydirs=removedirs,
120 129 removefiles=removefiles,
121 130 abortonerror=opts.get(b'abort_on_err'),
122 131 noop=not act,
123 132 )
124 133
125 134 for path in paths:
126 135 if not act:
127 136 ui.write(b'%s%s' % (path, eol))
@@ -1,2769 +1,2772 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 errno
11 11 import shutil
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 bin,
19 19 hex,
20 20 modifiednodeid,
21 21 nullhex,
22 22 nullid,
23 23 nullrev,
24 24 )
25 25 from .pycompat import delattr
26 26 from .thirdparty import attr
27 27 from . import (
28 28 copies,
29 29 encoding,
30 30 error,
31 31 filemerge,
32 32 match as matchmod,
33 33 obsutil,
34 34 pathutil,
35 35 pycompat,
36 36 scmutil,
37 37 subrepoutil,
38 38 util,
39 39 worker,
40 40 )
41 41 from .utils import hashutil
42 42
43 43 _pack = struct.pack
44 44 _unpack = struct.unpack
45 45
46 46
47 47 def _droponode(data):
48 48 # used for compatibility for v1
49 49 bits = data.split(b'\0')
50 50 bits = bits[:-2] + bits[-1:]
51 51 return b'\0'.join(bits)
52 52
53 53
54 54 # Merge state record types. See ``mergestate`` docs for more.
55 55 RECORD_LOCAL = b'L'
56 56 RECORD_OTHER = b'O'
57 57 RECORD_MERGED = b'F'
58 58 RECORD_CHANGEDELETE_CONFLICT = b'C'
59 59 RECORD_MERGE_DRIVER_MERGE = b'D'
60 60 RECORD_PATH_CONFLICT = b'P'
61 61 RECORD_MERGE_DRIVER_STATE = b'm'
62 62 RECORD_FILE_VALUES = b'f'
63 63 RECORD_LABELS = b'l'
64 64 RECORD_OVERRIDE = b't'
65 65 RECORD_UNSUPPORTED_MANDATORY = b'X'
66 66 RECORD_UNSUPPORTED_ADVISORY = b'x'
67 67
68 68 MERGE_DRIVER_STATE_UNMARKED = b'u'
69 69 MERGE_DRIVER_STATE_MARKED = b'm'
70 70 MERGE_DRIVER_STATE_SUCCESS = b's'
71 71
72 72 MERGE_RECORD_UNRESOLVED = b'u'
73 73 MERGE_RECORD_RESOLVED = b'r'
74 74 MERGE_RECORD_UNRESOLVED_PATH = b'pu'
75 75 MERGE_RECORD_RESOLVED_PATH = b'pr'
76 76 MERGE_RECORD_DRIVER_RESOLVED = b'd'
77 77
78 78 ACTION_FORGET = b'f'
79 79 ACTION_REMOVE = b'r'
80 80 ACTION_ADD = b'a'
81 81 ACTION_GET = b'g'
82 82 ACTION_PATH_CONFLICT = b'p'
83 83 ACTION_PATH_CONFLICT_RESOLVE = b'pr'
84 84 ACTION_ADD_MODIFIED = b'am'
85 85 ACTION_CREATED = b'c'
86 86 ACTION_DELETED_CHANGED = b'dc'
87 87 ACTION_CHANGED_DELETED = b'cd'
88 88 ACTION_MERGE = b'm'
89 89 ACTION_LOCAL_DIR_RENAME_GET = b'dg'
90 90 ACTION_DIR_RENAME_MOVE_LOCAL = b'dm'
91 91 ACTION_KEEP = b'k'
92 92 ACTION_EXEC = b'e'
93 93 ACTION_CREATED_MERGE = b'cm'
94 94
95 95
96 96 class mergestate(object):
97 97 '''track 3-way merge state of individual files
98 98
99 99 The merge state is stored on disk when needed. Two files are used: one with
100 100 an old format (version 1), and one with a new format (version 2). Version 2
101 101 stores a superset of the data in version 1, including new kinds of records
102 102 in the future. For more about the new format, see the documentation for
103 103 `_readrecordsv2`.
104 104
105 105 Each record can contain arbitrary content, and has an associated type. This
106 106 `type` should be a letter. If `type` is uppercase, the record is mandatory:
107 107 versions of Mercurial that don't support it should abort. If `type` is
108 108 lowercase, the record can be safely ignored.
109 109
110 110 Currently known records:
111 111
112 112 L: the node of the "local" part of the merge (hexified version)
113 113 O: the node of the "other" part of the merge (hexified version)
114 114 F: a file to be merged entry
115 115 C: a change/delete or delete/change conflict
116 116 D: a file that the external merge driver will merge internally
117 117 (experimental)
118 118 P: a path conflict (file vs directory)
119 119 m: the external merge driver defined for this merge plus its run state
120 120 (experimental)
121 121 f: a (filename, dictionary) tuple of optional values for a given file
122 122 X: unsupported mandatory record type (used in tests)
123 123 x: unsupported advisory record type (used in tests)
124 124 l: the labels for the parts of the merge.
125 125
126 126 Merge driver run states (experimental):
127 127 u: driver-resolved files unmarked -- needs to be run next time we're about
128 128 to resolve or commit
129 129 m: driver-resolved files marked -- only needs to be run before commit
130 130 s: success/skipped -- does not need to be run any more
131 131
132 132 Merge record states (stored in self._state, indexed by filename):
133 133 u: unresolved conflict
134 134 r: resolved conflict
135 135 pu: unresolved path conflict (file conflicts with directory)
136 136 pr: resolved path conflict
137 137 d: driver-resolved conflict
138 138
139 139 The resolve command transitions between 'u' and 'r' for conflicts and
140 140 'pu' and 'pr' for path conflicts.
141 141 '''
142 142
143 143 statepathv1 = b'merge/state'
144 144 statepathv2 = b'merge/state2'
145 145
146 146 @staticmethod
147 147 def clean(repo, node=None, other=None, labels=None):
148 148 """Initialize a brand new merge state, removing any existing state on
149 149 disk."""
150 150 ms = mergestate(repo)
151 151 ms.reset(node, other, labels)
152 152 return ms
153 153
154 154 @staticmethod
155 155 def read(repo):
156 156 """Initialize the merge state, reading it from disk."""
157 157 ms = mergestate(repo)
158 158 ms._read()
159 159 return ms
160 160
161 161 def __init__(self, repo):
162 162 """Initialize the merge state.
163 163
164 164 Do not use this directly! Instead call read() or clean()."""
165 165 self._repo = repo
166 166 self._dirty = False
167 167 self._labels = None
168 168
169 169 def reset(self, node=None, other=None, labels=None):
170 170 self._state = {}
171 171 self._stateextras = {}
172 172 self._local = None
173 173 self._other = None
174 174 self._labels = labels
175 175 for var in ('localctx', 'otherctx'):
176 176 if var in vars(self):
177 177 delattr(self, var)
178 178 if node:
179 179 self._local = node
180 180 self._other = other
181 181 self._readmergedriver = None
182 182 if self.mergedriver:
183 183 self._mdstate = MERGE_DRIVER_STATE_SUCCESS
184 184 else:
185 185 self._mdstate = MERGE_DRIVER_STATE_UNMARKED
186 186 shutil.rmtree(self._repo.vfs.join(b'merge'), True)
187 187 self._results = {}
188 188 self._dirty = False
189 189
190 190 def _read(self):
191 191 """Analyse each record content to restore a serialized state from disk
192 192
193 193 This function process "record" entry produced by the de-serialization
194 194 of on disk file.
195 195 """
196 196 self._state = {}
197 197 self._stateextras = {}
198 198 self._local = None
199 199 self._other = None
200 200 for var in ('localctx', 'otherctx'):
201 201 if var in vars(self):
202 202 delattr(self, var)
203 203 self._readmergedriver = None
204 204 self._mdstate = MERGE_DRIVER_STATE_SUCCESS
205 205 unsupported = set()
206 206 records = self._readrecords()
207 207 for rtype, record in records:
208 208 if rtype == RECORD_LOCAL:
209 209 self._local = bin(record)
210 210 elif rtype == RECORD_OTHER:
211 211 self._other = bin(record)
212 212 elif rtype == RECORD_MERGE_DRIVER_STATE:
213 213 bits = record.split(b'\0', 1)
214 214 mdstate = bits[1]
215 215 if len(mdstate) != 1 or mdstate not in (
216 216 MERGE_DRIVER_STATE_UNMARKED,
217 217 MERGE_DRIVER_STATE_MARKED,
218 218 MERGE_DRIVER_STATE_SUCCESS,
219 219 ):
220 220 # the merge driver should be idempotent, so just rerun it
221 221 mdstate = MERGE_DRIVER_STATE_UNMARKED
222 222
223 223 self._readmergedriver = bits[0]
224 224 self._mdstate = mdstate
225 225 elif rtype in (
226 226 RECORD_MERGED,
227 227 RECORD_CHANGEDELETE_CONFLICT,
228 228 RECORD_PATH_CONFLICT,
229 229 RECORD_MERGE_DRIVER_MERGE,
230 230 ):
231 231 bits = record.split(b'\0')
232 232 self._state[bits[0]] = bits[1:]
233 233 elif rtype == RECORD_FILE_VALUES:
234 234 filename, rawextras = record.split(b'\0', 1)
235 235 extraparts = rawextras.split(b'\0')
236 236 extras = {}
237 237 i = 0
238 238 while i < len(extraparts):
239 239 extras[extraparts[i]] = extraparts[i + 1]
240 240 i += 2
241 241
242 242 self._stateextras[filename] = extras
243 243 elif rtype == RECORD_LABELS:
244 244 labels = record.split(b'\0', 2)
245 245 self._labels = [l for l in labels if len(l) > 0]
246 246 elif not rtype.islower():
247 247 unsupported.add(rtype)
248 248 self._results = {}
249 249 self._dirty = False
250 250
251 251 if unsupported:
252 252 raise error.UnsupportedMergeRecords(unsupported)
253 253
254 254 def _readrecords(self):
255 255 """Read merge state from disk and return a list of record (TYPE, data)
256 256
257 257 We read data from both v1 and v2 files and decide which one to use.
258 258
259 259 V1 has been used by version prior to 2.9.1 and contains less data than
260 260 v2. We read both versions and check if no data in v2 contradicts
261 261 v1. If there is not contradiction we can safely assume that both v1
262 262 and v2 were written at the same time and use the extract data in v2. If
263 263 there is contradiction we ignore v2 content as we assume an old version
264 264 of Mercurial has overwritten the mergestate file and left an old v2
265 265 file around.
266 266
267 267 returns list of record [(TYPE, data), ...]"""
268 268 v1records = self._readrecordsv1()
269 269 v2records = self._readrecordsv2()
270 270 if self._v1v2match(v1records, v2records):
271 271 return v2records
272 272 else:
273 273 # v1 file is newer than v2 file, use it
274 274 # we have to infer the "other" changeset of the merge
275 275 # we cannot do better than that with v1 of the format
276 276 mctx = self._repo[None].parents()[-1]
277 277 v1records.append((RECORD_OTHER, mctx.hex()))
278 278 # add place holder "other" file node information
279 279 # nobody is using it yet so we do no need to fetch the data
280 280 # if mctx was wrong `mctx[bits[-2]]` may fails.
281 281 for idx, r in enumerate(v1records):
282 282 if r[0] == RECORD_MERGED:
283 283 bits = r[1].split(b'\0')
284 284 bits.insert(-2, b'')
285 285 v1records[idx] = (r[0], b'\0'.join(bits))
286 286 return v1records
287 287
288 288 def _v1v2match(self, v1records, v2records):
289 289 oldv2 = set() # old format version of v2 record
290 290 for rec in v2records:
291 291 if rec[0] == RECORD_LOCAL:
292 292 oldv2.add(rec)
293 293 elif rec[0] == RECORD_MERGED:
294 294 # drop the onode data (not contained in v1)
295 295 oldv2.add((RECORD_MERGED, _droponode(rec[1])))
296 296 for rec in v1records:
297 297 if rec not in oldv2:
298 298 return False
299 299 else:
300 300 return True
301 301
302 302 def _readrecordsv1(self):
303 303 """read on disk merge state for version 1 file
304 304
305 305 returns list of record [(TYPE, data), ...]
306 306
307 307 Note: the "F" data from this file are one entry short
308 308 (no "other file node" entry)
309 309 """
310 310 records = []
311 311 try:
312 312 f = self._repo.vfs(self.statepathv1)
313 313 for i, l in enumerate(f):
314 314 if i == 0:
315 315 records.append((RECORD_LOCAL, l[:-1]))
316 316 else:
317 317 records.append((RECORD_MERGED, l[:-1]))
318 318 f.close()
319 319 except IOError as err:
320 320 if err.errno != errno.ENOENT:
321 321 raise
322 322 return records
323 323
324 324 def _readrecordsv2(self):
325 325 """read on disk merge state for version 2 file
326 326
327 327 This format is a list of arbitrary records of the form:
328 328
329 329 [type][length][content]
330 330
331 331 `type` is a single character, `length` is a 4 byte integer, and
332 332 `content` is an arbitrary byte sequence of length `length`.
333 333
334 334 Mercurial versions prior to 3.7 have a bug where if there are
335 335 unsupported mandatory merge records, attempting to clear out the merge
336 336 state with hg update --clean or similar aborts. The 't' record type
337 337 works around that by writing out what those versions treat as an
338 338 advisory record, but later versions interpret as special: the first
339 339 character is the 'real' record type and everything onwards is the data.
340 340
341 341 Returns list of records [(TYPE, data), ...]."""
342 342 records = []
343 343 try:
344 344 f = self._repo.vfs(self.statepathv2)
345 345 data = f.read()
346 346 off = 0
347 347 end = len(data)
348 348 while off < end:
349 349 rtype = data[off : off + 1]
350 350 off += 1
351 351 length = _unpack(b'>I', data[off : (off + 4)])[0]
352 352 off += 4
353 353 record = data[off : (off + length)]
354 354 off += length
355 355 if rtype == RECORD_OVERRIDE:
356 356 rtype, record = record[0:1], record[1:]
357 357 records.append((rtype, record))
358 358 f.close()
359 359 except IOError as err:
360 360 if err.errno != errno.ENOENT:
361 361 raise
362 362 return records
363 363
364 364 @util.propertycache
365 365 def mergedriver(self):
366 366 # protect against the following:
367 367 # - A configures a malicious merge driver in their hgrc, then
368 368 # pauses the merge
369 369 # - A edits their hgrc to remove references to the merge driver
370 370 # - A gives a copy of their entire repo, including .hg, to B
371 371 # - B inspects .hgrc and finds it to be clean
372 372 # - B then continues the merge and the malicious merge driver
373 373 # gets invoked
374 374 configmergedriver = self._repo.ui.config(
375 375 b'experimental', b'mergedriver'
376 376 )
377 377 if (
378 378 self._readmergedriver is not None
379 379 and self._readmergedriver != configmergedriver
380 380 ):
381 381 raise error.ConfigError(
382 382 _(b"merge driver changed since merge started"),
383 383 hint=_(b"revert merge driver change or abort merge"),
384 384 )
385 385
386 386 return configmergedriver
387 387
388 388 @util.propertycache
389 389 def local(self):
390 390 if self._local is None:
391 391 msg = b"local accessed but self._local isn't set"
392 392 raise error.ProgrammingError(msg)
393 393 return self._local
394 394
395 395 @util.propertycache
396 396 def localctx(self):
397 397 return self._repo[self.local]
398 398
399 399 @util.propertycache
400 400 def other(self):
401 401 if self._other is None:
402 402 msg = b"other accessed but self._other isn't set"
403 403 raise error.ProgrammingError(msg)
404 404 return self._other
405 405
406 406 @util.propertycache
407 407 def otherctx(self):
408 408 return self._repo[self.other]
409 409
410 410 def active(self):
411 411 """Whether mergestate is active.
412 412
413 413 Returns True if there appears to be mergestate. This is a rough proxy
414 414 for "is a merge in progress."
415 415 """
416 416 # Check local variables before looking at filesystem for performance
417 417 # reasons.
418 418 return (
419 419 bool(self._local)
420 420 or bool(self._state)
421 421 or self._repo.vfs.exists(self.statepathv1)
422 422 or self._repo.vfs.exists(self.statepathv2)
423 423 )
424 424
425 425 def commit(self):
426 426 """Write current state on disk (if necessary)"""
427 427 if self._dirty:
428 428 records = self._makerecords()
429 429 self._writerecords(records)
430 430 self._dirty = False
431 431
432 432 def _makerecords(self):
433 433 records = []
434 434 records.append((RECORD_LOCAL, hex(self._local)))
435 435 records.append((RECORD_OTHER, hex(self._other)))
436 436 if self.mergedriver:
437 437 records.append(
438 438 (
439 439 RECORD_MERGE_DRIVER_STATE,
440 440 b'\0'.join([self.mergedriver, self._mdstate]),
441 441 )
442 442 )
443 443 # Write out state items. In all cases, the value of the state map entry
444 444 # is written as the contents of the record. The record type depends on
445 445 # the type of state that is stored, and capital-letter records are used
446 446 # to prevent older versions of Mercurial that do not support the feature
447 447 # from loading them.
448 448 for filename, v in pycompat.iteritems(self._state):
449 449 if v[0] == MERGE_RECORD_DRIVER_RESOLVED:
450 450 # Driver-resolved merge. These are stored in 'D' records.
451 451 records.append(
452 452 (RECORD_MERGE_DRIVER_MERGE, b'\0'.join([filename] + v))
453 453 )
454 454 elif v[0] in (
455 455 MERGE_RECORD_UNRESOLVED_PATH,
456 456 MERGE_RECORD_RESOLVED_PATH,
457 457 ):
458 458 # Path conflicts. These are stored in 'P' records. The current
459 459 # resolution state ('pu' or 'pr') is stored within the record.
460 460 records.append(
461 461 (RECORD_PATH_CONFLICT, b'\0'.join([filename] + v))
462 462 )
463 463 elif v[1] == nullhex or v[6] == nullhex:
464 464 # Change/Delete or Delete/Change conflicts. These are stored in
465 465 # 'C' records. v[1] is the local file, and is nullhex when the
466 466 # file is deleted locally ('dc'). v[6] is the remote file, and
467 467 # is nullhex when the file is deleted remotely ('cd').
468 468 records.append(
469 469 (RECORD_CHANGEDELETE_CONFLICT, b'\0'.join([filename] + v))
470 470 )
471 471 else:
472 472 # Normal files. These are stored in 'F' records.
473 473 records.append((RECORD_MERGED, b'\0'.join([filename] + v)))
474 474 for filename, extras in sorted(pycompat.iteritems(self._stateextras)):
475 475 rawextras = b'\0'.join(
476 476 b'%s\0%s' % (k, v) for k, v in pycompat.iteritems(extras)
477 477 )
478 478 records.append(
479 479 (RECORD_FILE_VALUES, b'%s\0%s' % (filename, rawextras))
480 480 )
481 481 if self._labels is not None:
482 482 labels = b'\0'.join(self._labels)
483 483 records.append((RECORD_LABELS, labels))
484 484 return records
485 485
486 486 def _writerecords(self, records):
487 487 """Write current state on disk (both v1 and v2)"""
488 488 self._writerecordsv1(records)
489 489 self._writerecordsv2(records)
490 490
491 491 def _writerecordsv1(self, records):
492 492 """Write current state on disk in a version 1 file"""
493 493 f = self._repo.vfs(self.statepathv1, b'wb')
494 494 irecords = iter(records)
495 495 lrecords = next(irecords)
496 496 assert lrecords[0] == RECORD_LOCAL
497 497 f.write(hex(self._local) + b'\n')
498 498 for rtype, data in irecords:
499 499 if rtype == RECORD_MERGED:
500 500 f.write(b'%s\n' % _droponode(data))
501 501 f.close()
502 502
503 503 def _writerecordsv2(self, records):
504 504 """Write current state on disk in a version 2 file
505 505
506 506 See the docstring for _readrecordsv2 for why we use 't'."""
507 507 # these are the records that all version 2 clients can read
508 508 allowlist = (RECORD_LOCAL, RECORD_OTHER, RECORD_MERGED)
509 509 f = self._repo.vfs(self.statepathv2, b'wb')
510 510 for key, data in records:
511 511 assert len(key) == 1
512 512 if key not in allowlist:
513 513 key, data = RECORD_OVERRIDE, b'%s%s' % (key, data)
514 514 format = b'>sI%is' % len(data)
515 515 f.write(_pack(format, key, len(data), data))
516 516 f.close()
517 517
518 518 @staticmethod
519 519 def getlocalkey(path):
520 520 """hash the path of a local file context for storage in the .hg/merge
521 521 directory."""
522 522
523 523 return hex(hashutil.sha1(path).digest())
524 524
525 525 def add(self, fcl, fco, fca, fd):
526 526 """add a new (potentially?) conflicting file the merge state
527 527 fcl: file context for local,
528 528 fco: file context for remote,
529 529 fca: file context for ancestors,
530 530 fd: file path of the resulting merge.
531 531
532 532 note: also write the local version to the `.hg/merge` directory.
533 533 """
534 534 if fcl.isabsent():
535 535 localkey = nullhex
536 536 else:
537 537 localkey = mergestate.getlocalkey(fcl.path())
538 538 self._repo.vfs.write(b'merge/' + localkey, fcl.data())
539 539 self._state[fd] = [
540 540 MERGE_RECORD_UNRESOLVED,
541 541 localkey,
542 542 fcl.path(),
543 543 fca.path(),
544 544 hex(fca.filenode()),
545 545 fco.path(),
546 546 hex(fco.filenode()),
547 547 fcl.flags(),
548 548 ]
549 549 self._stateextras[fd] = {b'ancestorlinknode': hex(fca.node())}
550 550 self._dirty = True
551 551
552 552 def addpath(self, path, frename, forigin):
553 553 """add a new conflicting path to the merge state
554 554 path: the path that conflicts
555 555 frename: the filename the conflicting file was renamed to
556 556 forigin: origin of the file ('l' or 'r' for local/remote)
557 557 """
558 558 self._state[path] = [MERGE_RECORD_UNRESOLVED_PATH, frename, forigin]
559 559 self._dirty = True
560 560
561 561 def __contains__(self, dfile):
562 562 return dfile in self._state
563 563
564 564 def __getitem__(self, dfile):
565 565 return self._state[dfile][0]
566 566
567 567 def __iter__(self):
568 568 return iter(sorted(self._state))
569 569
570 570 def files(self):
571 571 return self._state.keys()
572 572
573 573 def mark(self, dfile, state):
574 574 self._state[dfile][0] = state
575 575 self._dirty = True
576 576
577 577 def mdstate(self):
578 578 return self._mdstate
579 579
580 580 def unresolved(self):
581 581 """Obtain the paths of unresolved files."""
582 582
583 583 for f, entry in pycompat.iteritems(self._state):
584 584 if entry[0] in (
585 585 MERGE_RECORD_UNRESOLVED,
586 586 MERGE_RECORD_UNRESOLVED_PATH,
587 587 ):
588 588 yield f
589 589
590 590 def driverresolved(self):
591 591 """Obtain the paths of driver-resolved files."""
592 592
593 593 for f, entry in self._state.items():
594 594 if entry[0] == MERGE_RECORD_DRIVER_RESOLVED:
595 595 yield f
596 596
597 597 def extras(self, filename):
598 598 return self._stateextras.setdefault(filename, {})
599 599
600 600 def _resolve(self, preresolve, dfile, wctx):
601 601 """rerun merge process for file path `dfile`"""
602 602 if self[dfile] in (MERGE_RECORD_RESOLVED, MERGE_RECORD_DRIVER_RESOLVED):
603 603 return True, 0
604 604 stateentry = self._state[dfile]
605 605 state, localkey, lfile, afile, anode, ofile, onode, flags = stateentry
606 606 octx = self._repo[self._other]
607 607 extras = self.extras(dfile)
608 608 anccommitnode = extras.get(b'ancestorlinknode')
609 609 if anccommitnode:
610 610 actx = self._repo[anccommitnode]
611 611 else:
612 612 actx = None
613 613 fcd = self._filectxorabsent(localkey, wctx, dfile)
614 614 fco = self._filectxorabsent(onode, octx, ofile)
615 615 # TODO: move this to filectxorabsent
616 616 fca = self._repo.filectx(afile, fileid=anode, changectx=actx)
617 617 # "premerge" x flags
618 618 flo = fco.flags()
619 619 fla = fca.flags()
620 620 if b'x' in flags + flo + fla and b'l' not in flags + flo + fla:
621 621 if fca.node() == nullid and flags != flo:
622 622 if preresolve:
623 623 self._repo.ui.warn(
624 624 _(
625 625 b'warning: cannot merge flags for %s '
626 626 b'without common ancestor - keeping local flags\n'
627 627 )
628 628 % afile
629 629 )
630 630 elif flags == fla:
631 631 flags = flo
632 632 if preresolve:
633 633 # restore local
634 634 if localkey != nullhex:
635 635 f = self._repo.vfs(b'merge/' + localkey)
636 636 wctx[dfile].write(f.read(), flags)
637 637 f.close()
638 638 else:
639 639 wctx[dfile].remove(ignoremissing=True)
640 640 complete, r, deleted = filemerge.premerge(
641 641 self._repo,
642 642 wctx,
643 643 self._local,
644 644 lfile,
645 645 fcd,
646 646 fco,
647 647 fca,
648 648 labels=self._labels,
649 649 )
650 650 else:
651 651 complete, r, deleted = filemerge.filemerge(
652 652 self._repo,
653 653 wctx,
654 654 self._local,
655 655 lfile,
656 656 fcd,
657 657 fco,
658 658 fca,
659 659 labels=self._labels,
660 660 )
661 661 if r is None:
662 662 # no real conflict
663 663 del self._state[dfile]
664 664 self._stateextras.pop(dfile, None)
665 665 self._dirty = True
666 666 elif not r:
667 667 self.mark(dfile, MERGE_RECORD_RESOLVED)
668 668
669 669 if complete:
670 670 action = None
671 671 if deleted:
672 672 if fcd.isabsent():
673 673 # dc: local picked. Need to drop if present, which may
674 674 # happen on re-resolves.
675 675 action = ACTION_FORGET
676 676 else:
677 677 # cd: remote picked (or otherwise deleted)
678 678 action = ACTION_REMOVE
679 679 else:
680 680 if fcd.isabsent(): # dc: remote picked
681 681 action = ACTION_GET
682 682 elif fco.isabsent(): # cd: local picked
683 683 if dfile in self.localctx:
684 684 action = ACTION_ADD_MODIFIED
685 685 else:
686 686 action = ACTION_ADD
687 687 # else: regular merges (no action necessary)
688 688 self._results[dfile] = r, action
689 689
690 690 return complete, r
691 691
692 692 def _filectxorabsent(self, hexnode, ctx, f):
693 693 if hexnode == nullhex:
694 694 return filemerge.absentfilectx(ctx, f)
695 695 else:
696 696 return ctx[f]
697 697
698 698 def preresolve(self, dfile, wctx):
699 699 """run premerge process for dfile
700 700
701 701 Returns whether the merge is complete, and the exit code."""
702 702 return self._resolve(True, dfile, wctx)
703 703
704 704 def resolve(self, dfile, wctx):
705 705 """run merge process (assuming premerge was run) for dfile
706 706
707 707 Returns the exit code of the merge."""
708 708 return self._resolve(False, dfile, wctx)[1]
709 709
710 710 def counts(self):
711 711 """return counts for updated, merged and removed files in this
712 712 session"""
713 713 updated, merged, removed = 0, 0, 0
714 714 for r, action in pycompat.itervalues(self._results):
715 715 if r is None:
716 716 updated += 1
717 717 elif r == 0:
718 718 if action == ACTION_REMOVE:
719 719 removed += 1
720 720 else:
721 721 merged += 1
722 722 return updated, merged, removed
723 723
724 724 def unresolvedcount(self):
725 725 """get unresolved count for this merge (persistent)"""
726 726 return len(list(self.unresolved()))
727 727
728 728 def actions(self):
729 729 """return lists of actions to perform on the dirstate"""
730 730 actions = {
731 731 ACTION_REMOVE: [],
732 732 ACTION_FORGET: [],
733 733 ACTION_ADD: [],
734 734 ACTION_ADD_MODIFIED: [],
735 735 ACTION_GET: [],
736 736 }
737 737 for f, (r, action) in pycompat.iteritems(self._results):
738 738 if action is not None:
739 739 actions[action].append((f, None, b"merge result"))
740 740 return actions
741 741
742 742 def recordactions(self):
743 743 """record remove/add/get actions in the dirstate"""
744 744 branchmerge = self._repo.dirstate.p2() != nullid
745 745 recordupdates(self._repo, self.actions(), branchmerge, None)
746 746
747 747 def queueremove(self, f):
748 748 """queues a file to be removed from the dirstate
749 749
750 750 Meant for use by custom merge drivers."""
751 751 self._results[f] = 0, ACTION_REMOVE
752 752
753 753 def queueadd(self, f):
754 754 """queues a file to be added to the dirstate
755 755
756 756 Meant for use by custom merge drivers."""
757 757 self._results[f] = 0, ACTION_ADD
758 758
759 759 def queueget(self, f):
760 760 """queues a file to be marked modified in the dirstate
761 761
762 762 Meant for use by custom merge drivers."""
763 763 self._results[f] = 0, ACTION_GET
764 764
765 765
766 766 def _getcheckunknownconfig(repo, section, name):
767 767 config = repo.ui.config(section, name)
768 768 valid = [b'abort', b'ignore', b'warn']
769 769 if config not in valid:
770 770 validstr = b', '.join([b"'" + v + b"'" for v in valid])
771 771 raise error.ConfigError(
772 772 _(b"%s.%s not valid ('%s' is none of %s)")
773 773 % (section, name, config, validstr)
774 774 )
775 775 return config
776 776
777 777
778 778 def _checkunknownfile(repo, wctx, mctx, f, f2=None):
779 779 if wctx.isinmemory():
780 780 # Nothing to do in IMM because nothing in the "working copy" can be an
781 781 # unknown file.
782 782 #
783 783 # Note that we should bail out here, not in ``_checkunknownfiles()``,
784 784 # because that function does other useful work.
785 785 return False
786 786
787 787 if f2 is None:
788 788 f2 = f
789 789 return (
790 790 repo.wvfs.audit.check(f)
791 791 and repo.wvfs.isfileorlink(f)
792 792 and repo.dirstate.normalize(f) not in repo.dirstate
793 793 and mctx[f2].cmp(wctx[f])
794 794 )
795 795
796 796
797 797 class _unknowndirschecker(object):
798 798 """
799 799 Look for any unknown files or directories that may have a path conflict
800 800 with a file. If any path prefix of the file exists as a file or link,
801 801 then it conflicts. If the file itself is a directory that contains any
802 802 file that is not tracked, then it conflicts.
803 803
804 804 Returns the shortest path at which a conflict occurs, or None if there is
805 805 no conflict.
806 806 """
807 807
808 808 def __init__(self):
809 809 # A set of paths known to be good. This prevents repeated checking of
810 810 # dirs. It will be updated with any new dirs that are checked and found
811 811 # to be safe.
812 812 self._unknowndircache = set()
813 813
814 814 # A set of paths that are known to be absent. This prevents repeated
815 815 # checking of subdirectories that are known not to exist. It will be
816 816 # updated with any new dirs that are checked and found to be absent.
817 817 self._missingdircache = set()
818 818
819 819 def __call__(self, repo, wctx, f):
820 820 if wctx.isinmemory():
821 821 # Nothing to do in IMM for the same reason as ``_checkunknownfile``.
822 822 return False
823 823
824 824 # Check for path prefixes that exist as unknown files.
825 825 for p in reversed(list(pathutil.finddirs(f))):
826 826 if p in self._missingdircache:
827 827 return
828 828 if p in self._unknowndircache:
829 829 continue
830 830 if repo.wvfs.audit.check(p):
831 831 if (
832 832 repo.wvfs.isfileorlink(p)
833 833 and repo.dirstate.normalize(p) not in repo.dirstate
834 834 ):
835 835 return p
836 836 if not repo.wvfs.lexists(p):
837 837 self._missingdircache.add(p)
838 838 return
839 839 self._unknowndircache.add(p)
840 840
841 841 # Check if the file conflicts with a directory containing unknown files.
842 842 if repo.wvfs.audit.check(f) and repo.wvfs.isdir(f):
843 843 # Does the directory contain any files that are not in the dirstate?
844 844 for p, dirs, files in repo.wvfs.walk(f):
845 845 for fn in files:
846 846 relf = util.pconvert(repo.wvfs.reljoin(p, fn))
847 847 relf = repo.dirstate.normalize(relf, isknown=True)
848 848 if relf not in repo.dirstate:
849 849 return f
850 850 return None
851 851
852 852
853 853 def _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce):
854 854 """
855 855 Considers any actions that care about the presence of conflicting unknown
856 856 files. For some actions, the result is to abort; for others, it is to
857 857 choose a different action.
858 858 """
859 859 fileconflicts = set()
860 860 pathconflicts = set()
861 861 warnconflicts = set()
862 862 abortconflicts = set()
863 863 unknownconfig = _getcheckunknownconfig(repo, b'merge', b'checkunknown')
864 864 ignoredconfig = _getcheckunknownconfig(repo, b'merge', b'checkignored')
865 865 pathconfig = repo.ui.configbool(
866 866 b'experimental', b'merge.checkpathconflicts'
867 867 )
868 868 if not force:
869 869
870 870 def collectconflicts(conflicts, config):
871 871 if config == b'abort':
872 872 abortconflicts.update(conflicts)
873 873 elif config == b'warn':
874 874 warnconflicts.update(conflicts)
875 875
876 876 checkunknowndirs = _unknowndirschecker()
877 877 for f, (m, args, msg) in pycompat.iteritems(actions):
878 878 if m in (ACTION_CREATED, ACTION_DELETED_CHANGED):
879 879 if _checkunknownfile(repo, wctx, mctx, f):
880 880 fileconflicts.add(f)
881 881 elif pathconfig and f not in wctx:
882 882 path = checkunknowndirs(repo, wctx, f)
883 883 if path is not None:
884 884 pathconflicts.add(path)
885 885 elif m == ACTION_LOCAL_DIR_RENAME_GET:
886 886 if _checkunknownfile(repo, wctx, mctx, f, args[0]):
887 887 fileconflicts.add(f)
888 888
889 889 allconflicts = fileconflicts | pathconflicts
890 890 ignoredconflicts = {c for c in allconflicts if repo.dirstate._ignore(c)}
891 891 unknownconflicts = allconflicts - ignoredconflicts
892 892 collectconflicts(ignoredconflicts, ignoredconfig)
893 893 collectconflicts(unknownconflicts, unknownconfig)
894 894 else:
895 895 for f, (m, args, msg) in pycompat.iteritems(actions):
896 896 if m == ACTION_CREATED_MERGE:
897 897 fl2, anc = args
898 898 different = _checkunknownfile(repo, wctx, mctx, f)
899 899 if repo.dirstate._ignore(f):
900 900 config = ignoredconfig
901 901 else:
902 902 config = unknownconfig
903 903
904 904 # The behavior when force is True is described by this table:
905 905 # config different mergeforce | action backup
906 906 # * n * | get n
907 907 # * y y | merge -
908 908 # abort y n | merge - (1)
909 909 # warn y n | warn + get y
910 910 # ignore y n | get y
911 911 #
912 912 # (1) this is probably the wrong behavior here -- we should
913 913 # probably abort, but some actions like rebases currently
914 914 # don't like an abort happening in the middle of
915 915 # merge.update.
916 916 if not different:
917 917 actions[f] = (ACTION_GET, (fl2, False), b'remote created')
918 918 elif mergeforce or config == b'abort':
919 919 actions[f] = (
920 920 ACTION_MERGE,
921 921 (f, f, None, False, anc),
922 922 b'remote differs from untracked local',
923 923 )
924 924 elif config == b'abort':
925 925 abortconflicts.add(f)
926 926 else:
927 927 if config == b'warn':
928 928 warnconflicts.add(f)
929 929 actions[f] = (ACTION_GET, (fl2, True), b'remote created')
930 930
931 931 for f in sorted(abortconflicts):
932 932 warn = repo.ui.warn
933 933 if f in pathconflicts:
934 934 if repo.wvfs.isfileorlink(f):
935 935 warn(_(b"%s: untracked file conflicts with directory\n") % f)
936 936 else:
937 937 warn(_(b"%s: untracked directory conflicts with file\n") % f)
938 938 else:
939 939 warn(_(b"%s: untracked file differs\n") % f)
940 940 if abortconflicts:
941 941 raise error.Abort(
942 942 _(
943 943 b"untracked files in working directory "
944 944 b"differ from files in requested revision"
945 945 )
946 946 )
947 947
948 948 for f in sorted(warnconflicts):
949 949 if repo.wvfs.isfileorlink(f):
950 950 repo.ui.warn(_(b"%s: replacing untracked file\n") % f)
951 951 else:
952 952 repo.ui.warn(_(b"%s: replacing untracked files in directory\n") % f)
953 953
954 954 for f, (m, args, msg) in pycompat.iteritems(actions):
955 955 if m == ACTION_CREATED:
956 956 backup = (
957 957 f in fileconflicts
958 958 or f in pathconflicts
959 959 or any(p in pathconflicts for p in pathutil.finddirs(f))
960 960 )
961 961 (flags,) = args
962 962 actions[f] = (ACTION_GET, (flags, backup), msg)
963 963
964 964
965 965 def _forgetremoved(wctx, mctx, branchmerge):
966 966 """
967 967 Forget removed files
968 968
969 969 If we're jumping between revisions (as opposed to merging), and if
970 970 neither the working directory nor the target rev has the file,
971 971 then we need to remove it from the dirstate, to prevent the
972 972 dirstate from listing the file when it is no longer in the
973 973 manifest.
974 974
975 975 If we're merging, and the other revision has removed a file
976 976 that is not present in the working directory, we need to mark it
977 977 as removed.
978 978 """
979 979
980 980 actions = {}
981 981 m = ACTION_FORGET
982 982 if branchmerge:
983 983 m = ACTION_REMOVE
984 984 for f in wctx.deleted():
985 985 if f not in mctx:
986 986 actions[f] = m, None, b"forget deleted"
987 987
988 988 if not branchmerge:
989 989 for f in wctx.removed():
990 990 if f not in mctx:
991 991 actions[f] = ACTION_FORGET, None, b"forget removed"
992 992
993 993 return actions
994 994
995 995
996 996 def _checkcollision(repo, wmf, actions):
997 997 """
998 998 Check for case-folding collisions.
999 999 """
1000 1000 # If the repo is narrowed, filter out files outside the narrowspec.
1001 1001 narrowmatch = repo.narrowmatch()
1002 1002 if not narrowmatch.always():
1003 1003 pmmf = set(wmf.walk(narrowmatch))
1004 1004 if actions:
1005 1005 narrowactions = {}
1006 1006 for m, actionsfortype in pycompat.iteritems(actions):
1007 1007 narrowactions[m] = []
1008 1008 for (f, args, msg) in actionsfortype:
1009 1009 if narrowmatch(f):
1010 1010 narrowactions[m].append((f, args, msg))
1011 1011 actions = narrowactions
1012 1012 else:
1013 1013 # build provisional merged manifest up
1014 1014 pmmf = set(wmf)
1015 1015
1016 1016 if actions:
1017 1017 # KEEP and EXEC are no-op
1018 1018 for m in (
1019 1019 ACTION_ADD,
1020 1020 ACTION_ADD_MODIFIED,
1021 1021 ACTION_FORGET,
1022 1022 ACTION_GET,
1023 1023 ACTION_CHANGED_DELETED,
1024 1024 ACTION_DELETED_CHANGED,
1025 1025 ):
1026 1026 for f, args, msg in actions[m]:
1027 1027 pmmf.add(f)
1028 1028 for f, args, msg in actions[ACTION_REMOVE]:
1029 1029 pmmf.discard(f)
1030 1030 for f, args, msg in actions[ACTION_DIR_RENAME_MOVE_LOCAL]:
1031 1031 f2, flags = args
1032 1032 pmmf.discard(f2)
1033 1033 pmmf.add(f)
1034 1034 for f, args, msg in actions[ACTION_LOCAL_DIR_RENAME_GET]:
1035 1035 pmmf.add(f)
1036 1036 for f, args, msg in actions[ACTION_MERGE]:
1037 1037 f1, f2, fa, move, anc = args
1038 1038 if move:
1039 1039 pmmf.discard(f1)
1040 1040 pmmf.add(f)
1041 1041
1042 1042 # check case-folding collision in provisional merged manifest
1043 1043 foldmap = {}
1044 1044 for f in pmmf:
1045 1045 fold = util.normcase(f)
1046 1046 if fold in foldmap:
1047 1047 raise error.Abort(
1048 1048 _(b"case-folding collision between %s and %s")
1049 1049 % (f, foldmap[fold])
1050 1050 )
1051 1051 foldmap[fold] = f
1052 1052
1053 1053 # check case-folding of directories
1054 1054 foldprefix = unfoldprefix = lastfull = b''
1055 1055 for fold, f in sorted(foldmap.items()):
1056 1056 if fold.startswith(foldprefix) and not f.startswith(unfoldprefix):
1057 1057 # the folded prefix matches but actual casing is different
1058 1058 raise error.Abort(
1059 1059 _(b"case-folding collision between %s and directory of %s")
1060 1060 % (lastfull, f)
1061 1061 )
1062 1062 foldprefix = fold + b'/'
1063 1063 unfoldprefix = f + b'/'
1064 1064 lastfull = f
1065 1065
1066 1066
1067 1067 def driverpreprocess(repo, ms, wctx, labels=None):
1068 1068 """run the preprocess step of the merge driver, if any
1069 1069
1070 1070 This is currently not implemented -- it's an extension point."""
1071 1071 return True
1072 1072
1073 1073
1074 1074 def driverconclude(repo, ms, wctx, labels=None):
1075 1075 """run the conclude step of the merge driver, if any
1076 1076
1077 1077 This is currently not implemented -- it's an extension point."""
1078 1078 return True
1079 1079
1080 1080
1081 1081 def _filesindirs(repo, manifest, dirs):
1082 1082 """
1083 1083 Generator that yields pairs of all the files in the manifest that are found
1084 1084 inside the directories listed in dirs, and which directory they are found
1085 1085 in.
1086 1086 """
1087 1087 for f in manifest:
1088 1088 for p in pathutil.finddirs(f):
1089 1089 if p in dirs:
1090 1090 yield f, p
1091 1091 break
1092 1092
1093 1093
1094 1094 def checkpathconflicts(repo, wctx, mctx, actions):
1095 1095 """
1096 1096 Check if any actions introduce path conflicts in the repository, updating
1097 1097 actions to record or handle the path conflict accordingly.
1098 1098 """
1099 1099 mf = wctx.manifest()
1100 1100
1101 1101 # The set of local files that conflict with a remote directory.
1102 1102 localconflicts = set()
1103 1103
1104 1104 # The set of directories that conflict with a remote file, and so may cause
1105 1105 # conflicts if they still contain any files after the merge.
1106 1106 remoteconflicts = set()
1107 1107
1108 1108 # The set of directories that appear as both a file and a directory in the
1109 1109 # remote manifest. These indicate an invalid remote manifest, which
1110 1110 # can't be updated to cleanly.
1111 1111 invalidconflicts = set()
1112 1112
1113 1113 # The set of directories that contain files that are being created.
1114 1114 createdfiledirs = set()
1115 1115
1116 1116 # The set of files deleted by all the actions.
1117 1117 deletedfiles = set()
1118 1118
1119 1119 for f, (m, args, msg) in actions.items():
1120 1120 if m in (
1121 1121 ACTION_CREATED,
1122 1122 ACTION_DELETED_CHANGED,
1123 1123 ACTION_MERGE,
1124 1124 ACTION_CREATED_MERGE,
1125 1125 ):
1126 1126 # This action may create a new local file.
1127 1127 createdfiledirs.update(pathutil.finddirs(f))
1128 1128 if mf.hasdir(f):
1129 1129 # The file aliases a local directory. This might be ok if all
1130 1130 # the files in the local directory are being deleted. This
1131 1131 # will be checked once we know what all the deleted files are.
1132 1132 remoteconflicts.add(f)
1133 1133 # Track the names of all deleted files.
1134 1134 if m == ACTION_REMOVE:
1135 1135 deletedfiles.add(f)
1136 1136 if m == ACTION_MERGE:
1137 1137 f1, f2, fa, move, anc = args
1138 1138 if move:
1139 1139 deletedfiles.add(f1)
1140 1140 if m == ACTION_DIR_RENAME_MOVE_LOCAL:
1141 1141 f2, flags = args
1142 1142 deletedfiles.add(f2)
1143 1143
1144 1144 # Check all directories that contain created files for path conflicts.
1145 1145 for p in createdfiledirs:
1146 1146 if p in mf:
1147 1147 if p in mctx:
1148 1148 # A file is in a directory which aliases both a local
1149 1149 # and a remote file. This is an internal inconsistency
1150 1150 # within the remote manifest.
1151 1151 invalidconflicts.add(p)
1152 1152 else:
1153 1153 # A file is in a directory which aliases a local file.
1154 1154 # We will need to rename the local file.
1155 1155 localconflicts.add(p)
1156 1156 if p in actions and actions[p][0] in (
1157 1157 ACTION_CREATED,
1158 1158 ACTION_DELETED_CHANGED,
1159 1159 ACTION_MERGE,
1160 1160 ACTION_CREATED_MERGE,
1161 1161 ):
1162 1162 # The file is in a directory which aliases a remote file.
1163 1163 # This is an internal inconsistency within the remote
1164 1164 # manifest.
1165 1165 invalidconflicts.add(p)
1166 1166
1167 1167 # Rename all local conflicting files that have not been deleted.
1168 1168 for p in localconflicts:
1169 1169 if p not in deletedfiles:
1170 1170 ctxname = bytes(wctx).rstrip(b'+')
1171 1171 pnew = util.safename(p, ctxname, wctx, set(actions.keys()))
1172 1172 actions[pnew] = (
1173 1173 ACTION_PATH_CONFLICT_RESOLVE,
1174 1174 (p,),
1175 1175 b'local path conflict',
1176 1176 )
1177 1177 actions[p] = (ACTION_PATH_CONFLICT, (pnew, b'l'), b'path conflict')
1178 1178
1179 1179 if remoteconflicts:
1180 1180 # Check if all files in the conflicting directories have been removed.
1181 1181 ctxname = bytes(mctx).rstrip(b'+')
1182 1182 for f, p in _filesindirs(repo, mf, remoteconflicts):
1183 1183 if f not in deletedfiles:
1184 1184 m, args, msg = actions[p]
1185 1185 pnew = util.safename(p, ctxname, wctx, set(actions.keys()))
1186 1186 if m in (ACTION_DELETED_CHANGED, ACTION_MERGE):
1187 1187 # Action was merge, just update target.
1188 1188 actions[pnew] = (m, args, msg)
1189 1189 else:
1190 1190 # Action was create, change to renamed get action.
1191 1191 fl = args[0]
1192 1192 actions[pnew] = (
1193 1193 ACTION_LOCAL_DIR_RENAME_GET,
1194 1194 (p, fl),
1195 1195 b'remote path conflict',
1196 1196 )
1197 1197 actions[p] = (
1198 1198 ACTION_PATH_CONFLICT,
1199 1199 (pnew, ACTION_REMOVE),
1200 1200 b'path conflict',
1201 1201 )
1202 1202 remoteconflicts.remove(p)
1203 1203 break
1204 1204
1205 1205 if invalidconflicts:
1206 1206 for p in invalidconflicts:
1207 1207 repo.ui.warn(_(b"%s: is both a file and a directory\n") % p)
1208 1208 raise error.Abort(_(b"destination manifest contains path conflicts"))
1209 1209
1210 1210
1211 1211 def _filternarrowactions(narrowmatch, branchmerge, actions):
1212 1212 """
1213 1213 Filters out actions that can ignored because the repo is narrowed.
1214 1214
1215 1215 Raise an exception if the merge cannot be completed because the repo is
1216 1216 narrowed.
1217 1217 """
1218 1218 nooptypes = {b'k'} # TODO: handle with nonconflicttypes
1219 1219 nonconflicttypes = set(b'a am c cm f g r e'.split())
1220 1220 # We mutate the items in the dict during iteration, so iterate
1221 1221 # over a copy.
1222 1222 for f, action in list(actions.items()):
1223 1223 if narrowmatch(f):
1224 1224 pass
1225 1225 elif not branchmerge:
1226 1226 del actions[f] # just updating, ignore changes outside clone
1227 1227 elif action[0] in nooptypes:
1228 1228 del actions[f] # merge does not affect file
1229 1229 elif action[0] in nonconflicttypes:
1230 1230 raise error.Abort(
1231 1231 _(
1232 1232 b'merge affects file \'%s\' outside narrow, '
1233 1233 b'which is not yet supported'
1234 1234 )
1235 1235 % f,
1236 1236 hint=_(b'merging in the other direction may work'),
1237 1237 )
1238 1238 else:
1239 1239 raise error.Abort(
1240 1240 _(b'conflict in file \'%s\' is outside narrow clone') % f
1241 1241 )
1242 1242
1243 1243
1244 1244 def manifestmerge(
1245 1245 repo,
1246 1246 wctx,
1247 1247 p2,
1248 1248 pa,
1249 1249 branchmerge,
1250 1250 force,
1251 1251 matcher,
1252 1252 acceptremote,
1253 1253 followcopies,
1254 1254 forcefulldiff=False,
1255 1255 ):
1256 1256 """
1257 1257 Merge wctx and p2 with ancestor pa and generate merge action list
1258 1258
1259 1259 branchmerge and force are as passed in to update
1260 1260 matcher = matcher to filter file lists
1261 1261 acceptremote = accept the incoming changes without prompting
1262 1262 """
1263 1263 if matcher is not None and matcher.always():
1264 1264 matcher = None
1265 1265
1266 1266 # manifests fetched in order are going to be faster, so prime the caches
1267 1267 [
1268 1268 x.manifest()
1269 1269 for x in sorted(wctx.parents() + [p2, pa], key=scmutil.intrev)
1270 1270 ]
1271 1271
1272 1272 branch_copies1 = copies.branch_copies()
1273 1273 branch_copies2 = copies.branch_copies()
1274 1274 diverge = {}
1275 1275 if followcopies:
1276 1276 branch_copies1, branch_copies2, diverge = copies.mergecopies(
1277 1277 repo, wctx, p2, pa
1278 1278 )
1279 1279
1280 1280 boolbm = pycompat.bytestr(bool(branchmerge))
1281 1281 boolf = pycompat.bytestr(bool(force))
1282 1282 boolm = pycompat.bytestr(bool(matcher))
1283 1283 repo.ui.note(_(b"resolving manifests\n"))
1284 1284 repo.ui.debug(
1285 1285 b" branchmerge: %s, force: %s, partial: %s\n" % (boolbm, boolf, boolm)
1286 1286 )
1287 1287 repo.ui.debug(b" ancestor: %s, local: %s, remote: %s\n" % (pa, wctx, p2))
1288 1288
1289 1289 m1, m2, ma = wctx.manifest(), p2.manifest(), pa.manifest()
1290 1290 copied1 = set(branch_copies1.copy.values())
1291 1291 copied1.update(branch_copies1.movewithdir.values())
1292 1292 copied2 = set(branch_copies2.copy.values())
1293 1293 copied2.update(branch_copies2.movewithdir.values())
1294 1294
1295 1295 if b'.hgsubstate' in m1 and wctx.rev() is None:
1296 1296 # Check whether sub state is modified, and overwrite the manifest
1297 1297 # to flag the change. If wctx is a committed revision, we shouldn't
1298 1298 # care for the dirty state of the working directory.
1299 1299 if any(wctx.sub(s).dirty() for s in wctx.substate):
1300 1300 m1[b'.hgsubstate'] = modifiednodeid
1301 1301
1302 1302 # Don't use m2-vs-ma optimization if:
1303 1303 # - ma is the same as m1 or m2, which we're just going to diff again later
1304 1304 # - The caller specifically asks for a full diff, which is useful during bid
1305 1305 # merge.
1306 1306 if pa not in ([wctx, p2] + wctx.parents()) and not forcefulldiff:
1307 1307 # Identify which files are relevant to the merge, so we can limit the
1308 1308 # total m1-vs-m2 diff to just those files. This has significant
1309 1309 # performance benefits in large repositories.
1310 1310 relevantfiles = set(ma.diff(m2).keys())
1311 1311
1312 1312 # For copied and moved files, we need to add the source file too.
1313 1313 for copykey, copyvalue in pycompat.iteritems(branch_copies1.copy):
1314 1314 if copyvalue in relevantfiles:
1315 1315 relevantfiles.add(copykey)
1316 1316 for movedirkey in branch_copies1.movewithdir:
1317 1317 relevantfiles.add(movedirkey)
1318 1318 filesmatcher = scmutil.matchfiles(repo, relevantfiles)
1319 1319 matcher = matchmod.intersectmatchers(matcher, filesmatcher)
1320 1320
1321 1321 diff = m1.diff(m2, match=matcher)
1322 1322
1323 1323 actions = {}
1324 1324 for f, ((n1, fl1), (n2, fl2)) in pycompat.iteritems(diff):
1325 1325 if n1 and n2: # file exists on both local and remote side
1326 1326 if f not in ma:
1327 1327 # TODO: what if they're renamed from different sources?
1328 1328 fa = branch_copies1.copy.get(
1329 1329 f, None
1330 1330 ) or branch_copies2.copy.get(f, None)
1331 1331 if fa is not None:
1332 1332 actions[f] = (
1333 1333 ACTION_MERGE,
1334 1334 (f, f, fa, False, pa.node()),
1335 1335 b'both renamed from %s' % fa,
1336 1336 )
1337 1337 else:
1338 1338 actions[f] = (
1339 1339 ACTION_MERGE,
1340 1340 (f, f, None, False, pa.node()),
1341 1341 b'both created',
1342 1342 )
1343 1343 else:
1344 1344 a = ma[f]
1345 1345 fla = ma.flags(f)
1346 1346 nol = b'l' not in fl1 + fl2 + fla
1347 1347 if n2 == a and fl2 == fla:
1348 1348 actions[f] = (ACTION_KEEP, (), b'remote unchanged')
1349 1349 elif n1 == a and fl1 == fla: # local unchanged - use remote
1350 1350 if n1 == n2: # optimization: keep local content
1351 1351 actions[f] = (
1352 1352 ACTION_EXEC,
1353 1353 (fl2,),
1354 1354 b'update permissions',
1355 1355 )
1356 1356 else:
1357 1357 actions[f] = (
1358 1358 ACTION_GET,
1359 1359 (fl2, False),
1360 1360 b'remote is newer',
1361 1361 )
1362 1362 elif nol and n2 == a: # remote only changed 'x'
1363 1363 actions[f] = (ACTION_EXEC, (fl2,), b'update permissions')
1364 1364 elif nol and n1 == a: # local only changed 'x'
1365 1365 actions[f] = (ACTION_GET, (fl1, False), b'remote is newer')
1366 1366 else: # both changed something
1367 1367 actions[f] = (
1368 1368 ACTION_MERGE,
1369 1369 (f, f, f, False, pa.node()),
1370 1370 b'versions differ',
1371 1371 )
1372 1372 elif n1: # file exists only on local side
1373 1373 if f in copied2:
1374 1374 pass # we'll deal with it on m2 side
1375 1375 elif (
1376 1376 f in branch_copies1.movewithdir
1377 1377 ): # directory rename, move local
1378 1378 f2 = branch_copies1.movewithdir[f]
1379 1379 if f2 in m2:
1380 1380 actions[f2] = (
1381 1381 ACTION_MERGE,
1382 1382 (f, f2, None, True, pa.node()),
1383 1383 b'remote directory rename, both created',
1384 1384 )
1385 1385 else:
1386 1386 actions[f2] = (
1387 1387 ACTION_DIR_RENAME_MOVE_LOCAL,
1388 1388 (f, fl1),
1389 1389 b'remote directory rename - move from %s' % f,
1390 1390 )
1391 1391 elif f in branch_copies1.copy:
1392 1392 f2 = branch_copies1.copy[f]
1393 1393 actions[f] = (
1394 1394 ACTION_MERGE,
1395 1395 (f, f2, f2, False, pa.node()),
1396 1396 b'local copied/moved from %s' % f2,
1397 1397 )
1398 1398 elif f in ma: # clean, a different, no remote
1399 1399 if n1 != ma[f]:
1400 1400 if acceptremote:
1401 1401 actions[f] = (ACTION_REMOVE, None, b'remote delete')
1402 1402 else:
1403 1403 actions[f] = (
1404 1404 ACTION_CHANGED_DELETED,
1405 1405 (f, None, f, False, pa.node()),
1406 1406 b'prompt changed/deleted',
1407 1407 )
1408 1408 elif n1 == addednodeid:
1409 1409 # This extra 'a' is added by working copy manifest to mark
1410 1410 # the file as locally added. We should forget it instead of
1411 1411 # deleting it.
1412 1412 actions[f] = (ACTION_FORGET, None, b'remote deleted')
1413 1413 else:
1414 1414 actions[f] = (ACTION_REMOVE, None, b'other deleted')
1415 1415 elif n2: # file exists only on remote side
1416 1416 if f in copied1:
1417 1417 pass # we'll deal with it on m1 side
1418 1418 elif f in branch_copies2.movewithdir:
1419 1419 f2 = branch_copies2.movewithdir[f]
1420 1420 if f2 in m1:
1421 1421 actions[f2] = (
1422 1422 ACTION_MERGE,
1423 1423 (f2, f, None, False, pa.node()),
1424 1424 b'local directory rename, both created',
1425 1425 )
1426 1426 else:
1427 1427 actions[f2] = (
1428 1428 ACTION_LOCAL_DIR_RENAME_GET,
1429 1429 (f, fl2),
1430 1430 b'local directory rename - get from %s' % f,
1431 1431 )
1432 1432 elif f in branch_copies2.copy:
1433 1433 f2 = branch_copies2.copy[f]
1434 1434 if f2 in m2:
1435 1435 actions[f] = (
1436 1436 ACTION_MERGE,
1437 1437 (f2, f, f2, False, pa.node()),
1438 1438 b'remote copied from %s' % f2,
1439 1439 )
1440 1440 else:
1441 1441 actions[f] = (
1442 1442 ACTION_MERGE,
1443 1443 (f2, f, f2, True, pa.node()),
1444 1444 b'remote moved from %s' % f2,
1445 1445 )
1446 1446 elif f not in ma:
1447 1447 # local unknown, remote created: the logic is described by the
1448 1448 # following table:
1449 1449 #
1450 1450 # force branchmerge different | action
1451 1451 # n * * | create
1452 1452 # y n * | create
1453 1453 # y y n | create
1454 1454 # y y y | merge
1455 1455 #
1456 1456 # Checking whether the files are different is expensive, so we
1457 1457 # don't do that when we can avoid it.
1458 1458 if not force:
1459 1459 actions[f] = (ACTION_CREATED, (fl2,), b'remote created')
1460 1460 elif not branchmerge:
1461 1461 actions[f] = (ACTION_CREATED, (fl2,), b'remote created')
1462 1462 else:
1463 1463 actions[f] = (
1464 1464 ACTION_CREATED_MERGE,
1465 1465 (fl2, pa.node()),
1466 1466 b'remote created, get or merge',
1467 1467 )
1468 1468 elif n2 != ma[f]:
1469 1469 df = None
1470 1470 for d in branch_copies1.dirmove:
1471 1471 if f.startswith(d):
1472 1472 # new file added in a directory that was moved
1473 1473 df = branch_copies1.dirmove[d] + f[len(d) :]
1474 1474 break
1475 1475 if df is not None and df in m1:
1476 1476 actions[df] = (
1477 1477 ACTION_MERGE,
1478 1478 (df, f, f, False, pa.node()),
1479 1479 b'local directory rename - respect move '
1480 1480 b'from %s' % f,
1481 1481 )
1482 1482 elif acceptremote:
1483 1483 actions[f] = (ACTION_CREATED, (fl2,), b'remote recreating')
1484 1484 else:
1485 1485 actions[f] = (
1486 1486 ACTION_DELETED_CHANGED,
1487 1487 (None, f, f, False, pa.node()),
1488 1488 b'prompt deleted/changed',
1489 1489 )
1490 1490
1491 1491 if repo.ui.configbool(b'experimental', b'merge.checkpathconflicts'):
1492 1492 # If we are merging, look for path conflicts.
1493 1493 checkpathconflicts(repo, wctx, p2, actions)
1494 1494
1495 1495 narrowmatch = repo.narrowmatch()
1496 1496 if not narrowmatch.always():
1497 1497 # Updates "actions" in place
1498 1498 _filternarrowactions(narrowmatch, branchmerge, actions)
1499 1499
1500 1500 renamedelete = branch_copies1.renamedelete
1501 1501 renamedelete.update(branch_copies2.renamedelete)
1502 1502
1503 1503 return actions, diverge, renamedelete
1504 1504
1505 1505
1506 1506 def _resolvetrivial(repo, wctx, mctx, ancestor, actions):
1507 1507 """Resolves false conflicts where the nodeid changed but the content
1508 1508 remained the same."""
1509 1509 # We force a copy of actions.items() because we're going to mutate
1510 1510 # actions as we resolve trivial conflicts.
1511 1511 for f, (m, args, msg) in list(actions.items()):
1512 1512 if (
1513 1513 m == ACTION_CHANGED_DELETED
1514 1514 and f in ancestor
1515 1515 and not wctx[f].cmp(ancestor[f])
1516 1516 ):
1517 1517 # local did change but ended up with same content
1518 1518 actions[f] = ACTION_REMOVE, None, b'prompt same'
1519 1519 elif (
1520 1520 m == ACTION_DELETED_CHANGED
1521 1521 and f in ancestor
1522 1522 and not mctx[f].cmp(ancestor[f])
1523 1523 ):
1524 1524 # remote did change but ended up with same content
1525 1525 del actions[f] # don't get = keep local deleted
1526 1526
1527 1527
1528 1528 def calculateupdates(
1529 1529 repo,
1530 1530 wctx,
1531 1531 mctx,
1532 1532 ancestors,
1533 1533 branchmerge,
1534 1534 force,
1535 1535 acceptremote,
1536 1536 followcopies,
1537 1537 matcher=None,
1538 1538 mergeforce=False,
1539 1539 ):
1540 1540 """Calculate the actions needed to merge mctx into wctx using ancestors"""
1541 1541 # Avoid cycle.
1542 1542 from . import sparse
1543 1543
1544 1544 if len(ancestors) == 1: # default
1545 1545 actions, diverge, renamedelete = manifestmerge(
1546 1546 repo,
1547 1547 wctx,
1548 1548 mctx,
1549 1549 ancestors[0],
1550 1550 branchmerge,
1551 1551 force,
1552 1552 matcher,
1553 1553 acceptremote,
1554 1554 followcopies,
1555 1555 )
1556 1556 _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce)
1557 1557
1558 1558 else: # only when merge.preferancestor=* - the default
1559 1559 repo.ui.note(
1560 1560 _(b"note: merging %s and %s using bids from ancestors %s\n")
1561 1561 % (
1562 1562 wctx,
1563 1563 mctx,
1564 1564 _(b' and ').join(pycompat.bytestr(anc) for anc in ancestors),
1565 1565 )
1566 1566 )
1567 1567
1568 1568 # Call for bids
1569 1569 fbids = (
1570 1570 {}
1571 1571 ) # mapping filename to bids (action method to list af actions)
1572 1572 diverge, renamedelete = None, None
1573 1573 for ancestor in ancestors:
1574 1574 repo.ui.note(_(b'\ncalculating bids for ancestor %s\n') % ancestor)
1575 1575 actions, diverge1, renamedelete1 = manifestmerge(
1576 1576 repo,
1577 1577 wctx,
1578 1578 mctx,
1579 1579 ancestor,
1580 1580 branchmerge,
1581 1581 force,
1582 1582 matcher,
1583 1583 acceptremote,
1584 1584 followcopies,
1585 1585 forcefulldiff=True,
1586 1586 )
1587 1587 _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce)
1588 1588
1589 1589 # Track the shortest set of warning on the theory that bid
1590 1590 # merge will correctly incorporate more information
1591 1591 if diverge is None or len(diverge1) < len(diverge):
1592 1592 diverge = diverge1
1593 1593 if renamedelete is None or len(renamedelete) < len(renamedelete1):
1594 1594 renamedelete = renamedelete1
1595 1595
1596 1596 for f, a in sorted(pycompat.iteritems(actions)):
1597 1597 m, args, msg = a
1598 1598 repo.ui.debug(b' %s: %s -> %s\n' % (f, msg, m))
1599 1599 if f in fbids:
1600 1600 d = fbids[f]
1601 1601 if m in d:
1602 1602 d[m].append(a)
1603 1603 else:
1604 1604 d[m] = [a]
1605 1605 else:
1606 1606 fbids[f] = {m: [a]}
1607 1607
1608 1608 # Pick the best bid for each file
1609 1609 repo.ui.note(_(b'\nauction for merging merge bids\n'))
1610 1610 actions = {}
1611 1611 for f, bids in sorted(fbids.items()):
1612 1612 # bids is a mapping from action method to list af actions
1613 1613 # Consensus?
1614 1614 if len(bids) == 1: # all bids are the same kind of method
1615 1615 m, l = list(bids.items())[0]
1616 1616 if all(a == l[0] for a in l[1:]): # len(bids) is > 1
1617 1617 repo.ui.note(_(b" %s: consensus for %s\n") % (f, m))
1618 1618 actions[f] = l[0]
1619 1619 continue
1620 1620 # If keep is an option, just do it.
1621 1621 if ACTION_KEEP in bids:
1622 1622 repo.ui.note(_(b" %s: picking 'keep' action\n") % f)
1623 1623 actions[f] = bids[ACTION_KEEP][0]
1624 1624 continue
1625 1625 # If there are gets and they all agree [how could they not?], do it.
1626 1626 if ACTION_GET in bids:
1627 1627 ga0 = bids[ACTION_GET][0]
1628 1628 if all(a == ga0 for a in bids[ACTION_GET][1:]):
1629 1629 repo.ui.note(_(b" %s: picking 'get' action\n") % f)
1630 1630 actions[f] = ga0
1631 1631 continue
1632 1632 # TODO: Consider other simple actions such as mode changes
1633 1633 # Handle inefficient democrazy.
1634 1634 repo.ui.note(_(b' %s: multiple bids for merge action:\n') % f)
1635 1635 for m, l in sorted(bids.items()):
1636 1636 for _f, args, msg in l:
1637 1637 repo.ui.note(b' %s -> %s\n' % (msg, m))
1638 1638 # Pick random action. TODO: Instead, prompt user when resolving
1639 1639 m, l = list(bids.items())[0]
1640 1640 repo.ui.warn(
1641 1641 _(b' %s: ambiguous merge - picked %s action\n') % (f, m)
1642 1642 )
1643 1643 actions[f] = l[0]
1644 1644 continue
1645 1645 repo.ui.note(_(b'end of auction\n\n'))
1646 1646
1647 1647 if wctx.rev() is None:
1648 1648 fractions = _forgetremoved(wctx, mctx, branchmerge)
1649 1649 actions.update(fractions)
1650 1650
1651 1651 prunedactions = sparse.filterupdatesactions(
1652 1652 repo, wctx, mctx, branchmerge, actions
1653 1653 )
1654 1654 _resolvetrivial(repo, wctx, mctx, ancestors[0], actions)
1655 1655
1656 1656 return prunedactions, diverge, renamedelete
1657 1657
1658 1658
1659 1659 def _getcwd():
1660 1660 try:
1661 1661 return encoding.getcwd()
1662 1662 except OSError as err:
1663 1663 if err.errno == errno.ENOENT:
1664 1664 return None
1665 1665 raise
1666 1666
1667 1667
1668 1668 def batchremove(repo, wctx, actions):
1669 1669 """apply removes to the working directory
1670 1670
1671 1671 yields tuples for progress updates
1672 1672 """
1673 1673 verbose = repo.ui.verbose
1674 1674 cwd = _getcwd()
1675 1675 i = 0
1676 1676 for f, args, msg in actions:
1677 1677 repo.ui.debug(b" %s: %s -> r\n" % (f, msg))
1678 1678 if verbose:
1679 1679 repo.ui.note(_(b"removing %s\n") % f)
1680 1680 wctx[f].audit()
1681 1681 try:
1682 1682 wctx[f].remove(ignoremissing=True)
1683 1683 except OSError as inst:
1684 1684 repo.ui.warn(
1685 1685 _(b"update failed to remove %s: %s!\n") % (f, inst.strerror)
1686 1686 )
1687 1687 if i == 100:
1688 1688 yield i, f
1689 1689 i = 0
1690 1690 i += 1
1691 1691 if i > 0:
1692 1692 yield i, f
1693 1693
1694 1694 if cwd and not _getcwd():
1695 1695 # cwd was removed in the course of removing files; print a helpful
1696 1696 # warning.
1697 1697 repo.ui.warn(
1698 1698 _(
1699 1699 b"current directory was removed\n"
1700 1700 b"(consider changing to repo root: %s)\n"
1701 1701 )
1702 1702 % repo.root
1703 1703 )
1704 1704
1705 1705
1706 1706 def batchget(repo, mctx, wctx, wantfiledata, actions):
1707 1707 """apply gets to the working directory
1708 1708
1709 1709 mctx is the context to get from
1710 1710
1711 1711 Yields arbitrarily many (False, tuple) for progress updates, followed by
1712 1712 exactly one (True, filedata). When wantfiledata is false, filedata is an
1713 1713 empty dict. When wantfiledata is true, filedata[f] is a triple (mode, size,
1714 1714 mtime) of the file f written for each action.
1715 1715 """
1716 1716 filedata = {}
1717 1717 verbose = repo.ui.verbose
1718 1718 fctx = mctx.filectx
1719 1719 ui = repo.ui
1720 1720 i = 0
1721 1721 with repo.wvfs.backgroundclosing(ui, expectedcount=len(actions)):
1722 1722 for f, (flags, backup), msg in actions:
1723 1723 repo.ui.debug(b" %s: %s -> g\n" % (f, msg))
1724 1724 if verbose:
1725 1725 repo.ui.note(_(b"getting %s\n") % f)
1726 1726
1727 1727 if backup:
1728 1728 # If a file or directory exists with the same name, back that
1729 1729 # up. Otherwise, look to see if there is a file that conflicts
1730 1730 # with a directory this file is in, and if so, back that up.
1731 1731 conflicting = f
1732 1732 if not repo.wvfs.lexists(f):
1733 1733 for p in pathutil.finddirs(f):
1734 1734 if repo.wvfs.isfileorlink(p):
1735 1735 conflicting = p
1736 1736 break
1737 1737 if repo.wvfs.lexists(conflicting):
1738 1738 orig = scmutil.backuppath(ui, repo, conflicting)
1739 1739 util.rename(repo.wjoin(conflicting), orig)
1740 1740 wfctx = wctx[f]
1741 1741 wfctx.clearunknown()
1742 1742 atomictemp = ui.configbool(b"experimental", b"update.atomic-file")
1743 1743 size = wfctx.write(
1744 1744 fctx(f).data(),
1745 1745 flags,
1746 1746 backgroundclose=True,
1747 1747 atomictemp=atomictemp,
1748 1748 )
1749 1749 if wantfiledata:
1750 1750 s = wfctx.lstat()
1751 1751 mode = s.st_mode
1752 1752 mtime = s[stat.ST_MTIME]
1753 1753 filedata[f] = (mode, size, mtime) # for dirstate.normal
1754 1754 if i == 100:
1755 1755 yield False, (i, f)
1756 1756 i = 0
1757 1757 i += 1
1758 1758 if i > 0:
1759 1759 yield False, (i, f)
1760 1760 yield True, filedata
1761 1761
1762 1762
1763 1763 def _prefetchfiles(repo, ctx, actions):
1764 1764 """Invoke ``scmutil.prefetchfiles()`` for the files relevant to the dict
1765 1765 of merge actions. ``ctx`` is the context being merged in."""
1766 1766
1767 1767 # Skipping 'a', 'am', 'f', 'r', 'dm', 'e', 'k', 'p' and 'pr', because they
1768 1768 # don't touch the context to be merged in. 'cd' is skipped, because
1769 1769 # changed/deleted never resolves to something from the remote side.
1770 1770 oplist = [
1771 1771 actions[a]
1772 1772 for a in (
1773 1773 ACTION_GET,
1774 1774 ACTION_DELETED_CHANGED,
1775 1775 ACTION_LOCAL_DIR_RENAME_GET,
1776 1776 ACTION_MERGE,
1777 1777 )
1778 1778 ]
1779 1779 prefetch = scmutil.prefetchfiles
1780 1780 matchfiles = scmutil.matchfiles
1781 1781 prefetch(
1782 1782 repo,
1783 1783 [ctx.rev()],
1784 1784 matchfiles(repo, [f for sublist in oplist for f, args, msg in sublist]),
1785 1785 )
1786 1786
1787 1787
1788 1788 @attr.s(frozen=True)
1789 1789 class updateresult(object):
1790 1790 updatedcount = attr.ib()
1791 1791 mergedcount = attr.ib()
1792 1792 removedcount = attr.ib()
1793 1793 unresolvedcount = attr.ib()
1794 1794
1795 1795 def isempty(self):
1796 1796 return not (
1797 1797 self.updatedcount
1798 1798 or self.mergedcount
1799 1799 or self.removedcount
1800 1800 or self.unresolvedcount
1801 1801 )
1802 1802
1803 1803
1804 1804 def emptyactions():
1805 1805 """create an actions dict, to be populated and passed to applyupdates()"""
1806 1806 return dict(
1807 1807 (m, [])
1808 1808 for m in (
1809 1809 ACTION_ADD,
1810 1810 ACTION_ADD_MODIFIED,
1811 1811 ACTION_FORGET,
1812 1812 ACTION_GET,
1813 1813 ACTION_CHANGED_DELETED,
1814 1814 ACTION_DELETED_CHANGED,
1815 1815 ACTION_REMOVE,
1816 1816 ACTION_DIR_RENAME_MOVE_LOCAL,
1817 1817 ACTION_LOCAL_DIR_RENAME_GET,
1818 1818 ACTION_MERGE,
1819 1819 ACTION_EXEC,
1820 1820 ACTION_KEEP,
1821 1821 ACTION_PATH_CONFLICT,
1822 1822 ACTION_PATH_CONFLICT_RESOLVE,
1823 1823 )
1824 1824 )
1825 1825
1826 1826
1827 1827 def applyupdates(
1828 1828 repo, actions, wctx, mctx, overwrite, wantfiledata, labels=None
1829 1829 ):
1830 1830 """apply the merge action list to the working directory
1831 1831
1832 1832 wctx is the working copy context
1833 1833 mctx is the context to be merged into the working copy
1834 1834
1835 1835 Return a tuple of (counts, filedata), where counts is a tuple
1836 1836 (updated, merged, removed, unresolved) that describes how many
1837 1837 files were affected by the update, and filedata is as described in
1838 1838 batchget.
1839 1839 """
1840 1840
1841 1841 _prefetchfiles(repo, mctx, actions)
1842 1842
1843 1843 updated, merged, removed = 0, 0, 0
1844 1844 ms = mergestate.clean(repo, wctx.p1().node(), mctx.node(), labels)
1845 1845 moves = []
1846 1846 for m, l in actions.items():
1847 1847 l.sort()
1848 1848
1849 1849 # 'cd' and 'dc' actions are treated like other merge conflicts
1850 1850 mergeactions = sorted(actions[ACTION_CHANGED_DELETED])
1851 1851 mergeactions.extend(sorted(actions[ACTION_DELETED_CHANGED]))
1852 1852 mergeactions.extend(actions[ACTION_MERGE])
1853 1853 for f, args, msg in mergeactions:
1854 1854 f1, f2, fa, move, anc = args
1855 1855 if f == b'.hgsubstate': # merged internally
1856 1856 continue
1857 1857 if f1 is None:
1858 1858 fcl = filemerge.absentfilectx(wctx, fa)
1859 1859 else:
1860 1860 repo.ui.debug(b" preserving %s for resolve of %s\n" % (f1, f))
1861 1861 fcl = wctx[f1]
1862 1862 if f2 is None:
1863 1863 fco = filemerge.absentfilectx(mctx, fa)
1864 1864 else:
1865 1865 fco = mctx[f2]
1866 1866 actx = repo[anc]
1867 1867 if fa in actx:
1868 1868 fca = actx[fa]
1869 1869 else:
1870 1870 # TODO: move to absentfilectx
1871 1871 fca = repo.filectx(f1, fileid=nullrev)
1872 1872 ms.add(fcl, fco, fca, f)
1873 1873 if f1 != f and move:
1874 1874 moves.append(f1)
1875 1875
1876 1876 # remove renamed files after safely stored
1877 1877 for f in moves:
1878 1878 if wctx[f].lexists():
1879 1879 repo.ui.debug(b"removing %s\n" % f)
1880 1880 wctx[f].audit()
1881 1881 wctx[f].remove()
1882 1882
1883 1883 numupdates = sum(len(l) for m, l in actions.items() if m != ACTION_KEEP)
1884 1884 progress = repo.ui.makeprogress(
1885 1885 _(b'updating'), unit=_(b'files'), total=numupdates
1886 1886 )
1887 1887
1888 1888 if [a for a in actions[ACTION_REMOVE] if a[0] == b'.hgsubstate']:
1889 1889 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1890 1890
1891 1891 # record path conflicts
1892 1892 for f, args, msg in actions[ACTION_PATH_CONFLICT]:
1893 1893 f1, fo = args
1894 1894 s = repo.ui.status
1895 1895 s(
1896 1896 _(
1897 1897 b"%s: path conflict - a file or link has the same name as a "
1898 1898 b"directory\n"
1899 1899 )
1900 1900 % f
1901 1901 )
1902 1902 if fo == b'l':
1903 1903 s(_(b"the local file has been renamed to %s\n") % f1)
1904 1904 else:
1905 1905 s(_(b"the remote file has been renamed to %s\n") % f1)
1906 1906 s(_(b"resolve manually then use 'hg resolve --mark %s'\n") % f)
1907 1907 ms.addpath(f, f1, fo)
1908 1908 progress.increment(item=f)
1909 1909
1910 1910 # When merging in-memory, we can't support worker processes, so set the
1911 1911 # per-item cost at 0 in that case.
1912 1912 cost = 0 if wctx.isinmemory() else 0.001
1913 1913
1914 1914 # remove in parallel (must come before resolving path conflicts and getting)
1915 1915 prog = worker.worker(
1916 1916 repo.ui, cost, batchremove, (repo, wctx), actions[ACTION_REMOVE]
1917 1917 )
1918 1918 for i, item in prog:
1919 1919 progress.increment(step=i, item=item)
1920 1920 removed = len(actions[ACTION_REMOVE])
1921 1921
1922 1922 # resolve path conflicts (must come before getting)
1923 1923 for f, args, msg in actions[ACTION_PATH_CONFLICT_RESOLVE]:
1924 1924 repo.ui.debug(b" %s: %s -> pr\n" % (f, msg))
1925 1925 (f0,) = args
1926 1926 if wctx[f0].lexists():
1927 1927 repo.ui.note(_(b"moving %s to %s\n") % (f0, f))
1928 1928 wctx[f].audit()
1929 1929 wctx[f].write(wctx.filectx(f0).data(), wctx.filectx(f0).flags())
1930 1930 wctx[f0].remove()
1931 1931 progress.increment(item=f)
1932 1932
1933 1933 # get in parallel.
1934 1934 threadsafe = repo.ui.configbool(
1935 1935 b'experimental', b'worker.wdir-get-thread-safe'
1936 1936 )
1937 1937 prog = worker.worker(
1938 1938 repo.ui,
1939 1939 cost,
1940 1940 batchget,
1941 1941 (repo, mctx, wctx, wantfiledata),
1942 1942 actions[ACTION_GET],
1943 1943 threadsafe=threadsafe,
1944 1944 hasretval=True,
1945 1945 )
1946 1946 getfiledata = {}
1947 1947 for final, res in prog:
1948 1948 if final:
1949 1949 getfiledata = res
1950 1950 else:
1951 1951 i, item = res
1952 1952 progress.increment(step=i, item=item)
1953 1953 updated = len(actions[ACTION_GET])
1954 1954
1955 1955 if [a for a in actions[ACTION_GET] if a[0] == b'.hgsubstate']:
1956 1956 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1957 1957
1958 1958 # forget (manifest only, just log it) (must come first)
1959 1959 for f, args, msg in actions[ACTION_FORGET]:
1960 1960 repo.ui.debug(b" %s: %s -> f\n" % (f, msg))
1961 1961 progress.increment(item=f)
1962 1962
1963 1963 # re-add (manifest only, just log it)
1964 1964 for f, args, msg in actions[ACTION_ADD]:
1965 1965 repo.ui.debug(b" %s: %s -> a\n" % (f, msg))
1966 1966 progress.increment(item=f)
1967 1967
1968 1968 # re-add/mark as modified (manifest only, just log it)
1969 1969 for f, args, msg in actions[ACTION_ADD_MODIFIED]:
1970 1970 repo.ui.debug(b" %s: %s -> am\n" % (f, msg))
1971 1971 progress.increment(item=f)
1972 1972
1973 1973 # keep (noop, just log it)
1974 1974 for f, args, msg in actions[ACTION_KEEP]:
1975 1975 repo.ui.debug(b" %s: %s -> k\n" % (f, msg))
1976 1976 # no progress
1977 1977
1978 1978 # directory rename, move local
1979 1979 for f, args, msg in actions[ACTION_DIR_RENAME_MOVE_LOCAL]:
1980 1980 repo.ui.debug(b" %s: %s -> dm\n" % (f, msg))
1981 1981 progress.increment(item=f)
1982 1982 f0, flags = args
1983 1983 repo.ui.note(_(b"moving %s to %s\n") % (f0, f))
1984 1984 wctx[f].audit()
1985 1985 wctx[f].write(wctx.filectx(f0).data(), flags)
1986 1986 wctx[f0].remove()
1987 1987 updated += 1
1988 1988
1989 1989 # local directory rename, get
1990 1990 for f, args, msg in actions[ACTION_LOCAL_DIR_RENAME_GET]:
1991 1991 repo.ui.debug(b" %s: %s -> dg\n" % (f, msg))
1992 1992 progress.increment(item=f)
1993 1993 f0, flags = args
1994 1994 repo.ui.note(_(b"getting %s to %s\n") % (f0, f))
1995 1995 wctx[f].write(mctx.filectx(f0).data(), flags)
1996 1996 updated += 1
1997 1997
1998 1998 # exec
1999 1999 for f, args, msg in actions[ACTION_EXEC]:
2000 2000 repo.ui.debug(b" %s: %s -> e\n" % (f, msg))
2001 2001 progress.increment(item=f)
2002 2002 (flags,) = args
2003 2003 wctx[f].audit()
2004 2004 wctx[f].setflags(b'l' in flags, b'x' in flags)
2005 2005 updated += 1
2006 2006
2007 2007 # the ordering is important here -- ms.mergedriver will raise if the merge
2008 2008 # driver has changed, and we want to be able to bypass it when overwrite is
2009 2009 # True
2010 2010 usemergedriver = not overwrite and mergeactions and ms.mergedriver
2011 2011
2012 2012 if usemergedriver:
2013 2013 if wctx.isinmemory():
2014 2014 raise error.InMemoryMergeConflictsError(
2015 2015 b"in-memory merge does not support mergedriver"
2016 2016 )
2017 2017 ms.commit()
2018 2018 proceed = driverpreprocess(repo, ms, wctx, labels=labels)
2019 2019 # the driver might leave some files unresolved
2020 2020 unresolvedf = set(ms.unresolved())
2021 2021 if not proceed:
2022 2022 # XXX setting unresolved to at least 1 is a hack to make sure we
2023 2023 # error out
2024 2024 return updateresult(
2025 2025 updated, merged, removed, max(len(unresolvedf), 1)
2026 2026 )
2027 2027 newactions = []
2028 2028 for f, args, msg in mergeactions:
2029 2029 if f in unresolvedf:
2030 2030 newactions.append((f, args, msg))
2031 2031 mergeactions = newactions
2032 2032
2033 2033 try:
2034 2034 # premerge
2035 2035 tocomplete = []
2036 2036 for f, args, msg in mergeactions:
2037 2037 repo.ui.debug(b" %s: %s -> m (premerge)\n" % (f, msg))
2038 2038 progress.increment(item=f)
2039 2039 if f == b'.hgsubstate': # subrepo states need updating
2040 2040 subrepoutil.submerge(
2041 2041 repo, wctx, mctx, wctx.ancestor(mctx), overwrite, labels
2042 2042 )
2043 2043 continue
2044 2044 wctx[f].audit()
2045 2045 complete, r = ms.preresolve(f, wctx)
2046 2046 if not complete:
2047 2047 numupdates += 1
2048 2048 tocomplete.append((f, args, msg))
2049 2049
2050 2050 # merge
2051 2051 for f, args, msg in tocomplete:
2052 2052 repo.ui.debug(b" %s: %s -> m (merge)\n" % (f, msg))
2053 2053 progress.increment(item=f, total=numupdates)
2054 2054 ms.resolve(f, wctx)
2055 2055
2056 2056 finally:
2057 2057 ms.commit()
2058 2058
2059 2059 unresolved = ms.unresolvedcount()
2060 2060
2061 2061 if (
2062 2062 usemergedriver
2063 2063 and not unresolved
2064 2064 and ms.mdstate() != MERGE_DRIVER_STATE_SUCCESS
2065 2065 ):
2066 2066 if not driverconclude(repo, ms, wctx, labels=labels):
2067 2067 # XXX setting unresolved to at least 1 is a hack to make sure we
2068 2068 # error out
2069 2069 unresolved = max(unresolved, 1)
2070 2070
2071 2071 ms.commit()
2072 2072
2073 2073 msupdated, msmerged, msremoved = ms.counts()
2074 2074 updated += msupdated
2075 2075 merged += msmerged
2076 2076 removed += msremoved
2077 2077
2078 2078 extraactions = ms.actions()
2079 2079 if extraactions:
2080 2080 mfiles = set(a[0] for a in actions[ACTION_MERGE])
2081 2081 for k, acts in pycompat.iteritems(extraactions):
2082 2082 actions[k].extend(acts)
2083 2083 if k == ACTION_GET and wantfiledata:
2084 2084 # no filedata until mergestate is updated to provide it
2085 2085 for a in acts:
2086 2086 getfiledata[a[0]] = None
2087 2087 # Remove these files from actions[ACTION_MERGE] as well. This is
2088 2088 # important because in recordupdates, files in actions[ACTION_MERGE]
2089 2089 # are processed after files in other actions, and the merge driver
2090 2090 # might add files to those actions via extraactions above. This can
2091 2091 # lead to a file being recorded twice, with poor results. This is
2092 2092 # especially problematic for actions[ACTION_REMOVE] (currently only
2093 2093 # possible with the merge driver in the initial merge process;
2094 2094 # interrupted merges don't go through this flow).
2095 2095 #
2096 2096 # The real fix here is to have indexes by both file and action so
2097 2097 # that when the action for a file is changed it is automatically
2098 2098 # reflected in the other action lists. But that involves a more
2099 2099 # complex data structure, so this will do for now.
2100 2100 #
2101 2101 # We don't need to do the same operation for 'dc' and 'cd' because
2102 2102 # those lists aren't consulted again.
2103 2103 mfiles.difference_update(a[0] for a in acts)
2104 2104
2105 2105 actions[ACTION_MERGE] = [
2106 2106 a for a in actions[ACTION_MERGE] if a[0] in mfiles
2107 2107 ]
2108 2108
2109 2109 progress.complete()
2110 2110 assert len(getfiledata) == (len(actions[ACTION_GET]) if wantfiledata else 0)
2111 2111 return updateresult(updated, merged, removed, unresolved), getfiledata
2112 2112
2113 2113
2114 2114 def recordupdates(repo, actions, branchmerge, getfiledata):
2115 2115 """record merge actions to the dirstate"""
2116 2116 # remove (must come first)
2117 2117 for f, args, msg in actions.get(ACTION_REMOVE, []):
2118 2118 if branchmerge:
2119 2119 repo.dirstate.remove(f)
2120 2120 else:
2121 2121 repo.dirstate.drop(f)
2122 2122
2123 2123 # forget (must come first)
2124 2124 for f, args, msg in actions.get(ACTION_FORGET, []):
2125 2125 repo.dirstate.drop(f)
2126 2126
2127 2127 # resolve path conflicts
2128 2128 for f, args, msg in actions.get(ACTION_PATH_CONFLICT_RESOLVE, []):
2129 2129 (f0,) = args
2130 2130 origf0 = repo.dirstate.copied(f0) or f0
2131 2131 repo.dirstate.add(f)
2132 2132 repo.dirstate.copy(origf0, f)
2133 2133 if f0 == origf0:
2134 2134 repo.dirstate.remove(f0)
2135 2135 else:
2136 2136 repo.dirstate.drop(f0)
2137 2137
2138 2138 # re-add
2139 2139 for f, args, msg in actions.get(ACTION_ADD, []):
2140 2140 repo.dirstate.add(f)
2141 2141
2142 2142 # re-add/mark as modified
2143 2143 for f, args, msg in actions.get(ACTION_ADD_MODIFIED, []):
2144 2144 if branchmerge:
2145 2145 repo.dirstate.normallookup(f)
2146 2146 else:
2147 2147 repo.dirstate.add(f)
2148 2148
2149 2149 # exec change
2150 2150 for f, args, msg in actions.get(ACTION_EXEC, []):
2151 2151 repo.dirstate.normallookup(f)
2152 2152
2153 2153 # keep
2154 2154 for f, args, msg in actions.get(ACTION_KEEP, []):
2155 2155 pass
2156 2156
2157 2157 # get
2158 2158 for f, args, msg in actions.get(ACTION_GET, []):
2159 2159 if branchmerge:
2160 2160 repo.dirstate.otherparent(f)
2161 2161 else:
2162 2162 parentfiledata = getfiledata[f] if getfiledata else None
2163 2163 repo.dirstate.normal(f, parentfiledata=parentfiledata)
2164 2164
2165 2165 # merge
2166 2166 for f, args, msg in actions.get(ACTION_MERGE, []):
2167 2167 f1, f2, fa, move, anc = args
2168 2168 if branchmerge:
2169 2169 # We've done a branch merge, mark this file as merged
2170 2170 # so that we properly record the merger later
2171 2171 repo.dirstate.merge(f)
2172 2172 if f1 != f2: # copy/rename
2173 2173 if move:
2174 2174 repo.dirstate.remove(f1)
2175 2175 if f1 != f:
2176 2176 repo.dirstate.copy(f1, f)
2177 2177 else:
2178 2178 repo.dirstate.copy(f2, f)
2179 2179 else:
2180 2180 # We've update-merged a locally modified file, so
2181 2181 # we set the dirstate to emulate a normal checkout
2182 2182 # of that file some time in the past. Thus our
2183 2183 # merge will appear as a normal local file
2184 2184 # modification.
2185 2185 if f2 == f: # file not locally copied/moved
2186 2186 repo.dirstate.normallookup(f)
2187 2187 if move:
2188 2188 repo.dirstate.drop(f1)
2189 2189
2190 2190 # directory rename, move local
2191 2191 for f, args, msg in actions.get(ACTION_DIR_RENAME_MOVE_LOCAL, []):
2192 2192 f0, flag = args
2193 2193 if branchmerge:
2194 2194 repo.dirstate.add(f)
2195 2195 repo.dirstate.remove(f0)
2196 2196 repo.dirstate.copy(f0, f)
2197 2197 else:
2198 2198 repo.dirstate.normal(f)
2199 2199 repo.dirstate.drop(f0)
2200 2200
2201 2201 # directory rename, get
2202 2202 for f, args, msg in actions.get(ACTION_LOCAL_DIR_RENAME_GET, []):
2203 2203 f0, flag = args
2204 2204 if branchmerge:
2205 2205 repo.dirstate.add(f)
2206 2206 repo.dirstate.copy(f0, f)
2207 2207 else:
2208 2208 repo.dirstate.normal(f)
2209 2209
2210 2210
2211 2211 UPDATECHECK_ABORT = b'abort' # handled at higher layers
2212 2212 UPDATECHECK_NONE = b'none'
2213 2213 UPDATECHECK_LINEAR = b'linear'
2214 2214 UPDATECHECK_NO_CONFLICT = b'noconflict'
2215 2215
2216 2216
2217 2217 def update(
2218 2218 repo,
2219 2219 node,
2220 2220 branchmerge,
2221 2221 force,
2222 2222 ancestor=None,
2223 2223 mergeancestor=False,
2224 2224 labels=None,
2225 2225 matcher=None,
2226 2226 mergeforce=False,
2227 2227 updatedirstate=True,
2228 2228 updatecheck=None,
2229 2229 wc=None,
2230 2230 ):
2231 2231 """
2232 2232 Perform a merge between the working directory and the given node
2233 2233
2234 2234 node = the node to update to
2235 2235 branchmerge = whether to merge between branches
2236 2236 force = whether to force branch merging or file overwriting
2237 2237 matcher = a matcher to filter file lists (dirstate not updated)
2238 2238 mergeancestor = whether it is merging with an ancestor. If true,
2239 2239 we should accept the incoming changes for any prompts that occur.
2240 2240 If false, merging with an ancestor (fast-forward) is only allowed
2241 2241 between different named branches. This flag is used by rebase extension
2242 2242 as a temporary fix and should be avoided in general.
2243 2243 labels = labels to use for base, local and other
2244 2244 mergeforce = whether the merge was run with 'merge --force' (deprecated): if
2245 2245 this is True, then 'force' should be True as well.
2246 2246
2247 2247 The table below shows all the behaviors of the update command given the
2248 2248 -c/--check and -C/--clean or no options, whether the working directory is
2249 2249 dirty, whether a revision is specified, and the relationship of the parent
2250 2250 rev to the target rev (linear or not). Match from top first. The -n
2251 2251 option doesn't exist on the command line, but represents the
2252 2252 experimental.updatecheck=noconflict option.
2253 2253
2254 2254 This logic is tested by test-update-branches.t.
2255 2255
2256 2256 -c -C -n -m dirty rev linear | result
2257 2257 y y * * * * * | (1)
2258 2258 y * y * * * * | (1)
2259 2259 y * * y * * * | (1)
2260 2260 * y y * * * * | (1)
2261 2261 * y * y * * * | (1)
2262 2262 * * y y * * * | (1)
2263 2263 * * * * * n n | x
2264 2264 * * * * n * * | ok
2265 2265 n n n n y * y | merge
2266 2266 n n n n y y n | (2)
2267 2267 n n n y y * * | merge
2268 2268 n n y n y * * | merge if no conflict
2269 2269 n y n n y * * | discard
2270 2270 y n n n y * * | (3)
2271 2271
2272 2272 x = can't happen
2273 2273 * = don't-care
2274 2274 1 = incompatible options (checked in commands.py)
2275 2275 2 = abort: uncommitted changes (commit or update --clean to discard changes)
2276 2276 3 = abort: uncommitted changes (checked in commands.py)
2277 2277
2278 2278 The merge is performed inside ``wc``, a workingctx-like objects. It defaults
2279 2279 to repo[None] if None is passed.
2280 2280
2281 2281 Return the same tuple as applyupdates().
2282 2282 """
2283 2283 # Avoid cycle.
2284 2284 from . import sparse
2285 2285
2286 2286 # This function used to find the default destination if node was None, but
2287 2287 # that's now in destutil.py.
2288 2288 assert node is not None
2289 2289 if not branchmerge and not force:
2290 2290 # TODO: remove the default once all callers that pass branchmerge=False
2291 2291 # and force=False pass a value for updatecheck. We may want to allow
2292 2292 # updatecheck='abort' to better suppport some of these callers.
2293 2293 if updatecheck is None:
2294 2294 updatecheck = UPDATECHECK_LINEAR
2295 2295 if updatecheck not in (
2296 2296 UPDATECHECK_NONE,
2297 2297 UPDATECHECK_LINEAR,
2298 2298 UPDATECHECK_NO_CONFLICT,
2299 2299 ):
2300 2300 raise ValueError(
2301 2301 r'Invalid updatecheck %r (can accept %r)'
2302 2302 % (
2303 2303 updatecheck,
2304 2304 (
2305 2305 UPDATECHECK_NONE,
2306 2306 UPDATECHECK_LINEAR,
2307 2307 UPDATECHECK_NO_CONFLICT,
2308 2308 ),
2309 2309 )
2310 2310 )
2311 2311 with repo.wlock():
2312 2312 if wc is None:
2313 2313 wc = repo[None]
2314 2314 pl = wc.parents()
2315 2315 p1 = pl[0]
2316 2316 p2 = repo[node]
2317 2317 if ancestor is not None:
2318 2318 pas = [repo[ancestor]]
2319 2319 else:
2320 2320 if repo.ui.configlist(b'merge', b'preferancestor') == [b'*']:
2321 2321 cahs = repo.changelog.commonancestorsheads(p1.node(), p2.node())
2322 2322 pas = [repo[anc] for anc in (sorted(cahs) or [nullid])]
2323 2323 else:
2324 2324 pas = [p1.ancestor(p2, warn=branchmerge)]
2325 2325
2326 2326 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), bytes(p1), bytes(p2)
2327 2327
2328 2328 overwrite = force and not branchmerge
2329 2329 ### check phase
2330 2330 if not overwrite:
2331 2331 if len(pl) > 1:
2332 2332 raise error.Abort(_(b"outstanding uncommitted merge"))
2333 2333 ms = mergestate.read(repo)
2334 2334 if list(ms.unresolved()):
2335 2335 raise error.Abort(
2336 2336 _(b"outstanding merge conflicts"),
2337 2337 hint=_(b"use 'hg resolve' to resolve"),
2338 2338 )
2339 2339 if branchmerge:
2340 2340 if pas == [p2]:
2341 2341 raise error.Abort(
2342 2342 _(
2343 2343 b"merging with a working directory ancestor"
2344 2344 b" has no effect"
2345 2345 )
2346 2346 )
2347 2347 elif pas == [p1]:
2348 2348 if not mergeancestor and wc.branch() == p2.branch():
2349 2349 raise error.Abort(
2350 2350 _(b"nothing to merge"),
2351 2351 hint=_(b"use 'hg update' or check 'hg heads'"),
2352 2352 )
2353 2353 if not force and (wc.files() or wc.deleted()):
2354 2354 raise error.Abort(
2355 2355 _(b"uncommitted changes"),
2356 2356 hint=_(b"use 'hg status' to list changes"),
2357 2357 )
2358 2358 if not wc.isinmemory():
2359 2359 for s in sorted(wc.substate):
2360 2360 wc.sub(s).bailifchanged()
2361 2361
2362 2362 elif not overwrite:
2363 2363 if p1 == p2: # no-op update
2364 2364 # call the hooks and exit early
2365 2365 repo.hook(b'preupdate', throw=True, parent1=xp2, parent2=b'')
2366 2366 repo.hook(b'update', parent1=xp2, parent2=b'', error=0)
2367 2367 return updateresult(0, 0, 0, 0)
2368 2368
2369 2369 if updatecheck == UPDATECHECK_LINEAR and pas not in (
2370 2370 [p1],
2371 2371 [p2],
2372 2372 ): # nonlinear
2373 2373 dirty = wc.dirty(missing=True)
2374 2374 if dirty:
2375 2375 # Branching is a bit strange to ensure we do the minimal
2376 2376 # amount of call to obsutil.foreground.
2377 2377 foreground = obsutil.foreground(repo, [p1.node()])
2378 2378 # note: the <node> variable contains a random identifier
2379 2379 if repo[node].node() in foreground:
2380 2380 pass # allow updating to successors
2381 2381 else:
2382 2382 msg = _(b"uncommitted changes")
2383 2383 hint = _(b"commit or update --clean to discard changes")
2384 2384 raise error.UpdateAbort(msg, hint=hint)
2385 2385 else:
2386 2386 # Allow jumping branches if clean and specific rev given
2387 2387 pass
2388 2388
2389 2389 if overwrite:
2390 2390 pas = [wc]
2391 2391 elif not branchmerge:
2392 2392 pas = [p1]
2393 2393
2394 2394 # deprecated config: merge.followcopies
2395 2395 followcopies = repo.ui.configbool(b'merge', b'followcopies')
2396 2396 if overwrite:
2397 2397 followcopies = False
2398 2398 elif not pas[0]:
2399 2399 followcopies = False
2400 2400 if not branchmerge and not wc.dirty(missing=True):
2401 2401 followcopies = False
2402 2402
2403 2403 ### calculate phase
2404 2404 actionbyfile, diverge, renamedelete = calculateupdates(
2405 2405 repo,
2406 2406 wc,
2407 2407 p2,
2408 2408 pas,
2409 2409 branchmerge,
2410 2410 force,
2411 2411 mergeancestor,
2412 2412 followcopies,
2413 2413 matcher=matcher,
2414 2414 mergeforce=mergeforce,
2415 2415 )
2416 2416
2417 2417 if updatecheck == UPDATECHECK_NO_CONFLICT:
2418 2418 for f, (m, args, msg) in pycompat.iteritems(actionbyfile):
2419 2419 if m not in (
2420 2420 ACTION_GET,
2421 2421 ACTION_KEEP,
2422 2422 ACTION_EXEC,
2423 2423 ACTION_REMOVE,
2424 2424 ACTION_PATH_CONFLICT_RESOLVE,
2425 2425 ):
2426 2426 msg = _(b"conflicting changes")
2427 2427 hint = _(b"commit or update --clean to discard changes")
2428 2428 raise error.Abort(msg, hint=hint)
2429 2429
2430 2430 # Prompt and create actions. Most of this is in the resolve phase
2431 2431 # already, but we can't handle .hgsubstate in filemerge or
2432 2432 # subrepoutil.submerge yet so we have to keep prompting for it.
2433 2433 if b'.hgsubstate' in actionbyfile:
2434 2434 f = b'.hgsubstate'
2435 2435 m, args, msg = actionbyfile[f]
2436 2436 prompts = filemerge.partextras(labels)
2437 2437 prompts[b'f'] = f
2438 2438 if m == ACTION_CHANGED_DELETED:
2439 2439 if repo.ui.promptchoice(
2440 2440 _(
2441 2441 b"local%(l)s changed %(f)s which other%(o)s deleted\n"
2442 2442 b"use (c)hanged version or (d)elete?"
2443 2443 b"$$ &Changed $$ &Delete"
2444 2444 )
2445 2445 % prompts,
2446 2446 0,
2447 2447 ):
2448 2448 actionbyfile[f] = (ACTION_REMOVE, None, b'prompt delete')
2449 2449 elif f in p1:
2450 2450 actionbyfile[f] = (
2451 2451 ACTION_ADD_MODIFIED,
2452 2452 None,
2453 2453 b'prompt keep',
2454 2454 )
2455 2455 else:
2456 2456 actionbyfile[f] = (ACTION_ADD, None, b'prompt keep')
2457 2457 elif m == ACTION_DELETED_CHANGED:
2458 2458 f1, f2, fa, move, anc = args
2459 2459 flags = p2[f2].flags()
2460 2460 if (
2461 2461 repo.ui.promptchoice(
2462 2462 _(
2463 2463 b"other%(o)s changed %(f)s which local%(l)s deleted\n"
2464 2464 b"use (c)hanged version or leave (d)eleted?"
2465 2465 b"$$ &Changed $$ &Deleted"
2466 2466 )
2467 2467 % prompts,
2468 2468 0,
2469 2469 )
2470 2470 == 0
2471 2471 ):
2472 2472 actionbyfile[f] = (
2473 2473 ACTION_GET,
2474 2474 (flags, False),
2475 2475 b'prompt recreating',
2476 2476 )
2477 2477 else:
2478 2478 del actionbyfile[f]
2479 2479
2480 2480 # Convert to dictionary-of-lists format
2481 2481 actions = emptyactions()
2482 2482 for f, (m, args, msg) in pycompat.iteritems(actionbyfile):
2483 2483 if m not in actions:
2484 2484 actions[m] = []
2485 2485 actions[m].append((f, args, msg))
2486 2486
2487 2487 if not util.fscasesensitive(repo.path):
2488 2488 # check collision between files only in p2 for clean update
2489 2489 if not branchmerge and (
2490 2490 force or not wc.dirty(missing=True, branch=False)
2491 2491 ):
2492 2492 _checkcollision(repo, p2.manifest(), None)
2493 2493 else:
2494 2494 _checkcollision(repo, wc.manifest(), actions)
2495 2495
2496 2496 # divergent renames
2497 2497 for f, fl in sorted(pycompat.iteritems(diverge)):
2498 2498 repo.ui.warn(
2499 2499 _(
2500 2500 b"note: possible conflict - %s was renamed "
2501 2501 b"multiple times to:\n"
2502 2502 )
2503 2503 % f
2504 2504 )
2505 2505 for nf in sorted(fl):
2506 2506 repo.ui.warn(b" %s\n" % nf)
2507 2507
2508 2508 # rename and delete
2509 2509 for f, fl in sorted(pycompat.iteritems(renamedelete)):
2510 2510 repo.ui.warn(
2511 2511 _(
2512 2512 b"note: possible conflict - %s was deleted "
2513 2513 b"and renamed to:\n"
2514 2514 )
2515 2515 % f
2516 2516 )
2517 2517 for nf in sorted(fl):
2518 2518 repo.ui.warn(b" %s\n" % nf)
2519 2519
2520 2520 ### apply phase
2521 2521 if not branchmerge: # just jump to the new rev
2522 2522 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, b''
2523 2523 # If we're doing a partial update, we need to skip updating
2524 2524 # the dirstate.
2525 2525 always = matcher is None or matcher.always()
2526 2526 updatedirstate = updatedirstate and always and not wc.isinmemory()
2527 2527 if updatedirstate:
2528 2528 repo.hook(b'preupdate', throw=True, parent1=xp1, parent2=xp2)
2529 2529 # note that we're in the middle of an update
2530 2530 repo.vfs.write(b'updatestate', p2.hex())
2531 2531
2532 2532 # Advertise fsmonitor when its presence could be useful.
2533 2533 #
2534 2534 # We only advertise when performing an update from an empty working
2535 2535 # directory. This typically only occurs during initial clone.
2536 2536 #
2537 2537 # We give users a mechanism to disable the warning in case it is
2538 2538 # annoying.
2539 2539 #
2540 2540 # We only allow on Linux and MacOS because that's where fsmonitor is
2541 2541 # considered stable.
2542 2542 fsmonitorwarning = repo.ui.configbool(b'fsmonitor', b'warn_when_unused')
2543 2543 fsmonitorthreshold = repo.ui.configint(
2544 2544 b'fsmonitor', b'warn_update_file_count'
2545 2545 )
2546 2546 try:
2547 2547 # avoid cycle: extensions -> cmdutil -> merge
2548 2548 from . import extensions
2549 2549
2550 2550 extensions.find(b'fsmonitor')
2551 2551 fsmonitorenabled = repo.ui.config(b'fsmonitor', b'mode') != b'off'
2552 2552 # We intentionally don't look at whether fsmonitor has disabled
2553 2553 # itself because a) fsmonitor may have already printed a warning
2554 2554 # b) we only care about the config state here.
2555 2555 except KeyError:
2556 2556 fsmonitorenabled = False
2557 2557
2558 2558 if (
2559 2559 fsmonitorwarning
2560 2560 and not fsmonitorenabled
2561 2561 and p1.node() == nullid
2562 2562 and len(actions[ACTION_GET]) >= fsmonitorthreshold
2563 2563 and pycompat.sysplatform.startswith((b'linux', b'darwin'))
2564 2564 ):
2565 2565 repo.ui.warn(
2566 2566 _(
2567 2567 b'(warning: large working directory being used without '
2568 2568 b'fsmonitor enabled; enable fsmonitor to improve performance; '
2569 2569 b'see "hg help -e fsmonitor")\n'
2570 2570 )
2571 2571 )
2572 2572
2573 2573 wantfiledata = updatedirstate and not branchmerge
2574 2574 stats, getfiledata = applyupdates(
2575 2575 repo, actions, wc, p2, overwrite, wantfiledata, labels=labels
2576 2576 )
2577 2577
2578 2578 if updatedirstate:
2579 2579 with repo.dirstate.parentchange():
2580 2580 repo.setparents(fp1, fp2)
2581 2581 recordupdates(repo, actions, branchmerge, getfiledata)
2582 2582 # update completed, clear state
2583 2583 util.unlink(repo.vfs.join(b'updatestate'))
2584 2584
2585 2585 if not branchmerge:
2586 2586 repo.dirstate.setbranch(p2.branch())
2587 2587
2588 2588 # If we're updating to a location, clean up any stale temporary includes
2589 2589 # (ex: this happens during hg rebase --abort).
2590 2590 if not branchmerge:
2591 2591 sparse.prunetemporaryincludes(repo)
2592 2592
2593 2593 if updatedirstate:
2594 2594 repo.hook(
2595 2595 b'update', parent1=xp1, parent2=xp2, error=stats.unresolvedcount
2596 2596 )
2597 2597 return stats
2598 2598
2599 2599
2600 2600 def clean_update(ctx, wc=None):
2601 2601 """Do a clean update to the given commit.
2602 2602
2603 2603 This involves updating to the commit and discarding any changes in the
2604 2604 working copy.
2605 2605 """
2606 2606 return update(ctx.repo(), ctx.rev(), branchmerge=False, force=True, wc=wc)
2607 2607
2608 2608
2609 2609 def revert_to(ctx, matcher=None, wc=None):
2610 2610 """Revert the working copy to the given commit.
2611 2611
2612 2612 The working copy will keep its current parent(s) but its content will
2613 2613 be the same as in the given commit.
2614 2614 """
2615 2615
2616 2616 return update(
2617 2617 ctx.repo(),
2618 2618 ctx.rev(),
2619 2619 branchmerge=False,
2620 2620 force=True,
2621 2621 updatedirstate=False,
2622 2622 matcher=matcher,
2623 2623 wc=wc,
2624 2624 )
2625 2625
2626 2626
2627 2627 def graft(
2628 2628 repo,
2629 2629 ctx,
2630 2630 base=None,
2631 2631 labels=None,
2632 2632 keepparent=False,
2633 2633 keepconflictparent=False,
2634 2634 wctx=None,
2635 2635 ):
2636 2636 """Do a graft-like merge.
2637 2637
2638 2638 This is a merge where the merge ancestor is chosen such that one
2639 2639 or more changesets are grafted onto the current changeset. In
2640 2640 addition to the merge, this fixes up the dirstate to include only
2641 2641 a single parent (if keepparent is False) and tries to duplicate any
2642 2642 renames/copies appropriately.
2643 2643
2644 2644 ctx - changeset to rebase
2645 2645 base - merge base, or ctx.p1() if not specified
2646 2646 labels - merge labels eg ['local', 'graft']
2647 2647 keepparent - keep second parent if any
2648 2648 keepconflictparent - if unresolved, keep parent used for the merge
2649 2649
2650 2650 """
2651 2651 # If we're grafting a descendant onto an ancestor, be sure to pass
2652 2652 # mergeancestor=True to update. This does two things: 1) allows the merge if
2653 2653 # the destination is the same as the parent of the ctx (so we can use graft
2654 2654 # to copy commits), and 2) informs update that the incoming changes are
2655 2655 # newer than the destination so it doesn't prompt about "remote changed foo
2656 2656 # which local deleted".
2657 2657 wctx = wctx or repo[None]
2658 2658 pctx = wctx.p1()
2659 2659 base = base or ctx.p1()
2660 2660 mergeancestor = repo.changelog.isancestor(pctx.node(), ctx.node())
2661 2661
2662 2662 stats = update(
2663 2663 repo,
2664 2664 ctx.node(),
2665 2665 True,
2666 2666 True,
2667 2667 base.node(),
2668 2668 mergeancestor=mergeancestor,
2669 2669 labels=labels,
2670 2670 wc=wctx,
2671 2671 )
2672 2672
2673 2673 if keepconflictparent and stats.unresolvedcount:
2674 2674 pother = ctx.node()
2675 2675 else:
2676 2676 pother = nullid
2677 2677 parents = ctx.parents()
2678 2678 if keepparent and len(parents) == 2 and base in parents:
2679 2679 parents.remove(base)
2680 2680 pother = parents[0].node()
2681 2681 # Never set both parents equal to each other
2682 2682 if pother == pctx.node():
2683 2683 pother = nullid
2684 2684
2685 2685 if wctx.isinmemory():
2686 2686 wctx.setparents(pctx.node(), pother)
2687 2687 # fix up dirstate for copies and renames
2688 2688 copies.graftcopies(wctx, ctx, base)
2689 2689 else:
2690 2690 with repo.dirstate.parentchange():
2691 2691 repo.setparents(pctx.node(), pother)
2692 2692 repo.dirstate.write(repo.currenttransaction())
2693 2693 # fix up dirstate for copies and renames
2694 2694 copies.graftcopies(wctx, ctx, base)
2695 2695 return stats
2696 2696
2697 2697
2698 2698 def purge(
2699 2699 repo,
2700 2700 matcher,
2701 unknown=True,
2701 2702 ignored=False,
2702 2703 removeemptydirs=True,
2703 2704 removefiles=True,
2704 2705 abortonerror=False,
2705 2706 noop=False,
2706 2707 ):
2707 2708 """Purge the working directory of untracked files.
2708 2709
2709 2710 ``matcher`` is a matcher configured to scan the working directory -
2710 2711 potentially a subset.
2711 2712
2712 ``ignored`` controls whether ignored files should also be purged.
2713 ``unknown`` controls whether unknown files should be purged.
2714
2715 ``ignored`` controls whether ignored files should be purged.
2713 2716
2714 2717 ``removeemptydirs`` controls whether empty directories should be removed.
2715 2718
2716 2719 ``removefiles`` controls whether files are removed.
2717 2720
2718 2721 ``abortonerror`` causes an exception to be raised if an error occurs
2719 2722 deleting a file or directory.
2720 2723
2721 2724 ``noop`` controls whether to actually remove files. If not defined, actions
2722 2725 will be taken.
2723 2726
2724 2727 Returns an iterable of relative paths in the working directory that were
2725 2728 or would be removed.
2726 2729 """
2727 2730
2728 2731 def remove(removefn, path):
2729 2732 try:
2730 2733 removefn(path)
2731 2734 except OSError:
2732 2735 m = _(b'%s cannot be removed') % path
2733 2736 if abortonerror:
2734 2737 raise error.Abort(m)
2735 2738 else:
2736 2739 repo.ui.warn(_(b'warning: %s\n') % m)
2737 2740
2738 2741 # There's no API to copy a matcher. So mutate the passed matcher and
2739 2742 # restore it when we're done.
2740 2743 oldtraversedir = matcher.traversedir
2741 2744
2742 2745 res = []
2743 2746
2744 2747 try:
2745 2748 if removeemptydirs:
2746 2749 directories = []
2747 2750 matcher.traversedir = directories.append
2748 2751
2749 status = repo.status(match=matcher, ignored=ignored, unknown=True)
2752 status = repo.status(match=matcher, ignored=ignored, unknown=unknown)
2750 2753
2751 2754 if removefiles:
2752 2755 for f in sorted(status.unknown + status.ignored):
2753 2756 if not noop:
2754 2757 repo.ui.note(_(b'removing file %s\n') % f)
2755 2758 remove(repo.wvfs.unlink, f)
2756 2759 res.append(f)
2757 2760
2758 2761 if removeemptydirs:
2759 2762 for f in sorted(directories, reverse=True):
2760 2763 if matcher(f) and not repo.wvfs.listdir(f):
2761 2764 if not noop:
2762 2765 repo.ui.note(_(b'removing directory %s\n') % f)
2763 2766 remove(repo.wvfs.rmdir, f)
2764 2767 res.append(f)
2765 2768
2766 2769 return res
2767 2770
2768 2771 finally:
2769 2772 matcher.traversedir = oldtraversedir
@@ -1,22 +1,24 b''
1 1 == New Features ==
2 2
3 * `hg purge`/`hg clean` can now delete ignored files instead of
4 untracked files, with the new -i flag.
3 5
4 6 == New Experimental Features ==
5 7
6 8
7 9 == Bug Fixes ==
8 10
9 11
10 12 == Backwards Compatibility Changes ==
11 13
12 14
13 15 == Internal API Changes ==
14 16
15 17 * The deprecated `ui.progress()` has now been deleted. Please use
16 18 `ui.makeprogress()` instead.
17 19
18 20 * `hg.merge()` has lost its `abort` argument. Please call
19 21 `hg.abortmerge()` directly instead.
20 22
21 23 * The `*others` argument of `cmdutil.check_incompatible_arguments()`
22 24 changed from being varargs argument to being a single collection.
@@ -1,265 +1,278 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
29 29 directory
30 30 r1
31 31
32 32 delete an untracked directory
33 33
34 34 $ mkdir untracked_dir
35 35 $ touch untracked_dir/untracked_file1
36 36 $ touch untracked_dir/untracked_file2
37 37 $ hg purge -p
38 38 untracked_dir/untracked_file1
39 39 untracked_dir/untracked_file2
40 40 $ hg purge -v
41 41 removing file untracked_dir/untracked_file1
42 42 removing file untracked_dir/untracked_file2
43 43 removing directory untracked_dir
44 44 $ ls
45 45 directory
46 46 r1
47 47
48 48 delete an untracked file
49 49
50 50 $ touch untracked_file
51 51 $ touch untracked_file_readonly
52 52 $ "$PYTHON" <<EOF
53 53 > import os
54 54 > import stat
55 55 > f = 'untracked_file_readonly'
56 56 > os.chmod(f, stat.S_IMODE(os.stat(f).st_mode) & ~stat.S_IWRITE)
57 57 > EOF
58 58 $ hg purge -p
59 59 untracked_file
60 60 untracked_file_readonly
61 61 $ hg purge -v
62 62 removing file untracked_file
63 63 removing file untracked_file_readonly
64 64 $ ls
65 65 directory
66 66 r1
67 67
68 68 delete an untracked file in a tracked directory
69 69
70 70 $ touch directory/untracked_file
71 71 $ hg purge -p
72 72 directory/untracked_file
73 73 $ hg purge -v
74 74 removing file directory/untracked_file
75 75 $ ls
76 76 directory
77 77 r1
78 78
79 79 delete nested directories
80 80
81 81 $ mkdir -p untracked_directory/nested_directory
82 82 $ hg purge -p
83 83 untracked_directory/nested_directory
84 84 $ hg purge -v
85 85 removing directory untracked_directory/nested_directory
86 86 removing directory untracked_directory
87 87 $ ls
88 88 directory
89 89 r1
90 90
91 91 delete nested directories from a subdir
92 92
93 93 $ mkdir -p untracked_directory/nested_directory
94 94 $ cd directory
95 95 $ hg purge -p
96 96 untracked_directory/nested_directory
97 97 $ hg purge -v
98 98 removing directory untracked_directory/nested_directory
99 99 removing directory untracked_directory
100 100 $ cd ..
101 101 $ ls
102 102 directory
103 103 r1
104 104
105 105 delete only part of the tree
106 106
107 107 $ mkdir -p untracked_directory/nested_directory
108 108 $ touch directory/untracked_file
109 109 $ cd directory
110 110 $ hg purge -p ../untracked_directory
111 111 untracked_directory/nested_directory
112 112 $ hg purge -v ../untracked_directory
113 113 removing directory untracked_directory/nested_directory
114 114 removing directory untracked_directory
115 115 $ cd ..
116 116 $ ls
117 117 directory
118 118 r1
119 119 $ ls directory/untracked_file
120 120 directory/untracked_file
121 121 $ rm directory/untracked_file
122 122
123 skip ignored files if --all not specified
123 skip ignored files if -i or --all not specified
124 124
125 125 $ touch ignored
126 126 $ hg purge -p
127 127 $ hg purge -v
128 $ touch untracked_file
128 129 $ ls
129 130 directory
130 131 ignored
131 132 r1
133 untracked_file
134 $ hg purge -p -i
135 ignored
136 $ hg purge -v -i
137 removing file ignored
138 $ ls
139 directory
140 r1
141 untracked_file
142 $ touch ignored
132 143 $ hg purge -p --all
133 144 ignored
145 untracked_file
134 146 $ hg purge -v --all
135 147 removing file ignored
148 removing file untracked_file
136 149 $ ls
137 150 directory
138 151 r1
139 152
140 153 abort with missing files until we support name mangling filesystems
141 154
142 155 $ touch untracked_file
143 156 $ rm r1
144 157
145 158 hide error messages to avoid changing the output when the text changes
146 159
147 160 $ hg purge -p 2> /dev/null
148 161 untracked_file
149 162 $ hg st
150 163 ! r1
151 164 ? untracked_file
152 165
153 166 $ hg purge -p
154 167 untracked_file
155 168 $ hg purge -v 2> /dev/null
156 169 removing file untracked_file
157 170 $ hg st
158 171 ! r1
159 172
160 173 $ hg purge -v
161 174 $ hg revert --all --quiet
162 175 $ hg st -a
163 176
164 177 tracked file in ignored directory (issue621)
165 178
166 179 $ echo directory >> .hgignore
167 180 $ hg ci -m 'ignore directory'
168 181 $ touch untracked_file
169 182 $ hg purge -p
170 183 untracked_file
171 184 $ hg purge -v
172 185 removing file untracked_file
173 186
174 187 skip excluded files
175 188
176 189 $ touch excluded_file
177 190 $ hg purge -p -X excluded_file
178 191 $ hg purge -v -X excluded_file
179 192 $ ls
180 193 directory
181 194 excluded_file
182 195 r1
183 196 $ rm excluded_file
184 197
185 198 skip files in excluded dirs
186 199
187 200 $ mkdir excluded_dir
188 201 $ touch excluded_dir/file
189 202 $ hg purge -p -X excluded_dir
190 203 $ hg purge -v -X excluded_dir
191 204 $ ls
192 205 directory
193 206 excluded_dir
194 207 r1
195 208 $ ls excluded_dir
196 209 file
197 210 $ rm -R excluded_dir
198 211
199 212 skip excluded empty dirs
200 213
201 214 $ mkdir excluded_dir
202 215 $ hg purge -p -X excluded_dir
203 216 $ hg purge -v -X excluded_dir
204 217 $ ls
205 218 directory
206 219 excluded_dir
207 220 r1
208 221 $ rmdir excluded_dir
209 222
210 223 skip patterns
211 224
212 225 $ mkdir .svn
213 226 $ touch .svn/foo
214 227 $ mkdir directory/.svn
215 228 $ touch directory/.svn/foo
216 229 $ hg purge -p -X .svn -X '*/.svn'
217 230 $ hg purge -p -X re:.*.svn
218 231
219 232 $ rm -R .svn directory r1
220 233
221 234 only remove files
222 235
223 236 $ mkdir -p empty_dir dir
224 237 $ touch untracked_file dir/untracked_file
225 238 $ hg purge -p --files
226 239 dir/untracked_file
227 240 untracked_file
228 241 $ hg purge -v --files
229 242 removing file dir/untracked_file
230 243 removing file untracked_file
231 244 $ ls
232 245 dir
233 246 empty_dir
234 247 $ ls dir
235 248
236 249 only remove dirs
237 250
238 251 $ mkdir -p empty_dir dir
239 252 $ touch untracked_file dir/untracked_file
240 253 $ hg purge -p --dirs
241 254 empty_dir
242 255 $ hg purge -v --dirs
243 256 removing directory empty_dir
244 257 $ ls
245 258 dir
246 259 untracked_file
247 260 $ ls dir
248 261 untracked_file
249 262
250 263 remove both files and dirs
251 264
252 265 $ mkdir -p empty_dir dir
253 266 $ touch untracked_file dir/untracked_file
254 267 $ hg purge -p --files --dirs
255 268 dir/untracked_file
256 269 untracked_file
257 270 empty_dir
258 271 $ hg purge -v --files --dirs
259 272 removing file dir/untracked_file
260 273 removing file untracked_file
261 274 removing directory empty_dir
262 275 removing directory dir
263 276 $ ls
264 277
265 278 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now