Changeset View
Changeset View
Standalone View
Standalone View
mercurial/mergestate.py
Show First 20 Lines • Show All 42 Lines • ▼ Show 20 Line(s) | |||||
#### | #### | ||||
# merge records which records metadata about a current merge | # merge records which records metadata about a current merge | ||||
# exists only once in a mergestate | # exists only once in a mergestate | ||||
##### | ##### | ||||
RECORD_LOCAL = b'L' | RECORD_LOCAL = b'L' | ||||
RECORD_OTHER = b'O' | RECORD_OTHER = b'O' | ||||
# record merge labels | # record merge labels | ||||
RECORD_LABELS = b'l' | RECORD_LABELS = b'l' | ||||
# store info about merge driver used and it's state | |||||
RECORD_MERGE_DRIVER_STATE = b'm' | |||||
##### | ##### | ||||
# record extra information about files, with one entry containing info about one | # record extra information about files, with one entry containing info about one | ||||
# file. Hence, multiple of them can exists | # file. Hence, multiple of them can exists | ||||
##### | ##### | ||||
RECORD_FILE_VALUES = b'f' | RECORD_FILE_VALUES = b'f' | ||||
##### | ##### | ||||
# merge records which represents state of individual merges of files/folders | # merge records which represents state of individual merges of files/folders | ||||
# These are top level records for each entry containing merge related info. | # These are top level records for each entry containing merge related info. | ||||
# Each record of these has info about one file. Hence multiple of them can | # Each record of these has info about one file. Hence multiple of them can | ||||
# exists | # exists | ||||
##### | ##### | ||||
RECORD_MERGED = b'F' | RECORD_MERGED = b'F' | ||||
RECORD_CHANGEDELETE_CONFLICT = b'C' | RECORD_CHANGEDELETE_CONFLICT = b'C' | ||||
RECORD_MERGE_DRIVER_MERGE = b'D' | |||||
# the path was dir on one side of merge and file on another | # the path was dir on one side of merge and file on another | ||||
RECORD_PATH_CONFLICT = b'P' | RECORD_PATH_CONFLICT = b'P' | ||||
##### | ##### | ||||
# possible state which a merge entry can have. These are stored inside top-level | # possible state which a merge entry can have. These are stored inside top-level | ||||
# merge records mentioned just above. | # merge records mentioned just above. | ||||
##### | ##### | ||||
MERGE_RECORD_UNRESOLVED = b'u' | MERGE_RECORD_UNRESOLVED = b'u' | ||||
MERGE_RECORD_RESOLVED = b'r' | MERGE_RECORD_RESOLVED = b'r' | ||||
MERGE_RECORD_UNRESOLVED_PATH = b'pu' | MERGE_RECORD_UNRESOLVED_PATH = b'pu' | ||||
MERGE_RECORD_RESOLVED_PATH = b'pr' | MERGE_RECORD_RESOLVED_PATH = b'pr' | ||||
MERGE_RECORD_DRIVER_RESOLVED = b'd' | |||||
# represents that the file was automatically merged in favor | # represents that the file was automatically merged in favor | ||||
# of other version. This info is used on commit. | # of other version. This info is used on commit. | ||||
# This is now deprecated and commit related information is now | # This is now deprecated and commit related information is now | ||||
# stored in RECORD_FILE_VALUES | # stored in RECORD_FILE_VALUES | ||||
MERGE_RECORD_MERGED_OTHER = b'o' | MERGE_RECORD_MERGED_OTHER = b'o' | ||||
##### | ##### | ||||
# top level record which stores other unknown records. Multiple of these can | # top level record which stores other unknown records. Multiple of these can | ||||
# exists | # exists | ||||
##### | ##### | ||||
RECORD_OVERRIDE = b't' | RECORD_OVERRIDE = b't' | ||||
##### | ##### | ||||
# possible states which a merge driver can have. These are stored inside a | |||||
# RECORD_MERGE_DRIVER_STATE entry | |||||
##### | |||||
MERGE_DRIVER_STATE_UNMARKED = b'u' | |||||
MERGE_DRIVER_STATE_MARKED = b'm' | |||||
MERGE_DRIVER_STATE_SUCCESS = b's' | |||||
##### | |||||
# legacy records which are no longer used but kept to prevent breaking BC | # legacy records which are no longer used but kept to prevent breaking BC | ||||
##### | ##### | ||||
# This record was release in 5.4 and usage was removed in 5.5 | # This record was release in 5.4 and usage was removed in 5.5 | ||||
LEGACY_RECORD_RESOLVED_OTHER = b'R' | LEGACY_RECORD_RESOLVED_OTHER = b'R' | ||||
# This record was release in 3.7 and usage was removed in 5.6 | |||||
LEGACY_RECORD_DRIVER_RESOLVED = b'd' | |||||
# This record was release in 3.7 and usage was removed in 5.6 | |||||
LEGACY_MERGE_DRIVER_STATE = b'm' | |||||
# This record was release in 3.7 and usage was removed in 5.6 | |||||
LEGACY_MERGE_DRIVER_MERGE = b'D' | |||||
ACTION_FORGET = b'f' | ACTION_FORGET = b'f' | ||||
ACTION_REMOVE = b'r' | ACTION_REMOVE = b'r' | ||||
ACTION_ADD = b'a' | ACTION_ADD = b'a' | ||||
ACTION_GET = b'g' | ACTION_GET = b'g' | ||||
ACTION_PATH_CONFLICT = b'p' | ACTION_PATH_CONFLICT = b'p' | ||||
ACTION_PATH_CONFLICT_RESOLVE = b'pr' | ACTION_PATH_CONFLICT_RESOLVE = b'pr' | ||||
Show All 28 Lines | class _mergestate_base(object): | ||||
lowercase, the record can be safely ignored. | lowercase, the record can be safely ignored. | ||||
Currently known records: | Currently known records: | ||||
L: the node of the "local" part of the merge (hexified version) | L: the node of the "local" part of the merge (hexified version) | ||||
O: the node of the "other" part of the merge (hexified version) | O: the node of the "other" part of the merge (hexified version) | ||||
F: a file to be merged entry | F: a file to be merged entry | ||||
C: a change/delete or delete/change conflict | C: a change/delete or delete/change conflict | ||||
D: a file that the external merge driver will merge internally | |||||
(experimental) | |||||
P: a path conflict (file vs directory) | P: a path conflict (file vs directory) | ||||
m: the external merge driver defined for this merge plus its run state | |||||
(experimental) | |||||
f: a (filename, dictionary) tuple of optional values for a given file | f: a (filename, dictionary) tuple of optional values for a given file | ||||
l: the labels for the parts of the merge. | l: the labels for the parts of the merge. | ||||
Merge driver run states (experimental): | |||||
u: driver-resolved files unmarked -- needs to be run next time we're about | |||||
to resolve or commit | |||||
m: driver-resolved files marked -- only needs to be run before commit | |||||
s: success/skipped -- does not need to be run any more | |||||
Merge record states (stored in self._state, indexed by filename): | Merge record states (stored in self._state, indexed by filename): | ||||
u: unresolved conflict | u: unresolved conflict | ||||
r: resolved conflict | r: resolved conflict | ||||
pu: unresolved path conflict (file conflicts with directory) | pu: unresolved path conflict (file conflicts with directory) | ||||
pr: resolved path conflict | pr: resolved path conflict | ||||
d: driver-resolved conflict | |||||
The resolve command transitions between 'u' and 'r' for conflicts and | The resolve command transitions between 'u' and 'r' for conflicts and | ||||
'pu' and 'pr' for path conflicts. | 'pu' and 'pr' for path conflicts. | ||||
''' | ''' | ||||
def __init__(self, repo): | def __init__(self, repo): | ||||
"""Initialize the merge state. | """Initialize the merge state. | ||||
Do not use this directly! Instead call read() or clean().""" | Do not use this directly! Instead call read() or clean().""" | ||||
self._repo = repo | self._repo = repo | ||||
self._state = {} | self._state = {} | ||||
self._stateextras = collections.defaultdict(dict) | self._stateextras = collections.defaultdict(dict) | ||||
self._local = None | self._local = None | ||||
self._other = None | self._other = None | ||||
self._labels = None | self._labels = None | ||||
self._readmergedriver = None | |||||
self._mdstate = MERGE_DRIVER_STATE_UNMARKED | |||||
# contains a mapping of form: | # contains a mapping of form: | ||||
# {filename : (merge_return_value, action_to_be_performed} | # {filename : (merge_return_value, action_to_be_performed} | ||||
# these are results of re-running merge process | # these are results of re-running merge process | ||||
# this dict is used to perform actions on dirstate caused by re-running | # this dict is used to perform actions on dirstate caused by re-running | ||||
# the merge | # the merge | ||||
self._results = {} | self._results = {} | ||||
self._dirty = False | self._dirty = False | ||||
def reset(self): | def reset(self): | ||||
pass | pass | ||||
def start(self, node, other, labels=None): | def start(self, node, other, labels=None): | ||||
self._local = node | self._local = node | ||||
self._other = other | self._other = other | ||||
self._labels = labels | self._labels = labels | ||||
if self.mergedriver: | |||||
self._mdstate = MERGE_DRIVER_STATE_SUCCESS | |||||
@util.propertycache | |||||
def mergedriver(self): | |||||
# protect against the following: | |||||
# - A configures a malicious merge driver in their hgrc, then | |||||
# pauses the merge | |||||
# - A edits their hgrc to remove references to the merge driver | |||||
# - A gives a copy of their entire repo, including .hg, to B | |||||
# - B inspects .hgrc and finds it to be clean | |||||
# - B then continues the merge and the malicious merge driver | |||||
# gets invoked | |||||
configmergedriver = self._repo.ui.config( | |||||
b'experimental', b'mergedriver' | |||||
) | |||||
if ( | |||||
self._readmergedriver is not None | |||||
and self._readmergedriver != configmergedriver | |||||
): | |||||
raise error.ConfigError( | |||||
_(b"merge driver changed since merge started"), | |||||
hint=_(b"revert merge driver change or abort merge"), | |||||
) | |||||
return configmergedriver | |||||
@util.propertycache | @util.propertycache | ||||
def local(self): | def local(self): | ||||
if self._local is None: | if self._local is None: | ||||
msg = b"local accessed but self._local isn't set" | msg = b"local accessed but self._local isn't set" | ||||
raise error.ProgrammingError(msg) | raise error.ProgrammingError(msg) | ||||
return self._local | return self._local | ||||
▲ Show 20 Lines • Show All 89 Lines • ▼ Show 20 Line(s) | class _mergestate_base(object): | ||||
def files(self): | def files(self): | ||||
return self._state.keys() | return self._state.keys() | ||||
def mark(self, dfile, state): | def mark(self, dfile, state): | ||||
self._state[dfile][0] = state | self._state[dfile][0] = state | ||||
self._dirty = True | self._dirty = True | ||||
def mdstate(self): | |||||
return self._mdstate | |||||
def unresolved(self): | def unresolved(self): | ||||
"""Obtain the paths of unresolved files.""" | """Obtain the paths of unresolved files.""" | ||||
for f, entry in pycompat.iteritems(self._state): | for f, entry in pycompat.iteritems(self._state): | ||||
if entry[0] in ( | if entry[0] in ( | ||||
MERGE_RECORD_UNRESOLVED, | MERGE_RECORD_UNRESOLVED, | ||||
MERGE_RECORD_UNRESOLVED_PATH, | MERGE_RECORD_UNRESOLVED_PATH, | ||||
): | ): | ||||
yield f | yield f | ||||
def driverresolved(self): | |||||
"""Obtain the paths of driver-resolved files.""" | |||||
for f, entry in self._state.items(): | |||||
if entry[0] == MERGE_RECORD_DRIVER_RESOLVED: | |||||
yield f | |||||
def extras(self, filename): | def extras(self, filename): | ||||
return self._stateextras[filename] | return self._stateextras[filename] | ||||
def _resolve(self, preresolve, dfile, wctx): | def _resolve(self, preresolve, dfile, wctx): | ||||
"""rerun merge process for file path `dfile`. | """rerun merge process for file path `dfile`. | ||||
Returns whether the merge was completed and the return value of merge | Returns whether the merge was completed and the return value of merge | ||||
obtained from filemerge._filemerge(). | obtained from filemerge._filemerge(). | ||||
""" | """ | ||||
if self[dfile] in (MERGE_RECORD_RESOLVED, MERGE_RECORD_DRIVER_RESOLVED): | if self[dfile] in ( | ||||
MERGE_RECORD_RESOLVED, | |||||
LEGACY_RECORD_DRIVER_RESOLVED, | |||||
): | |||||
return True, 0 | return True, 0 | ||||
stateentry = self._state[dfile] | stateentry = self._state[dfile] | ||||
state, localkey, lfile, afile, anode, ofile, onode, flags = stateentry | state, localkey, lfile, afile, anode, ofile, onode, flags = stateentry | ||||
octx = self._repo[self._other] | octx = self._repo[self._other] | ||||
extras = self.extras(dfile) | extras = self.extras(dfile) | ||||
anccommitnode = extras.get(b'ancestorlinknode') | anccommitnode = extras.get(b'ancestorlinknode') | ||||
if anccommitnode: | if anccommitnode: | ||||
actx = self._repo[anccommitnode] | actx = self._repo[anccommitnode] | ||||
▲ Show 20 Lines • Show All 115 Lines • ▼ Show 20 Line(s) | def actions(self): | ||||
ACTION_ADD_MODIFIED: [], | ACTION_ADD_MODIFIED: [], | ||||
ACTION_GET: [], | ACTION_GET: [], | ||||
} | } | ||||
for f, (r, action) in pycompat.iteritems(self._results): | for f, (r, action) in pycompat.iteritems(self._results): | ||||
if action is not None: | if action is not None: | ||||
actions[action].append((f, None, b"merge result")) | actions[action].append((f, None, b"merge result")) | ||||
return actions | return actions | ||||
def queueremove(self, f): | |||||
"""queues a file to be removed from the dirstate | |||||
Meant for use by custom merge drivers.""" | |||||
self._results[f] = 0, ACTION_REMOVE | |||||
def queueadd(self, f): | |||||
"""queues a file to be added to the dirstate | |||||
Meant for use by custom merge drivers.""" | |||||
self._results[f] = 0, ACTION_ADD | |||||
def queueget(self, f): | |||||
"""queues a file to be marked modified in the dirstate | |||||
Meant for use by custom merge drivers.""" | |||||
self._results[f] = 0, ACTION_GET | |||||
class mergestate(_mergestate_base): | class mergestate(_mergestate_base): | ||||
statepathv1 = b'merge/state' | statepathv1 = b'merge/state' | ||||
statepathv2 = b'merge/state2' | statepathv2 = b'merge/state2' | ||||
@staticmethod | @staticmethod | ||||
def clean(repo): | def clean(repo): | ||||
Show All 11 Lines | def read(repo): | ||||
return ms | return ms | ||||
def _read(self): | def _read(self): | ||||
"""Analyse each record content to restore a serialized state from disk | """Analyse each record content to restore a serialized state from disk | ||||
This function process "record" entry produced by the de-serialization | This function process "record" entry produced by the de-serialization | ||||
of on disk file. | of on disk file. | ||||
""" | """ | ||||
self._mdstate = MERGE_DRIVER_STATE_SUCCESS | |||||
unsupported = set() | unsupported = set() | ||||
records = self._readrecords() | records = self._readrecords() | ||||
for rtype, record in records: | for rtype, record in records: | ||||
if rtype == RECORD_LOCAL: | if rtype == RECORD_LOCAL: | ||||
self._local = bin(record) | self._local = bin(record) | ||||
elif rtype == RECORD_OTHER: | elif rtype == RECORD_OTHER: | ||||
self._other = bin(record) | self._other = bin(record) | ||||
elif rtype == RECORD_MERGE_DRIVER_STATE: | elif rtype == LEGACY_MERGE_DRIVER_STATE: | ||||
bits = record.split(b'\0', 1) | pass | ||||
mdstate = bits[1] | |||||
if len(mdstate) != 1 or mdstate not in ( | |||||
MERGE_DRIVER_STATE_UNMARKED, | |||||
MERGE_DRIVER_STATE_MARKED, | |||||
MERGE_DRIVER_STATE_SUCCESS, | |||||
): | |||||
# the merge driver should be idempotent, so just rerun it | |||||
mdstate = MERGE_DRIVER_STATE_UNMARKED | |||||
self._readmergedriver = bits[0] | |||||
self._mdstate = mdstate | |||||
elif rtype in ( | elif rtype in ( | ||||
RECORD_MERGED, | RECORD_MERGED, | ||||
RECORD_CHANGEDELETE_CONFLICT, | RECORD_CHANGEDELETE_CONFLICT, | ||||
RECORD_PATH_CONFLICT, | RECORD_PATH_CONFLICT, | ||||
RECORD_MERGE_DRIVER_MERGE, | LEGACY_MERGE_DRIVER_MERGE, | ||||
LEGACY_RECORD_RESOLVED_OTHER, | LEGACY_RECORD_RESOLVED_OTHER, | ||||
): | ): | ||||
bits = record.split(b'\0') | bits = record.split(b'\0') | ||||
# merge entry type MERGE_RECORD_MERGED_OTHER is deprecated | # merge entry type MERGE_RECORD_MERGED_OTHER is deprecated | ||||
# and we now store related information in _stateextras, so | # and we now store related information in _stateextras, so | ||||
# lets write to _stateextras directly | # lets write to _stateextras directly | ||||
if bits[1] == MERGE_RECORD_MERGED_OTHER: | if bits[1] == MERGE_RECORD_MERGED_OTHER: | ||||
self._stateextras[bits[0]][b'filenode-source'] = b'other' | self._stateextras[bits[0]][b'filenode-source'] = b'other' | ||||
▲ Show 20 Lines • Show All 133 Lines • ▼ Show 20 Line(s) | def commit(self): | ||||
records = self._makerecords() | records = self._makerecords() | ||||
self._writerecords(records) | self._writerecords(records) | ||||
self._dirty = False | self._dirty = False | ||||
def _makerecords(self): | def _makerecords(self): | ||||
records = [] | records = [] | ||||
records.append((RECORD_LOCAL, hex(self._local))) | records.append((RECORD_LOCAL, hex(self._local))) | ||||
records.append((RECORD_OTHER, hex(self._other))) | records.append((RECORD_OTHER, hex(self._other))) | ||||
if self.mergedriver: | |||||
records.append( | |||||
( | |||||
RECORD_MERGE_DRIVER_STATE, | |||||
b'\0'.join([self.mergedriver, self._mdstate]), | |||||
) | |||||
) | |||||
# Write out state items. In all cases, the value of the state map entry | # Write out state items. In all cases, the value of the state map entry | ||||
# is written as the contents of the record. The record type depends on | # is written as the contents of the record. The record type depends on | ||||
# the type of state that is stored, and capital-letter records are used | # the type of state that is stored, and capital-letter records are used | ||||
# to prevent older versions of Mercurial that do not support the feature | # to prevent older versions of Mercurial that do not support the feature | ||||
# from loading them. | # from loading them. | ||||
for filename, v in pycompat.iteritems(self._state): | for filename, v in pycompat.iteritems(self._state): | ||||
if v[0] == MERGE_RECORD_DRIVER_RESOLVED: | if v[0] in ( | ||||
# Driver-resolved merge. These are stored in 'D' records. | |||||
records.append( | |||||
(RECORD_MERGE_DRIVER_MERGE, b'\0'.join([filename] + v)) | |||||
) | |||||
elif v[0] in ( | |||||
MERGE_RECORD_UNRESOLVED_PATH, | MERGE_RECORD_UNRESOLVED_PATH, | ||||
MERGE_RECORD_RESOLVED_PATH, | MERGE_RECORD_RESOLVED_PATH, | ||||
): | ): | ||||
# Path conflicts. These are stored in 'P' records. The current | # Path conflicts. These are stored in 'P' records. The current | ||||
# resolution state ('pu' or 'pr') is stored within the record. | # resolution state ('pu' or 'pr') is stored within the record. | ||||
records.append( | records.append( | ||||
(RECORD_PATH_CONFLICT, b'\0'.join([filename] + v)) | (RECORD_PATH_CONFLICT, b'\0'.join([filename] + v)) | ||||
) | ) | ||||
▲ Show 20 Lines • Show All 69 Lines • ▼ Show 20 Line(s) | def __init__(self, repo): | ||||
self._backups = {} | self._backups = {} | ||||
def _make_backup(self, fctx, localkey): | def _make_backup(self, fctx, localkey): | ||||
self._backups[localkey] = fctx.data() | self._backups[localkey] = fctx.data() | ||||
def _restore_backup(self, fctx, localkey, flags): | def _restore_backup(self, fctx, localkey, flags): | ||||
fctx.write(self._backups[localkey], flags) | fctx.write(self._backups[localkey], flags) | ||||
@util.propertycache | |||||
def mergedriver(self): | |||||
configmergedriver = self._repo.ui.config( | |||||
b'experimental', b'mergedriver' | |||||
) | |||||
if configmergedriver: | |||||
raise error.InMemoryMergeConflictsError( | |||||
b"in-memory merge does not support mergedriver" | |||||
) | |||||
return None | |||||
def recordupdates(repo, actions, branchmerge, getfiledata): | def recordupdates(repo, actions, branchmerge, getfiledata): | ||||
"""record merge actions to the dirstate""" | """record merge actions to the dirstate""" | ||||
# remove (must come first) | # remove (must come first) | ||||
for f, args, msg in actions.get(ACTION_REMOVE, []): | for f, args, msg in actions.get(ACTION_REMOVE, []): | ||||
if branchmerge: | if branchmerge: | ||||
repo.dirstate.remove(f) | repo.dirstate.remove(f) | ||||
else: | else: | ||||
▲ Show 20 Lines • Show All 91 Lines • Show Last 20 Lines |