Mercurial > public > mercurial-scm > hg
comparison mercurial/merge.py @ 44687:1b8fd4af3318
mergestate: store about files resolved in favour of other
Committing a merge sometimes wrongly creates a new filenode where it can re-use
an existing one. This happens because the commit code does it's own calculation
and does not know what happened on merge.
This starts storing information in mergestate about files which were
automatically merged and the other/remote version of file was used.
We need this information at commit to pick the filenode parent for the new
commit.
This issue was found by Pierre-Yves David and idea to store the relevant parts
in mergestate is also suggested by him.
Somethings which can be further investigated are:
1) refactoring of commit logic more to depend on this information
2) maybe a more generic solution?
Differential Revision: https://phab.mercurial-scm.org/D8392
author | Pulkit Goyal <7895pulkit@gmail.com> |
---|---|
date | Thu, 09 Apr 2020 16:06:03 +0530 |
parents | 9d2b2df2c2ba |
children | b7808443ed6a |
comparison
equal
deleted
inserted
replaced
44686:bca57ad9e630 | 44687:1b8fd4af3318 |
---|---|
62 RECORD_FILE_VALUES = b'f' | 62 RECORD_FILE_VALUES = b'f' |
63 RECORD_LABELS = b'l' | 63 RECORD_LABELS = b'l' |
64 RECORD_OVERRIDE = b't' | 64 RECORD_OVERRIDE = b't' |
65 RECORD_UNSUPPORTED_MANDATORY = b'X' | 65 RECORD_UNSUPPORTED_MANDATORY = b'X' |
66 RECORD_UNSUPPORTED_ADVISORY = b'x' | 66 RECORD_UNSUPPORTED_ADVISORY = b'x' |
67 RECORD_RESOLVED_OTHER = b'R' | |
67 | 68 |
68 MERGE_DRIVER_STATE_UNMARKED = b'u' | 69 MERGE_DRIVER_STATE_UNMARKED = b'u' |
69 MERGE_DRIVER_STATE_MARKED = b'm' | 70 MERGE_DRIVER_STATE_MARKED = b'm' |
70 MERGE_DRIVER_STATE_SUCCESS = b's' | 71 MERGE_DRIVER_STATE_SUCCESS = b's' |
71 | 72 |
72 MERGE_RECORD_UNRESOLVED = b'u' | 73 MERGE_RECORD_UNRESOLVED = b'u' |
73 MERGE_RECORD_RESOLVED = b'r' | 74 MERGE_RECORD_RESOLVED = b'r' |
74 MERGE_RECORD_UNRESOLVED_PATH = b'pu' | 75 MERGE_RECORD_UNRESOLVED_PATH = b'pu' |
75 MERGE_RECORD_RESOLVED_PATH = b'pr' | 76 MERGE_RECORD_RESOLVED_PATH = b'pr' |
76 MERGE_RECORD_DRIVER_RESOLVED = b'd' | 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' | |
77 | 81 |
78 ACTION_FORGET = b'f' | 82 ACTION_FORGET = b'f' |
79 ACTION_REMOVE = b'r' | 83 ACTION_REMOVE = b'r' |
80 ACTION_ADD = b'a' | 84 ACTION_ADD = b'a' |
81 ACTION_GET = b'g' | 85 ACTION_GET = b'g' |
89 ACTION_LOCAL_DIR_RENAME_GET = b'dg' | 93 ACTION_LOCAL_DIR_RENAME_GET = b'dg' |
90 ACTION_DIR_RENAME_MOVE_LOCAL = b'dm' | 94 ACTION_DIR_RENAME_MOVE_LOCAL = b'dm' |
91 ACTION_KEEP = b'k' | 95 ACTION_KEEP = b'k' |
92 ACTION_EXEC = b'e' | 96 ACTION_EXEC = b'e' |
93 ACTION_CREATED_MERGE = b'cm' | 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' | |
94 | 100 |
95 | 101 |
96 class mergestate(object): | 102 class mergestate(object): |
97 '''track 3-way merge state of individual files | 103 '''track 3-way merge state of individual files |
98 | 104 |
225 elif rtype in ( | 231 elif rtype in ( |
226 RECORD_MERGED, | 232 RECORD_MERGED, |
227 RECORD_CHANGEDELETE_CONFLICT, | 233 RECORD_CHANGEDELETE_CONFLICT, |
228 RECORD_PATH_CONFLICT, | 234 RECORD_PATH_CONFLICT, |
229 RECORD_MERGE_DRIVER_MERGE, | 235 RECORD_MERGE_DRIVER_MERGE, |
236 RECORD_RESOLVED_OTHER, | |
230 ): | 237 ): |
231 bits = record.split(b'\0') | 238 bits = record.split(b'\0') |
232 self._state[bits[0]] = bits[1:] | 239 self._state[bits[0]] = bits[1:] |
233 elif rtype == RECORD_FILE_VALUES: | 240 elif rtype == RECORD_FILE_VALUES: |
234 filename, rawextras = record.split(b'\0', 1) | 241 filename, rawextras = record.split(b'\0', 1) |
451 # Path conflicts. These are stored in 'P' records. The current | 458 # Path conflicts. These are stored in 'P' records. The current |
452 # resolution state ('pu' or 'pr') is stored within the record. | 459 # resolution state ('pu' or 'pr') is stored within the record. |
453 records.append( | 460 records.append( |
454 (RECORD_PATH_CONFLICT, b'\0'.join([filename] + v)) | 461 (RECORD_PATH_CONFLICT, b'\0'.join([filename] + v)) |
455 ) | 462 ) |
463 elif v[0] == MERGE_RECORD_MERGED_OTHER: | |
464 records.append( | |
465 (RECORD_RESOLVED_OTHER, b'\0'.join([filename] + v)) | |
466 ) | |
456 elif v[1] == nullhex or v[6] == nullhex: | 467 elif v[1] == nullhex or v[6] == nullhex: |
457 # Change/Delete or Delete/Change conflicts. These are stored in | 468 # Change/Delete or Delete/Change conflicts. These are stored in |
458 # 'C' records. v[1] is the local file, and is nullhex when the | 469 # 'C' records. v[1] is the local file, and is nullhex when the |
459 # file is deleted locally ('dc'). v[6] is the remote file, and | 470 # file is deleted locally ('dc'). v[6] is the remote file, and |
460 # is nullhex when the file is deleted remotely ('cd'). | 471 # is nullhex when the file is deleted remotely ('cd'). |
549 forigin: origin of the file ('l' or 'r' for local/remote) | 560 forigin: origin of the file ('l' or 'r' for local/remote) |
550 """ | 561 """ |
551 self._state[path] = [MERGE_RECORD_UNRESOLVED_PATH, frename, forigin] | 562 self._state[path] = [MERGE_RECORD_UNRESOLVED_PATH, frename, forigin] |
552 self._dirty = True | 563 self._dirty = True |
553 | 564 |
565 def addmergedother(self, path): | |
566 self._state[path] = [MERGE_RECORD_MERGED_OTHER, nullhex, nullhex] | |
567 self._dirty = True | |
568 | |
554 def __contains__(self, dfile): | 569 def __contains__(self, dfile): |
555 return dfile in self._state | 570 return dfile in self._state |
556 | 571 |
557 def __getitem__(self, dfile): | 572 def __getitem__(self, dfile): |
558 return self._state[dfile][0] | 573 return self._state[dfile][0] |
591 return self._stateextras.setdefault(filename, {}) | 606 return self._stateextras.setdefault(filename, {}) |
592 | 607 |
593 def _resolve(self, preresolve, dfile, wctx): | 608 def _resolve(self, preresolve, dfile, wctx): |
594 """rerun merge process for file path `dfile`""" | 609 """rerun merge process for file path `dfile`""" |
595 if self[dfile] in (MERGE_RECORD_RESOLVED, MERGE_RECORD_DRIVER_RESOLVED): | 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: | |
596 return True, 0 | 613 return True, 0 |
597 stateentry = self._state[dfile] | 614 stateentry = self._state[dfile] |
598 state, localkey, lfile, afile, anode, ofile, onode, flags = stateentry | 615 state, localkey, lfile, afile, anode, ofile, onode, flags = stateentry |
599 octx = self._repo[self._other] | 616 octx = self._repo[self._other] |
600 extras = self.extras(dfile) | 617 extras = self.extras(dfile) |
1207 | 1224 |
1208 Raise an exception if the merge cannot be completed because the repo is | 1225 Raise an exception if the merge cannot be completed because the repo is |
1209 narrowed. | 1226 narrowed. |
1210 """ | 1227 """ |
1211 nooptypes = {b'k'} # TODO: handle with nonconflicttypes | 1228 nooptypes = {b'k'} # TODO: handle with nonconflicttypes |
1212 nonconflicttypes = set(b'a am c cm f g r e'.split()) | 1229 nonconflicttypes = set(b'a am c cm f g gs r e'.split()) |
1213 # We mutate the items in the dict during iteration, so iterate | 1230 # We mutate the items in the dict during iteration, so iterate |
1214 # over a copy. | 1231 # over a copy. |
1215 for f, action in list(actions.items()): | 1232 for f, action in list(actions.items()): |
1216 if narrowmatch(f): | 1233 if narrowmatch(f): |
1217 pass | 1234 pass |
1346 (fl2,), | 1363 (fl2,), |
1347 b'update permissions', | 1364 b'update permissions', |
1348 ) | 1365 ) |
1349 else: | 1366 else: |
1350 actions[f] = ( | 1367 actions[f] = ( |
1351 ACTION_GET, | 1368 ACTION_GET_OTHER_AND_STORE |
1369 if branchmerge | |
1370 else ACTION_GET, | |
1352 (fl2, False), | 1371 (fl2, False), |
1353 b'remote is newer', | 1372 b'remote is newer', |
1354 ) | 1373 ) |
1355 elif nol and n2 == a: # remote only changed 'x' | 1374 elif nol and n2 == a: # remote only changed 'x' |
1356 actions[f] = (ACTION_EXEC, (fl2,), b'update permissions') | 1375 actions[f] = (ACTION_EXEC, (fl2,), b'update permissions') |
1357 elif nol and n1 == a: # local only changed 'x' | 1376 elif nol and n1 == a: # local only changed 'x' |
1358 actions[f] = (ACTION_GET, (fl1, False), b'remote is newer') | 1377 actions[f] = ( |
1378 ACTION_GET_OTHER_AND_STORE | |
1379 if branchmerge | |
1380 else ACTION_GET, | |
1381 (fl1, False), | |
1382 b'remote is newer', | |
1383 ) | |
1359 else: # both changed something | 1384 else: # both changed something |
1360 actions[f] = ( | 1385 actions[f] = ( |
1361 ACTION_MERGE, | 1386 ACTION_MERGE, |
1362 (f, f, f, False, pa.node()), | 1387 (f, f, f, False, pa.node()), |
1363 b'versions differ', | 1388 b'versions differ', |
1586 if renamedelete is None or len(renamedelete) < len(renamedelete1): | 1611 if renamedelete is None or len(renamedelete) < len(renamedelete1): |
1587 renamedelete = renamedelete1 | 1612 renamedelete = renamedelete1 |
1588 | 1613 |
1589 for f, a in sorted(pycompat.iteritems(actions)): | 1614 for f, a in sorted(pycompat.iteritems(actions)): |
1590 m, args, msg = a | 1615 m, args, msg = a |
1616 if m == ACTION_GET_OTHER_AND_STORE: | |
1617 m = ACTION_GET | |
1591 repo.ui.debug(b' %s: %s -> %s\n' % (f, msg, m)) | 1618 repo.ui.debug(b' %s: %s -> %s\n' % (f, msg, m)) |
1592 if f in fbids: | 1619 if f in fbids: |
1593 d = fbids[f] | 1620 d = fbids[f] |
1594 if m in d: | 1621 if m in d: |
1595 d[m].append(a) | 1622 d[m].append(a) |
1811 ACTION_MERGE, | 1838 ACTION_MERGE, |
1812 ACTION_EXEC, | 1839 ACTION_EXEC, |
1813 ACTION_KEEP, | 1840 ACTION_KEEP, |
1814 ACTION_PATH_CONFLICT, | 1841 ACTION_PATH_CONFLICT, |
1815 ACTION_PATH_CONFLICT_RESOLVE, | 1842 ACTION_PATH_CONFLICT_RESOLVE, |
1843 ACTION_GET_OTHER_AND_STORE, | |
1816 ) | 1844 ) |
1817 } | 1845 } |
1818 | 1846 |
1819 | 1847 |
1820 def applyupdates( | 1848 def applyupdates( |
1833 | 1861 |
1834 _prefetchfiles(repo, mctx, actions) | 1862 _prefetchfiles(repo, mctx, actions) |
1835 | 1863 |
1836 updated, merged, removed = 0, 0, 0 | 1864 updated, merged, removed = 0, 0, 0 |
1837 ms = mergestate.clean(repo, wctx.p1().node(), mctx.node(), labels) | 1865 ms = mergestate.clean(repo, wctx.p1().node(), mctx.node(), labels) |
1866 | |
1867 # add ACTION_GET_OTHER_AND_STORE to mergestate | |
1868 for e in actions[ACTION_GET_OTHER_AND_STORE]: | |
1869 ms.addmergedother(e[0]) | |
1870 | |
1838 moves = [] | 1871 moves = [] |
1839 for m, l in actions.items(): | 1872 for m, l in actions.items(): |
1840 l.sort() | 1873 l.sort() |
1841 | 1874 |
1842 # 'cd' and 'dc' actions are treated like other merge conflicts | 1875 # 'cd' and 'dc' actions are treated like other merge conflicts |
2413 ACTION_GET, | 2446 ACTION_GET, |
2414 ACTION_KEEP, | 2447 ACTION_KEEP, |
2415 ACTION_EXEC, | 2448 ACTION_EXEC, |
2416 ACTION_REMOVE, | 2449 ACTION_REMOVE, |
2417 ACTION_PATH_CONFLICT_RESOLVE, | 2450 ACTION_PATH_CONFLICT_RESOLVE, |
2451 ACTION_GET_OTHER_AND_STORE, | |
2418 ): | 2452 ): |
2419 msg = _(b"conflicting changes") | 2453 msg = _(b"conflicting changes") |
2420 hint = _(b"commit or update --clean to discard changes") | 2454 hint = _(b"commit or update --clean to discard changes") |
2421 raise error.Abort(msg, hint=hint) | 2455 raise error.Abort(msg, hint=hint) |
2422 | 2456 |
2475 for f, (m, args, msg) in pycompat.iteritems(actionbyfile): | 2509 for f, (m, args, msg) in pycompat.iteritems(actionbyfile): |
2476 if m not in actions: | 2510 if m not in actions: |
2477 actions[m] = [] | 2511 actions[m] = [] |
2478 actions[m].append((f, args, msg)) | 2512 actions[m].append((f, args, msg)) |
2479 | 2513 |
2514 # ACTION_GET_OTHER_AND_STORE is a ACTION_GET + store in mergestate | |
2515 for e in actions[ACTION_GET_OTHER_AND_STORE]: | |
2516 actions[ACTION_GET].append(e) | |
2517 | |
2480 if not util.fscasesensitive(repo.path): | 2518 if not util.fscasesensitive(repo.path): |
2481 # check collision between files only in p2 for clean update | 2519 # check collision between files only in p2 for clean update |
2482 if not branchmerge and ( | 2520 if not branchmerge and ( |
2483 force or not wc.dirty(missing=True, branch=False) | 2521 force or not wc.dirty(missing=True, branch=False) |
2484 ): | 2522 ): |