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