mergestate.py
866 lines
| 29.7 KiB
| text/x-python
|
PythonLexer
/ mercurial / mergestate.py
|
r45383 | from __future__ import absolute_import | ||
|
r45941 | import collections | ||
|
r45383 | import errno | ||
import shutil | ||||
import struct | ||||
from .i18n import _ | ||||
from .node import ( | ||||
bin, | ||||
hex, | ||||
|
r47601 | nullrev, | ||
|
r45383 | ) | ||
from . import ( | ||||
error, | ||||
filemerge, | ||||
pycompat, | ||||
util, | ||||
) | ||||
from .utils import hashutil | ||||
_pack = struct.pack | ||||
_unpack = struct.unpack | ||||
def _droponode(data): | ||||
# used for compatibility for v1 | ||||
bits = data.split(b'\0') | ||||
bits = bits[:-2] + bits[-1:] | ||||
return b'\0'.join(bits) | ||||
|
r45407 | def _filectxorabsent(hexnode, ctx, f): | ||
|
r47771 | if hexnode == ctx.repo().nodeconstants.nullhex: | ||
|
r45407 | return filemerge.absentfilectx(ctx, f) | ||
else: | ||||
return ctx[f] | ||||
|
r45383 | # Merge state record types. See ``mergestate`` docs for more. | ||
|
r45723 | |||
#### | ||||
# merge records which records metadata about a current merge | ||||
# exists only once in a mergestate | ||||
##### | ||||
|
r45383 | RECORD_LOCAL = b'L' | ||
RECORD_OTHER = b'O' | ||||
|
r45721 | # record merge labels | ||
RECORD_LABELS = b'l' | ||||
|
r45723 | ##### | ||
# record extra information about files, with one entry containing info about one | ||||
# file. Hence, multiple of them can exists | ||||
##### | ||||
RECORD_FILE_VALUES = b'f' | ||||
##### | ||||
# merge records which represents state of individual merges of files/folders | ||||
# 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 | ||||
# exists | ||||
##### | ||||
|
r45383 | RECORD_MERGED = b'F' | ||
RECORD_CHANGEDELETE_CONFLICT = b'C' | ||||
|
r45723 | # the path was dir on one side of merge and file on another | ||
|
r45383 | RECORD_PATH_CONFLICT = b'P' | ||
|
r45721 | |||
|
r45723 | ##### | ||
# possible state which a merge entry can have. These are stored inside top-level | ||||
# merge records mentioned just above. | ||||
##### | ||||
|
r45383 | MERGE_RECORD_UNRESOLVED = b'u' | ||
MERGE_RECORD_RESOLVED = b'r' | ||||
MERGE_RECORD_UNRESOLVED_PATH = b'pu' | ||||
MERGE_RECORD_RESOLVED_PATH = b'pr' | ||||
# represents that the file was automatically merged in favor | ||||
# of other version. This info is used on commit. | ||||
|
r45942 | # This is now deprecated and commit related information is now | ||
# stored in RECORD_FILE_VALUES | ||||
|
r45383 | MERGE_RECORD_MERGED_OTHER = b'o' | ||
|
r45723 | ##### | ||
# top level record which stores other unknown records. Multiple of these can | ||||
# exists | ||||
##### | ||||
RECORD_OVERRIDE = b't' | ||||
##### | ||||
|
r45859 | # 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 | ||||
LEGACY_RECORD_RESOLVED_OTHER = b'R' | ||||
|
r46091 | # 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' | ||||
|
r45859 | |||
|
r45723 | |||
|
r45383 | ACTION_FORGET = b'f' | ||
ACTION_REMOVE = b'r' | ||||
ACTION_ADD = b'a' | ||||
ACTION_GET = b'g' | ||||
ACTION_PATH_CONFLICT = b'p' | ||||
ACTION_PATH_CONFLICT_RESOLVE = b'pr' | ||||
ACTION_ADD_MODIFIED = b'am' | ||||
ACTION_CREATED = b'c' | ||||
ACTION_DELETED_CHANGED = b'dc' | ||||
ACTION_CHANGED_DELETED = b'cd' | ||||
ACTION_MERGE = b'm' | ||||
ACTION_LOCAL_DIR_RENAME_GET = b'dg' | ||||
ACTION_DIR_RENAME_MOVE_LOCAL = b'dm' | ||||
ACTION_KEEP = b'k' | ||||
|
r46039 | # the file was absent on local side before merge and we should | ||
# keep it absent (absent means file not present, it can be a result | ||||
# of file deletion, rename etc.) | ||||
ACTION_KEEP_ABSENT = b'ka' | ||||
|
r46095 | # the file is absent on the ancestor and remote side of the merge | ||
# hence this file is new and we should keep it | ||||
ACTION_KEEP_NEW = b'kn' | ||||
|
r45383 | ACTION_EXEC = b'e' | ||
ACTION_CREATED_MERGE = b'cm' | ||||
|
r46096 | # actions which are no op | ||
NO_OP_ACTIONS = ( | ||||
ACTION_KEEP, | ||||
ACTION_KEEP_ABSENT, | ||||
ACTION_KEEP_NEW, | ||||
) | ||||
|
r45383 | |||
|
r46070 | class _mergestate_base(object): | ||
|
r46554 | """track 3-way merge state of individual files | ||
|
r45383 | |||
The merge state is stored on disk when needed. Two files are used: one with | ||||
an old format (version 1), and one with a new format (version 2). Version 2 | ||||
stores a superset of the data in version 1, including new kinds of records | ||||
in the future. For more about the new format, see the documentation for | ||||
`_readrecordsv2`. | ||||
Each record can contain arbitrary content, and has an associated type. This | ||||
`type` should be a letter. If `type` is uppercase, the record is mandatory: | ||||
versions of Mercurial that don't support it should abort. If `type` is | ||||
lowercase, the record can be safely ignored. | ||||
Currently known records: | ||||
L: the node of the "local" 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 | ||||
C: a change/delete or delete/change conflict | ||||
P: a path conflict (file vs directory) | ||||
f: a (filename, dictionary) tuple of optional values for a given file | ||||
l: the labels for the parts of the merge. | ||||
Merge record states (stored in self._state, indexed by filename): | ||||
u: unresolved conflict | ||||
r: resolved conflict | ||||
pu: unresolved path conflict (file conflicts with directory) | ||||
pr: resolved path conflict | ||||
|
r46306 | o: file was merged in favor of other parent of merge (DEPRECATED) | ||
|
r45383 | |||
The resolve command transitions between 'u' and 'r' for conflicts and | ||||
'pu' and 'pr' for path conflicts. | ||||
|
r46554 | """ | ||
|
r45383 | |||
def __init__(self, repo): | ||||
"""Initialize the merge state. | ||||
Do not use this directly! Instead call read() or clean().""" | ||||
self._repo = repo | ||||
|
r46068 | self._state = {} | ||
self._stateextras = collections.defaultdict(dict) | ||||
self._local = None | ||||
self._other = None | ||||
self._labels = None | ||||
# contains a mapping of form: | ||||
# {filename : (merge_return_value, action_to_be_performed} | ||||
# these are results of re-running merge process | ||||
# this dict is used to perform actions on dirstate caused by re-running | ||||
# the merge | ||||
self._results = {} | ||||
|
r45383 | self._dirty = False | ||
|
r46064 | def reset(self): | ||
|
r46072 | pass | ||
|
r46066 | |||
def start(self, node, other, labels=None): | ||||
self._local = node | ||||
self._other = other | ||||
self._labels = labels | ||||
|
r45383 | |||
@util.propertycache | ||||
def local(self): | ||||
if self._local is None: | ||||
msg = b"local accessed but self._local isn't set" | ||||
raise error.ProgrammingError(msg) | ||||
return self._local | ||||
@util.propertycache | ||||
def localctx(self): | ||||
return self._repo[self.local] | ||||
@util.propertycache | ||||
def other(self): | ||||
if self._other is None: | ||||
msg = b"other accessed but self._other isn't set" | ||||
raise error.ProgrammingError(msg) | ||||
return self._other | ||||
@util.propertycache | ||||
def otherctx(self): | ||||
return self._repo[self.other] | ||||
def active(self): | ||||
"""Whether mergestate is active. | ||||
Returns True if there appears to be mergestate. This is a rough proxy | ||||
for "is a merge in progress." | ||||
""" | ||||
return bool(self._local) or bool(self._state) | ||||
def commit(self): | ||||
"""Write current state on disk (if necessary)""" | ||||
@staticmethod | ||||
def getlocalkey(path): | ||||
"""hash the path of a local file context for storage in the .hg/merge | ||||
directory.""" | ||||
return hex(hashutil.sha1(path).digest()) | ||||
|
r46069 | def _make_backup(self, fctx, localkey): | ||
|
r46070 | raise NotImplementedError() | ||
|
r46069 | |||
def _restore_backup(self, fctx, localkey, flags): | ||||
|
r46070 | raise NotImplementedError() | ||
|
r46069 | |||
|
r45383 | def add(self, fcl, fco, fca, fd): | ||
"""add a new (potentially?) conflicting file the merge state | ||||
fcl: file context for local, | ||||
fco: file context for remote, | ||||
fca: file context for ancestors, | ||||
fd: file path of the resulting merge. | ||||
note: also write the local version to the `.hg/merge` directory. | ||||
""" | ||||
if fcl.isabsent(): | ||||
|
r47771 | localkey = self._repo.nodeconstants.nullhex | ||
|
r45383 | else: | ||
localkey = mergestate.getlocalkey(fcl.path()) | ||||
|
r46069 | self._make_backup(fcl, localkey) | ||
|
r45383 | self._state[fd] = [ | ||
MERGE_RECORD_UNRESOLVED, | ||||
localkey, | ||||
fcl.path(), | ||||
fca.path(), | ||||
hex(fca.filenode()), | ||||
fco.path(), | ||||
hex(fco.filenode()), | ||||
fcl.flags(), | ||||
] | ||||
|
r46157 | self._stateextras[fd][b'ancestorlinknode'] = hex(fca.node()) | ||
|
r45383 | self._dirty = True | ||
|
r45719 | def addpathconflict(self, path, frename, forigin): | ||
|
r45383 | """add a new conflicting path to the merge state | ||
path: the path that conflicts | ||||
frename: the filename the conflicting file was renamed to | ||||
forigin: origin of the file ('l' or 'r' for local/remote) | ||||
""" | ||||
self._state[path] = [MERGE_RECORD_UNRESOLVED_PATH, frename, forigin] | ||||
self._dirty = True | ||||
|
r45945 | def addcommitinfo(self, path, data): | ||
|
r46554 | """stores information which is required at commit | ||
into _stateextras""" | ||||
|
r45945 | self._stateextras[path].update(data) | ||
|
r45383 | self._dirty = True | ||
def __contains__(self, dfile): | ||||
return dfile in self._state | ||||
def __getitem__(self, dfile): | ||||
return self._state[dfile][0] | ||||
def __iter__(self): | ||||
return iter(sorted(self._state)) | ||||
def files(self): | ||||
return self._state.keys() | ||||
def mark(self, dfile, state): | ||||
self._state[dfile][0] = state | ||||
self._dirty = True | ||||
def unresolved(self): | ||||
"""Obtain the paths of unresolved files.""" | ||||
for f, entry in pycompat.iteritems(self._state): | ||||
if entry[0] in ( | ||||
MERGE_RECORD_UNRESOLVED, | ||||
MERGE_RECORD_UNRESOLVED_PATH, | ||||
): | ||||
yield f | ||||
|
r46307 | def allextras(self): | ||
|
r47856 | """return all extras information stored with the mergestate""" | ||
|
r46307 | return self._stateextras | ||
|
r45383 | def extras(self, filename): | ||
|
r47856 | """return extras stored with the mergestate for the given filename""" | ||
|
r45941 | return self._stateextras[filename] | ||
|
r45383 | |||
def _resolve(self, preresolve, dfile, wctx): | ||||
|
r45725 | """rerun merge process for file path `dfile`. | ||
Returns whether the merge was completed and the return value of merge | ||||
obtained from filemerge._filemerge(). | ||||
""" | ||||
|
r46091 | if self[dfile] in ( | ||
MERGE_RECORD_RESOLVED, | ||||
LEGACY_RECORD_DRIVER_RESOLVED, | ||||
): | ||||
|
r45383 | return True, 0 | ||
stateentry = self._state[dfile] | ||||
state, localkey, lfile, afile, anode, ofile, onode, flags = stateentry | ||||
octx = self._repo[self._other] | ||||
extras = self.extras(dfile) | ||||
anccommitnode = extras.get(b'ancestorlinknode') | ||||
if anccommitnode: | ||||
actx = self._repo[anccommitnode] | ||||
else: | ||||
actx = None | ||||
|
r45407 | fcd = _filectxorabsent(localkey, wctx, dfile) | ||
fco = _filectxorabsent(onode, octx, ofile) | ||||
|
r45383 | # TODO: move this to filectxorabsent | ||
fca = self._repo.filectx(afile, fileid=anode, changectx=actx) | ||||
# "premerge" x flags | ||||
flo = fco.flags() | ||||
fla = fca.flags() | ||||
if b'x' in flags + flo + fla and b'l' not in flags + flo + fla: | ||||
|
r47601 | if fca.rev() == nullrev and flags != flo: | ||
|
r45383 | if preresolve: | ||
self._repo.ui.warn( | ||||
_( | ||||
b'warning: cannot merge flags for %s ' | ||||
b'without common ancestor - keeping local flags\n' | ||||
) | ||||
% afile | ||||
) | ||||
elif flags == fla: | ||||
flags = flo | ||||
if preresolve: | ||||
# restore local | ||||
|
r47771 | if localkey != self._repo.nodeconstants.nullhex: | ||
|
r46069 | self._restore_backup(wctx[dfile], localkey, flags) | ||
|
r45383 | else: | ||
wctx[dfile].remove(ignoremissing=True) | ||||
|
r45725 | complete, merge_ret, deleted = filemerge.premerge( | ||
|
r45383 | self._repo, | ||
wctx, | ||||
self._local, | ||||
lfile, | ||||
fcd, | ||||
fco, | ||||
fca, | ||||
labels=self._labels, | ||||
) | ||||
else: | ||||
|
r45725 | complete, merge_ret, deleted = filemerge.filemerge( | ||
|
r45383 | self._repo, | ||
wctx, | ||||
self._local, | ||||
lfile, | ||||
fcd, | ||||
fco, | ||||
fca, | ||||
labels=self._labels, | ||||
) | ||||
|
r45725 | if merge_ret is None: | ||
# If return value of merge is None, then there are no real conflict | ||||
|
r45383 | del self._state[dfile] | ||
self._dirty = True | ||||
|
r45725 | elif not merge_ret: | ||
|
r45383 | self.mark(dfile, MERGE_RECORD_RESOLVED) | ||
if complete: | ||||
action = None | ||||
if deleted: | ||||
if fcd.isabsent(): | ||||
# dc: local picked. Need to drop if present, which may | ||||
# happen on re-resolves. | ||||
action = ACTION_FORGET | ||||
else: | ||||
# cd: remote picked (or otherwise deleted) | ||||
action = ACTION_REMOVE | ||||
else: | ||||
if fcd.isabsent(): # dc: remote picked | ||||
action = ACTION_GET | ||||
elif fco.isabsent(): # cd: local picked | ||||
if dfile in self.localctx: | ||||
action = ACTION_ADD_MODIFIED | ||||
else: | ||||
action = ACTION_ADD | ||||
# else: regular merges (no action necessary) | ||||
|
r45725 | self._results[dfile] = merge_ret, action | ||
|
r45383 | |||
|
r45725 | return complete, merge_ret | ||
|
r45383 | |||
def preresolve(self, dfile, wctx): | ||||
"""run premerge process for dfile | ||||
Returns whether the merge is complete, and the exit code.""" | ||||
return self._resolve(True, dfile, wctx) | ||||
def resolve(self, dfile, wctx): | ||||
"""run merge process (assuming premerge was run) for dfile | ||||
Returns the exit code of the merge.""" | ||||
return self._resolve(False, dfile, wctx)[1] | ||||
def counts(self): | ||||
"""return counts for updated, merged and removed files in this | ||||
session""" | ||||
updated, merged, removed = 0, 0, 0 | ||||
for r, action in pycompat.itervalues(self._results): | ||||
if r is None: | ||||
updated += 1 | ||||
elif r == 0: | ||||
if action == ACTION_REMOVE: | ||||
removed += 1 | ||||
else: | ||||
merged += 1 | ||||
return updated, merged, removed | ||||
def unresolvedcount(self): | ||||
"""get unresolved count for this merge (persistent)""" | ||||
return len(list(self.unresolved())) | ||||
def actions(self): | ||||
"""return lists of actions to perform on the dirstate""" | ||||
actions = { | ||||
ACTION_REMOVE: [], | ||||
ACTION_FORGET: [], | ||||
ACTION_ADD: [], | ||||
ACTION_ADD_MODIFIED: [], | ||||
ACTION_GET: [], | ||||
} | ||||
for f, (r, action) in pycompat.iteritems(self._results): | ||||
if action is not None: | ||||
actions[action].append((f, None, b"merge result")) | ||||
return actions | ||||
|
r46070 | class mergestate(_mergestate_base): | ||
statepathv1 = b'merge/state' | ||||
statepathv2 = b'merge/state2' | ||||
@staticmethod | ||||
def clean(repo): | ||||
"""Initialize a brand new merge state, removing any existing state on | ||||
disk.""" | ||||
ms = mergestate(repo) | ||||
ms.reset() | ||||
return ms | ||||
@staticmethod | ||||
def read(repo): | ||||
"""Initialize the merge state, reading it from disk.""" | ||||
ms = mergestate(repo) | ||||
ms._read() | ||||
return ms | ||||
def _read(self): | ||||
"""Analyse each record content to restore a serialized state from disk | ||||
This function process "record" entry produced by the de-serialization | ||||
of on disk file. | ||||
""" | ||||
unsupported = set() | ||||
records = self._readrecords() | ||||
for rtype, record in records: | ||||
if rtype == RECORD_LOCAL: | ||||
self._local = bin(record) | ||||
elif rtype == RECORD_OTHER: | ||||
self._other = bin(record) | ||||
|
r46091 | elif rtype == LEGACY_MERGE_DRIVER_STATE: | ||
pass | ||||
|
r46070 | elif rtype in ( | ||
RECORD_MERGED, | ||||
RECORD_CHANGEDELETE_CONFLICT, | ||||
RECORD_PATH_CONFLICT, | ||||
|
r46091 | LEGACY_MERGE_DRIVER_MERGE, | ||
|
r46070 | LEGACY_RECORD_RESOLVED_OTHER, | ||
): | ||||
bits = record.split(b'\0') | ||||
# merge entry type MERGE_RECORD_MERGED_OTHER is deprecated | ||||
# and we now store related information in _stateextras, so | ||||
# lets write to _stateextras directly | ||||
if bits[1] == MERGE_RECORD_MERGED_OTHER: | ||||
self._stateextras[bits[0]][b'filenode-source'] = b'other' | ||||
else: | ||||
self._state[bits[0]] = bits[1:] | ||||
elif rtype == RECORD_FILE_VALUES: | ||||
filename, rawextras = record.split(b'\0', 1) | ||||
extraparts = rawextras.split(b'\0') | ||||
extras = {} | ||||
i = 0 | ||||
while i < len(extraparts): | ||||
extras[extraparts[i]] = extraparts[i + 1] | ||||
i += 2 | ||||
self._stateextras[filename] = extras | ||||
elif rtype == RECORD_LABELS: | ||||
labels = record.split(b'\0', 2) | ||||
self._labels = [l for l in labels if len(l) > 0] | ||||
elif not rtype.islower(): | ||||
unsupported.add(rtype) | ||||
if unsupported: | ||||
raise error.UnsupportedMergeRecords(unsupported) | ||||
def _readrecords(self): | ||||
"""Read merge state from disk and return a list of record (TYPE, data) | ||||
We read data from both v1 and v2 files and decide which one to use. | ||||
V1 has been used by version prior to 2.9.1 and contains less data than | ||||
v2. We read both versions and check if no data in v2 contradicts | ||||
v1. If there is not contradiction we can safely assume that both v1 | ||||
and v2 were written at the same time and use the extract data in v2. If | ||||
there is contradiction we ignore v2 content as we assume an old version | ||||
of Mercurial has overwritten the mergestate file and left an old v2 | ||||
file around. | ||||
returns list of record [(TYPE, data), ...]""" | ||||
v1records = self._readrecordsv1() | ||||
v2records = self._readrecordsv2() | ||||
if self._v1v2match(v1records, v2records): | ||||
return v2records | ||||
else: | ||||
# v1 file is newer than v2 file, use it | ||||
# we have to infer the "other" changeset of the merge | ||||
# we cannot do better than that with v1 of the format | ||||
mctx = self._repo[None].parents()[-1] | ||||
v1records.append((RECORD_OTHER, mctx.hex())) | ||||
# add place holder "other" file node information | ||||
# nobody is using it yet so we do no need to fetch the data | ||||
# if mctx was wrong `mctx[bits[-2]]` may fails. | ||||
for idx, r in enumerate(v1records): | ||||
if r[0] == RECORD_MERGED: | ||||
bits = r[1].split(b'\0') | ||||
bits.insert(-2, b'') | ||||
v1records[idx] = (r[0], b'\0'.join(bits)) | ||||
return v1records | ||||
def _v1v2match(self, v1records, v2records): | ||||
oldv2 = set() # old format version of v2 record | ||||
for rec in v2records: | ||||
if rec[0] == RECORD_LOCAL: | ||||
oldv2.add(rec) | ||||
elif rec[0] == RECORD_MERGED: | ||||
# drop the onode data (not contained in v1) | ||||
oldv2.add((RECORD_MERGED, _droponode(rec[1]))) | ||||
for rec in v1records: | ||||
if rec not in oldv2: | ||||
return False | ||||
else: | ||||
return True | ||||
def _readrecordsv1(self): | ||||
"""read on disk merge state for version 1 file | ||||
returns list of record [(TYPE, data), ...] | ||||
Note: the "F" data from this file are one entry short | ||||
(no "other file node" entry) | ||||
""" | ||||
records = [] | ||||
try: | ||||
f = self._repo.vfs(self.statepathv1) | ||||
for i, l in enumerate(f): | ||||
if i == 0: | ||||
records.append((RECORD_LOCAL, l[:-1])) | ||||
else: | ||||
records.append((RECORD_MERGED, l[:-1])) | ||||
f.close() | ||||
except IOError as err: | ||||
if err.errno != errno.ENOENT: | ||||
raise | ||||
return records | ||||
def _readrecordsv2(self): | ||||
"""read on disk merge state for version 2 file | ||||
This format is a list of arbitrary records of the form: | ||||
[type][length][content] | ||||
`type` is a single character, `length` is a 4 byte integer, and | ||||
`content` is an arbitrary byte sequence of length `length`. | ||||
Mercurial versions prior to 3.7 have a bug where if there are | ||||
unsupported mandatory merge records, attempting to clear out the merge | ||||
state with hg update --clean or similar aborts. The 't' record type | ||||
works around that by writing out what those versions treat as an | ||||
advisory record, but later versions interpret as special: the first | ||||
character is the 'real' record type and everything onwards is the data. | ||||
Returns list of records [(TYPE, data), ...].""" | ||||
records = [] | ||||
try: | ||||
f = self._repo.vfs(self.statepathv2) | ||||
data = f.read() | ||||
off = 0 | ||||
end = len(data) | ||||
while off < end: | ||||
rtype = data[off : off + 1] | ||||
off += 1 | ||||
length = _unpack(b'>I', data[off : (off + 4)])[0] | ||||
off += 4 | ||||
record = data[off : (off + length)] | ||||
off += length | ||||
if rtype == RECORD_OVERRIDE: | ||||
rtype, record = record[0:1], record[1:] | ||||
records.append((rtype, record)) | ||||
f.close() | ||||
except IOError as err: | ||||
if err.errno != errno.ENOENT: | ||||
raise | ||||
return records | ||||
|
r46073 | def commit(self): | ||
if self._dirty: | ||||
records = self._makerecords() | ||||
self._writerecords(records) | ||||
self._dirty = False | ||||
def _makerecords(self): | ||||
records = [] | ||||
records.append((RECORD_LOCAL, hex(self._local))) | ||||
records.append((RECORD_OTHER, hex(self._other))) | ||||
# 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 | ||||
# 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 | ||||
# from loading them. | ||||
for filename, v in pycompat.iteritems(self._state): | ||||
|
r46091 | if v[0] in ( | ||
|
r46073 | MERGE_RECORD_UNRESOLVED_PATH, | ||
MERGE_RECORD_RESOLVED_PATH, | ||||
): | ||||
# Path conflicts. These are stored in 'P' records. The current | ||||
# resolution state ('pu' or 'pr') is stored within the record. | ||||
records.append( | ||||
(RECORD_PATH_CONFLICT, b'\0'.join([filename] + v)) | ||||
) | ||||
|
r47771 | elif ( | ||
v[1] == self._repo.nodeconstants.nullhex | ||||
or v[6] == self._repo.nodeconstants.nullhex | ||||
): | ||||
|
r46073 | # Change/Delete or Delete/Change conflicts. These are stored in | ||
# 'C' records. v[1] is the local file, and is nullhex when the | ||||
# file is deleted locally ('dc'). v[6] is the remote file, and | ||||
# is nullhex when the file is deleted remotely ('cd'). | ||||
records.append( | ||||
(RECORD_CHANGEDELETE_CONFLICT, b'\0'.join([filename] + v)) | ||||
) | ||||
else: | ||||
# Normal files. These are stored in 'F' records. | ||||
records.append((RECORD_MERGED, b'\0'.join([filename] + v))) | ||||
for filename, extras in sorted(pycompat.iteritems(self._stateextras)): | ||||
rawextras = b'\0'.join( | ||||
b'%s\0%s' % (k, v) for k, v in pycompat.iteritems(extras) | ||||
) | ||||
records.append( | ||||
(RECORD_FILE_VALUES, b'%s\0%s' % (filename, rawextras)) | ||||
) | ||||
if self._labels is not None: | ||||
labels = b'\0'.join(self._labels) | ||||
records.append((RECORD_LABELS, labels)) | ||||
return records | ||||
|
r46070 | def _writerecords(self, records): | ||
"""Write current state on disk (both v1 and v2)""" | ||||
self._writerecordsv1(records) | ||||
self._writerecordsv2(records) | ||||
def _writerecordsv1(self, records): | ||||
"""Write current state on disk in a version 1 file""" | ||||
f = self._repo.vfs(self.statepathv1, b'wb') | ||||
irecords = iter(records) | ||||
lrecords = next(irecords) | ||||
assert lrecords[0] == RECORD_LOCAL | ||||
f.write(hex(self._local) + b'\n') | ||||
for rtype, data in irecords: | ||||
if rtype == RECORD_MERGED: | ||||
f.write(b'%s\n' % _droponode(data)) | ||||
f.close() | ||||
def _writerecordsv2(self, records): | ||||
"""Write current state on disk in a version 2 file | ||||
See the docstring for _readrecordsv2 for why we use 't'.""" | ||||
# these are the records that all version 2 clients can read | ||||
allowlist = (RECORD_LOCAL, RECORD_OTHER, RECORD_MERGED) | ||||
f = self._repo.vfs(self.statepathv2, b'wb') | ||||
for key, data in records: | ||||
assert len(key) == 1 | ||||
if key not in allowlist: | ||||
key, data = RECORD_OVERRIDE, b'%s%s' % (key, data) | ||||
format = b'>sI%is' % len(data) | ||||
f.write(_pack(format, key, len(data), data)) | ||||
f.close() | ||||
def _make_backup(self, fctx, localkey): | ||||
self._repo.vfs.write(b'merge/' + localkey, fctx.data()) | ||||
def _restore_backup(self, fctx, localkey, flags): | ||||
with self._repo.vfs(b'merge/' + localkey) as f: | ||||
fctx.write(f.read(), flags) | ||||
def reset(self): | ||||
shutil.rmtree(self._repo.vfs.join(b'merge'), True) | ||||
|
r46071 | class memmergestate(_mergestate_base): | ||
def __init__(self, repo): | ||||
super(memmergestate, self).__init__(repo) | ||||
self._backups = {} | ||||
def _make_backup(self, fctx, localkey): | ||||
self._backups[localkey] = fctx.data() | ||||
def _restore_backup(self, fctx, localkey, flags): | ||||
fctx.write(self._backups[localkey], flags) | ||||
|
r45383 | def recordupdates(repo, actions, branchmerge, getfiledata): | ||
"""record merge actions to the dirstate""" | ||||
# remove (must come first) | ||||
for f, args, msg in actions.get(ACTION_REMOVE, []): | ||||
if branchmerge: | ||||
|
r48413 | repo.dirstate.update_file(f, p1_tracked=True, wc_tracked=False) | ||
|
r45383 | else: | ||
|
r48413 | repo.dirstate.update_file(f, p1_tracked=False, wc_tracked=False) | ||
|
r45383 | |||
# forget (must come first) | ||||
for f, args, msg in actions.get(ACTION_FORGET, []): | ||||
|
r48413 | repo.dirstate.update_file(f, p1_tracked=False, wc_tracked=False) | ||
|
r45383 | |||
# resolve path conflicts | ||||
for f, args, msg in actions.get(ACTION_PATH_CONFLICT_RESOLVE, []): | ||||
|
r45465 | (f0, origf0) = args | ||
|
r48413 | repo.dirstate.update_file(f, p1_tracked=False, wc_tracked=True) | ||
|
r45383 | repo.dirstate.copy(origf0, f) | ||
if f0 == origf0: | ||||
|
r48413 | repo.dirstate.update_file(f0, p1_tracked=True, wc_tracked=False) | ||
|
r45383 | else: | ||
|
r48413 | repo.dirstate.update_file(f0, p1_tracked=False, wc_tracked=False) | ||
|
r45383 | |||
# re-add | ||||
for f, args, msg in actions.get(ACTION_ADD, []): | ||||
|
r48413 | repo.dirstate.update_file(f, p1_tracked=False, wc_tracked=True) | ||
|
r45383 | |||
# re-add/mark as modified | ||||
for f, args, msg in actions.get(ACTION_ADD_MODIFIED, []): | ||||
if branchmerge: | ||||
r48526 | repo.dirstate.update_file( | |||
f, p1_tracked=True, wc_tracked=True, possibly_dirty=True | ||||
) | ||||
|
r45383 | else: | ||
|
r48413 | repo.dirstate.update_file(f, p1_tracked=False, wc_tracked=True) | ||
|
r45383 | |||
# exec change | ||||
for f, args, msg in actions.get(ACTION_EXEC, []): | ||||
r48527 | repo.dirstate.update_file( | |||
f, p1_tracked=True, wc_tracked=True, possibly_dirty=True | ||||
) | ||||
|
r45383 | |||
# keep | ||||
for f, args, msg in actions.get(ACTION_KEEP, []): | ||||
pass | ||||
|
r46039 | # keep deleted | ||
for f, args, msg in actions.get(ACTION_KEEP_ABSENT, []): | ||||
pass | ||||
|
r46095 | # keep new | ||
for f, args, msg in actions.get(ACTION_KEEP_NEW, []): | ||||
pass | ||||
|
r45383 | # get | ||
for f, args, msg in actions.get(ACTION_GET, []): | ||||
if branchmerge: | ||||
|
r48413 | # tracked in p1 can be True also but update_file should not care | ||
repo.dirstate.update_file( | ||||
f, | ||||
p1_tracked=False, | ||||
p2_tracked=True, | ||||
wc_tracked=True, | ||||
clean_p2=True, | ||||
) | ||||
|
r45383 | else: | ||
parentfiledata = getfiledata[f] if getfiledata else None | ||||
r48491 | repo.dirstate.update_file( | |||
f, | ||||
p1_tracked=True, | ||||
wc_tracked=True, | ||||
parentfiledata=parentfiledata, | ||||
) | ||||
|
r45383 | |||
# merge | ||||
for f, args, msg in actions.get(ACTION_MERGE, []): | ||||
f1, f2, fa, move, anc = args | ||||
if branchmerge: | ||||
# We've done a branch merge, mark this file as merged | ||||
# so that we properly record the merger later | ||||
|
r48413 | repo.dirstate.update_file( | ||
f, p1_tracked=True, wc_tracked=True, merged=True | ||||
) | ||||
|
r45383 | if f1 != f2: # copy/rename | ||
if move: | ||||
|
r48413 | repo.dirstate.update_file( | ||
f1, p1_tracked=True, wc_tracked=False | ||||
) | ||||
|
r45383 | if f1 != f: | ||
repo.dirstate.copy(f1, f) | ||||
else: | ||||
repo.dirstate.copy(f2, f) | ||||
else: | ||||
# We've update-merged a locally modified file, so | ||||
# we set the dirstate to emulate a normal checkout | ||||
# of that file some time in the past. Thus our | ||||
# merge will appear as a normal local file | ||||
# modification. | ||||
if f2 == f: # file not locally copied/moved | ||||
|
r48413 | repo.dirstate.update_file( | ||
f, p1_tracked=True, wc_tracked=True, possibly_dirty=True | ||||
) | ||||
|
r45383 | if move: | ||
|
r48413 | repo.dirstate.update_file( | ||
f1, p1_tracked=False, wc_tracked=False | ||||
) | ||||
|
r45383 | |||
# directory rename, move local | ||||
for f, args, msg in actions.get(ACTION_DIR_RENAME_MOVE_LOCAL, []): | ||||
f0, flag = args | ||||
if branchmerge: | ||||
|
r48413 | repo.dirstate.update_file(f, p1_tracked=False, wc_tracked=True) | ||
repo.dirstate.update_file(f0, p1_tracked=True, wc_tracked=False) | ||||
|
r45383 | repo.dirstate.copy(f0, f) | ||
else: | ||||
|
r48413 | repo.dirstate.update_file(f, p1_tracked=True, wc_tracked=True) | ||
repo.dirstate.update_file(f0, p1_tracked=False, wc_tracked=False) | ||||
|
r45383 | |||
# directory rename, get | ||||
for f, args, msg in actions.get(ACTION_LOCAL_DIR_RENAME_GET, []): | ||||
f0, flag = args | ||||
if branchmerge: | ||||
|
r48413 | repo.dirstate.update_file(f, p1_tracked=False, wc_tracked=True) | ||
|
r45383 | repo.dirstate.copy(f0, f) | ||
else: | ||||
|
r48413 | repo.dirstate.update_file(f, p1_tracked=True, wc_tracked=True) | ||