Mercurial > public > mercurial-scm > hg
comparison mercurial/merge.py @ 44856:b7808443ed6a
mergestate: split out merge state handling code from main merge module
There's already some pretty reasonable encapsulation here, but I want
to make the mergestate storage a property of the context so memctx
instances can do a reasonable thing. This is the first step in a
reshuffle to make that easier.
Differential Revision: https://phab.mercurial-scm.org/D8550
author | Augie Fackler <augie@google.com> |
---|---|
date | Mon, 18 May 2020 14:59:59 -0400 |
parents | 1b8fd4af3318 |
children | 818b4f19ef23 |
comparison
equal
deleted
inserted
replaced
44855:1d2d353e5c4a | 44856:b7808443ed6a |
---|---|
6 # GNU General Public License version 2 or any later version. | 6 # GNU General Public License version 2 or any later version. |
7 | 7 |
8 from __future__ import absolute_import | 8 from __future__ import absolute_import |
9 | 9 |
10 import errno | 10 import errno |
11 import shutil | |
12 import stat | 11 import stat |
13 import struct | 12 import struct |
14 | 13 |
15 from .i18n import _ | 14 from .i18n import _ |
16 from .node import ( | 15 from .node import ( |
17 addednodeid, | 16 addednodeid, |
18 bin, | |
19 hex, | |
20 modifiednodeid, | 17 modifiednodeid, |
21 nullhex, | |
22 nullid, | 18 nullid, |
23 nullrev, | 19 nullrev, |
24 ) | 20 ) |
25 from .pycompat import delattr | |
26 from .thirdparty import attr | 21 from .thirdparty import attr |
27 from . import ( | 22 from . import ( |
28 copies, | 23 copies, |
29 encoding, | 24 encoding, |
30 error, | 25 error, |
31 filemerge, | 26 filemerge, |
32 match as matchmod, | 27 match as matchmod, |
28 mergestate as mergestatemod, | |
33 obsutil, | 29 obsutil, |
34 pathutil, | 30 pathutil, |
35 pycompat, | 31 pycompat, |
36 scmutil, | 32 scmutil, |
37 subrepoutil, | 33 subrepoutil, |
38 util, | 34 util, |
39 worker, | 35 worker, |
40 ) | 36 ) |
41 from .utils import hashutil | |
42 | 37 |
43 _pack = struct.pack | 38 _pack = struct.pack |
44 _unpack = struct.unpack | 39 _unpack = struct.unpack |
45 | |
46 | |
47 def _droponode(data): | |
48 # used for compatibility for v1 | |
49 bits = data.split(b'\0') | |
50 bits = bits[:-2] + bits[-1:] | |
51 return b'\0'.join(bits) | |
52 | |
53 | |
54 # Merge state record types. See ``mergestate`` docs for more. | |
55 RECORD_LOCAL = b'L' | |
56 RECORD_OTHER = b'O' | |
57 RECORD_MERGED = b'F' | |
58 RECORD_CHANGEDELETE_CONFLICT = b'C' | |
59 RECORD_MERGE_DRIVER_MERGE = b'D' | |
60 RECORD_PATH_CONFLICT = b'P' | |
61 RECORD_MERGE_DRIVER_STATE = b'm' | |
62 RECORD_FILE_VALUES = b'f' | |
63 RECORD_LABELS = b'l' | |
64 RECORD_OVERRIDE = b't' | |
65 RECORD_UNSUPPORTED_MANDATORY = b'X' | |
66 RECORD_UNSUPPORTED_ADVISORY = b'x' | |
67 RECORD_RESOLVED_OTHER = b'R' | |
68 | |
69 MERGE_DRIVER_STATE_UNMARKED = b'u' | |
70 MERGE_DRIVER_STATE_MARKED = b'm' | |
71 MERGE_DRIVER_STATE_SUCCESS = b's' | |
72 | |
73 MERGE_RECORD_UNRESOLVED = b'u' | |
74 MERGE_RECORD_RESOLVED = b'r' | |
75 MERGE_RECORD_UNRESOLVED_PATH = b'pu' | |
76 MERGE_RECORD_RESOLVED_PATH = b'pr' | |
77 MERGE_RECORD_DRIVER_RESOLVED = b'd' | |
78 # represents that the file was automatically merged in favor | |
79 # of other version. This info is used on commit. | |
80 MERGE_RECORD_MERGED_OTHER = b'o' | |
81 | |
82 ACTION_FORGET = b'f' | |
83 ACTION_REMOVE = b'r' | |
84 ACTION_ADD = b'a' | |
85 ACTION_GET = b'g' | |
86 ACTION_PATH_CONFLICT = b'p' | |
87 ACTION_PATH_CONFLICT_RESOLVE = b'pr' | |
88 ACTION_ADD_MODIFIED = b'am' | |
89 ACTION_CREATED = b'c' | |
90 ACTION_DELETED_CHANGED = b'dc' | |
91 ACTION_CHANGED_DELETED = b'cd' | |
92 ACTION_MERGE = b'm' | |
93 ACTION_LOCAL_DIR_RENAME_GET = b'dg' | |
94 ACTION_DIR_RENAME_MOVE_LOCAL = b'dm' | |
95 ACTION_KEEP = b'k' | |
96 ACTION_EXEC = b'e' | |
97 ACTION_CREATED_MERGE = b'cm' | |
98 # GET the other/remote side and store this info in mergestate | |
99 ACTION_GET_OTHER_AND_STORE = b'gs' | |
100 | |
101 | |
102 class mergestate(object): | |
103 '''track 3-way merge state of individual files | |
104 | |
105 The merge state is stored on disk when needed. Two files are used: one with | |
106 an old format (version 1), and one with a new format (version 2). Version 2 | |
107 stores a superset of the data in version 1, including new kinds of records | |
108 in the future. For more about the new format, see the documentation for | |
109 `_readrecordsv2`. | |
110 | |
111 Each record can contain arbitrary content, and has an associated type. This | |
112 `type` should be a letter. If `type` is uppercase, the record is mandatory: | |
113 versions of Mercurial that don't support it should abort. If `type` is | |
114 lowercase, the record can be safely ignored. | |
115 | |
116 Currently known records: | |
117 | |
118 L: the node of the "local" part of the merge (hexified version) | |
119 O: the node of the "other" part of the merge (hexified version) | |
120 F: a file to be merged entry | |
121 C: a change/delete or delete/change conflict | |
122 D: a file that the external merge driver will merge internally | |
123 (experimental) | |
124 P: a path conflict (file vs directory) | |
125 m: the external merge driver defined for this merge plus its run state | |
126 (experimental) | |
127 f: a (filename, dictionary) tuple of optional values for a given file | |
128 X: unsupported mandatory record type (used in tests) | |
129 x: unsupported advisory record type (used in tests) | |
130 l: the labels for the parts of the merge. | |
131 | |
132 Merge driver run states (experimental): | |
133 u: driver-resolved files unmarked -- needs to be run next time we're about | |
134 to resolve or commit | |
135 m: driver-resolved files marked -- only needs to be run before commit | |
136 s: success/skipped -- does not need to be run any more | |
137 | |
138 Merge record states (stored in self._state, indexed by filename): | |
139 u: unresolved conflict | |
140 r: resolved conflict | |
141 pu: unresolved path conflict (file conflicts with directory) | |
142 pr: resolved path conflict | |
143 d: driver-resolved conflict | |
144 | |
145 The resolve command transitions between 'u' and 'r' for conflicts and | |
146 'pu' and 'pr' for path conflicts. | |
147 ''' | |
148 | |
149 statepathv1 = b'merge/state' | |
150 statepathv2 = b'merge/state2' | |
151 | |
152 @staticmethod | |
153 def clean(repo, node=None, other=None, labels=None): | |
154 """Initialize a brand new merge state, removing any existing state on | |
155 disk.""" | |
156 ms = mergestate(repo) | |
157 ms.reset(node, other, labels) | |
158 return ms | |
159 | |
160 @staticmethod | |
161 def read(repo): | |
162 """Initialize the merge state, reading it from disk.""" | |
163 ms = mergestate(repo) | |
164 ms._read() | |
165 return ms | |
166 | |
167 def __init__(self, repo): | |
168 """Initialize the merge state. | |
169 | |
170 Do not use this directly! Instead call read() or clean().""" | |
171 self._repo = repo | |
172 self._dirty = False | |
173 self._labels = None | |
174 | |
175 def reset(self, node=None, other=None, labels=None): | |
176 self._state = {} | |
177 self._stateextras = {} | |
178 self._local = None | |
179 self._other = None | |
180 self._labels = labels | |
181 for var in ('localctx', 'otherctx'): | |
182 if var in vars(self): | |
183 delattr(self, var) | |
184 if node: | |
185 self._local = node | |
186 self._other = other | |
187 self._readmergedriver = None | |
188 if self.mergedriver: | |
189 self._mdstate = MERGE_DRIVER_STATE_SUCCESS | |
190 else: | |
191 self._mdstate = MERGE_DRIVER_STATE_UNMARKED | |
192 shutil.rmtree(self._repo.vfs.join(b'merge'), True) | |
193 self._results = {} | |
194 self._dirty = False | |
195 | |
196 def _read(self): | |
197 """Analyse each record content to restore a serialized state from disk | |
198 | |
199 This function process "record" entry produced by the de-serialization | |
200 of on disk file. | |
201 """ | |
202 self._state = {} | |
203 self._stateextras = {} | |
204 self._local = None | |
205 self._other = None | |
206 for var in ('localctx', 'otherctx'): | |
207 if var in vars(self): | |
208 delattr(self, var) | |
209 self._readmergedriver = None | |
210 self._mdstate = MERGE_DRIVER_STATE_SUCCESS | |
211 unsupported = set() | |
212 records = self._readrecords() | |
213 for rtype, record in records: | |
214 if rtype == RECORD_LOCAL: | |
215 self._local = bin(record) | |
216 elif rtype == RECORD_OTHER: | |
217 self._other = bin(record) | |
218 elif rtype == RECORD_MERGE_DRIVER_STATE: | |
219 bits = record.split(b'\0', 1) | |
220 mdstate = bits[1] | |
221 if len(mdstate) != 1 or mdstate not in ( | |
222 MERGE_DRIVER_STATE_UNMARKED, | |
223 MERGE_DRIVER_STATE_MARKED, | |
224 MERGE_DRIVER_STATE_SUCCESS, | |
225 ): | |
226 # the merge driver should be idempotent, so just rerun it | |
227 mdstate = MERGE_DRIVER_STATE_UNMARKED | |
228 | |
229 self._readmergedriver = bits[0] | |
230 self._mdstate = mdstate | |
231 elif rtype in ( | |
232 RECORD_MERGED, | |
233 RECORD_CHANGEDELETE_CONFLICT, | |
234 RECORD_PATH_CONFLICT, | |
235 RECORD_MERGE_DRIVER_MERGE, | |
236 RECORD_RESOLVED_OTHER, | |
237 ): | |
238 bits = record.split(b'\0') | |
239 self._state[bits[0]] = bits[1:] | |
240 elif rtype == RECORD_FILE_VALUES: | |
241 filename, rawextras = record.split(b'\0', 1) | |
242 extraparts = rawextras.split(b'\0') | |
243 extras = {} | |
244 i = 0 | |
245 while i < len(extraparts): | |
246 extras[extraparts[i]] = extraparts[i + 1] | |
247 i += 2 | |
248 | |
249 self._stateextras[filename] = extras | |
250 elif rtype == RECORD_LABELS: | |
251 labels = record.split(b'\0', 2) | |
252 self._labels = [l for l in labels if len(l) > 0] | |
253 elif not rtype.islower(): | |
254 unsupported.add(rtype) | |
255 self._results = {} | |
256 self._dirty = False | |
257 | |
258 if unsupported: | |
259 raise error.UnsupportedMergeRecords(unsupported) | |
260 | |
261 def _readrecords(self): | |
262 """Read merge state from disk and return a list of record (TYPE, data) | |
263 | |
264 We read data from both v1 and v2 files and decide which one to use. | |
265 | |
266 V1 has been used by version prior to 2.9.1 and contains less data than | |
267 v2. We read both versions and check if no data in v2 contradicts | |
268 v1. If there is not contradiction we can safely assume that both v1 | |
269 and v2 were written at the same time and use the extract data in v2. If | |
270 there is contradiction we ignore v2 content as we assume an old version | |
271 of Mercurial has overwritten the mergestate file and left an old v2 | |
272 file around. | |
273 | |
274 returns list of record [(TYPE, data), ...]""" | |
275 v1records = self._readrecordsv1() | |
276 v2records = self._readrecordsv2() | |
277 if self._v1v2match(v1records, v2records): | |
278 return v2records | |
279 else: | |
280 # v1 file is newer than v2 file, use it | |
281 # we have to infer the "other" changeset of the merge | |
282 # we cannot do better than that with v1 of the format | |
283 mctx = self._repo[None].parents()[-1] | |
284 v1records.append((RECORD_OTHER, mctx.hex())) | |
285 # add place holder "other" file node information | |
286 # nobody is using it yet so we do no need to fetch the data | |
287 # if mctx was wrong `mctx[bits[-2]]` may fails. | |
288 for idx, r in enumerate(v1records): | |
289 if r[0] == RECORD_MERGED: | |
290 bits = r[1].split(b'\0') | |
291 bits.insert(-2, b'') | |
292 v1records[idx] = (r[0], b'\0'.join(bits)) | |
293 return v1records | |
294 | |
295 def _v1v2match(self, v1records, v2records): | |
296 oldv2 = set() # old format version of v2 record | |
297 for rec in v2records: | |
298 if rec[0] == RECORD_LOCAL: | |
299 oldv2.add(rec) | |
300 elif rec[0] == RECORD_MERGED: | |
301 # drop the onode data (not contained in v1) | |
302 oldv2.add((RECORD_MERGED, _droponode(rec[1]))) | |
303 for rec in v1records: | |
304 if rec not in oldv2: | |
305 return False | |
306 else: | |
307 return True | |
308 | |
309 def _readrecordsv1(self): | |
310 """read on disk merge state for version 1 file | |
311 | |
312 returns list of record [(TYPE, data), ...] | |
313 | |
314 Note: the "F" data from this file are one entry short | |
315 (no "other file node" entry) | |
316 """ | |
317 records = [] | |
318 try: | |
319 f = self._repo.vfs(self.statepathv1) | |
320 for i, l in enumerate(f): | |
321 if i == 0: | |
322 records.append((RECORD_LOCAL, l[:-1])) | |
323 else: | |
324 records.append((RECORD_MERGED, l[:-1])) | |
325 f.close() | |
326 except IOError as err: | |
327 if err.errno != errno.ENOENT: | |
328 raise | |
329 return records | |
330 | |
331 def _readrecordsv2(self): | |
332 """read on disk merge state for version 2 file | |
333 | |
334 This format is a list of arbitrary records of the form: | |
335 | |
336 [type][length][content] | |
337 | |
338 `type` is a single character, `length` is a 4 byte integer, and | |
339 `content` is an arbitrary byte sequence of length `length`. | |
340 | |
341 Mercurial versions prior to 3.7 have a bug where if there are | |
342 unsupported mandatory merge records, attempting to clear out the merge | |
343 state with hg update --clean or similar aborts. The 't' record type | |
344 works around that by writing out what those versions treat as an | |
345 advisory record, but later versions interpret as special: the first | |
346 character is the 'real' record type and everything onwards is the data. | |
347 | |
348 Returns list of records [(TYPE, data), ...].""" | |
349 records = [] | |
350 try: | |
351 f = self._repo.vfs(self.statepathv2) | |
352 data = f.read() | |
353 off = 0 | |
354 end = len(data) | |
355 while off < end: | |
356 rtype = data[off : off + 1] | |
357 off += 1 | |
358 length = _unpack(b'>I', data[off : (off + 4)])[0] | |
359 off += 4 | |
360 record = data[off : (off + length)] | |
361 off += length | |
362 if rtype == RECORD_OVERRIDE: | |
363 rtype, record = record[0:1], record[1:] | |
364 records.append((rtype, record)) | |
365 f.close() | |
366 except IOError as err: | |
367 if err.errno != errno.ENOENT: | |
368 raise | |
369 return records | |
370 | |
371 @util.propertycache | |
372 def mergedriver(self): | |
373 # protect against the following: | |
374 # - A configures a malicious merge driver in their hgrc, then | |
375 # pauses the merge | |
376 # - A edits their hgrc to remove references to the merge driver | |
377 # - A gives a copy of their entire repo, including .hg, to B | |
378 # - B inspects .hgrc and finds it to be clean | |
379 # - B then continues the merge and the malicious merge driver | |
380 # gets invoked | |
381 configmergedriver = self._repo.ui.config( | |
382 b'experimental', b'mergedriver' | |
383 ) | |
384 if ( | |
385 self._readmergedriver is not None | |
386 and self._readmergedriver != configmergedriver | |
387 ): | |
388 raise error.ConfigError( | |
389 _(b"merge driver changed since merge started"), | |
390 hint=_(b"revert merge driver change or abort merge"), | |
391 ) | |
392 | |
393 return configmergedriver | |
394 | |
395 @util.propertycache | |
396 def local(self): | |
397 if self._local is None: | |
398 msg = b"local accessed but self._local isn't set" | |
399 raise error.ProgrammingError(msg) | |
400 return self._local | |
401 | |
402 @util.propertycache | |
403 def localctx(self): | |
404 return self._repo[self.local] | |
405 | |
406 @util.propertycache | |
407 def other(self): | |
408 if self._other is None: | |
409 msg = b"other accessed but self._other isn't set" | |
410 raise error.ProgrammingError(msg) | |
411 return self._other | |
412 | |
413 @util.propertycache | |
414 def otherctx(self): | |
415 return self._repo[self.other] | |
416 | |
417 def active(self): | |
418 """Whether mergestate is active. | |
419 | |
420 Returns True if there appears to be mergestate. This is a rough proxy | |
421 for "is a merge in progress." | |
422 """ | |
423 return bool(self._local) or bool(self._state) | |
424 | |
425 def commit(self): | |
426 """Write current state on disk (if necessary)""" | |
427 if self._dirty: | |
428 records = self._makerecords() | |
429 self._writerecords(records) | |
430 self._dirty = False | |
431 | |
432 def _makerecords(self): | |
433 records = [] | |
434 records.append((RECORD_LOCAL, hex(self._local))) | |
435 records.append((RECORD_OTHER, hex(self._other))) | |
436 if self.mergedriver: | |
437 records.append( | |
438 ( | |
439 RECORD_MERGE_DRIVER_STATE, | |
440 b'\0'.join([self.mergedriver, self._mdstate]), | |
441 ) | |
442 ) | |
443 # Write out state items. In all cases, the value of the state map entry | |
444 # is written as the contents of the record. The record type depends on | |
445 # the type of state that is stored, and capital-letter records are used | |
446 # to prevent older versions of Mercurial that do not support the feature | |
447 # from loading them. | |
448 for filename, v in pycompat.iteritems(self._state): | |
449 if v[0] == MERGE_RECORD_DRIVER_RESOLVED: | |
450 # Driver-resolved merge. These are stored in 'D' records. | |
451 records.append( | |
452 (RECORD_MERGE_DRIVER_MERGE, b'\0'.join([filename] + v)) | |
453 ) | |
454 elif v[0] in ( | |
455 MERGE_RECORD_UNRESOLVED_PATH, | |
456 MERGE_RECORD_RESOLVED_PATH, | |
457 ): | |
458 # Path conflicts. These are stored in 'P' records. The current | |
459 # resolution state ('pu' or 'pr') is stored within the record. | |
460 records.append( | |
461 (RECORD_PATH_CONFLICT, b'\0'.join([filename] + v)) | |
462 ) | |
463 elif v[0] == MERGE_RECORD_MERGED_OTHER: | |
464 records.append( | |
465 (RECORD_RESOLVED_OTHER, b'\0'.join([filename] + v)) | |
466 ) | |
467 elif v[1] == nullhex or v[6] == nullhex: | |
468 # Change/Delete or Delete/Change conflicts. These are stored in | |
469 # 'C' records. v[1] is the local file, and is nullhex when the | |
470 # file is deleted locally ('dc'). v[6] is the remote file, and | |
471 # is nullhex when the file is deleted remotely ('cd'). | |
472 records.append( | |
473 (RECORD_CHANGEDELETE_CONFLICT, b'\0'.join([filename] + v)) | |
474 ) | |
475 else: | |
476 # Normal files. These are stored in 'F' records. | |
477 records.append((RECORD_MERGED, b'\0'.join([filename] + v))) | |
478 for filename, extras in sorted(pycompat.iteritems(self._stateextras)): | |
479 rawextras = b'\0'.join( | |
480 b'%s\0%s' % (k, v) for k, v in pycompat.iteritems(extras) | |
481 ) | |
482 records.append( | |
483 (RECORD_FILE_VALUES, b'%s\0%s' % (filename, rawextras)) | |
484 ) | |
485 if self._labels is not None: | |
486 labels = b'\0'.join(self._labels) | |
487 records.append((RECORD_LABELS, labels)) | |
488 return records | |
489 | |
490 def _writerecords(self, records): | |
491 """Write current state on disk (both v1 and v2)""" | |
492 self._writerecordsv1(records) | |
493 self._writerecordsv2(records) | |
494 | |
495 def _writerecordsv1(self, records): | |
496 """Write current state on disk in a version 1 file""" | |
497 f = self._repo.vfs(self.statepathv1, b'wb') | |
498 irecords = iter(records) | |
499 lrecords = next(irecords) | |
500 assert lrecords[0] == RECORD_LOCAL | |
501 f.write(hex(self._local) + b'\n') | |
502 for rtype, data in irecords: | |
503 if rtype == RECORD_MERGED: | |
504 f.write(b'%s\n' % _droponode(data)) | |
505 f.close() | |
506 | |
507 def _writerecordsv2(self, records): | |
508 """Write current state on disk in a version 2 file | |
509 | |
510 See the docstring for _readrecordsv2 for why we use 't'.""" | |
511 # these are the records that all version 2 clients can read | |
512 allowlist = (RECORD_LOCAL, RECORD_OTHER, RECORD_MERGED) | |
513 f = self._repo.vfs(self.statepathv2, b'wb') | |
514 for key, data in records: | |
515 assert len(key) == 1 | |
516 if key not in allowlist: | |
517 key, data = RECORD_OVERRIDE, b'%s%s' % (key, data) | |
518 format = b'>sI%is' % len(data) | |
519 f.write(_pack(format, key, len(data), data)) | |
520 f.close() | |
521 | |
522 @staticmethod | |
523 def getlocalkey(path): | |
524 """hash the path of a local file context for storage in the .hg/merge | |
525 directory.""" | |
526 | |
527 return hex(hashutil.sha1(path).digest()) | |
528 | |
529 def add(self, fcl, fco, fca, fd): | |
530 """add a new (potentially?) conflicting file the merge state | |
531 fcl: file context for local, | |
532 fco: file context for remote, | |
533 fca: file context for ancestors, | |
534 fd: file path of the resulting merge. | |
535 | |
536 note: also write the local version to the `.hg/merge` directory. | |
537 """ | |
538 if fcl.isabsent(): | |
539 localkey = nullhex | |
540 else: | |
541 localkey = mergestate.getlocalkey(fcl.path()) | |
542 self._repo.vfs.write(b'merge/' + localkey, fcl.data()) | |
543 self._state[fd] = [ | |
544 MERGE_RECORD_UNRESOLVED, | |
545 localkey, | |
546 fcl.path(), | |
547 fca.path(), | |
548 hex(fca.filenode()), | |
549 fco.path(), | |
550 hex(fco.filenode()), | |
551 fcl.flags(), | |
552 ] | |
553 self._stateextras[fd] = {b'ancestorlinknode': hex(fca.node())} | |
554 self._dirty = True | |
555 | |
556 def addpath(self, path, frename, forigin): | |
557 """add a new conflicting path to the merge state | |
558 path: the path that conflicts | |
559 frename: the filename the conflicting file was renamed to | |
560 forigin: origin of the file ('l' or 'r' for local/remote) | |
561 """ | |
562 self._state[path] = [MERGE_RECORD_UNRESOLVED_PATH, frename, forigin] | |
563 self._dirty = True | |
564 | |
565 def addmergedother(self, path): | |
566 self._state[path] = [MERGE_RECORD_MERGED_OTHER, nullhex, nullhex] | |
567 self._dirty = True | |
568 | |
569 def __contains__(self, dfile): | |
570 return dfile in self._state | |
571 | |
572 def __getitem__(self, dfile): | |
573 return self._state[dfile][0] | |
574 | |
575 def __iter__(self): | |
576 return iter(sorted(self._state)) | |
577 | |
578 def files(self): | |
579 return self._state.keys() | |
580 | |
581 def mark(self, dfile, state): | |
582 self._state[dfile][0] = state | |
583 self._dirty = True | |
584 | |
585 def mdstate(self): | |
586 return self._mdstate | |
587 | |
588 def unresolved(self): | |
589 """Obtain the paths of unresolved files.""" | |
590 | |
591 for f, entry in pycompat.iteritems(self._state): | |
592 if entry[0] in ( | |
593 MERGE_RECORD_UNRESOLVED, | |
594 MERGE_RECORD_UNRESOLVED_PATH, | |
595 ): | |
596 yield f | |
597 | |
598 def driverresolved(self): | |
599 """Obtain the paths of driver-resolved files.""" | |
600 | |
601 for f, entry in self._state.items(): | |
602 if entry[0] == MERGE_RECORD_DRIVER_RESOLVED: | |
603 yield f | |
604 | |
605 def extras(self, filename): | |
606 return self._stateextras.setdefault(filename, {}) | |
607 | |
608 def _resolve(self, preresolve, dfile, wctx): | |
609 """rerun merge process for file path `dfile`""" | |
610 if self[dfile] in (MERGE_RECORD_RESOLVED, MERGE_RECORD_DRIVER_RESOLVED): | |
611 return True, 0 | |
612 if self._state[dfile][0] == MERGE_RECORD_MERGED_OTHER: | |
613 return True, 0 | |
614 stateentry = self._state[dfile] | |
615 state, localkey, lfile, afile, anode, ofile, onode, flags = stateentry | |
616 octx = self._repo[self._other] | |
617 extras = self.extras(dfile) | |
618 anccommitnode = extras.get(b'ancestorlinknode') | |
619 if anccommitnode: | |
620 actx = self._repo[anccommitnode] | |
621 else: | |
622 actx = None | |
623 fcd = self._filectxorabsent(localkey, wctx, dfile) | |
624 fco = self._filectxorabsent(onode, octx, ofile) | |
625 # TODO: move this to filectxorabsent | |
626 fca = self._repo.filectx(afile, fileid=anode, changectx=actx) | |
627 # "premerge" x flags | |
628 flo = fco.flags() | |
629 fla = fca.flags() | |
630 if b'x' in flags + flo + fla and b'l' not in flags + flo + fla: | |
631 if fca.node() == nullid and flags != flo: | |
632 if preresolve: | |
633 self._repo.ui.warn( | |
634 _( | |
635 b'warning: cannot merge flags for %s ' | |
636 b'without common ancestor - keeping local flags\n' | |
637 ) | |
638 % afile | |
639 ) | |
640 elif flags == fla: | |
641 flags = flo | |
642 if preresolve: | |
643 # restore local | |
644 if localkey != nullhex: | |
645 f = self._repo.vfs(b'merge/' + localkey) | |
646 wctx[dfile].write(f.read(), flags) | |
647 f.close() | |
648 else: | |
649 wctx[dfile].remove(ignoremissing=True) | |
650 complete, r, deleted = filemerge.premerge( | |
651 self._repo, | |
652 wctx, | |
653 self._local, | |
654 lfile, | |
655 fcd, | |
656 fco, | |
657 fca, | |
658 labels=self._labels, | |
659 ) | |
660 else: | |
661 complete, r, deleted = filemerge.filemerge( | |
662 self._repo, | |
663 wctx, | |
664 self._local, | |
665 lfile, | |
666 fcd, | |
667 fco, | |
668 fca, | |
669 labels=self._labels, | |
670 ) | |
671 if r is None: | |
672 # no real conflict | |
673 del self._state[dfile] | |
674 self._stateextras.pop(dfile, None) | |
675 self._dirty = True | |
676 elif not r: | |
677 self.mark(dfile, MERGE_RECORD_RESOLVED) | |
678 | |
679 if complete: | |
680 action = None | |
681 if deleted: | |
682 if fcd.isabsent(): | |
683 # dc: local picked. Need to drop if present, which may | |
684 # happen on re-resolves. | |
685 action = ACTION_FORGET | |
686 else: | |
687 # cd: remote picked (or otherwise deleted) | |
688 action = ACTION_REMOVE | |
689 else: | |
690 if fcd.isabsent(): # dc: remote picked | |
691 action = ACTION_GET | |
692 elif fco.isabsent(): # cd: local picked | |
693 if dfile in self.localctx: | |
694 action = ACTION_ADD_MODIFIED | |
695 else: | |
696 action = ACTION_ADD | |
697 # else: regular merges (no action necessary) | |
698 self._results[dfile] = r, action | |
699 | |
700 return complete, r | |
701 | |
702 def _filectxorabsent(self, hexnode, ctx, f): | |
703 if hexnode == nullhex: | |
704 return filemerge.absentfilectx(ctx, f) | |
705 else: | |
706 return ctx[f] | |
707 | |
708 def preresolve(self, dfile, wctx): | |
709 """run premerge process for dfile | |
710 | |
711 Returns whether the merge is complete, and the exit code.""" | |
712 return self._resolve(True, dfile, wctx) | |
713 | |
714 def resolve(self, dfile, wctx): | |
715 """run merge process (assuming premerge was run) for dfile | |
716 | |
717 Returns the exit code of the merge.""" | |
718 return self._resolve(False, dfile, wctx)[1] | |
719 | |
720 def counts(self): | |
721 """return counts for updated, merged and removed files in this | |
722 session""" | |
723 updated, merged, removed = 0, 0, 0 | |
724 for r, action in pycompat.itervalues(self._results): | |
725 if r is None: | |
726 updated += 1 | |
727 elif r == 0: | |
728 if action == ACTION_REMOVE: | |
729 removed += 1 | |
730 else: | |
731 merged += 1 | |
732 return updated, merged, removed | |
733 | |
734 def unresolvedcount(self): | |
735 """get unresolved count for this merge (persistent)""" | |
736 return len(list(self.unresolved())) | |
737 | |
738 def actions(self): | |
739 """return lists of actions to perform on the dirstate""" | |
740 actions = { | |
741 ACTION_REMOVE: [], | |
742 ACTION_FORGET: [], | |
743 ACTION_ADD: [], | |
744 ACTION_ADD_MODIFIED: [], | |
745 ACTION_GET: [], | |
746 } | |
747 for f, (r, action) in pycompat.iteritems(self._results): | |
748 if action is not None: | |
749 actions[action].append((f, None, b"merge result")) | |
750 return actions | |
751 | |
752 def recordactions(self): | |
753 """record remove/add/get actions in the dirstate""" | |
754 branchmerge = self._repo.dirstate.p2() != nullid | |
755 recordupdates(self._repo, self.actions(), branchmerge, None) | |
756 | |
757 def queueremove(self, f): | |
758 """queues a file to be removed from the dirstate | |
759 | |
760 Meant for use by custom merge drivers.""" | |
761 self._results[f] = 0, ACTION_REMOVE | |
762 | |
763 def queueadd(self, f): | |
764 """queues a file to be added to the dirstate | |
765 | |
766 Meant for use by custom merge drivers.""" | |
767 self._results[f] = 0, ACTION_ADD | |
768 | |
769 def queueget(self, f): | |
770 """queues a file to be marked modified in the dirstate | |
771 | |
772 Meant for use by custom merge drivers.""" | |
773 self._results[f] = 0, ACTION_GET | |
774 | 40 |
775 | 41 |
776 def _getcheckunknownconfig(repo, section, name): | 42 def _getcheckunknownconfig(repo, section, name): |
777 config = repo.ui.config(section, name) | 43 config = repo.ui.config(section, name) |
778 valid = [b'abort', b'ignore', b'warn'] | 44 valid = [b'abort', b'ignore', b'warn'] |
883 elif config == b'warn': | 149 elif config == b'warn': |
884 warnconflicts.update(conflicts) | 150 warnconflicts.update(conflicts) |
885 | 151 |
886 checkunknowndirs = _unknowndirschecker() | 152 checkunknowndirs = _unknowndirschecker() |
887 for f, (m, args, msg) in pycompat.iteritems(actions): | 153 for f, (m, args, msg) in pycompat.iteritems(actions): |
888 if m in (ACTION_CREATED, ACTION_DELETED_CHANGED): | 154 if m in ( |
155 mergestatemod.ACTION_CREATED, | |
156 mergestatemod.ACTION_DELETED_CHANGED, | |
157 ): | |
889 if _checkunknownfile(repo, wctx, mctx, f): | 158 if _checkunknownfile(repo, wctx, mctx, f): |
890 fileconflicts.add(f) | 159 fileconflicts.add(f) |
891 elif pathconfig and f not in wctx: | 160 elif pathconfig and f not in wctx: |
892 path = checkunknowndirs(repo, wctx, f) | 161 path = checkunknowndirs(repo, wctx, f) |
893 if path is not None: | 162 if path is not None: |
894 pathconflicts.add(path) | 163 pathconflicts.add(path) |
895 elif m == ACTION_LOCAL_DIR_RENAME_GET: | 164 elif m == mergestatemod.ACTION_LOCAL_DIR_RENAME_GET: |
896 if _checkunknownfile(repo, wctx, mctx, f, args[0]): | 165 if _checkunknownfile(repo, wctx, mctx, f, args[0]): |
897 fileconflicts.add(f) | 166 fileconflicts.add(f) |
898 | 167 |
899 allconflicts = fileconflicts | pathconflicts | 168 allconflicts = fileconflicts | pathconflicts |
900 ignoredconflicts = {c for c in allconflicts if repo.dirstate._ignore(c)} | 169 ignoredconflicts = {c for c in allconflicts if repo.dirstate._ignore(c)} |
901 unknownconflicts = allconflicts - ignoredconflicts | 170 unknownconflicts = allconflicts - ignoredconflicts |
902 collectconflicts(ignoredconflicts, ignoredconfig) | 171 collectconflicts(ignoredconflicts, ignoredconfig) |
903 collectconflicts(unknownconflicts, unknownconfig) | 172 collectconflicts(unknownconflicts, unknownconfig) |
904 else: | 173 else: |
905 for f, (m, args, msg) in pycompat.iteritems(actions): | 174 for f, (m, args, msg) in pycompat.iteritems(actions): |
906 if m == ACTION_CREATED_MERGE: | 175 if m == mergestatemod.ACTION_CREATED_MERGE: |
907 fl2, anc = args | 176 fl2, anc = args |
908 different = _checkunknownfile(repo, wctx, mctx, f) | 177 different = _checkunknownfile(repo, wctx, mctx, f) |
909 if repo.dirstate._ignore(f): | 178 if repo.dirstate._ignore(f): |
910 config = ignoredconfig | 179 config = ignoredconfig |
911 else: | 180 else: |
922 # (1) this is probably the wrong behavior here -- we should | 191 # (1) this is probably the wrong behavior here -- we should |
923 # probably abort, but some actions like rebases currently | 192 # probably abort, but some actions like rebases currently |
924 # don't like an abort happening in the middle of | 193 # don't like an abort happening in the middle of |
925 # merge.update. | 194 # merge.update. |
926 if not different: | 195 if not different: |
927 actions[f] = (ACTION_GET, (fl2, False), b'remote created') | 196 actions[f] = ( |
197 mergestatemod.ACTION_GET, | |
198 (fl2, False), | |
199 b'remote created', | |
200 ) | |
928 elif mergeforce or config == b'abort': | 201 elif mergeforce or config == b'abort': |
929 actions[f] = ( | 202 actions[f] = ( |
930 ACTION_MERGE, | 203 mergestatemod.ACTION_MERGE, |
931 (f, f, None, False, anc), | 204 (f, f, None, False, anc), |
932 b'remote differs from untracked local', | 205 b'remote differs from untracked local', |
933 ) | 206 ) |
934 elif config == b'abort': | 207 elif config == b'abort': |
935 abortconflicts.add(f) | 208 abortconflicts.add(f) |
936 else: | 209 else: |
937 if config == b'warn': | 210 if config == b'warn': |
938 warnconflicts.add(f) | 211 warnconflicts.add(f) |
939 actions[f] = (ACTION_GET, (fl2, True), b'remote created') | 212 actions[f] = ( |
213 mergestatemod.ACTION_GET, | |
214 (fl2, True), | |
215 b'remote created', | |
216 ) | |
940 | 217 |
941 for f in sorted(abortconflicts): | 218 for f in sorted(abortconflicts): |
942 warn = repo.ui.warn | 219 warn = repo.ui.warn |
943 if f in pathconflicts: | 220 if f in pathconflicts: |
944 if repo.wvfs.isfileorlink(f): | 221 if repo.wvfs.isfileorlink(f): |
960 repo.ui.warn(_(b"%s: replacing untracked file\n") % f) | 237 repo.ui.warn(_(b"%s: replacing untracked file\n") % f) |
961 else: | 238 else: |
962 repo.ui.warn(_(b"%s: replacing untracked files in directory\n") % f) | 239 repo.ui.warn(_(b"%s: replacing untracked files in directory\n") % f) |
963 | 240 |
964 for f, (m, args, msg) in pycompat.iteritems(actions): | 241 for f, (m, args, msg) in pycompat.iteritems(actions): |
965 if m == ACTION_CREATED: | 242 if m == mergestatemod.ACTION_CREATED: |
966 backup = ( | 243 backup = ( |
967 f in fileconflicts | 244 f in fileconflicts |
968 or f in pathconflicts | 245 or f in pathconflicts |
969 or any(p in pathconflicts for p in pathutil.finddirs(f)) | 246 or any(p in pathconflicts for p in pathutil.finddirs(f)) |
970 ) | 247 ) |
971 (flags,) = args | 248 (flags,) = args |
972 actions[f] = (ACTION_GET, (flags, backup), msg) | 249 actions[f] = (mergestatemod.ACTION_GET, (flags, backup), msg) |
973 | 250 |
974 | 251 |
975 def _forgetremoved(wctx, mctx, branchmerge): | 252 def _forgetremoved(wctx, mctx, branchmerge): |
976 """ | 253 """ |
977 Forget removed files | 254 Forget removed files |
986 that is not present in the working directory, we need to mark it | 263 that is not present in the working directory, we need to mark it |
987 as removed. | 264 as removed. |
988 """ | 265 """ |
989 | 266 |
990 actions = {} | 267 actions = {} |
991 m = ACTION_FORGET | 268 m = mergestatemod.ACTION_FORGET |
992 if branchmerge: | 269 if branchmerge: |
993 m = ACTION_REMOVE | 270 m = mergestatemod.ACTION_REMOVE |
994 for f in wctx.deleted(): | 271 for f in wctx.deleted(): |
995 if f not in mctx: | 272 if f not in mctx: |
996 actions[f] = m, None, b"forget deleted" | 273 actions[f] = m, None, b"forget deleted" |
997 | 274 |
998 if not branchmerge: | 275 if not branchmerge: |
999 for f in wctx.removed(): | 276 for f in wctx.removed(): |
1000 if f not in mctx: | 277 if f not in mctx: |
1001 actions[f] = ACTION_FORGET, None, b"forget removed" | 278 actions[f] = ( |
279 mergestatemod.ACTION_FORGET, | |
280 None, | |
281 b"forget removed", | |
282 ) | |
1002 | 283 |
1003 return actions | 284 return actions |
1004 | 285 |
1005 | 286 |
1006 def _checkcollision(repo, wmf, actions): | 287 def _checkcollision(repo, wmf, actions): |
1024 pmmf = set(wmf) | 305 pmmf = set(wmf) |
1025 | 306 |
1026 if actions: | 307 if actions: |
1027 # KEEP and EXEC are no-op | 308 # KEEP and EXEC are no-op |
1028 for m in ( | 309 for m in ( |
1029 ACTION_ADD, | 310 mergestatemod.ACTION_ADD, |
1030 ACTION_ADD_MODIFIED, | 311 mergestatemod.ACTION_ADD_MODIFIED, |
1031 ACTION_FORGET, | 312 mergestatemod.ACTION_FORGET, |
1032 ACTION_GET, | 313 mergestatemod.ACTION_GET, |
1033 ACTION_CHANGED_DELETED, | 314 mergestatemod.ACTION_CHANGED_DELETED, |
1034 ACTION_DELETED_CHANGED, | 315 mergestatemod.ACTION_DELETED_CHANGED, |
1035 ): | 316 ): |
1036 for f, args, msg in actions[m]: | 317 for f, args, msg in actions[m]: |
1037 pmmf.add(f) | 318 pmmf.add(f) |
1038 for f, args, msg in actions[ACTION_REMOVE]: | 319 for f, args, msg in actions[mergestatemod.ACTION_REMOVE]: |
1039 pmmf.discard(f) | 320 pmmf.discard(f) |
1040 for f, args, msg in actions[ACTION_DIR_RENAME_MOVE_LOCAL]: | 321 for f, args, msg in actions[mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL]: |
1041 f2, flags = args | 322 f2, flags = args |
1042 pmmf.discard(f2) | 323 pmmf.discard(f2) |
1043 pmmf.add(f) | 324 pmmf.add(f) |
1044 for f, args, msg in actions[ACTION_LOCAL_DIR_RENAME_GET]: | 325 for f, args, msg in actions[mergestatemod.ACTION_LOCAL_DIR_RENAME_GET]: |
1045 pmmf.add(f) | 326 pmmf.add(f) |
1046 for f, args, msg in actions[ACTION_MERGE]: | 327 for f, args, msg in actions[mergestatemod.ACTION_MERGE]: |
1047 f1, f2, fa, move, anc = args | 328 f1, f2, fa, move, anc = args |
1048 if move: | 329 if move: |
1049 pmmf.discard(f1) | 330 pmmf.discard(f1) |
1050 pmmf.add(f) | 331 pmmf.add(f) |
1051 | 332 |
1126 # The set of files deleted by all the actions. | 407 # The set of files deleted by all the actions. |
1127 deletedfiles = set() | 408 deletedfiles = set() |
1128 | 409 |
1129 for f, (m, args, msg) in actions.items(): | 410 for f, (m, args, msg) in actions.items(): |
1130 if m in ( | 411 if m in ( |
1131 ACTION_CREATED, | 412 mergestatemod.ACTION_CREATED, |
1132 ACTION_DELETED_CHANGED, | 413 mergestatemod.ACTION_DELETED_CHANGED, |
1133 ACTION_MERGE, | 414 mergestatemod.ACTION_MERGE, |
1134 ACTION_CREATED_MERGE, | 415 mergestatemod.ACTION_CREATED_MERGE, |
1135 ): | 416 ): |
1136 # This action may create a new local file. | 417 # This action may create a new local file. |
1137 createdfiledirs.update(pathutil.finddirs(f)) | 418 createdfiledirs.update(pathutil.finddirs(f)) |
1138 if mf.hasdir(f): | 419 if mf.hasdir(f): |
1139 # The file aliases a local directory. This might be ok if all | 420 # The file aliases a local directory. This might be ok if all |
1140 # the files in the local directory are being deleted. This | 421 # the files in the local directory are being deleted. This |
1141 # will be checked once we know what all the deleted files are. | 422 # will be checked once we know what all the deleted files are. |
1142 remoteconflicts.add(f) | 423 remoteconflicts.add(f) |
1143 # Track the names of all deleted files. | 424 # Track the names of all deleted files. |
1144 if m == ACTION_REMOVE: | 425 if m == mergestatemod.ACTION_REMOVE: |
1145 deletedfiles.add(f) | 426 deletedfiles.add(f) |
1146 if m == ACTION_MERGE: | 427 if m == mergestatemod.ACTION_MERGE: |
1147 f1, f2, fa, move, anc = args | 428 f1, f2, fa, move, anc = args |
1148 if move: | 429 if move: |
1149 deletedfiles.add(f1) | 430 deletedfiles.add(f1) |
1150 if m == ACTION_DIR_RENAME_MOVE_LOCAL: | 431 if m == mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL: |
1151 f2, flags = args | 432 f2, flags = args |
1152 deletedfiles.add(f2) | 433 deletedfiles.add(f2) |
1153 | 434 |
1154 # Check all directories that contain created files for path conflicts. | 435 # Check all directories that contain created files for path conflicts. |
1155 for p in createdfiledirs: | 436 for p in createdfiledirs: |
1162 else: | 443 else: |
1163 # A file is in a directory which aliases a local file. | 444 # A file is in a directory which aliases a local file. |
1164 # We will need to rename the local file. | 445 # We will need to rename the local file. |
1165 localconflicts.add(p) | 446 localconflicts.add(p) |
1166 if p in actions and actions[p][0] in ( | 447 if p in actions and actions[p][0] in ( |
1167 ACTION_CREATED, | 448 mergestatemod.ACTION_CREATED, |
1168 ACTION_DELETED_CHANGED, | 449 mergestatemod.ACTION_DELETED_CHANGED, |
1169 ACTION_MERGE, | 450 mergestatemod.ACTION_MERGE, |
1170 ACTION_CREATED_MERGE, | 451 mergestatemod.ACTION_CREATED_MERGE, |
1171 ): | 452 ): |
1172 # The file is in a directory which aliases a remote file. | 453 # The file is in a directory which aliases a remote file. |
1173 # This is an internal inconsistency within the remote | 454 # This is an internal inconsistency within the remote |
1174 # manifest. | 455 # manifest. |
1175 invalidconflicts.add(p) | 456 invalidconflicts.add(p) |
1178 for p in localconflicts: | 459 for p in localconflicts: |
1179 if p not in deletedfiles: | 460 if p not in deletedfiles: |
1180 ctxname = bytes(wctx).rstrip(b'+') | 461 ctxname = bytes(wctx).rstrip(b'+') |
1181 pnew = util.safename(p, ctxname, wctx, set(actions.keys())) | 462 pnew = util.safename(p, ctxname, wctx, set(actions.keys())) |
1182 actions[pnew] = ( | 463 actions[pnew] = ( |
1183 ACTION_PATH_CONFLICT_RESOLVE, | 464 mergestatemod.ACTION_PATH_CONFLICT_RESOLVE, |
1184 (p,), | 465 (p,), |
1185 b'local path conflict', | 466 b'local path conflict', |
1186 ) | 467 ) |
1187 actions[p] = (ACTION_PATH_CONFLICT, (pnew, b'l'), b'path conflict') | 468 actions[p] = ( |
469 mergestatemod.ACTION_PATH_CONFLICT, | |
470 (pnew, b'l'), | |
471 b'path conflict', | |
472 ) | |
1188 | 473 |
1189 if remoteconflicts: | 474 if remoteconflicts: |
1190 # Check if all files in the conflicting directories have been removed. | 475 # Check if all files in the conflicting directories have been removed. |
1191 ctxname = bytes(mctx).rstrip(b'+') | 476 ctxname = bytes(mctx).rstrip(b'+') |
1192 for f, p in _filesindirs(repo, mf, remoteconflicts): | 477 for f, p in _filesindirs(repo, mf, remoteconflicts): |
1193 if f not in deletedfiles: | 478 if f not in deletedfiles: |
1194 m, args, msg = actions[p] | 479 m, args, msg = actions[p] |
1195 pnew = util.safename(p, ctxname, wctx, set(actions.keys())) | 480 pnew = util.safename(p, ctxname, wctx, set(actions.keys())) |
1196 if m in (ACTION_DELETED_CHANGED, ACTION_MERGE): | 481 if m in ( |
482 mergestatemod.ACTION_DELETED_CHANGED, | |
483 mergestatemod.ACTION_MERGE, | |
484 ): | |
1197 # Action was merge, just update target. | 485 # Action was merge, just update target. |
1198 actions[pnew] = (m, args, msg) | 486 actions[pnew] = (m, args, msg) |
1199 else: | 487 else: |
1200 # Action was create, change to renamed get action. | 488 # Action was create, change to renamed get action. |
1201 fl = args[0] | 489 fl = args[0] |
1202 actions[pnew] = ( | 490 actions[pnew] = ( |
1203 ACTION_LOCAL_DIR_RENAME_GET, | 491 mergestatemod.ACTION_LOCAL_DIR_RENAME_GET, |
1204 (p, fl), | 492 (p, fl), |
1205 b'remote path conflict', | 493 b'remote path conflict', |
1206 ) | 494 ) |
1207 actions[p] = ( | 495 actions[p] = ( |
1208 ACTION_PATH_CONFLICT, | 496 mergestatemod.ACTION_PATH_CONFLICT, |
1209 (pnew, ACTION_REMOVE), | 497 (pnew, mergestatemod.ACTION_REMOVE), |
1210 b'path conflict', | 498 b'path conflict', |
1211 ) | 499 ) |
1212 remoteconflicts.remove(p) | 500 remoteconflicts.remove(p) |
1213 break | 501 break |
1214 | 502 |
1338 fa = branch_copies1.copy.get( | 626 fa = branch_copies1.copy.get( |
1339 f, None | 627 f, None |
1340 ) or branch_copies2.copy.get(f, None) | 628 ) or branch_copies2.copy.get(f, None) |
1341 if fa is not None: | 629 if fa is not None: |
1342 actions[f] = ( | 630 actions[f] = ( |
1343 ACTION_MERGE, | 631 mergestatemod.ACTION_MERGE, |
1344 (f, f, fa, False, pa.node()), | 632 (f, f, fa, False, pa.node()), |
1345 b'both renamed from %s' % fa, | 633 b'both renamed from %s' % fa, |
1346 ) | 634 ) |
1347 else: | 635 else: |
1348 actions[f] = ( | 636 actions[f] = ( |
1349 ACTION_MERGE, | 637 mergestatemod.ACTION_MERGE, |
1350 (f, f, None, False, pa.node()), | 638 (f, f, None, False, pa.node()), |
1351 b'both created', | 639 b'both created', |
1352 ) | 640 ) |
1353 else: | 641 else: |
1354 a = ma[f] | 642 a = ma[f] |
1355 fla = ma.flags(f) | 643 fla = ma.flags(f) |
1356 nol = b'l' not in fl1 + fl2 + fla | 644 nol = b'l' not in fl1 + fl2 + fla |
1357 if n2 == a and fl2 == fla: | 645 if n2 == a and fl2 == fla: |
1358 actions[f] = (ACTION_KEEP, (), b'remote unchanged') | 646 actions[f] = ( |
647 mergestatemod.ACTION_KEEP, | |
648 (), | |
649 b'remote unchanged', | |
650 ) | |
1359 elif n1 == a and fl1 == fla: # local unchanged - use remote | 651 elif n1 == a and fl1 == fla: # local unchanged - use remote |
1360 if n1 == n2: # optimization: keep local content | 652 if n1 == n2: # optimization: keep local content |
1361 actions[f] = ( | 653 actions[f] = ( |
1362 ACTION_EXEC, | 654 mergestatemod.ACTION_EXEC, |
1363 (fl2,), | 655 (fl2,), |
1364 b'update permissions', | 656 b'update permissions', |
1365 ) | 657 ) |
1366 else: | 658 else: |
1367 actions[f] = ( | 659 actions[f] = ( |
1368 ACTION_GET_OTHER_AND_STORE | 660 mergestatemod.ACTION_GET_OTHER_AND_STORE |
1369 if branchmerge | 661 if branchmerge |
1370 else ACTION_GET, | 662 else mergestatemod.ACTION_GET, |
1371 (fl2, False), | 663 (fl2, False), |
1372 b'remote is newer', | 664 b'remote is newer', |
1373 ) | 665 ) |
1374 elif nol and n2 == a: # remote only changed 'x' | 666 elif nol and n2 == a: # remote only changed 'x' |
1375 actions[f] = (ACTION_EXEC, (fl2,), b'update permissions') | 667 actions[f] = ( |
668 mergestatemod.ACTION_EXEC, | |
669 (fl2,), | |
670 b'update permissions', | |
671 ) | |
1376 elif nol and n1 == a: # local only changed 'x' | 672 elif nol and n1 == a: # local only changed 'x' |
1377 actions[f] = ( | 673 actions[f] = ( |
1378 ACTION_GET_OTHER_AND_STORE | 674 mergestatemod.ACTION_GET_OTHER_AND_STORE |
1379 if branchmerge | 675 if branchmerge |
1380 else ACTION_GET, | 676 else mergestatemod.ACTION_GET, |
1381 (fl1, False), | 677 (fl1, False), |
1382 b'remote is newer', | 678 b'remote is newer', |
1383 ) | 679 ) |
1384 else: # both changed something | 680 else: # both changed something |
1385 actions[f] = ( | 681 actions[f] = ( |
1386 ACTION_MERGE, | 682 mergestatemod.ACTION_MERGE, |
1387 (f, f, f, False, pa.node()), | 683 (f, f, f, False, pa.node()), |
1388 b'versions differ', | 684 b'versions differ', |
1389 ) | 685 ) |
1390 elif n1: # file exists only on local side | 686 elif n1: # file exists only on local side |
1391 if f in copied2: | 687 if f in copied2: |
1394 f in branch_copies1.movewithdir | 690 f in branch_copies1.movewithdir |
1395 ): # directory rename, move local | 691 ): # directory rename, move local |
1396 f2 = branch_copies1.movewithdir[f] | 692 f2 = branch_copies1.movewithdir[f] |
1397 if f2 in m2: | 693 if f2 in m2: |
1398 actions[f2] = ( | 694 actions[f2] = ( |
1399 ACTION_MERGE, | 695 mergestatemod.ACTION_MERGE, |
1400 (f, f2, None, True, pa.node()), | 696 (f, f2, None, True, pa.node()), |
1401 b'remote directory rename, both created', | 697 b'remote directory rename, both created', |
1402 ) | 698 ) |
1403 else: | 699 else: |
1404 actions[f2] = ( | 700 actions[f2] = ( |
1405 ACTION_DIR_RENAME_MOVE_LOCAL, | 701 mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL, |
1406 (f, fl1), | 702 (f, fl1), |
1407 b'remote directory rename - move from %s' % f, | 703 b'remote directory rename - move from %s' % f, |
1408 ) | 704 ) |
1409 elif f in branch_copies1.copy: | 705 elif f in branch_copies1.copy: |
1410 f2 = branch_copies1.copy[f] | 706 f2 = branch_copies1.copy[f] |
1411 actions[f] = ( | 707 actions[f] = ( |
1412 ACTION_MERGE, | 708 mergestatemod.ACTION_MERGE, |
1413 (f, f2, f2, False, pa.node()), | 709 (f, f2, f2, False, pa.node()), |
1414 b'local copied/moved from %s' % f2, | 710 b'local copied/moved from %s' % f2, |
1415 ) | 711 ) |
1416 elif f in ma: # clean, a different, no remote | 712 elif f in ma: # clean, a different, no remote |
1417 if n1 != ma[f]: | 713 if n1 != ma[f]: |
1418 if acceptremote: | 714 if acceptremote: |
1419 actions[f] = (ACTION_REMOVE, None, b'remote delete') | 715 actions[f] = ( |
716 mergestatemod.ACTION_REMOVE, | |
717 None, | |
718 b'remote delete', | |
719 ) | |
1420 else: | 720 else: |
1421 actions[f] = ( | 721 actions[f] = ( |
1422 ACTION_CHANGED_DELETED, | 722 mergestatemod.ACTION_CHANGED_DELETED, |
1423 (f, None, f, False, pa.node()), | 723 (f, None, f, False, pa.node()), |
1424 b'prompt changed/deleted', | 724 b'prompt changed/deleted', |
1425 ) | 725 ) |
1426 elif n1 == addednodeid: | 726 elif n1 == addednodeid: |
1427 # This extra 'a' is added by working copy manifest to mark | 727 # This extra 'a' is added by working copy manifest to mark |
1428 # the file as locally added. We should forget it instead of | 728 # the file as locally added. We should forget it instead of |
1429 # deleting it. | 729 # deleting it. |
1430 actions[f] = (ACTION_FORGET, None, b'remote deleted') | 730 actions[f] = ( |
731 mergestatemod.ACTION_FORGET, | |
732 None, | |
733 b'remote deleted', | |
734 ) | |
1431 else: | 735 else: |
1432 actions[f] = (ACTION_REMOVE, None, b'other deleted') | 736 actions[f] = ( |
737 mergestatemod.ACTION_REMOVE, | |
738 None, | |
739 b'other deleted', | |
740 ) | |
1433 elif n2: # file exists only on remote side | 741 elif n2: # file exists only on remote side |
1434 if f in copied1: | 742 if f in copied1: |
1435 pass # we'll deal with it on m1 side | 743 pass # we'll deal with it on m1 side |
1436 elif f in branch_copies2.movewithdir: | 744 elif f in branch_copies2.movewithdir: |
1437 f2 = branch_copies2.movewithdir[f] | 745 f2 = branch_copies2.movewithdir[f] |
1438 if f2 in m1: | 746 if f2 in m1: |
1439 actions[f2] = ( | 747 actions[f2] = ( |
1440 ACTION_MERGE, | 748 mergestatemod.ACTION_MERGE, |
1441 (f2, f, None, False, pa.node()), | 749 (f2, f, None, False, pa.node()), |
1442 b'local directory rename, both created', | 750 b'local directory rename, both created', |
1443 ) | 751 ) |
1444 else: | 752 else: |
1445 actions[f2] = ( | 753 actions[f2] = ( |
1446 ACTION_LOCAL_DIR_RENAME_GET, | 754 mergestatemod.ACTION_LOCAL_DIR_RENAME_GET, |
1447 (f, fl2), | 755 (f, fl2), |
1448 b'local directory rename - get from %s' % f, | 756 b'local directory rename - get from %s' % f, |
1449 ) | 757 ) |
1450 elif f in branch_copies2.copy: | 758 elif f in branch_copies2.copy: |
1451 f2 = branch_copies2.copy[f] | 759 f2 = branch_copies2.copy[f] |
1452 if f2 in m2: | 760 if f2 in m2: |
1453 actions[f] = ( | 761 actions[f] = ( |
1454 ACTION_MERGE, | 762 mergestatemod.ACTION_MERGE, |
1455 (f2, f, f2, False, pa.node()), | 763 (f2, f, f2, False, pa.node()), |
1456 b'remote copied from %s' % f2, | 764 b'remote copied from %s' % f2, |
1457 ) | 765 ) |
1458 else: | 766 else: |
1459 actions[f] = ( | 767 actions[f] = ( |
1460 ACTION_MERGE, | 768 mergestatemod.ACTION_MERGE, |
1461 (f2, f, f2, True, pa.node()), | 769 (f2, f, f2, True, pa.node()), |
1462 b'remote moved from %s' % f2, | 770 b'remote moved from %s' % f2, |
1463 ) | 771 ) |
1464 elif f not in ma: | 772 elif f not in ma: |
1465 # local unknown, remote created: the logic is described by the | 773 # local unknown, remote created: the logic is described by the |
1472 # y y y | merge | 780 # y y y | merge |
1473 # | 781 # |
1474 # Checking whether the files are different is expensive, so we | 782 # Checking whether the files are different is expensive, so we |
1475 # don't do that when we can avoid it. | 783 # don't do that when we can avoid it. |
1476 if not force: | 784 if not force: |
1477 actions[f] = (ACTION_CREATED, (fl2,), b'remote created') | 785 actions[f] = ( |
786 mergestatemod.ACTION_CREATED, | |
787 (fl2,), | |
788 b'remote created', | |
789 ) | |
1478 elif not branchmerge: | 790 elif not branchmerge: |
1479 actions[f] = (ACTION_CREATED, (fl2,), b'remote created') | 791 actions[f] = ( |
792 mergestatemod.ACTION_CREATED, | |
793 (fl2,), | |
794 b'remote created', | |
795 ) | |
1480 else: | 796 else: |
1481 actions[f] = ( | 797 actions[f] = ( |
1482 ACTION_CREATED_MERGE, | 798 mergestatemod.ACTION_CREATED_MERGE, |
1483 (fl2, pa.node()), | 799 (fl2, pa.node()), |
1484 b'remote created, get or merge', | 800 b'remote created, get or merge', |
1485 ) | 801 ) |
1486 elif n2 != ma[f]: | 802 elif n2 != ma[f]: |
1487 df = None | 803 df = None |
1490 # new file added in a directory that was moved | 806 # new file added in a directory that was moved |
1491 df = branch_copies1.dirmove[d] + f[len(d) :] | 807 df = branch_copies1.dirmove[d] + f[len(d) :] |
1492 break | 808 break |
1493 if df is not None and df in m1: | 809 if df is not None and df in m1: |
1494 actions[df] = ( | 810 actions[df] = ( |
1495 ACTION_MERGE, | 811 mergestatemod.ACTION_MERGE, |
1496 (df, f, f, False, pa.node()), | 812 (df, f, f, False, pa.node()), |
1497 b'local directory rename - respect move ' | 813 b'local directory rename - respect move ' |
1498 b'from %s' % f, | 814 b'from %s' % f, |
1499 ) | 815 ) |
1500 elif acceptremote: | 816 elif acceptremote: |
1501 actions[f] = (ACTION_CREATED, (fl2,), b'remote recreating') | 817 actions[f] = ( |
818 mergestatemod.ACTION_CREATED, | |
819 (fl2,), | |
820 b'remote recreating', | |
821 ) | |
1502 else: | 822 else: |
1503 actions[f] = ( | 823 actions[f] = ( |
1504 ACTION_DELETED_CHANGED, | 824 mergestatemod.ACTION_DELETED_CHANGED, |
1505 (None, f, f, False, pa.node()), | 825 (None, f, f, False, pa.node()), |
1506 b'prompt deleted/changed', | 826 b'prompt deleted/changed', |
1507 ) | 827 ) |
1508 | 828 |
1509 if repo.ui.configbool(b'experimental', b'merge.checkpathconflicts'): | 829 if repo.ui.configbool(b'experimental', b'merge.checkpathconflicts'): |
1526 remained the same.""" | 846 remained the same.""" |
1527 # We force a copy of actions.items() because we're going to mutate | 847 # We force a copy of actions.items() because we're going to mutate |
1528 # actions as we resolve trivial conflicts. | 848 # actions as we resolve trivial conflicts. |
1529 for f, (m, args, msg) in list(actions.items()): | 849 for f, (m, args, msg) in list(actions.items()): |
1530 if ( | 850 if ( |
1531 m == ACTION_CHANGED_DELETED | 851 m == mergestatemod.ACTION_CHANGED_DELETED |
1532 and f in ancestor | 852 and f in ancestor |
1533 and not wctx[f].cmp(ancestor[f]) | 853 and not wctx[f].cmp(ancestor[f]) |
1534 ): | 854 ): |
1535 # local did change but ended up with same content | 855 # local did change but ended up with same content |
1536 actions[f] = ACTION_REMOVE, None, b'prompt same' | 856 actions[f] = mergestatemod.ACTION_REMOVE, None, b'prompt same' |
1537 elif ( | 857 elif ( |
1538 m == ACTION_DELETED_CHANGED | 858 m == mergestatemod.ACTION_DELETED_CHANGED |
1539 and f in ancestor | 859 and f in ancestor |
1540 and not mctx[f].cmp(ancestor[f]) | 860 and not mctx[f].cmp(ancestor[f]) |
1541 ): | 861 ): |
1542 # remote did change but ended up with same content | 862 # remote did change but ended up with same content |
1543 del actions[f] # don't get = keep local deleted | 863 del actions[f] # don't get = keep local deleted |
1611 if renamedelete is None or len(renamedelete) < len(renamedelete1): | 931 if renamedelete is None or len(renamedelete) < len(renamedelete1): |
1612 renamedelete = renamedelete1 | 932 renamedelete = renamedelete1 |
1613 | 933 |
1614 for f, a in sorted(pycompat.iteritems(actions)): | 934 for f, a in sorted(pycompat.iteritems(actions)): |
1615 m, args, msg = a | 935 m, args, msg = a |
1616 if m == ACTION_GET_OTHER_AND_STORE: | 936 if m == mergestatemod.ACTION_GET_OTHER_AND_STORE: |
1617 m = ACTION_GET | 937 m = mergestatemod.ACTION_GET |
1618 repo.ui.debug(b' %s: %s -> %s\n' % (f, msg, m)) | 938 repo.ui.debug(b' %s: %s -> %s\n' % (f, msg, m)) |
1619 if f in fbids: | 939 if f in fbids: |
1620 d = fbids[f] | 940 d = fbids[f] |
1621 if m in d: | 941 if m in d: |
1622 d[m].append(a) | 942 d[m].append(a) |
1636 if all(a == l[0] for a in l[1:]): # len(bids) is > 1 | 956 if all(a == l[0] for a in l[1:]): # len(bids) is > 1 |
1637 repo.ui.note(_(b" %s: consensus for %s\n") % (f, m)) | 957 repo.ui.note(_(b" %s: consensus for %s\n") % (f, m)) |
1638 actions[f] = l[0] | 958 actions[f] = l[0] |
1639 continue | 959 continue |
1640 # If keep is an option, just do it. | 960 # If keep is an option, just do it. |
1641 if ACTION_KEEP in bids: | 961 if mergestatemod.ACTION_KEEP in bids: |
1642 repo.ui.note(_(b" %s: picking 'keep' action\n") % f) | 962 repo.ui.note(_(b" %s: picking 'keep' action\n") % f) |
1643 actions[f] = bids[ACTION_KEEP][0] | 963 actions[f] = bids[mergestatemod.ACTION_KEEP][0] |
1644 continue | 964 continue |
1645 # If there are gets and they all agree [how could they not?], do it. | 965 # If there are gets and they all agree [how could they not?], do it. |
1646 if ACTION_GET in bids: | 966 if mergestatemod.ACTION_GET in bids: |
1647 ga0 = bids[ACTION_GET][0] | 967 ga0 = bids[mergestatemod.ACTION_GET][0] |
1648 if all(a == ga0 for a in bids[ACTION_GET][1:]): | 968 if all(a == ga0 for a in bids[mergestatemod.ACTION_GET][1:]): |
1649 repo.ui.note(_(b" %s: picking 'get' action\n") % f) | 969 repo.ui.note(_(b" %s: picking 'get' action\n") % f) |
1650 actions[f] = ga0 | 970 actions[f] = ga0 |
1651 continue | 971 continue |
1652 # TODO: Consider other simple actions such as mode changes | 972 # TODO: Consider other simple actions such as mode changes |
1653 # Handle inefficient democrazy. | 973 # Handle inefficient democrazy. |
1788 # don't touch the context to be merged in. 'cd' is skipped, because | 1108 # don't touch the context to be merged in. 'cd' is skipped, because |
1789 # changed/deleted never resolves to something from the remote side. | 1109 # changed/deleted never resolves to something from the remote side. |
1790 oplist = [ | 1110 oplist = [ |
1791 actions[a] | 1111 actions[a] |
1792 for a in ( | 1112 for a in ( |
1793 ACTION_GET, | 1113 mergestatemod.ACTION_GET, |
1794 ACTION_DELETED_CHANGED, | 1114 mergestatemod.ACTION_DELETED_CHANGED, |
1795 ACTION_LOCAL_DIR_RENAME_GET, | 1115 mergestatemod.ACTION_LOCAL_DIR_RENAME_GET, |
1796 ACTION_MERGE, | 1116 mergestatemod.ACTION_MERGE, |
1797 ) | 1117 ) |
1798 ] | 1118 ] |
1799 prefetch = scmutil.prefetchfiles | 1119 prefetch = scmutil.prefetchfiles |
1800 matchfiles = scmutil.matchfiles | 1120 matchfiles = scmutil.matchfiles |
1801 prefetch( | 1121 prefetch( |
1824 def emptyactions(): | 1144 def emptyactions(): |
1825 """create an actions dict, to be populated and passed to applyupdates()""" | 1145 """create an actions dict, to be populated and passed to applyupdates()""" |
1826 return { | 1146 return { |
1827 m: [] | 1147 m: [] |
1828 for m in ( | 1148 for m in ( |
1829 ACTION_ADD, | 1149 mergestatemod.ACTION_ADD, |
1830 ACTION_ADD_MODIFIED, | 1150 mergestatemod.ACTION_ADD_MODIFIED, |
1831 ACTION_FORGET, | 1151 mergestatemod.ACTION_FORGET, |
1832 ACTION_GET, | 1152 mergestatemod.ACTION_GET, |
1833 ACTION_CHANGED_DELETED, | 1153 mergestatemod.ACTION_CHANGED_DELETED, |
1834 ACTION_DELETED_CHANGED, | 1154 mergestatemod.ACTION_DELETED_CHANGED, |
1835 ACTION_REMOVE, | 1155 mergestatemod.ACTION_REMOVE, |
1836 ACTION_DIR_RENAME_MOVE_LOCAL, | 1156 mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL, |
1837 ACTION_LOCAL_DIR_RENAME_GET, | 1157 mergestatemod.ACTION_LOCAL_DIR_RENAME_GET, |
1838 ACTION_MERGE, | 1158 mergestatemod.ACTION_MERGE, |
1839 ACTION_EXEC, | 1159 mergestatemod.ACTION_EXEC, |
1840 ACTION_KEEP, | 1160 mergestatemod.ACTION_KEEP, |
1841 ACTION_PATH_CONFLICT, | 1161 mergestatemod.ACTION_PATH_CONFLICT, |
1842 ACTION_PATH_CONFLICT_RESOLVE, | 1162 mergestatemod.ACTION_PATH_CONFLICT_RESOLVE, |
1843 ACTION_GET_OTHER_AND_STORE, | 1163 mergestatemod.ACTION_GET_OTHER_AND_STORE, |
1844 ) | 1164 ) |
1845 } | 1165 } |
1846 | 1166 |
1847 | 1167 |
1848 def applyupdates( | 1168 def applyupdates( |
1860 """ | 1180 """ |
1861 | 1181 |
1862 _prefetchfiles(repo, mctx, actions) | 1182 _prefetchfiles(repo, mctx, actions) |
1863 | 1183 |
1864 updated, merged, removed = 0, 0, 0 | 1184 updated, merged, removed = 0, 0, 0 |
1865 ms = mergestate.clean(repo, wctx.p1().node(), mctx.node(), labels) | 1185 ms = mergestatemod.mergestate.clean( |
1186 repo, wctx.p1().node(), mctx.node(), labels | |
1187 ) | |
1866 | 1188 |
1867 # add ACTION_GET_OTHER_AND_STORE to mergestate | 1189 # add ACTION_GET_OTHER_AND_STORE to mergestate |
1868 for e in actions[ACTION_GET_OTHER_AND_STORE]: | 1190 for e in actions[mergestatemod.ACTION_GET_OTHER_AND_STORE]: |
1869 ms.addmergedother(e[0]) | 1191 ms.addmergedother(e[0]) |
1870 | 1192 |
1871 moves = [] | 1193 moves = [] |
1872 for m, l in actions.items(): | 1194 for m, l in actions.items(): |
1873 l.sort() | 1195 l.sort() |
1874 | 1196 |
1875 # 'cd' and 'dc' actions are treated like other merge conflicts | 1197 # 'cd' and 'dc' actions are treated like other merge conflicts |
1876 mergeactions = sorted(actions[ACTION_CHANGED_DELETED]) | 1198 mergeactions = sorted(actions[mergestatemod.ACTION_CHANGED_DELETED]) |
1877 mergeactions.extend(sorted(actions[ACTION_DELETED_CHANGED])) | 1199 mergeactions.extend(sorted(actions[mergestatemod.ACTION_DELETED_CHANGED])) |
1878 mergeactions.extend(actions[ACTION_MERGE]) | 1200 mergeactions.extend(actions[mergestatemod.ACTION_MERGE]) |
1879 for f, args, msg in mergeactions: | 1201 for f, args, msg in mergeactions: |
1880 f1, f2, fa, move, anc = args | 1202 f1, f2, fa, move, anc = args |
1881 if f == b'.hgsubstate': # merged internally | 1203 if f == b'.hgsubstate': # merged internally |
1882 continue | 1204 continue |
1883 if f1 is None: | 1205 if f1 is None: |
1904 if wctx[f].lexists(): | 1226 if wctx[f].lexists(): |
1905 repo.ui.debug(b"removing %s\n" % f) | 1227 repo.ui.debug(b"removing %s\n" % f) |
1906 wctx[f].audit() | 1228 wctx[f].audit() |
1907 wctx[f].remove() | 1229 wctx[f].remove() |
1908 | 1230 |
1909 numupdates = sum(len(l) for m, l in actions.items() if m != ACTION_KEEP) | 1231 numupdates = sum( |
1232 len(l) for m, l in actions.items() if m != mergestatemod.ACTION_KEEP | |
1233 ) | |
1910 progress = repo.ui.makeprogress( | 1234 progress = repo.ui.makeprogress( |
1911 _(b'updating'), unit=_(b'files'), total=numupdates | 1235 _(b'updating'), unit=_(b'files'), total=numupdates |
1912 ) | 1236 ) |
1913 | 1237 |
1914 if [a for a in actions[ACTION_REMOVE] if a[0] == b'.hgsubstate']: | 1238 if [ |
1239 a | |
1240 for a in actions[mergestatemod.ACTION_REMOVE] | |
1241 if a[0] == b'.hgsubstate' | |
1242 ]: | |
1915 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels) | 1243 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels) |
1916 | 1244 |
1917 # record path conflicts | 1245 # record path conflicts |
1918 for f, args, msg in actions[ACTION_PATH_CONFLICT]: | 1246 for f, args, msg in actions[mergestatemod.ACTION_PATH_CONFLICT]: |
1919 f1, fo = args | 1247 f1, fo = args |
1920 s = repo.ui.status | 1248 s = repo.ui.status |
1921 s( | 1249 s( |
1922 _( | 1250 _( |
1923 b"%s: path conflict - a file or link has the same name as a " | 1251 b"%s: path conflict - a file or link has the same name as a " |
1937 # per-item cost at 0 in that case. | 1265 # per-item cost at 0 in that case. |
1938 cost = 0 if wctx.isinmemory() else 0.001 | 1266 cost = 0 if wctx.isinmemory() else 0.001 |
1939 | 1267 |
1940 # remove in parallel (must come before resolving path conflicts and getting) | 1268 # remove in parallel (must come before resolving path conflicts and getting) |
1941 prog = worker.worker( | 1269 prog = worker.worker( |
1942 repo.ui, cost, batchremove, (repo, wctx), actions[ACTION_REMOVE] | 1270 repo.ui, |
1271 cost, | |
1272 batchremove, | |
1273 (repo, wctx), | |
1274 actions[mergestatemod.ACTION_REMOVE], | |
1943 ) | 1275 ) |
1944 for i, item in prog: | 1276 for i, item in prog: |
1945 progress.increment(step=i, item=item) | 1277 progress.increment(step=i, item=item) |
1946 removed = len(actions[ACTION_REMOVE]) | 1278 removed = len(actions[mergestatemod.ACTION_REMOVE]) |
1947 | 1279 |
1948 # resolve path conflicts (must come before getting) | 1280 # resolve path conflicts (must come before getting) |
1949 for f, args, msg in actions[ACTION_PATH_CONFLICT_RESOLVE]: | 1281 for f, args, msg in actions[mergestatemod.ACTION_PATH_CONFLICT_RESOLVE]: |
1950 repo.ui.debug(b" %s: %s -> pr\n" % (f, msg)) | 1282 repo.ui.debug(b" %s: %s -> pr\n" % (f, msg)) |
1951 (f0,) = args | 1283 (f0,) = args |
1952 if wctx[f0].lexists(): | 1284 if wctx[f0].lexists(): |
1953 repo.ui.note(_(b"moving %s to %s\n") % (f0, f)) | 1285 repo.ui.note(_(b"moving %s to %s\n") % (f0, f)) |
1954 wctx[f].audit() | 1286 wctx[f].audit() |
1963 prog = worker.worker( | 1295 prog = worker.worker( |
1964 repo.ui, | 1296 repo.ui, |
1965 cost, | 1297 cost, |
1966 batchget, | 1298 batchget, |
1967 (repo, mctx, wctx, wantfiledata), | 1299 (repo, mctx, wctx, wantfiledata), |
1968 actions[ACTION_GET], | 1300 actions[mergestatemod.ACTION_GET], |
1969 threadsafe=threadsafe, | 1301 threadsafe=threadsafe, |
1970 hasretval=True, | 1302 hasretval=True, |
1971 ) | 1303 ) |
1972 getfiledata = {} | 1304 getfiledata = {} |
1973 for final, res in prog: | 1305 for final, res in prog: |
1974 if final: | 1306 if final: |
1975 getfiledata = res | 1307 getfiledata = res |
1976 else: | 1308 else: |
1977 i, item = res | 1309 i, item = res |
1978 progress.increment(step=i, item=item) | 1310 progress.increment(step=i, item=item) |
1979 updated = len(actions[ACTION_GET]) | 1311 updated = len(actions[mergestatemod.ACTION_GET]) |
1980 | 1312 |
1981 if [a for a in actions[ACTION_GET] if a[0] == b'.hgsubstate']: | 1313 if [a for a in actions[mergestatemod.ACTION_GET] if a[0] == b'.hgsubstate']: |
1982 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels) | 1314 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels) |
1983 | 1315 |
1984 # forget (manifest only, just log it) (must come first) | 1316 # forget (manifest only, just log it) (must come first) |
1985 for f, args, msg in actions[ACTION_FORGET]: | 1317 for f, args, msg in actions[mergestatemod.ACTION_FORGET]: |
1986 repo.ui.debug(b" %s: %s -> f\n" % (f, msg)) | 1318 repo.ui.debug(b" %s: %s -> f\n" % (f, msg)) |
1987 progress.increment(item=f) | 1319 progress.increment(item=f) |
1988 | 1320 |
1989 # re-add (manifest only, just log it) | 1321 # re-add (manifest only, just log it) |
1990 for f, args, msg in actions[ACTION_ADD]: | 1322 for f, args, msg in actions[mergestatemod.ACTION_ADD]: |
1991 repo.ui.debug(b" %s: %s -> a\n" % (f, msg)) | 1323 repo.ui.debug(b" %s: %s -> a\n" % (f, msg)) |
1992 progress.increment(item=f) | 1324 progress.increment(item=f) |
1993 | 1325 |
1994 # re-add/mark as modified (manifest only, just log it) | 1326 # re-add/mark as modified (manifest only, just log it) |
1995 for f, args, msg in actions[ACTION_ADD_MODIFIED]: | 1327 for f, args, msg in actions[mergestatemod.ACTION_ADD_MODIFIED]: |
1996 repo.ui.debug(b" %s: %s -> am\n" % (f, msg)) | 1328 repo.ui.debug(b" %s: %s -> am\n" % (f, msg)) |
1997 progress.increment(item=f) | 1329 progress.increment(item=f) |
1998 | 1330 |
1999 # keep (noop, just log it) | 1331 # keep (noop, just log it) |
2000 for f, args, msg in actions[ACTION_KEEP]: | 1332 for f, args, msg in actions[mergestatemod.ACTION_KEEP]: |
2001 repo.ui.debug(b" %s: %s -> k\n" % (f, msg)) | 1333 repo.ui.debug(b" %s: %s -> k\n" % (f, msg)) |
2002 # no progress | 1334 # no progress |
2003 | 1335 |
2004 # directory rename, move local | 1336 # directory rename, move local |
2005 for f, args, msg in actions[ACTION_DIR_RENAME_MOVE_LOCAL]: | 1337 for f, args, msg in actions[mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL]: |
2006 repo.ui.debug(b" %s: %s -> dm\n" % (f, msg)) | 1338 repo.ui.debug(b" %s: %s -> dm\n" % (f, msg)) |
2007 progress.increment(item=f) | 1339 progress.increment(item=f) |
2008 f0, flags = args | 1340 f0, flags = args |
2009 repo.ui.note(_(b"moving %s to %s\n") % (f0, f)) | 1341 repo.ui.note(_(b"moving %s to %s\n") % (f0, f)) |
2010 wctx[f].audit() | 1342 wctx[f].audit() |
2011 wctx[f].write(wctx.filectx(f0).data(), flags) | 1343 wctx[f].write(wctx.filectx(f0).data(), flags) |
2012 wctx[f0].remove() | 1344 wctx[f0].remove() |
2013 updated += 1 | 1345 updated += 1 |
2014 | 1346 |
2015 # local directory rename, get | 1347 # local directory rename, get |
2016 for f, args, msg in actions[ACTION_LOCAL_DIR_RENAME_GET]: | 1348 for f, args, msg in actions[mergestatemod.ACTION_LOCAL_DIR_RENAME_GET]: |
2017 repo.ui.debug(b" %s: %s -> dg\n" % (f, msg)) | 1349 repo.ui.debug(b" %s: %s -> dg\n" % (f, msg)) |
2018 progress.increment(item=f) | 1350 progress.increment(item=f) |
2019 f0, flags = args | 1351 f0, flags = args |
2020 repo.ui.note(_(b"getting %s to %s\n") % (f0, f)) | 1352 repo.ui.note(_(b"getting %s to %s\n") % (f0, f)) |
2021 wctx[f].write(mctx.filectx(f0).data(), flags) | 1353 wctx[f].write(mctx.filectx(f0).data(), flags) |
2022 updated += 1 | 1354 updated += 1 |
2023 | 1355 |
2024 # exec | 1356 # exec |
2025 for f, args, msg in actions[ACTION_EXEC]: | 1357 for f, args, msg in actions[mergestatemod.ACTION_EXEC]: |
2026 repo.ui.debug(b" %s: %s -> e\n" % (f, msg)) | 1358 repo.ui.debug(b" %s: %s -> e\n" % (f, msg)) |
2027 progress.increment(item=f) | 1359 progress.increment(item=f) |
2028 (flags,) = args | 1360 (flags,) = args |
2029 wctx[f].audit() | 1361 wctx[f].audit() |
2030 wctx[f].setflags(b'l' in flags, b'x' in flags) | 1362 wctx[f].setflags(b'l' in flags, b'x' in flags) |
2085 unresolved = ms.unresolvedcount() | 1417 unresolved = ms.unresolvedcount() |
2086 | 1418 |
2087 if ( | 1419 if ( |
2088 usemergedriver | 1420 usemergedriver |
2089 and not unresolved | 1421 and not unresolved |
2090 and ms.mdstate() != MERGE_DRIVER_STATE_SUCCESS | 1422 and ms.mdstate() != mergestatemod.MERGE_DRIVER_STATE_SUCCESS |
2091 ): | 1423 ): |
2092 if not driverconclude(repo, ms, wctx, labels=labels): | 1424 if not driverconclude(repo, ms, wctx, labels=labels): |
2093 # XXX setting unresolved to at least 1 is a hack to make sure we | 1425 # XXX setting unresolved to at least 1 is a hack to make sure we |
2094 # error out | 1426 # error out |
2095 unresolved = max(unresolved, 1) | 1427 unresolved = max(unresolved, 1) |
2101 merged += msmerged | 1433 merged += msmerged |
2102 removed += msremoved | 1434 removed += msremoved |
2103 | 1435 |
2104 extraactions = ms.actions() | 1436 extraactions = ms.actions() |
2105 if extraactions: | 1437 if extraactions: |
2106 mfiles = {a[0] for a in actions[ACTION_MERGE]} | 1438 mfiles = {a[0] for a in actions[mergestatemod.ACTION_MERGE]} |
2107 for k, acts in pycompat.iteritems(extraactions): | 1439 for k, acts in pycompat.iteritems(extraactions): |
2108 actions[k].extend(acts) | 1440 actions[k].extend(acts) |
2109 if k == ACTION_GET and wantfiledata: | 1441 if k == mergestatemod.ACTION_GET and wantfiledata: |
2110 # no filedata until mergestate is updated to provide it | 1442 # no filedata until mergestate is updated to provide it |
2111 for a in acts: | 1443 for a in acts: |
2112 getfiledata[a[0]] = None | 1444 getfiledata[a[0]] = None |
2113 # Remove these files from actions[ACTION_MERGE] as well. This is | 1445 # Remove these files from actions[ACTION_MERGE] as well. This is |
2114 # important because in recordupdates, files in actions[ACTION_MERGE] | 1446 # important because in recordupdates, files in actions[ACTION_MERGE] |
2126 # | 1458 # |
2127 # We don't need to do the same operation for 'dc' and 'cd' because | 1459 # We don't need to do the same operation for 'dc' and 'cd' because |
2128 # those lists aren't consulted again. | 1460 # those lists aren't consulted again. |
2129 mfiles.difference_update(a[0] for a in acts) | 1461 mfiles.difference_update(a[0] for a in acts) |
2130 | 1462 |
2131 actions[ACTION_MERGE] = [ | 1463 actions[mergestatemod.ACTION_MERGE] = [ |
2132 a for a in actions[ACTION_MERGE] if a[0] in mfiles | 1464 a for a in actions[mergestatemod.ACTION_MERGE] if a[0] in mfiles |
2133 ] | 1465 ] |
2134 | 1466 |
2135 progress.complete() | 1467 progress.complete() |
2136 assert len(getfiledata) == (len(actions[ACTION_GET]) if wantfiledata else 0) | 1468 assert len(getfiledata) == ( |
1469 len(actions[mergestatemod.ACTION_GET]) if wantfiledata else 0 | |
1470 ) | |
2137 return updateresult(updated, merged, removed, unresolved), getfiledata | 1471 return updateresult(updated, merged, removed, unresolved), getfiledata |
2138 | |
2139 | |
2140 def recordupdates(repo, actions, branchmerge, getfiledata): | |
2141 """record merge actions to the dirstate""" | |
2142 # remove (must come first) | |
2143 for f, args, msg in actions.get(ACTION_REMOVE, []): | |
2144 if branchmerge: | |
2145 repo.dirstate.remove(f) | |
2146 else: | |
2147 repo.dirstate.drop(f) | |
2148 | |
2149 # forget (must come first) | |
2150 for f, args, msg in actions.get(ACTION_FORGET, []): | |
2151 repo.dirstate.drop(f) | |
2152 | |
2153 # resolve path conflicts | |
2154 for f, args, msg in actions.get(ACTION_PATH_CONFLICT_RESOLVE, []): | |
2155 (f0,) = args | |
2156 origf0 = repo.dirstate.copied(f0) or f0 | |
2157 repo.dirstate.add(f) | |
2158 repo.dirstate.copy(origf0, f) | |
2159 if f0 == origf0: | |
2160 repo.dirstate.remove(f0) | |
2161 else: | |
2162 repo.dirstate.drop(f0) | |
2163 | |
2164 # re-add | |
2165 for f, args, msg in actions.get(ACTION_ADD, []): | |
2166 repo.dirstate.add(f) | |
2167 | |
2168 # re-add/mark as modified | |
2169 for f, args, msg in actions.get(ACTION_ADD_MODIFIED, []): | |
2170 if branchmerge: | |
2171 repo.dirstate.normallookup(f) | |
2172 else: | |
2173 repo.dirstate.add(f) | |
2174 | |
2175 # exec change | |
2176 for f, args, msg in actions.get(ACTION_EXEC, []): | |
2177 repo.dirstate.normallookup(f) | |
2178 | |
2179 # keep | |
2180 for f, args, msg in actions.get(ACTION_KEEP, []): | |
2181 pass | |
2182 | |
2183 # get | |
2184 for f, args, msg in actions.get(ACTION_GET, []): | |
2185 if branchmerge: | |
2186 repo.dirstate.otherparent(f) | |
2187 else: | |
2188 parentfiledata = getfiledata[f] if getfiledata else None | |
2189 repo.dirstate.normal(f, parentfiledata=parentfiledata) | |
2190 | |
2191 # merge | |
2192 for f, args, msg in actions.get(ACTION_MERGE, []): | |
2193 f1, f2, fa, move, anc = args | |
2194 if branchmerge: | |
2195 # We've done a branch merge, mark this file as merged | |
2196 # so that we properly record the merger later | |
2197 repo.dirstate.merge(f) | |
2198 if f1 != f2: # copy/rename | |
2199 if move: | |
2200 repo.dirstate.remove(f1) | |
2201 if f1 != f: | |
2202 repo.dirstate.copy(f1, f) | |
2203 else: | |
2204 repo.dirstate.copy(f2, f) | |
2205 else: | |
2206 # We've update-merged a locally modified file, so | |
2207 # we set the dirstate to emulate a normal checkout | |
2208 # of that file some time in the past. Thus our | |
2209 # merge will appear as a normal local file | |
2210 # modification. | |
2211 if f2 == f: # file not locally copied/moved | |
2212 repo.dirstate.normallookup(f) | |
2213 if move: | |
2214 repo.dirstate.drop(f1) | |
2215 | |
2216 # directory rename, move local | |
2217 for f, args, msg in actions.get(ACTION_DIR_RENAME_MOVE_LOCAL, []): | |
2218 f0, flag = args | |
2219 if branchmerge: | |
2220 repo.dirstate.add(f) | |
2221 repo.dirstate.remove(f0) | |
2222 repo.dirstate.copy(f0, f) | |
2223 else: | |
2224 repo.dirstate.normal(f) | |
2225 repo.dirstate.drop(f0) | |
2226 | |
2227 # directory rename, get | |
2228 for f, args, msg in actions.get(ACTION_LOCAL_DIR_RENAME_GET, []): | |
2229 f0, flag = args | |
2230 if branchmerge: | |
2231 repo.dirstate.add(f) | |
2232 repo.dirstate.copy(f0, f) | |
2233 else: | |
2234 repo.dirstate.normal(f) | |
2235 | 1472 |
2236 | 1473 |
2237 UPDATECHECK_ABORT = b'abort' # handled at higher layers | 1474 UPDATECHECK_ABORT = b'abort' # handled at higher layers |
2238 UPDATECHECK_NONE = b'none' | 1475 UPDATECHECK_NONE = b'none' |
2239 UPDATECHECK_LINEAR = b'linear' | 1476 UPDATECHECK_LINEAR = b'linear' |
2354 overwrite = force and not branchmerge | 1591 overwrite = force and not branchmerge |
2355 ### check phase | 1592 ### check phase |
2356 if not overwrite: | 1593 if not overwrite: |
2357 if len(pl) > 1: | 1594 if len(pl) > 1: |
2358 raise error.Abort(_(b"outstanding uncommitted merge")) | 1595 raise error.Abort(_(b"outstanding uncommitted merge")) |
2359 ms = mergestate.read(repo) | 1596 ms = mergestatemod.mergestate.read(repo) |
2360 if list(ms.unresolved()): | 1597 if list(ms.unresolved()): |
2361 raise error.Abort( | 1598 raise error.Abort( |
2362 _(b"outstanding merge conflicts"), | 1599 _(b"outstanding merge conflicts"), |
2363 hint=_(b"use 'hg resolve' to resolve"), | 1600 hint=_(b"use 'hg resolve' to resolve"), |
2364 ) | 1601 ) |
2441 ) | 1678 ) |
2442 | 1679 |
2443 if updatecheck == UPDATECHECK_NO_CONFLICT: | 1680 if updatecheck == UPDATECHECK_NO_CONFLICT: |
2444 for f, (m, args, msg) in pycompat.iteritems(actionbyfile): | 1681 for f, (m, args, msg) in pycompat.iteritems(actionbyfile): |
2445 if m not in ( | 1682 if m not in ( |
2446 ACTION_GET, | 1683 mergestatemod.ACTION_GET, |
2447 ACTION_KEEP, | 1684 mergestatemod.ACTION_KEEP, |
2448 ACTION_EXEC, | 1685 mergestatemod.ACTION_EXEC, |
2449 ACTION_REMOVE, | 1686 mergestatemod.ACTION_REMOVE, |
2450 ACTION_PATH_CONFLICT_RESOLVE, | 1687 mergestatemod.ACTION_PATH_CONFLICT_RESOLVE, |
2451 ACTION_GET_OTHER_AND_STORE, | 1688 mergestatemod.ACTION_GET_OTHER_AND_STORE, |
2452 ): | 1689 ): |
2453 msg = _(b"conflicting changes") | 1690 msg = _(b"conflicting changes") |
2454 hint = _(b"commit or update --clean to discard changes") | 1691 hint = _(b"commit or update --clean to discard changes") |
2455 raise error.Abort(msg, hint=hint) | 1692 raise error.Abort(msg, hint=hint) |
2456 | 1693 |
2460 if b'.hgsubstate' in actionbyfile: | 1697 if b'.hgsubstate' in actionbyfile: |
2461 f = b'.hgsubstate' | 1698 f = b'.hgsubstate' |
2462 m, args, msg = actionbyfile[f] | 1699 m, args, msg = actionbyfile[f] |
2463 prompts = filemerge.partextras(labels) | 1700 prompts = filemerge.partextras(labels) |
2464 prompts[b'f'] = f | 1701 prompts[b'f'] = f |
2465 if m == ACTION_CHANGED_DELETED: | 1702 if m == mergestatemod.ACTION_CHANGED_DELETED: |
2466 if repo.ui.promptchoice( | 1703 if repo.ui.promptchoice( |
2467 _( | 1704 _( |
2468 b"local%(l)s changed %(f)s which other%(o)s deleted\n" | 1705 b"local%(l)s changed %(f)s which other%(o)s deleted\n" |
2469 b"use (c)hanged version or (d)elete?" | 1706 b"use (c)hanged version or (d)elete?" |
2470 b"$$ &Changed $$ &Delete" | 1707 b"$$ &Changed $$ &Delete" |
2471 ) | 1708 ) |
2472 % prompts, | 1709 % prompts, |
2473 0, | 1710 0, |
2474 ): | 1711 ): |
2475 actionbyfile[f] = (ACTION_REMOVE, None, b'prompt delete') | 1712 actionbyfile[f] = ( |
1713 mergestatemod.ACTION_REMOVE, | |
1714 None, | |
1715 b'prompt delete', | |
1716 ) | |
2476 elif f in p1: | 1717 elif f in p1: |
2477 actionbyfile[f] = ( | 1718 actionbyfile[f] = ( |
2478 ACTION_ADD_MODIFIED, | 1719 mergestatemod.ACTION_ADD_MODIFIED, |
2479 None, | 1720 None, |
2480 b'prompt keep', | 1721 b'prompt keep', |
2481 ) | 1722 ) |
2482 else: | 1723 else: |
2483 actionbyfile[f] = (ACTION_ADD, None, b'prompt keep') | 1724 actionbyfile[f] = ( |
2484 elif m == ACTION_DELETED_CHANGED: | 1725 mergestatemod.ACTION_ADD, |
1726 None, | |
1727 b'prompt keep', | |
1728 ) | |
1729 elif m == mergestatemod.ACTION_DELETED_CHANGED: | |
2485 f1, f2, fa, move, anc = args | 1730 f1, f2, fa, move, anc = args |
2486 flags = p2[f2].flags() | 1731 flags = p2[f2].flags() |
2487 if ( | 1732 if ( |
2488 repo.ui.promptchoice( | 1733 repo.ui.promptchoice( |
2489 _( | 1734 _( |
2495 0, | 1740 0, |
2496 ) | 1741 ) |
2497 == 0 | 1742 == 0 |
2498 ): | 1743 ): |
2499 actionbyfile[f] = ( | 1744 actionbyfile[f] = ( |
2500 ACTION_GET, | 1745 mergestatemod.ACTION_GET, |
2501 (flags, False), | 1746 (flags, False), |
2502 b'prompt recreating', | 1747 b'prompt recreating', |
2503 ) | 1748 ) |
2504 else: | 1749 else: |
2505 del actionbyfile[f] | 1750 del actionbyfile[f] |
2509 for f, (m, args, msg) in pycompat.iteritems(actionbyfile): | 1754 for f, (m, args, msg) in pycompat.iteritems(actionbyfile): |
2510 if m not in actions: | 1755 if m not in actions: |
2511 actions[m] = [] | 1756 actions[m] = [] |
2512 actions[m].append((f, args, msg)) | 1757 actions[m].append((f, args, msg)) |
2513 | 1758 |
2514 # ACTION_GET_OTHER_AND_STORE is a ACTION_GET + store in mergestate | 1759 # ACTION_GET_OTHER_AND_STORE is a mergestatemod.ACTION_GET + store in mergestate |
2515 for e in actions[ACTION_GET_OTHER_AND_STORE]: | 1760 for e in actions[mergestatemod.ACTION_GET_OTHER_AND_STORE]: |
2516 actions[ACTION_GET].append(e) | 1761 actions[mergestatemod.ACTION_GET].append(e) |
2517 | 1762 |
2518 if not util.fscasesensitive(repo.path): | 1763 if not util.fscasesensitive(repo.path): |
2519 # check collision between files only in p2 for clean update | 1764 # check collision between files only in p2 for clean update |
2520 if not branchmerge and ( | 1765 if not branchmerge and ( |
2521 force or not wc.dirty(missing=True, branch=False) | 1766 force or not wc.dirty(missing=True, branch=False) |
2588 | 1833 |
2589 if ( | 1834 if ( |
2590 fsmonitorwarning | 1835 fsmonitorwarning |
2591 and not fsmonitorenabled | 1836 and not fsmonitorenabled |
2592 and p1.node() == nullid | 1837 and p1.node() == nullid |
2593 and len(actions[ACTION_GET]) >= fsmonitorthreshold | 1838 and len(actions[mergestatemod.ACTION_GET]) >= fsmonitorthreshold |
2594 and pycompat.sysplatform.startswith((b'linux', b'darwin')) | 1839 and pycompat.sysplatform.startswith((b'linux', b'darwin')) |
2595 ): | 1840 ): |
2596 repo.ui.warn( | 1841 repo.ui.warn( |
2597 _( | 1842 _( |
2598 b'(warning: large working directory being used without ' | 1843 b'(warning: large working directory being used without ' |
2607 ) | 1852 ) |
2608 | 1853 |
2609 if updatedirstate: | 1854 if updatedirstate: |
2610 with repo.dirstate.parentchange(): | 1855 with repo.dirstate.parentchange(): |
2611 repo.setparents(fp1, fp2) | 1856 repo.setparents(fp1, fp2) |
2612 recordupdates(repo, actions, branchmerge, getfiledata) | 1857 mergestatemod.recordupdates( |
1858 repo, actions, branchmerge, getfiledata | |
1859 ) | |
2613 # update completed, clear state | 1860 # update completed, clear state |
2614 util.unlink(repo.vfs.join(b'updatestate')) | 1861 util.unlink(repo.vfs.join(b'updatestate')) |
2615 | 1862 |
2616 if not branchmerge: | 1863 if not branchmerge: |
2617 repo.dirstate.setbranch(p2.branch()) | 1864 repo.dirstate.setbranch(p2.branch()) |