##// END OF EJS Templates
mergestate: reduce the number of attribute lookups...
Raphaël Gomès -
r52925:09a54892 default
parent child Browse files
Show More
@@ -1,911 +1,908 b''
1 from __future__ import annotations
1 from __future__ import annotations
2
2
3 import collections
3 import collections
4 import shutil
4 import shutil
5 import struct
5 import struct
6 import weakref
6 import weakref
7
7
8 from .i18n import _
8 from .i18n import _
9 from .node import (
9 from .node import (
10 bin,
10 bin,
11 hex,
11 hex,
12 nullrev,
12 nullrev,
13 )
13 )
14 from . import (
14 from . import (
15 error,
15 error,
16 filemerge,
16 filemerge,
17 util,
17 util,
18 )
18 )
19 from .utils import hashutil
19 from .utils import hashutil
20
20
21 _pack = struct.pack
21 _pack = struct.pack
22 _unpack = struct.unpack
22 _unpack = struct.unpack
23
23
24
24
25 def _droponode(data):
25 def _droponode(data):
26 # used for compatibility for v1
26 # used for compatibility for v1
27 bits = data.split(b'\0')
27 bits = data.split(b'\0')
28 bits = bits[:-2] + bits[-1:]
28 bits = bits[:-2] + bits[-1:]
29 return b'\0'.join(bits)
29 return b'\0'.join(bits)
30
30
31
31
32 def _filectxorabsent(hexnode, ctx, f):
32 def _filectxorabsent(hexnode, ctx, f):
33 if hexnode == ctx.repo().nodeconstants.nullhex:
33 if hexnode == ctx.repo().nodeconstants.nullhex:
34 return filemerge.absentfilectx(ctx, f)
34 return filemerge.absentfilectx(ctx, f)
35 else:
35 else:
36 return ctx[f]
36 return ctx[f]
37
37
38
38
39 # Merge state record types. See ``mergestate`` docs for more.
39 # Merge state record types. See ``mergestate`` docs for more.
40
40
41 ####
41 ####
42 # merge records which records metadata about a current merge
42 # merge records which records metadata about a current merge
43 # exists only once in a mergestate
43 # exists only once in a mergestate
44 #####
44 #####
45 RECORD_LOCAL = b'L'
45 RECORD_LOCAL = b'L'
46 RECORD_OTHER = b'O'
46 RECORD_OTHER = b'O'
47 # record merge labels
47 # record merge labels
48 RECORD_LABELS = b'l'
48 RECORD_LABELS = b'l'
49
49
50 #####
50 #####
51 # record extra information about files, with one entry containing info about one
51 # record extra information about files, with one entry containing info about one
52 # file. Hence, multiple of them can exists
52 # file. Hence, multiple of them can exists
53 #####
53 #####
54 RECORD_FILE_VALUES = b'f'
54 RECORD_FILE_VALUES = b'f'
55
55
56 #####
56 #####
57 # merge records which represents state of individual merges of files/folders
57 # merge records which represents state of individual merges of files/folders
58 # These are top level records for each entry containing merge related info.
58 # These are top level records for each entry containing merge related info.
59 # Each record of these has info about one file. Hence multiple of them can
59 # Each record of these has info about one file. Hence multiple of them can
60 # exists
60 # exists
61 #####
61 #####
62 RECORD_MERGED = b'F'
62 RECORD_MERGED = b'F'
63 RECORD_CHANGEDELETE_CONFLICT = b'C'
63 RECORD_CHANGEDELETE_CONFLICT = b'C'
64 # the path was dir on one side of merge and file on another
64 # the path was dir on one side of merge and file on another
65 RECORD_PATH_CONFLICT = b'P'
65 RECORD_PATH_CONFLICT = b'P'
66
66
67 #####
67 #####
68 # possible state which a merge entry can have. These are stored inside top-level
68 # possible state which a merge entry can have. These are stored inside top-level
69 # merge records mentioned just above.
69 # merge records mentioned just above.
70 #####
70 #####
71 MERGE_RECORD_UNRESOLVED = b'u'
71 MERGE_RECORD_UNRESOLVED = b'u'
72 MERGE_RECORD_RESOLVED = b'r'
72 MERGE_RECORD_RESOLVED = b'r'
73 MERGE_RECORD_UNRESOLVED_PATH = b'pu'
73 MERGE_RECORD_UNRESOLVED_PATH = b'pu'
74 MERGE_RECORD_RESOLVED_PATH = b'pr'
74 MERGE_RECORD_RESOLVED_PATH = b'pr'
75 # represents that the file was automatically merged in favor
75 # represents that the file was automatically merged in favor
76 # of other version. This info is used on commit.
76 # of other version. This info is used on commit.
77 # This is now deprecated and commit related information is now
77 # This is now deprecated and commit related information is now
78 # stored in RECORD_FILE_VALUES
78 # stored in RECORD_FILE_VALUES
79 MERGE_RECORD_MERGED_OTHER = b'o'
79 MERGE_RECORD_MERGED_OTHER = b'o'
80
80
81 #####
81 #####
82 # top level record which stores other unknown records. Multiple of these can
82 # top level record which stores other unknown records. Multiple of these can
83 # exists
83 # exists
84 #####
84 #####
85 RECORD_OVERRIDE = b't'
85 RECORD_OVERRIDE = b't'
86
86
87 #####
87 #####
88 # legacy records which are no longer used but kept to prevent breaking BC
88 # legacy records which are no longer used but kept to prevent breaking BC
89 #####
89 #####
90 # This record was release in 5.4 and usage was removed in 5.5
90 # This record was release in 5.4 and usage was removed in 5.5
91 LEGACY_RECORD_RESOLVED_OTHER = b'R'
91 LEGACY_RECORD_RESOLVED_OTHER = b'R'
92 # This record was release in 3.7 and usage was removed in 5.6
92 # This record was release in 3.7 and usage was removed in 5.6
93 LEGACY_RECORD_DRIVER_RESOLVED = b'd'
93 LEGACY_RECORD_DRIVER_RESOLVED = b'd'
94 # This record was release in 3.7 and usage was removed in 5.6
94 # This record was release in 3.7 and usage was removed in 5.6
95 LEGACY_MERGE_DRIVER_STATE = b'm'
95 LEGACY_MERGE_DRIVER_STATE = b'm'
96 # This record was release in 3.7 and usage was removed in 5.6
96 # This record was release in 3.7 and usage was removed in 5.6
97 LEGACY_MERGE_DRIVER_MERGE = b'D'
97 LEGACY_MERGE_DRIVER_MERGE = b'D'
98
98
99 CHANGE_ADDED = b'added'
99 CHANGE_ADDED = b'added'
100 CHANGE_REMOVED = b'removed'
100 CHANGE_REMOVED = b'removed'
101 CHANGE_MODIFIED = b'modified'
101 CHANGE_MODIFIED = b'modified'
102
102
103
103
104 class MergeAction:
104 class MergeAction:
105 """represent an "action" merge need to take for a given file
105 """represent an "action" merge need to take for a given file
106
106
107 Attributes:
107 Attributes:
108
108
109 _short: internal representation used to identify each action
109 _short: internal representation used to identify each action
110
110
111 no_op: True if the action does affect the file content or tracking status
111 no_op: True if the action does affect the file content or tracking status
112
112
113 narrow_safe:
113 narrow_safe:
114 True if the action can be safely used for a file outside of the narrow
114 True if the action can be safely used for a file outside of the narrow
115 set
115 set
116
116
117 changes:
117 changes:
118 The types of changes that this actions involves. This is a work in
118 The types of changes that this actions involves. This is a work in
119 progress and not all actions have one yet. In addition, some requires
119 progress and not all actions have one yet. In addition, some requires
120 user changes and cannot be fully decided. The value currently available
120 user changes and cannot be fully decided. The value currently available
121 are:
121 are:
122
122
123 - ADDED: the files is new in both parents
123 - ADDED: the files is new in both parents
124 - REMOVED: the files existed in one parent and is getting removed
124 - REMOVED: the files existed in one parent and is getting removed
125 - MODIFIED: the files existed in at least one parent and is getting changed
125 - MODIFIED: the files existed in at least one parent and is getting changed
126 """
126 """
127
127
128 ALL_ACTIONS = weakref.WeakSet()
128 ALL_ACTIONS = weakref.WeakSet()
129 NO_OP_ACTIONS = weakref.WeakSet()
129 NO_OP_ACTIONS = weakref.WeakSet()
130
130
131 def __init__(self, short, no_op=False, narrow_safe=False, changes=None):
131 def __init__(self, short, no_op=False, narrow_safe=False, changes=None):
132 self._short = short
132 self._short = short
133 self.ALL_ACTIONS.add(self)
133 self.ALL_ACTIONS.add(self)
134 self.no_op = no_op
134 self.no_op = no_op
135 if self.no_op:
135 if self.no_op:
136 self.NO_OP_ACTIONS.add(self)
136 self.NO_OP_ACTIONS.add(self)
137 self.narrow_safe = narrow_safe
137 self.narrow_safe = narrow_safe
138 self.changes = changes
138 self.changes = changes
139
139
140 def __hash__(self):
140 def __hash__(self):
141 return hash(self._short)
141 return hash(self._short)
142
142
143 def __repr__(self):
143 def __repr__(self):
144 return 'MergeAction<%s>' % self._short.decode('ascii')
144 return 'MergeAction<%s>' % self._short.decode('ascii')
145
145
146 def __bytes__(self):
146 def __bytes__(self):
147 return self._short
147 return self._short
148
148
149 def __eq__(self, other):
149 def __eq__(self, other):
150 if other is None:
150 if other is None:
151 return False
151 return False
152 assert isinstance(other, MergeAction)
152 assert isinstance(other, MergeAction)
153 return self._short == other._short
153 return self._short == other._short
154
154
155 def __lt__(self, other):
155 def __lt__(self, other):
156 return self._short < other._short
156 return self._short < other._short
157
157
158
158
159 ACTION_FORGET = MergeAction(b'f', narrow_safe=True, changes=CHANGE_REMOVED)
159 ACTION_FORGET = MergeAction(b'f', narrow_safe=True, changes=CHANGE_REMOVED)
160 ACTION_REMOVE = MergeAction(b'r', narrow_safe=True, changes=CHANGE_REMOVED)
160 ACTION_REMOVE = MergeAction(b'r', narrow_safe=True, changes=CHANGE_REMOVED)
161 ACTION_ADD = MergeAction(b'a', narrow_safe=True, changes=CHANGE_ADDED)
161 ACTION_ADD = MergeAction(b'a', narrow_safe=True, changes=CHANGE_ADDED)
162 ACTION_GET = MergeAction(b'g', narrow_safe=True, changes=CHANGE_MODIFIED)
162 ACTION_GET = MergeAction(b'g', narrow_safe=True, changes=CHANGE_MODIFIED)
163 ACTION_PATH_CONFLICT = MergeAction(b'p')
163 ACTION_PATH_CONFLICT = MergeAction(b'p')
164 ACTION_PATH_CONFLICT_RESOLVE = MergeAction(b'pr')
164 ACTION_PATH_CONFLICT_RESOLVE = MergeAction(b'pr')
165 ACTION_ADD_MODIFIED = MergeAction(
165 ACTION_ADD_MODIFIED = MergeAction(
166 b'am', narrow_safe=True, changes=CHANGE_ADDED
166 b'am', narrow_safe=True, changes=CHANGE_ADDED
167 ) # not 100% about the changes value here
167 ) # not 100% about the changes value here
168 ACTION_CREATED = MergeAction(b'c', narrow_safe=True, changes=CHANGE_ADDED)
168 ACTION_CREATED = MergeAction(b'c', narrow_safe=True, changes=CHANGE_ADDED)
169 ACTION_DELETED_CHANGED = MergeAction(b'dc')
169 ACTION_DELETED_CHANGED = MergeAction(b'dc')
170 ACTION_CHANGED_DELETED = MergeAction(b'cd')
170 ACTION_CHANGED_DELETED = MergeAction(b'cd')
171 ACTION_MERGE = MergeAction(b'm')
171 ACTION_MERGE = MergeAction(b'm')
172 ACTION_LOCAL_DIR_RENAME_GET = MergeAction(b'dg')
172 ACTION_LOCAL_DIR_RENAME_GET = MergeAction(b'dg')
173 ACTION_DIR_RENAME_MOVE_LOCAL = MergeAction(b'dm')
173 ACTION_DIR_RENAME_MOVE_LOCAL = MergeAction(b'dm')
174 ACTION_KEEP = MergeAction(b'k', no_op=True)
174 ACTION_KEEP = MergeAction(b'k', no_op=True)
175 # the file was absent on local side before merge and we should
175 # the file was absent on local side before merge and we should
176 # keep it absent (absent means file not present, it can be a result
176 # keep it absent (absent means file not present, it can be a result
177 # of file deletion, rename etc.)
177 # of file deletion, rename etc.)
178 ACTION_KEEP_ABSENT = MergeAction(b'ka', no_op=True)
178 ACTION_KEEP_ABSENT = MergeAction(b'ka', no_op=True)
179 # the file is absent on the ancestor and remote side of the merge
179 # the file is absent on the ancestor and remote side of the merge
180 # hence this file is new and we should keep it
180 # hence this file is new and we should keep it
181 ACTION_KEEP_NEW = MergeAction(b'kn', no_op=True)
181 ACTION_KEEP_NEW = MergeAction(b'kn', no_op=True)
182 ACTION_EXEC = MergeAction(b'e', narrow_safe=True, changes=CHANGE_MODIFIED)
182 ACTION_EXEC = MergeAction(b'e', narrow_safe=True, changes=CHANGE_MODIFIED)
183 ACTION_CREATED_MERGE = MergeAction(
183 ACTION_CREATED_MERGE = MergeAction(
184 b'cm', narrow_safe=True, changes=CHANGE_ADDED
184 b'cm', narrow_safe=True, changes=CHANGE_ADDED
185 )
185 )
186
186
187
187
188 # Used by concert to detect situation it does not like, not sure what the exact
188 # Used by concert to detect situation it does not like, not sure what the exact
189 # criteria is
189 # criteria is
190 CONVERT_MERGE_ACTIONS = (
190 CONVERT_MERGE_ACTIONS = (
191 ACTION_MERGE,
191 ACTION_MERGE,
192 ACTION_DIR_RENAME_MOVE_LOCAL,
192 ACTION_DIR_RENAME_MOVE_LOCAL,
193 ACTION_CHANGED_DELETED,
193 ACTION_CHANGED_DELETED,
194 ACTION_DELETED_CHANGED,
194 ACTION_DELETED_CHANGED,
195 )
195 )
196
196
197
197
198 class _mergestate_base:
198 class _mergestate_base:
199 """track 3-way merge state of individual files
199 """track 3-way merge state of individual files
200
200
201 The merge state is stored on disk when needed. Two files are used: one with
201 The merge state is stored on disk when needed. Two files are used: one with
202 an old format (version 1), and one with a new format (version 2). Version 2
202 an old format (version 1), and one with a new format (version 2). Version 2
203 stores a superset of the data in version 1, including new kinds of records
203 stores a superset of the data in version 1, including new kinds of records
204 in the future. For more about the new format, see the documentation for
204 in the future. For more about the new format, see the documentation for
205 `_readrecordsv2`.
205 `_readrecordsv2`.
206
206
207 Each record can contain arbitrary content, and has an associated type. This
207 Each record can contain arbitrary content, and has an associated type. This
208 `type` should be a letter. If `type` is uppercase, the record is mandatory:
208 `type` should be a letter. If `type` is uppercase, the record is mandatory:
209 versions of Mercurial that don't support it should abort. If `type` is
209 versions of Mercurial that don't support it should abort. If `type` is
210 lowercase, the record can be safely ignored.
210 lowercase, the record can be safely ignored.
211
211
212 Currently known records:
212 Currently known records:
213
213
214 L: the node of the "local" part of the merge (hexified version)
214 L: the node of the "local" part of the merge (hexified version)
215 O: the node of the "other" part of the merge (hexified version)
215 O: the node of the "other" part of the merge (hexified version)
216 F: a file to be merged entry
216 F: a file to be merged entry
217 C: a change/delete or delete/change conflict
217 C: a change/delete or delete/change conflict
218 P: a path conflict (file vs directory)
218 P: a path conflict (file vs directory)
219 f: a (filename, dictionary) tuple of optional values for a given file
219 f: a (filename, dictionary) tuple of optional values for a given file
220 l: the labels for the parts of the merge.
220 l: the labels for the parts of the merge.
221
221
222 Merge record states (stored in self._state, indexed by filename):
222 Merge record states (stored in self._state, indexed by filename):
223 u: unresolved conflict
223 u: unresolved conflict
224 r: resolved conflict
224 r: resolved conflict
225 pu: unresolved path conflict (file conflicts with directory)
225 pu: unresolved path conflict (file conflicts with directory)
226 pr: resolved path conflict
226 pr: resolved path conflict
227 o: file was merged in favor of other parent of merge (DEPRECATED)
227 o: file was merged in favor of other parent of merge (DEPRECATED)
228
228
229 The resolve command transitions between 'u' and 'r' for conflicts and
229 The resolve command transitions between 'u' and 'r' for conflicts and
230 'pu' and 'pr' for path conflicts.
230 'pu' and 'pr' for path conflicts.
231 """
231 """
232
232
233 def __init__(self, repo):
233 def __init__(self, repo):
234 """Initialize the merge state.
234 """Initialize the merge state.
235
235
236 Do not use this directly! Instead call read() or clean()."""
236 Do not use this directly! Instead call read() or clean()."""
237 self._repo = repo
237 self._repo = repo
238 self._state = {}
238 self._state = {}
239 self._stateextras = collections.defaultdict(dict)
239 self._stateextras = collections.defaultdict(dict)
240 self._local = None
240 self._local = None
241 self._other = None
241 self._other = None
242 self._labels = None
242 self._labels = None
243 # contains a mapping of form:
243 # contains a mapping of form:
244 # {filename : (merge_return_value, action_to_be_performed}
244 # {filename : (merge_return_value, action_to_be_performed}
245 # these are results of re-running merge process
245 # these are results of re-running merge process
246 # this dict is used to perform actions on dirstate caused by re-running
246 # this dict is used to perform actions on dirstate caused by re-running
247 # the merge
247 # the merge
248 self._results = {}
248 self._results = {}
249 self._dirty = False
249 self._dirty = False
250
250
251 def reset(self):
251 def reset(self):
252 pass
252 pass
253
253
254 def start(self, node, other, labels=None):
254 def start(self, node, other, labels=None):
255 self._local = node
255 self._local = node
256 self._other = other
256 self._other = other
257 self._labels = labels
257 self._labels = labels
258
258
259 @util.propertycache
259 @util.propertycache
260 def local(self):
260 def local(self):
261 if self._local is None:
261 if self._local is None:
262 msg = b"local accessed but self._local isn't set"
262 msg = b"local accessed but self._local isn't set"
263 raise error.ProgrammingError(msg)
263 raise error.ProgrammingError(msg)
264 return self._local
264 return self._local
265
265
266 @util.propertycache
266 @util.propertycache
267 def localctx(self):
267 def localctx(self):
268 return self._repo[self.local]
268 return self._repo[self.local]
269
269
270 @util.propertycache
270 @util.propertycache
271 def other(self):
271 def other(self):
272 if self._other is None:
272 if self._other is None:
273 msg = b"other accessed but self._other isn't set"
273 msg = b"other accessed but self._other isn't set"
274 raise error.ProgrammingError(msg)
274 raise error.ProgrammingError(msg)
275 return self._other
275 return self._other
276
276
277 @util.propertycache
277 @util.propertycache
278 def otherctx(self):
278 def otherctx(self):
279 return self._repo[self.other]
279 return self._repo[self.other]
280
280
281 def active(self):
281 def active(self):
282 """Whether mergestate is active.
282 """Whether mergestate is active.
283
283
284 Returns True if there appears to be mergestate. This is a rough proxy
284 Returns True if there appears to be mergestate. This is a rough proxy
285 for "is a merge in progress."
285 for "is a merge in progress."
286 """
286 """
287 return bool(self._local) or bool(self._state)
287 return bool(self._local) or bool(self._state)
288
288
289 def commit(self):
289 def commit(self):
290 """Write current state on disk (if necessary)"""
290 """Write current state on disk (if necessary)"""
291
291
292 @staticmethod
292 @staticmethod
293 def getlocalkey(path):
293 def getlocalkey(path):
294 """hash the path of a local file context for storage in the .hg/merge
294 """hash the path of a local file context for storage in the .hg/merge
295 directory."""
295 directory."""
296
296
297 return hex(hashutil.sha1(path).digest())
297 return hex(hashutil.sha1(path).digest())
298
298
299 def _make_backup(self, fctx, localkey):
299 def _make_backup(self, fctx, localkey):
300 raise NotImplementedError()
300 raise NotImplementedError()
301
301
302 def _restore_backup(self, fctx, localkey, flags):
302 def _restore_backup(self, fctx, localkey, flags):
303 raise NotImplementedError()
303 raise NotImplementedError()
304
304
305 def add(self, fcl, fco, fca, fd):
305 def add(self, fcl, fco, fca, fd):
306 """add a new (potentially?) conflicting file the merge state
306 """add a new (potentially?) conflicting file the merge state
307 fcl: file context for local,
307 fcl: file context for local,
308 fco: file context for remote,
308 fco: file context for remote,
309 fca: file context for ancestors,
309 fca: file context for ancestors,
310 fd: file path of the resulting merge.
310 fd: file path of the resulting merge.
311
311
312 note: also write the local version to the `.hg/merge` directory.
312 note: also write the local version to the `.hg/merge` directory.
313 """
313 """
314 if fcl.isabsent():
314 if fcl.isabsent():
315 localkey = self._repo.nodeconstants.nullhex
315 localkey = self._repo.nodeconstants.nullhex
316 else:
316 else:
317 localkey = mergestate.getlocalkey(fcl.path())
317 localkey = mergestate.getlocalkey(fcl.path())
318 self._make_backup(fcl, localkey)
318 self._make_backup(fcl, localkey)
319 self._state[fd] = [
319 self._state[fd] = [
320 MERGE_RECORD_UNRESOLVED,
320 MERGE_RECORD_UNRESOLVED,
321 localkey,
321 localkey,
322 fcl.path(),
322 fcl.path(),
323 fca.path(),
323 fca.path(),
324 hex(fca.filenode()),
324 hex(fca.filenode()),
325 fco.path(),
325 fco.path(),
326 hex(fco.filenode()),
326 hex(fco.filenode()),
327 fcl.flags(),
327 fcl.flags(),
328 ]
328 ]
329 self._stateextras[fd][b'ancestorlinknode'] = hex(fca.node())
329 self._stateextras[fd][b'ancestorlinknode'] = hex(fca.node())
330 self._dirty = True
330 self._dirty = True
331
331
332 def addpathconflict(self, path, frename, forigin):
332 def addpathconflict(self, path, frename, forigin):
333 """add a new conflicting path to the merge state
333 """add a new conflicting path to the merge state
334 path: the path that conflicts
334 path: the path that conflicts
335 frename: the filename the conflicting file was renamed to
335 frename: the filename the conflicting file was renamed to
336 forigin: origin of the file ('l' or 'r' for local/remote)
336 forigin: origin of the file ('l' or 'r' for local/remote)
337 """
337 """
338 self._state[path] = [MERGE_RECORD_UNRESOLVED_PATH, frename, forigin]
338 self._state[path] = [MERGE_RECORD_UNRESOLVED_PATH, frename, forigin]
339 self._dirty = True
339 self._dirty = True
340
340
341 def addcommitinfo(self, path, data):
341 def addcommitinfo(self, path, data):
342 """stores information which is required at commit
342 """stores information which is required at commit
343 into _stateextras"""
343 into _stateextras"""
344 self._stateextras[path].update(data)
344 self._stateextras[path].update(data)
345 self._dirty = True
345 self._dirty = True
346
346
347 def __contains__(self, dfile):
347 def __contains__(self, dfile):
348 return dfile in self._state
348 return dfile in self._state
349
349
350 def __getitem__(self, dfile):
350 def __getitem__(self, dfile):
351 return self._state[dfile][0]
351 return self._state[dfile][0]
352
352
353 def __iter__(self):
353 def __iter__(self):
354 return iter(sorted(self._state))
354 return iter(sorted(self._state))
355
355
356 def files(self):
356 def files(self):
357 return self._state.keys()
357 return self._state.keys()
358
358
359 def mark(self, dfile, state):
359 def mark(self, dfile, state):
360 self._state[dfile][0] = state
360 self._state[dfile][0] = state
361 self._dirty = True
361 self._dirty = True
362
362
363 def unresolved(self):
363 def unresolved(self):
364 """Obtain the paths of unresolved files."""
364 """Obtain the paths of unresolved files."""
365
365
366 for f, entry in self._state.items():
366 for f, entry in self._state.items():
367 if entry[0] in (
367 if entry[0] in (
368 MERGE_RECORD_UNRESOLVED,
368 MERGE_RECORD_UNRESOLVED,
369 MERGE_RECORD_UNRESOLVED_PATH,
369 MERGE_RECORD_UNRESOLVED_PATH,
370 ):
370 ):
371 yield f
371 yield f
372
372
373 def allextras(self):
373 def allextras(self):
374 """return all extras information stored with the mergestate"""
374 """return all extras information stored with the mergestate"""
375 return self._stateextras
375 return self._stateextras
376
376
377 def extras(self, filename):
377 def extras(self, filename):
378 """return extras stored with the mergestate for the given filename"""
378 """return extras stored with the mergestate for the given filename"""
379 return self._stateextras[filename]
379 return self._stateextras[filename]
380
380
381 def resolve(self, dfile, wctx):
381 def resolve(self, dfile, wctx):
382 """run merge process for dfile
382 """run merge process for dfile
383
383
384 Returns the exit code of the merge."""
384 Returns the exit code of the merge."""
385 if self[dfile] in (
385 if self[dfile] in (
386 MERGE_RECORD_RESOLVED,
386 MERGE_RECORD_RESOLVED,
387 LEGACY_RECORD_DRIVER_RESOLVED,
387 LEGACY_RECORD_DRIVER_RESOLVED,
388 ):
388 ):
389 return 0
389 return 0
390 stateentry = self._state[dfile]
390 stateentry = self._state[dfile]
391 state, localkey, lfile, afile, anode, ofile, onode, flags = stateentry
391 state, localkey, lfile, afile, anode, ofile, onode, flags = stateentry
392 octx = self._repo[self._other]
392 octx = self._repo[self._other]
393 extras = self.extras(dfile)
393 extras = self.extras(dfile)
394 anccommitnode = extras.get(b'ancestorlinknode')
394 anccommitnode = extras.get(b'ancestorlinknode')
395 if anccommitnode:
395 if anccommitnode:
396 actx = self._repo[anccommitnode]
396 actx = self._repo[anccommitnode]
397 else:
397 else:
398 actx = None
398 actx = None
399 fcd = _filectxorabsent(localkey, wctx, dfile)
399 fcd = _filectxorabsent(localkey, wctx, dfile)
400 fco = _filectxorabsent(onode, octx, ofile)
400 fco = _filectxorabsent(onode, octx, ofile)
401 # TODO: move this to filectxorabsent
401 # TODO: move this to filectxorabsent
402 fca = self._repo.filectx(afile, fileid=anode, changectx=actx)
402 fca = self._repo.filectx(afile, fileid=anode, changectx=actx)
403 # "premerge" x flags
403 # "premerge" x flags
404 flo = fco.flags()
404 flo = fco.flags()
405 fla = fca.flags()
405 fla = fca.flags()
406 if b'x' in flags + flo + fla and b'l' not in flags + flo + fla:
406 if b'x' in flags + flo + fla and b'l' not in flags + flo + fla:
407 if fca.rev() == nullrev and flags != flo:
407 if fca.rev() == nullrev and flags != flo:
408 self._repo.ui.warn(
408 self._repo.ui.warn(
409 _(
409 _(
410 b'warning: cannot merge flags for %s '
410 b'warning: cannot merge flags for %s '
411 b'without common ancestor - keeping local flags\n'
411 b'without common ancestor - keeping local flags\n'
412 )
412 )
413 % afile
413 % afile
414 )
414 )
415 elif flags == fla:
415 elif flags == fla:
416 flags = flo
416 flags = flo
417 # restore local
417 # restore local
418 if localkey != self._repo.nodeconstants.nullhex:
418 if localkey != self._repo.nodeconstants.nullhex:
419 self._restore_backup(wctx[dfile], localkey, flags)
419 self._restore_backup(wctx[dfile], localkey, flags)
420 else:
420 else:
421 wctx[dfile].remove(ignoremissing=True)
421 wctx[dfile].remove(ignoremissing=True)
422
422
423 if not fco.cmp(fcd): # files identical?
423 if not fco.cmp(fcd): # files identical?
424 # If return value of merge is None, then there are no real conflict
424 # If return value of merge is None, then there are no real conflict
425 del self._state[dfile]
425 del self._state[dfile]
426 self._results[dfile] = None, None
426 self._results[dfile] = None, None
427 self._dirty = True
427 self._dirty = True
428 return None
428 return None
429
429
430 merge_ret, deleted = filemerge.filemerge(
430 merge_ret, deleted = filemerge.filemerge(
431 self._repo,
431 self._repo,
432 wctx,
432 wctx,
433 self._local,
433 self._local,
434 lfile,
434 lfile,
435 fcd,
435 fcd,
436 fco,
436 fco,
437 fca,
437 fca,
438 labels=self._labels,
438 labels=self._labels,
439 )
439 )
440
440
441 if not merge_ret:
441 if not merge_ret:
442 self.mark(dfile, MERGE_RECORD_RESOLVED)
442 self.mark(dfile, MERGE_RECORD_RESOLVED)
443
443
444 action = None
444 action = None
445 if deleted:
445 if deleted:
446 if fcd.isabsent():
446 if fcd.isabsent():
447 # dc: local picked. Need to drop if present, which may
447 # dc: local picked. Need to drop if present, which may
448 # happen on re-resolves.
448 # happen on re-resolves.
449 action = ACTION_FORGET
449 action = ACTION_FORGET
450 else:
450 else:
451 # cd: remote picked (or otherwise deleted)
451 # cd: remote picked (or otherwise deleted)
452 action = ACTION_REMOVE
452 action = ACTION_REMOVE
453 else:
453 else:
454 if fcd.isabsent(): # dc: remote picked
454 if fcd.isabsent(): # dc: remote picked
455 action = ACTION_GET
455 action = ACTION_GET
456 elif fco.isabsent(): # cd: local picked
456 elif fco.isabsent(): # cd: local picked
457 if dfile in self.localctx:
457 if dfile in self.localctx:
458 action = ACTION_ADD_MODIFIED
458 action = ACTION_ADD_MODIFIED
459 else:
459 else:
460 action = ACTION_ADD
460 action = ACTION_ADD
461 # else: regular merges (no action necessary)
461 # else: regular merges (no action necessary)
462 self._results[dfile] = merge_ret, action
462 self._results[dfile] = merge_ret, action
463
463
464 return merge_ret
464 return merge_ret
465
465
466 def counts(self):
466 def counts(self):
467 """return counts for updated, merged and removed files in this
467 """return counts for updated, merged and removed files in this
468 session"""
468 session"""
469 updated, merged, removed = 0, 0, 0
469 updated, merged, removed = 0, 0, 0
470 for r, action in self._results.values():
470 for r, action in self._results.values():
471 if r is None:
471 if r is None:
472 updated += 1
472 updated += 1
473 elif r == 0:
473 elif r == 0:
474 if action == ACTION_REMOVE:
474 if action == ACTION_REMOVE:
475 removed += 1
475 removed += 1
476 else:
476 else:
477 merged += 1
477 merged += 1
478 return updated, merged, removed
478 return updated, merged, removed
479
479
480 def unresolvedcount(self):
480 def unresolvedcount(self):
481 """get unresolved count for this merge (persistent)"""
481 """get unresolved count for this merge (persistent)"""
482 return len(list(self.unresolved()))
482 return len(list(self.unresolved()))
483
483
484 def actions(self):
484 def actions(self):
485 """return lists of actions to perform on the dirstate"""
485 """return lists of actions to perform on the dirstate"""
486 actions = {
486 actions = {
487 ACTION_REMOVE: [],
487 ACTION_REMOVE: [],
488 ACTION_FORGET: [],
488 ACTION_FORGET: [],
489 ACTION_ADD: [],
489 ACTION_ADD: [],
490 ACTION_ADD_MODIFIED: [],
490 ACTION_ADD_MODIFIED: [],
491 ACTION_GET: [],
491 ACTION_GET: [],
492 }
492 }
493 for f, (r, action) in self._results.items():
493 for f, (r, action) in self._results.items():
494 if action is not None:
494 if action is not None:
495 actions[action].append((f, None, b"merge result"))
495 actions[action].append((f, None, b"merge result"))
496 return actions
496 return actions
497
497
498
498
499 class mergestate(_mergestate_base):
499 class mergestate(_mergestate_base):
500 statepathv1 = b'merge/state'
500 statepathv1 = b'merge/state'
501 statepathv2 = b'merge/state2'
501 statepathv2 = b'merge/state2'
502
502
503 @staticmethod
503 @staticmethod
504 def clean(repo):
504 def clean(repo):
505 """Initialize a brand new merge state, removing any existing state on
505 """Initialize a brand new merge state, removing any existing state on
506 disk."""
506 disk."""
507 ms = mergestate(repo)
507 ms = mergestate(repo)
508 ms.reset()
508 ms.reset()
509 return ms
509 return ms
510
510
511 @staticmethod
511 @staticmethod
512 def read(repo):
512 def read(repo):
513 """Initialize the merge state, reading it from disk."""
513 """Initialize the merge state, reading it from disk."""
514 ms = mergestate(repo)
514 ms = mergestate(repo)
515 ms._read()
515 ms._read()
516 return ms
516 return ms
517
517
518 def _read(self):
518 def _read(self):
519 """Analyse each record content to restore a serialized state from disk
519 """Analyse each record content to restore a serialized state from disk
520
520
521 This function process "record" entry produced by the de-serialization
521 This function process "record" entry produced by the de-serialization
522 of on disk file.
522 of on disk file.
523 """
523 """
524 unsupported = set()
524 unsupported = set()
525 records = self._readrecords()
525 records = self._readrecords()
526 for rtype, record in records:
526 for rtype, record in records:
527 if rtype == RECORD_LOCAL:
527 if rtype == RECORD_LOCAL:
528 self._local = bin(record)
528 self._local = bin(record)
529 elif rtype == RECORD_OTHER:
529 elif rtype == RECORD_OTHER:
530 self._other = bin(record)
530 self._other = bin(record)
531 elif rtype == LEGACY_MERGE_DRIVER_STATE:
531 elif rtype == LEGACY_MERGE_DRIVER_STATE:
532 pass
532 pass
533 elif rtype in (
533 elif rtype in (
534 RECORD_MERGED,
534 RECORD_MERGED,
535 RECORD_CHANGEDELETE_CONFLICT,
535 RECORD_CHANGEDELETE_CONFLICT,
536 RECORD_PATH_CONFLICT,
536 RECORD_PATH_CONFLICT,
537 LEGACY_MERGE_DRIVER_MERGE,
537 LEGACY_MERGE_DRIVER_MERGE,
538 LEGACY_RECORD_RESOLVED_OTHER,
538 LEGACY_RECORD_RESOLVED_OTHER,
539 ):
539 ):
540 bits = record.split(b'\0')
540 bits = record.split(b'\0')
541 # merge entry type MERGE_RECORD_MERGED_OTHER is deprecated
541 # merge entry type MERGE_RECORD_MERGED_OTHER is deprecated
542 # and we now store related information in _stateextras, so
542 # and we now store related information in _stateextras, so
543 # lets write to _stateextras directly
543 # lets write to _stateextras directly
544 if bits[1] == MERGE_RECORD_MERGED_OTHER:
544 if bits[1] == MERGE_RECORD_MERGED_OTHER:
545 self._stateextras[bits[0]][b'filenode-source'] = b'other'
545 self._stateextras[bits[0]][b'filenode-source'] = b'other'
546 else:
546 else:
547 self._state[bits[0]] = bits[1:]
547 self._state[bits[0]] = bits[1:]
548 elif rtype == RECORD_FILE_VALUES:
548 elif rtype == RECORD_FILE_VALUES:
549 filename, rawextras = record.split(b'\0', 1)
549 filename, rawextras = record.split(b'\0', 1)
550 extraparts = rawextras.split(b'\0')
550 extraparts = rawextras.split(b'\0')
551 extras = {}
551 extras = {}
552 i = 0
552 i = 0
553 while i < len(extraparts):
553 while i < len(extraparts):
554 extras[extraparts[i]] = extraparts[i + 1]
554 extras[extraparts[i]] = extraparts[i + 1]
555 i += 2
555 i += 2
556
556
557 self._stateextras[filename] = extras
557 self._stateextras[filename] = extras
558 elif rtype == RECORD_LABELS:
558 elif rtype == RECORD_LABELS:
559 labels = record.split(b'\0', 2)
559 labels = record.split(b'\0', 2)
560 self._labels = [l for l in labels if len(l) > 0]
560 self._labels = [l for l in labels if len(l) > 0]
561 elif not rtype.islower():
561 elif not rtype.islower():
562 unsupported.add(rtype)
562 unsupported.add(rtype)
563
563
564 if unsupported:
564 if unsupported:
565 raise error.UnsupportedMergeRecords(unsupported)
565 raise error.UnsupportedMergeRecords(unsupported)
566
566
567 def _readrecords(self):
567 def _readrecords(self):
568 """Read merge state from disk and return a list of record (TYPE, data)
568 """Read merge state from disk and return a list of record (TYPE, data)
569
569
570 We read data from both v1 and v2 files and decide which one to use.
570 We read data from both v1 and v2 files and decide which one to use.
571
571
572 V1 has been used by version prior to 2.9.1 and contains less data than
572 V1 has been used by version prior to 2.9.1 and contains less data than
573 v2. We read both versions and check if no data in v2 contradicts
573 v2. We read both versions and check if no data in v2 contradicts
574 v1. If there is not contradiction we can safely assume that both v1
574 v1. If there is not contradiction we can safely assume that both v1
575 and v2 were written at the same time and use the extract data in v2. If
575 and v2 were written at the same time and use the extract data in v2. If
576 there is contradiction we ignore v2 content as we assume an old version
576 there is contradiction we ignore v2 content as we assume an old version
577 of Mercurial has overwritten the mergestate file and left an old v2
577 of Mercurial has overwritten the mergestate file and left an old v2
578 file around.
578 file around.
579
579
580 returns list of record [(TYPE, data), ...]"""
580 returns list of record [(TYPE, data), ...]"""
581 v1records = self._readrecordsv1()
581 v1records = self._readrecordsv1()
582 v2records = self._readrecordsv2()
582 v2records = self._readrecordsv2()
583 if self._v1v2match(v1records, v2records):
583 if self._v1v2match(v1records, v2records):
584 return v2records
584 return v2records
585 else:
585 else:
586 # v1 file is newer than v2 file, use it
586 # v1 file is newer than v2 file, use it
587 # we have to infer the "other" changeset of the merge
587 # we have to infer the "other" changeset of the merge
588 # we cannot do better than that with v1 of the format
588 # we cannot do better than that with v1 of the format
589 mctx = self._repo[None].parents()[-1]
589 mctx = self._repo[None].parents()[-1]
590 v1records.append((RECORD_OTHER, mctx.hex()))
590 v1records.append((RECORD_OTHER, mctx.hex()))
591 # add place holder "other" file node information
591 # add place holder "other" file node information
592 # nobody is using it yet so we do no need to fetch the data
592 # nobody is using it yet so we do no need to fetch the data
593 # if mctx was wrong `mctx[bits[-2]]` may fails.
593 # if mctx was wrong `mctx[bits[-2]]` may fails.
594 for idx, r in enumerate(v1records):
594 for idx, r in enumerate(v1records):
595 if r[0] == RECORD_MERGED:
595 if r[0] == RECORD_MERGED:
596 bits = r[1].split(b'\0')
596 bits = r[1].split(b'\0')
597 bits.insert(-2, b'')
597 bits.insert(-2, b'')
598 v1records[idx] = (r[0], b'\0'.join(bits))
598 v1records[idx] = (r[0], b'\0'.join(bits))
599 return v1records
599 return v1records
600
600
601 def _v1v2match(self, v1records, v2records):
601 def _v1v2match(self, v1records, v2records):
602 oldv2 = set() # old format version of v2 record
602 oldv2 = set() # old format version of v2 record
603 for rec in v2records:
603 for rec in v2records:
604 if rec[0] == RECORD_LOCAL:
604 if rec[0] == RECORD_LOCAL:
605 oldv2.add(rec)
605 oldv2.add(rec)
606 elif rec[0] == RECORD_MERGED:
606 elif rec[0] == RECORD_MERGED:
607 # drop the onode data (not contained in v1)
607 # drop the onode data (not contained in v1)
608 oldv2.add((RECORD_MERGED, _droponode(rec[1])))
608 oldv2.add((RECORD_MERGED, _droponode(rec[1])))
609 for rec in v1records:
609 for rec in v1records:
610 if rec not in oldv2:
610 if rec not in oldv2:
611 return False
611 return False
612 else:
612 else:
613 return True
613 return True
614
614
615 def _readrecordsv1(self):
615 def _readrecordsv1(self):
616 """read on disk merge state for version 1 file
616 """read on disk merge state for version 1 file
617
617
618 returns list of record [(TYPE, data), ...]
618 returns list of record [(TYPE, data), ...]
619
619
620 Note: the "F" data from this file are one entry short
620 Note: the "F" data from this file are one entry short
621 (no "other file node" entry)
621 (no "other file node" entry)
622 """
622 """
623 records = []
623 records = []
624 try:
624 try:
625 f = self._repo.vfs(self.statepathv1)
625 f = self._repo.vfs(self.statepathv1)
626 for i, l in enumerate(f):
626 for i, l in enumerate(f):
627 if i == 0:
627 if i == 0:
628 records.append((RECORD_LOCAL, l[:-1]))
628 records.append((RECORD_LOCAL, l[:-1]))
629 else:
629 else:
630 records.append((RECORD_MERGED, l[:-1]))
630 records.append((RECORD_MERGED, l[:-1]))
631 f.close()
631 f.close()
632 except FileNotFoundError:
632 except FileNotFoundError:
633 pass
633 pass
634 return records
634 return records
635
635
636 def _readrecordsv2(self):
636 def _readrecordsv2(self):
637 """read on disk merge state for version 2 file
637 """read on disk merge state for version 2 file
638
638
639 This format is a list of arbitrary records of the form:
639 This format is a list of arbitrary records of the form:
640
640
641 [type][length][content]
641 [type][length][content]
642
642
643 `type` is a single character, `length` is a 4 byte integer, and
643 `type` is a single character, `length` is a 4 byte integer, and
644 `content` is an arbitrary byte sequence of length `length`.
644 `content` is an arbitrary byte sequence of length `length`.
645
645
646 Mercurial versions prior to 3.7 have a bug where if there are
646 Mercurial versions prior to 3.7 have a bug where if there are
647 unsupported mandatory merge records, attempting to clear out the merge
647 unsupported mandatory merge records, attempting to clear out the merge
648 state with hg update --clean or similar aborts. The 't' record type
648 state with hg update --clean or similar aborts. The 't' record type
649 works around that by writing out what those versions treat as an
649 works around that by writing out what those versions treat as an
650 advisory record, but later versions interpret as special: the first
650 advisory record, but later versions interpret as special: the first
651 character is the 'real' record type and everything onwards is the data.
651 character is the 'real' record type and everything onwards is the data.
652
652
653 Returns list of records [(TYPE, data), ...]."""
653 Returns list of records [(TYPE, data), ...]."""
654 records = []
654 records = []
655 try:
655 try:
656 f = self._repo.vfs(self.statepathv2)
656 f = self._repo.vfs(self.statepathv2)
657 data = f.read()
657 data = f.read()
658 off = 0
658 off = 0
659 end = len(data)
659 end = len(data)
660 while off < end:
660 while off < end:
661 rtype = data[off : off + 1]
661 rtype = data[off : off + 1]
662 off += 1
662 off += 1
663 length = _unpack(b'>I', data[off : (off + 4)])[0]
663 length = _unpack(b'>I', data[off : (off + 4)])[0]
664 off += 4
664 off += 4
665 record = data[off : (off + length)]
665 record = data[off : (off + length)]
666 off += length
666 off += length
667 if rtype == RECORD_OVERRIDE:
667 if rtype == RECORD_OVERRIDE:
668 rtype, record = record[0:1], record[1:]
668 rtype, record = record[0:1], record[1:]
669 records.append((rtype, record))
669 records.append((rtype, record))
670 f.close()
670 f.close()
671 except FileNotFoundError:
671 except FileNotFoundError:
672 pass
672 pass
673 return records
673 return records
674
674
675 def commit(self):
675 def commit(self):
676 if self._dirty:
676 if self._dirty:
677 records = self._makerecords()
677 records = self._makerecords()
678 self._writerecords(records)
678 self._writerecords(records)
679 self._dirty = False
679 self._dirty = False
680
680
681 def _makerecords(self):
681 def _makerecords(self):
682 records = []
682 records = []
683 records.append((RECORD_LOCAL, hex(self._local)))
683 records.append((RECORD_LOCAL, hex(self._local)))
684 records.append((RECORD_OTHER, hex(self._other)))
684 records.append((RECORD_OTHER, hex(self._other)))
685 # Write out state items. In all cases, the value of the state map entry
685 # Write out state items. In all cases, the value of the state map entry
686 # is written as the contents of the record. The record type depends on
686 # is written as the contents of the record. The record type depends on
687 # the type of state that is stored, and capital-letter records are used
687 # the type of state that is stored, and capital-letter records are used
688 # to prevent older versions of Mercurial that do not support the feature
688 # to prevent older versions of Mercurial that do not support the feature
689 # from loading them.
689 # from loading them.
690 for filename, v in self._state.items():
690 for filename, v in self._state.items():
691 if v[0] in (
691 if v[0] in (
692 MERGE_RECORD_UNRESOLVED_PATH,
692 MERGE_RECORD_UNRESOLVED_PATH,
693 MERGE_RECORD_RESOLVED_PATH,
693 MERGE_RECORD_RESOLVED_PATH,
694 ):
694 ):
695 # Path conflicts. These are stored in 'P' records. The current
695 # Path conflicts. These are stored in 'P' records. The current
696 # resolution state ('pu' or 'pr') is stored within the record.
696 # resolution state ('pu' or 'pr') is stored within the record.
697 records.append(
697 records.append(
698 (RECORD_PATH_CONFLICT, b'\0'.join([filename] + v))
698 (RECORD_PATH_CONFLICT, b'\0'.join([filename] + v))
699 )
699 )
700 elif (
700 elif (
701 v[1] == self._repo.nodeconstants.nullhex
701 v[1] == self._repo.nodeconstants.nullhex
702 or v[6] == self._repo.nodeconstants.nullhex
702 or v[6] == self._repo.nodeconstants.nullhex
703 ):
703 ):
704 # Change/Delete or Delete/Change conflicts. These are stored in
704 # Change/Delete or Delete/Change conflicts. These are stored in
705 # 'C' records. v[1] is the local file, and is nullhex when the
705 # 'C' records. v[1] is the local file, and is nullhex when the
706 # file is deleted locally ('dc'). v[6] is the remote file, and
706 # file is deleted locally ('dc'). v[6] is the remote file, and
707 # is nullhex when the file is deleted remotely ('cd').
707 # is nullhex when the file is deleted remotely ('cd').
708 records.append(
708 records.append(
709 (RECORD_CHANGEDELETE_CONFLICT, b'\0'.join([filename] + v))
709 (RECORD_CHANGEDELETE_CONFLICT, b'\0'.join([filename] + v))
710 )
710 )
711 else:
711 else:
712 # Normal files. These are stored in 'F' records.
712 # Normal files. These are stored in 'F' records.
713 records.append((RECORD_MERGED, b'\0'.join([filename] + v)))
713 records.append((RECORD_MERGED, b'\0'.join([filename] + v)))
714 for filename, extras in sorted(self._stateextras.items()):
714 for filename, extras in sorted(self._stateextras.items()):
715 rawextras = b'\0'.join(
715 rawextras = b'\0'.join(
716 b'%s\0%s' % (k, v) for k, v in extras.items()
716 b'%s\0%s' % (k, v) for k, v in extras.items()
717 )
717 )
718 records.append(
718 records.append(
719 (RECORD_FILE_VALUES, b'%s\0%s' % (filename, rawextras))
719 (RECORD_FILE_VALUES, b'%s\0%s' % (filename, rawextras))
720 )
720 )
721 if self._labels is not None:
721 if self._labels is not None:
722 labels = b'\0'.join(self._labels)
722 labels = b'\0'.join(self._labels)
723 records.append((RECORD_LABELS, labels))
723 records.append((RECORD_LABELS, labels))
724 return records
724 return records
725
725
726 def _writerecords(self, records):
726 def _writerecords(self, records):
727 """Write current state on disk (both v1 and v2)"""
727 """Write current state on disk (both v1 and v2)"""
728 self._writerecordsv1(records)
728 self._writerecordsv1(records)
729 self._writerecordsv2(records)
729 self._writerecordsv2(records)
730
730
731 def _writerecordsv1(self, records):
731 def _writerecordsv1(self, records):
732 """Write current state on disk in a version 1 file"""
732 """Write current state on disk in a version 1 file"""
733 f = self._repo.vfs(self.statepathv1, b'wb')
733 f = self._repo.vfs(self.statepathv1, b'wb')
734 irecords = iter(records)
734 irecords = iter(records)
735 lrecords = next(irecords)
735 lrecords = next(irecords)
736 assert lrecords[0] == RECORD_LOCAL
736 assert lrecords[0] == RECORD_LOCAL
737 f.write(hex(self._local) + b'\n')
737 f.write(hex(self._local) + b'\n')
738 for rtype, data in irecords:
738 for rtype, data in irecords:
739 if rtype == RECORD_MERGED:
739 if rtype == RECORD_MERGED:
740 f.write(b'%s\n' % _droponode(data))
740 f.write(b'%s\n' % _droponode(data))
741 f.close()
741 f.close()
742
742
743 def _writerecordsv2(self, records):
743 def _writerecordsv2(self, records):
744 """Write current state on disk in a version 2 file
744 """Write current state on disk in a version 2 file
745
745
746 See the docstring for _readrecordsv2 for why we use 't'."""
746 See the docstring for _readrecordsv2 for why we use 't'."""
747 # these are the records that all version 2 clients can read
747 # these are the records that all version 2 clients can read
748 allowlist = (RECORD_LOCAL, RECORD_OTHER, RECORD_MERGED)
748 allowlist = (RECORD_LOCAL, RECORD_OTHER, RECORD_MERGED)
749 f = self._repo.vfs(self.statepathv2, b'wb')
749 f = self._repo.vfs(self.statepathv2, b'wb')
750 for key, data in records:
750 for key, data in records:
751 assert len(key) == 1
751 assert len(key) == 1
752 if key not in allowlist:
752 if key not in allowlist:
753 key, data = RECORD_OVERRIDE, b'%s%s' % (key, data)
753 key, data = RECORD_OVERRIDE, b'%s%s' % (key, data)
754 format = b'>sI%is' % len(data)
754 format = b'>sI%is' % len(data)
755 f.write(_pack(format, key, len(data), data))
755 f.write(_pack(format, key, len(data), data))
756 f.close()
756 f.close()
757
757
758 def _make_backup(self, fctx, localkey):
758 def _make_backup(self, fctx, localkey):
759 self._repo.vfs.write(b'merge/' + localkey, fctx.data())
759 self._repo.vfs.write(b'merge/' + localkey, fctx.data())
760
760
761 def _restore_backup(self, fctx, localkey, flags):
761 def _restore_backup(self, fctx, localkey, flags):
762 with self._repo.vfs(b'merge/' + localkey) as f:
762 with self._repo.vfs(b'merge/' + localkey) as f:
763 fctx.write(f.read(), flags)
763 fctx.write(f.read(), flags)
764
764
765 def reset(self):
765 def reset(self):
766 shutil.rmtree(self._repo.vfs.join(b'merge'), True)
766 shutil.rmtree(self._repo.vfs.join(b'merge'), True)
767
767
768
768
769 class memmergestate(_mergestate_base):
769 class memmergestate(_mergestate_base):
770 def __init__(self, repo):
770 def __init__(self, repo):
771 super(memmergestate, self).__init__(repo)
771 super(memmergestate, self).__init__(repo)
772 self._backups = {}
772 self._backups = {}
773
773
774 def _make_backup(self, fctx, localkey):
774 def _make_backup(self, fctx, localkey):
775 self._backups[localkey] = fctx.data()
775 self._backups[localkey] = fctx.data()
776
776
777 def _restore_backup(self, fctx, localkey, flags):
777 def _restore_backup(self, fctx, localkey, flags):
778 fctx.write(self._backups[localkey], flags)
778 fctx.write(self._backups[localkey], flags)
779
779
780
780
781 def recordupdates(repo, actions, branchmerge, getfiledata):
781 def recordupdates(repo, actions, branchmerge, getfiledata):
782 """record merge actions to the dirstate"""
782 """record merge actions to the dirstate"""
783 dirstate = repo.dirstate
784 update_file = dirstate.update_file
785
783 # remove (must come first)
786 # remove (must come first)
784 for f, args, msg in actions.get(ACTION_REMOVE, []):
787 for f, args, msg in actions.get(ACTION_REMOVE, []):
785 if branchmerge:
788 if branchmerge:
786 repo.dirstate.update_file(f, p1_tracked=True, wc_tracked=False)
789 update_file(f, p1_tracked=True, wc_tracked=False)
787 else:
790 else:
788 repo.dirstate.update_file(f, p1_tracked=False, wc_tracked=False)
791 update_file(f, p1_tracked=False, wc_tracked=False)
789
792
790 # forget (must come first)
793 # forget (must come first)
791 for f, args, msg in actions.get(ACTION_FORGET, []):
794 for f, args, msg in actions.get(ACTION_FORGET, []):
792 repo.dirstate.update_file(f, p1_tracked=False, wc_tracked=False)
795 update_file(f, p1_tracked=False, wc_tracked=False)
793
796
794 # resolve path conflicts
797 # resolve path conflicts
795 for f, args, msg in actions.get(ACTION_PATH_CONFLICT_RESOLVE, []):
798 for f, args, msg in actions.get(ACTION_PATH_CONFLICT_RESOLVE, []):
796 (f0, origf0) = args
799 (f0, origf0) = args
797 repo.dirstate.update_file(f, p1_tracked=False, wc_tracked=True)
800 update_file(f, p1_tracked=False, wc_tracked=True)
798 repo.dirstate.copy(origf0, f)
801 dirstate.copy(origf0, f)
799 if f0 == origf0:
802 if f0 == origf0:
800 repo.dirstate.update_file(f0, p1_tracked=True, wc_tracked=False)
803 update_file(f0, p1_tracked=True, wc_tracked=False)
801 else:
804 else:
802 repo.dirstate.update_file(f0, p1_tracked=False, wc_tracked=False)
805 update_file(f0, p1_tracked=False, wc_tracked=False)
803
806
804 # re-add
807 # re-add
805 for f, args, msg in actions.get(ACTION_ADD, []):
808 for f, args, msg in actions.get(ACTION_ADD, []):
806 repo.dirstate.update_file(f, p1_tracked=False, wc_tracked=True)
809 update_file(f, p1_tracked=False, wc_tracked=True)
807
810
808 # re-add/mark as modified
811 # re-add/mark as modified
809 for f, args, msg in actions.get(ACTION_ADD_MODIFIED, []):
812 for f, args, msg in actions.get(ACTION_ADD_MODIFIED, []):
810 if branchmerge:
813 if branchmerge:
811 repo.dirstate.update_file(
814 update_file(
812 f, p1_tracked=True, wc_tracked=True, possibly_dirty=True
815 f, p1_tracked=True, wc_tracked=True, possibly_dirty=True
813 )
816 )
814 else:
817 else:
815 repo.dirstate.update_file(f, p1_tracked=False, wc_tracked=True)
818 update_file(f, p1_tracked=False, wc_tracked=True)
816
819
817 # exec change
820 # exec change
818 for f, args, msg in actions.get(ACTION_EXEC, []):
821 for f, args, msg in actions.get(ACTION_EXEC, []):
819 repo.dirstate.update_file(
822 update_file(f, p1_tracked=True, wc_tracked=True, possibly_dirty=True)
820 f, p1_tracked=True, wc_tracked=True, possibly_dirty=True
821 )
822
823
823 # keep
824 # keep
824 for f, args, msg in actions.get(ACTION_KEEP, []):
825 for f, args, msg in actions.get(ACTION_KEEP, []):
825 pass
826 pass
826
827
827 # keep deleted
828 # keep deleted
828 for f, args, msg in actions.get(ACTION_KEEP_ABSENT, []):
829 for f, args, msg in actions.get(ACTION_KEEP_ABSENT, []):
829 pass
830 pass
830
831
831 # keep new
832 # keep new
832 for f, args, msg in actions.get(ACTION_KEEP_NEW, []):
833 for f, args, msg in actions.get(ACTION_KEEP_NEW, []):
833 pass
834 pass
834
835
835 # get
836 # get
836 for f, args, msg in actions.get(ACTION_GET, []):
837 for f, args, msg in actions.get(ACTION_GET, []):
837 if branchmerge:
838 if branchmerge:
838 # tracked in p1 can be True also but update_file should not care
839 # tracked in p1 can be True also but update_file should not care
839 old_entry = repo.dirstate.get_entry(f)
840 old_entry = dirstate.get_entry(f)
840 p1_tracked = old_entry.any_tracked and not old_entry.added
841 p1_tracked = old_entry.any_tracked and not old_entry.added
841 repo.dirstate.update_file(
842 update_file(
842 f,
843 f,
843 p1_tracked=p1_tracked,
844 p1_tracked=p1_tracked,
844 wc_tracked=True,
845 wc_tracked=True,
845 p2_info=True,
846 p2_info=True,
846 )
847 )
847 else:
848 else:
848 parentfiledata = getfiledata[f] if getfiledata else None
849 parentfiledata = getfiledata[f] if getfiledata else None
849 repo.dirstate.update_file(
850 update_file(
850 f,
851 f,
851 p1_tracked=True,
852 p1_tracked=True,
852 wc_tracked=True,
853 wc_tracked=True,
853 parentfiledata=parentfiledata,
854 parentfiledata=parentfiledata,
854 )
855 )
855
856
856 # merge
857 # merge
857 for f, args, msg in actions.get(ACTION_MERGE, []):
858 for f, args, msg in actions.get(ACTION_MERGE, []):
858 f1, f2, fa, move, anc = args
859 f1, f2, fa, move, anc = args
859 if branchmerge:
860 if branchmerge:
860 # We've done a branch merge, mark this file as merged
861 # We've done a branch merge, mark this file as merged
861 # so that we properly record the merger later
862 # so that we properly record the merger later
862 p1_tracked = f1 == f
863 p1_tracked = f1 == f
863 repo.dirstate.update_file(
864 update_file(
864 f,
865 f,
865 p1_tracked=p1_tracked,
866 p1_tracked=p1_tracked,
866 wc_tracked=True,
867 wc_tracked=True,
867 p2_info=True,
868 p2_info=True,
868 )
869 )
869 if f1 != f2: # copy/rename
870 if f1 != f2: # copy/rename
870 if move:
871 if move:
871 repo.dirstate.update_file(
872 update_file(f1, p1_tracked=True, wc_tracked=False)
872 f1, p1_tracked=True, wc_tracked=False
873 )
874 if f1 != f:
873 if f1 != f:
875 repo.dirstate.copy(f1, f)
874 dirstate.copy(f1, f)
876 else:
875 else:
877 repo.dirstate.copy(f2, f)
876 dirstate.copy(f2, f)
878 else:
877 else:
879 # We've update-merged a locally modified file, so
878 # We've update-merged a locally modified file, so
880 # we set the dirstate to emulate a normal checkout
879 # we set the dirstate to emulate a normal checkout
881 # of that file some time in the past. Thus our
880 # of that file some time in the past. Thus our
882 # merge will appear as a normal local file
881 # merge will appear as a normal local file
883 # modification.
882 # modification.
884 if f2 == f: # file not locally copied/moved
883 if f2 == f: # file not locally copied/moved
885 repo.dirstate.update_file(
884 update_file(
886 f, p1_tracked=True, wc_tracked=True, possibly_dirty=True
885 f, p1_tracked=True, wc_tracked=True, possibly_dirty=True
887 )
886 )
888 if move:
887 if move:
889 repo.dirstate.update_file(
888 update_file(f1, p1_tracked=False, wc_tracked=False)
890 f1, p1_tracked=False, wc_tracked=False
891 )
892
889
893 # directory rename, move local
890 # directory rename, move local
894 for f, args, msg in actions.get(ACTION_DIR_RENAME_MOVE_LOCAL, []):
891 for f, args, msg in actions.get(ACTION_DIR_RENAME_MOVE_LOCAL, []):
895 f0, flag = args
892 f0, flag = args
896 if branchmerge:
893 if branchmerge:
897 repo.dirstate.update_file(f, p1_tracked=False, wc_tracked=True)
894 update_file(f, p1_tracked=False, wc_tracked=True)
898 repo.dirstate.update_file(f0, p1_tracked=True, wc_tracked=False)
895 update_file(f0, p1_tracked=True, wc_tracked=False)
899 repo.dirstate.copy(f0, f)
896 dirstate.copy(f0, f)
900 else:
897 else:
901 repo.dirstate.update_file(f, p1_tracked=True, wc_tracked=True)
898 update_file(f, p1_tracked=True, wc_tracked=True)
902 repo.dirstate.update_file(f0, p1_tracked=False, wc_tracked=False)
899 update_file(f0, p1_tracked=False, wc_tracked=False)
903
900
904 # directory rename, get
901 # directory rename, get
905 for f, args, msg in actions.get(ACTION_LOCAL_DIR_RENAME_GET, []):
902 for f, args, msg in actions.get(ACTION_LOCAL_DIR_RENAME_GET, []):
906 f0, flag = args
903 f0, flag = args
907 if branchmerge:
904 if branchmerge:
908 repo.dirstate.update_file(f, p1_tracked=False, wc_tracked=True)
905 update_file(f, p1_tracked=False, wc_tracked=True)
909 repo.dirstate.copy(f0, f)
906 dirstate.copy(f0, f)
910 else:
907 else:
911 repo.dirstate.update_file(f, p1_tracked=True, wc_tracked=True)
908 update_file(f, p1_tracked=True, wc_tracked=True)
General Comments 0
You need to be logged in to leave comments. Login now