comparison mercurial/merge.py @ 37112:a532b2f54f95

merge: use constants for merge state record types merge.py is using multiple discrete sets of 1 and 2 letter constants to define types and behavior. To the uninitiated, the code is very difficult to reason about. I didn't even realize there were multiple sets of constants in play initially! We begin our sanity injection with merge state records. The record types (which are serialized to disk) are now defined in RECORD_* constants. Differential Revision: https://phab.mercurial-scm.org/D2698
author Gregory Szorc <gregory.szorc@gmail.com>
date Mon, 05 Mar 2018 14:09:23 -0500
parents 71543b942eea
children 1b158ca37ea4
comparison
equal deleted inserted replaced
37111:0351fb0153ba 37112:a532b2f54f95
45 # used for compatibility for v1 45 # used for compatibility for v1
46 bits = data.split('\0') 46 bits = data.split('\0')
47 bits = bits[:-2] + bits[-1:] 47 bits = bits[:-2] + bits[-1:]
48 return '\0'.join(bits) 48 return '\0'.join(bits)
49 49
50 # Merge state record types. See ``mergestate`` docs for more.
51 RECORD_LOCAL = b'L'
52 RECORD_OTHER = b'O'
53 RECORD_MERGED = b'F'
54 RECORD_CHANGEDELETE_CONFLICT = b'C'
55 RECORD_MERGE_DRIVER_MERGE = b'D'
56 RECORD_PATH_CONFLICT = b'P'
57 RECORD_MERGE_DRIVER_STATE = b'm'
58 RECORD_FILE_VALUES = b'f'
59 RECORD_LABELS = b'l'
60 RECORD_OVERRIDE = b't'
61 RECORD_UNSUPPORTED_MANDATORY = b'X'
62 RECORD_UNSUPPORTED_ADVISORY = b'x'
63
50 class mergestate(object): 64 class mergestate(object):
51 '''track 3-way merge state of individual files 65 '''track 3-way merge state of individual files
52 66
53 The merge state is stored on disk when needed. Two files are used: one with 67 The merge state is stored on disk when needed. Two files are used: one with
54 an old format (version 1), and one with a new format (version 2). Version 2 68 an old format (version 1), and one with a new format (version 2). Version 2
156 self._readmergedriver = None 170 self._readmergedriver = None
157 self._mdstate = 's' 171 self._mdstate = 's'
158 unsupported = set() 172 unsupported = set()
159 records = self._readrecords() 173 records = self._readrecords()
160 for rtype, record in records: 174 for rtype, record in records:
161 if rtype == 'L': 175 if rtype == RECORD_LOCAL:
162 self._local = bin(record) 176 self._local = bin(record)
163 elif rtype == 'O': 177 elif rtype == RECORD_OTHER:
164 self._other = bin(record) 178 self._other = bin(record)
165 elif rtype == 'm': 179 elif rtype == RECORD_MERGE_DRIVER_STATE:
166 bits = record.split('\0', 1) 180 bits = record.split('\0', 1)
167 mdstate = bits[1] 181 mdstate = bits[1]
168 if len(mdstate) != 1 or mdstate not in 'ums': 182 if len(mdstate) != 1 or mdstate not in 'ums':
169 # the merge driver should be idempotent, so just rerun it 183 # the merge driver should be idempotent, so just rerun it
170 mdstate = 'u' 184 mdstate = 'u'
171 185
172 self._readmergedriver = bits[0] 186 self._readmergedriver = bits[0]
173 self._mdstate = mdstate 187 self._mdstate = mdstate
174 elif rtype in 'FDCP': 188 elif rtype in (RECORD_MERGED, RECORD_CHANGEDELETE_CONFLICT,
189 RECORD_PATH_CONFLICT, RECORD_MERGE_DRIVER_MERGE):
175 bits = record.split('\0') 190 bits = record.split('\0')
176 self._state[bits[0]] = bits[1:] 191 self._state[bits[0]] = bits[1:]
177 elif rtype == 'f': 192 elif rtype == RECORD_FILE_VALUES:
178 filename, rawextras = record.split('\0', 1) 193 filename, rawextras = record.split('\0', 1)
179 extraparts = rawextras.split('\0') 194 extraparts = rawextras.split('\0')
180 extras = {} 195 extras = {}
181 i = 0 196 i = 0
182 while i < len(extraparts): 197 while i < len(extraparts):
183 extras[extraparts[i]] = extraparts[i + 1] 198 extras[extraparts[i]] = extraparts[i + 1]
184 i += 2 199 i += 2
185 200
186 self._stateextras[filename] = extras 201 self._stateextras[filename] = extras
187 elif rtype == 'l': 202 elif rtype == RECORD_LABELS:
188 labels = record.split('\0', 2) 203 labels = record.split('\0', 2)
189 self._labels = [l for l in labels if len(l) > 0] 204 self._labels = [l for l in labels if len(l) > 0]
190 elif not rtype.islower(): 205 elif not rtype.islower():
191 unsupported.add(rtype) 206 unsupported.add(rtype)
192 self._results = {} 207 self._results = {}
216 else: 231 else:
217 # v1 file is newer than v2 file, use it 232 # v1 file is newer than v2 file, use it
218 # we have to infer the "other" changeset of the merge 233 # we have to infer the "other" changeset of the merge
219 # we cannot do better than that with v1 of the format 234 # we cannot do better than that with v1 of the format
220 mctx = self._repo[None].parents()[-1] 235 mctx = self._repo[None].parents()[-1]
221 v1records.append(('O', mctx.hex())) 236 v1records.append((RECORD_OTHER, mctx.hex()))
222 # add place holder "other" file node information 237 # add place holder "other" file node information
223 # nobody is using it yet so we do no need to fetch the data 238 # nobody is using it yet so we do no need to fetch the data
224 # if mctx was wrong `mctx[bits[-2]]` may fails. 239 # if mctx was wrong `mctx[bits[-2]]` may fails.
225 for idx, r in enumerate(v1records): 240 for idx, r in enumerate(v1records):
226 if r[0] == 'F': 241 if r[0] == RECORD_MERGED:
227 bits = r[1].split('\0') 242 bits = r[1].split('\0')
228 bits.insert(-2, '') 243 bits.insert(-2, '')
229 v1records[idx] = (r[0], '\0'.join(bits)) 244 v1records[idx] = (r[0], '\0'.join(bits))
230 return v1records 245 return v1records
231 246
232 def _v1v2match(self, v1records, v2records): 247 def _v1v2match(self, v1records, v2records):
233 oldv2 = set() # old format version of v2 record 248 oldv2 = set() # old format version of v2 record
234 for rec in v2records: 249 for rec in v2records:
235 if rec[0] == 'L': 250 if rec[0] == RECORD_LOCAL:
236 oldv2.add(rec) 251 oldv2.add(rec)
237 elif rec[0] == 'F': 252 elif rec[0] == RECORD_MERGED:
238 # drop the onode data (not contained in v1) 253 # drop the onode data (not contained in v1)
239 oldv2.add(('F', _droponode(rec[1]))) 254 oldv2.add((RECORD_MERGED, _droponode(rec[1])))
240 for rec in v1records: 255 for rec in v1records:
241 if rec not in oldv2: 256 if rec not in oldv2:
242 return False 257 return False
243 else: 258 else:
244 return True 259 return True
254 records = [] 269 records = []
255 try: 270 try:
256 f = self._repo.vfs(self.statepathv1) 271 f = self._repo.vfs(self.statepathv1)
257 for i, l in enumerate(f): 272 for i, l in enumerate(f):
258 if i == 0: 273 if i == 0:
259 records.append(('L', l[:-1])) 274 records.append((RECORD_LOCAL, l[:-1]))
260 else: 275 else:
261 records.append(('F', l[:-1])) 276 records.append((RECORD_MERGED, l[:-1]))
262 f.close() 277 f.close()
263 except IOError as err: 278 except IOError as err:
264 if err.errno != errno.ENOENT: 279 if err.errno != errno.ENOENT:
265 raise 280 raise
266 return records 281 return records
294 off += 1 309 off += 1
295 length = _unpack('>I', data[off:(off + 4)])[0] 310 length = _unpack('>I', data[off:(off + 4)])[0]
296 off += 4 311 off += 4
297 record = data[off:(off + length)] 312 record = data[off:(off + length)]
298 off += length 313 off += length
299 if rtype == 't': 314 if rtype == RECORD_OVERRIDE:
300 rtype, record = record[0:1], record[1:] 315 rtype, record = record[0:1], record[1:]
301 records.append((rtype, record)) 316 records.append((rtype, record))
302 f.close() 317 f.close()
303 except IOError as err: 318 except IOError as err:
304 if err.errno != errno.ENOENT: 319 if err.errno != errno.ENOENT:
357 self._writerecords(records) 372 self._writerecords(records)
358 self._dirty = False 373 self._dirty = False
359 374
360 def _makerecords(self): 375 def _makerecords(self):
361 records = [] 376 records = []
362 records.append(('L', hex(self._local))) 377 records.append((RECORD_LOCAL, hex(self._local)))
363 records.append(('O', hex(self._other))) 378 records.append((RECORD_OTHER, hex(self._other)))
364 if self.mergedriver: 379 if self.mergedriver:
365 records.append(('m', '\0'.join([ 380 records.append((RECORD_MERGE_DRIVER_STATE, '\0'.join([
366 self.mergedriver, self._mdstate]))) 381 self.mergedriver, self._mdstate])))
367 # Write out state items. In all cases, the value of the state map entry 382 # Write out state items. In all cases, the value of the state map entry
368 # is written as the contents of the record. The record type depends on 383 # is written as the contents of the record. The record type depends on
369 # the type of state that is stored, and capital-letter records are used 384 # the type of state that is stored, and capital-letter records are used
370 # to prevent older versions of Mercurial that do not support the feature 385 # to prevent older versions of Mercurial that do not support the feature
371 # from loading them. 386 # from loading them.
372 for filename, v in self._state.iteritems(): 387 for filename, v in self._state.iteritems():
373 if v[0] == 'd': 388 if v[0] == 'd':
374 # Driver-resolved merge. These are stored in 'D' records. 389 # Driver-resolved merge. These are stored in 'D' records.
375 records.append(('D', '\0'.join([filename] + v))) 390 records.append((RECORD_MERGE_DRIVER_MERGE,
391 '\0'.join([filename] + v)))
376 elif v[0] in ('pu', 'pr'): 392 elif v[0] in ('pu', 'pr'):
377 # Path conflicts. These are stored in 'P' records. The current 393 # Path conflicts. These are stored in 'P' records. The current
378 # resolution state ('pu' or 'pr') is stored within the record. 394 # resolution state ('pu' or 'pr') is stored within the record.
379 records.append(('P', '\0'.join([filename] + v))) 395 records.append((RECORD_PATH_CONFLICT,
396 '\0'.join([filename] + v)))
380 elif v[1] == nullhex or v[6] == nullhex: 397 elif v[1] == nullhex or v[6] == nullhex:
381 # Change/Delete or Delete/Change conflicts. These are stored in 398 # Change/Delete or Delete/Change conflicts. These are stored in
382 # 'C' records. v[1] is the local file, and is nullhex when the 399 # 'C' records. v[1] is the local file, and is nullhex when the
383 # file is deleted locally ('dc'). v[6] is the remote file, and 400 # file is deleted locally ('dc'). v[6] is the remote file, and
384 # is nullhex when the file is deleted remotely ('cd'). 401 # is nullhex when the file is deleted remotely ('cd').
385 records.append(('C', '\0'.join([filename] + v))) 402 records.append((RECORD_CHANGEDELETE_CONFLICT,
403 '\0'.join([filename] + v)))
386 else: 404 else:
387 # Normal files. These are stored in 'F' records. 405 # Normal files. These are stored in 'F' records.
388 records.append(('F', '\0'.join([filename] + v))) 406 records.append((RECORD_MERGED,
407 '\0'.join([filename] + v)))
389 for filename, extras in sorted(self._stateextras.iteritems()): 408 for filename, extras in sorted(self._stateextras.iteritems()):
390 rawextras = '\0'.join('%s\0%s' % (k, v) for k, v in 409 rawextras = '\0'.join('%s\0%s' % (k, v) for k, v in
391 extras.iteritems()) 410 extras.iteritems())
392 records.append(('f', '%s\0%s' % (filename, rawextras))) 411 records.append((RECORD_FILE_VALUES,
412 '%s\0%s' % (filename, rawextras)))
393 if self._labels is not None: 413 if self._labels is not None:
394 labels = '\0'.join(self._labels) 414 labels = '\0'.join(self._labels)
395 records.append(('l', labels)) 415 records.append((RECORD_LABELS, labels))
396 return records 416 return records
397 417
398 def _writerecords(self, records): 418 def _writerecords(self, records):
399 """Write current state on disk (both v1 and v2)""" 419 """Write current state on disk (both v1 and v2)"""
400 self._writerecordsv1(records) 420 self._writerecordsv1(records)
403 def _writerecordsv1(self, records): 423 def _writerecordsv1(self, records):
404 """Write current state on disk in a version 1 file""" 424 """Write current state on disk in a version 1 file"""
405 f = self._repo.vfs(self.statepathv1, 'wb') 425 f = self._repo.vfs(self.statepathv1, 'wb')
406 irecords = iter(records) 426 irecords = iter(records)
407 lrecords = next(irecords) 427 lrecords = next(irecords)
408 assert lrecords[0] == 'L' 428 assert lrecords[0] == RECORD_LOCAL
409 f.write(hex(self._local) + '\n') 429 f.write(hex(self._local) + '\n')
410 for rtype, data in irecords: 430 for rtype, data in irecords:
411 if rtype == 'F': 431 if rtype == RECORD_MERGED:
412 f.write('%s\n' % _droponode(data)) 432 f.write('%s\n' % _droponode(data))
413 f.close() 433 f.close()
414 434
415 def _writerecordsv2(self, records): 435 def _writerecordsv2(self, records):
416 """Write current state on disk in a version 2 file 436 """Write current state on disk in a version 2 file
417 437
418 See the docstring for _readrecordsv2 for why we use 't'.""" 438 See the docstring for _readrecordsv2 for why we use 't'."""
419 # these are the records that all version 2 clients can read 439 # these are the records that all version 2 clients can read
420 whitelist = 'LOF' 440 allowlist = (RECORD_LOCAL, RECORD_OTHER, RECORD_MERGED)
421 f = self._repo.vfs(self.statepathv2, 'wb') 441 f = self._repo.vfs(self.statepathv2, 'wb')
422 for key, data in records: 442 for key, data in records:
423 assert len(key) == 1 443 assert len(key) == 1
424 if key not in whitelist: 444 if key not in allowlist:
425 key, data = 't', '%s%s' % (key, data) 445 key, data = RECORD_OVERRIDE, '%s%s' % (key, data)
426 format = '>sI%is' % len(data) 446 format = '>sI%is' % len(data)
427 f.write(_pack(format, key, len(data), data)) 447 f.write(_pack(format, key, len(data), data))
428 f.close() 448 f.close()
429 449
430 def add(self, fcl, fco, fca, fd): 450 def add(self, fcl, fco, fca, fd):