comparison mercurial/mergestate.py @ 45509:cc5f811b1f15

mergestate: extract a base class to be shared by future memmergestate This extracts a new base class from `mergestate` and leaves all the vfs-touching code in `mergestate`. Differential Revision: https://phab.mercurial-scm.org/D9039
author Martin von Zweigbergk <martinvonz@google.com>
date Tue, 15 Sep 2020 11:17:24 -0700
parents e833ff4dd0ea
children 19590b126764
comparison
equal deleted inserted replaced
45508:e833ff4dd0ea 45509:cc5f811b1f15
125 ACTION_KEEP_ABSENT = b'ka' 125 ACTION_KEEP_ABSENT = b'ka'
126 ACTION_EXEC = b'e' 126 ACTION_EXEC = b'e'
127 ACTION_CREATED_MERGE = b'cm' 127 ACTION_CREATED_MERGE = b'cm'
128 128
129 129
130 class mergestate(object): 130 class _mergestate_base(object):
131 '''track 3-way merge state of individual files 131 '''track 3-way merge state of individual files
132 132
133 The merge state is stored on disk when needed. Two files are used: one with 133 The merge state is stored on disk when needed. Two files are used: one with
134 an old format (version 1), and one with a new format (version 2). Version 2 134 an old format (version 1), and one with a new format (version 2). Version 2
135 stores a superset of the data in version 1, including new kinds of records 135 stores a superset of the data in version 1, including new kinds of records
169 d: driver-resolved conflict 169 d: driver-resolved conflict
170 170
171 The resolve command transitions between 'u' and 'r' for conflicts and 171 The resolve command transitions between 'u' and 'r' for conflicts and
172 'pu' and 'pr' for path conflicts. 172 'pu' and 'pr' for path conflicts.
173 ''' 173 '''
174
175 statepathv1 = b'merge/state'
176 statepathv2 = b'merge/state2'
177
178 @staticmethod
179 def clean(repo):
180 """Initialize a brand new merge state, removing any existing state on
181 disk."""
182 ms = mergestate(repo)
183 ms.reset()
184 return ms
185
186 @staticmethod
187 def read(repo):
188 """Initialize the merge state, reading it from disk."""
189 ms = mergestate(repo)
190 ms._read()
191 return ms
192 174
193 def __init__(self, repo): 175 def __init__(self, repo):
194 """Initialize the merge state. 176 """Initialize the merge state.
195 177
196 Do not use this directly! Instead call read() or clean().""" 178 Do not use this directly! Instead call read() or clean()."""
217 self._local = node 199 self._local = node
218 self._other = other 200 self._other = other
219 self._labels = labels 201 self._labels = labels
220 if self.mergedriver: 202 if self.mergedriver:
221 self._mdstate = MERGE_DRIVER_STATE_SUCCESS 203 self._mdstate = MERGE_DRIVER_STATE_SUCCESS
222
223 def _read(self):
224 """Analyse each record content to restore a serialized state from disk
225
226 This function process "record" entry produced by the de-serialization
227 of on disk file.
228 """
229 self._mdstate = MERGE_DRIVER_STATE_SUCCESS
230 unsupported = set()
231 records = self._readrecords()
232 for rtype, record in records:
233 if rtype == RECORD_LOCAL:
234 self._local = bin(record)
235 elif rtype == RECORD_OTHER:
236 self._other = bin(record)
237 elif rtype == RECORD_MERGE_DRIVER_STATE:
238 bits = record.split(b'\0', 1)
239 mdstate = bits[1]
240 if len(mdstate) != 1 or mdstate not in (
241 MERGE_DRIVER_STATE_UNMARKED,
242 MERGE_DRIVER_STATE_MARKED,
243 MERGE_DRIVER_STATE_SUCCESS,
244 ):
245 # the merge driver should be idempotent, so just rerun it
246 mdstate = MERGE_DRIVER_STATE_UNMARKED
247
248 self._readmergedriver = bits[0]
249 self._mdstate = mdstate
250 elif rtype in (
251 RECORD_MERGED,
252 RECORD_CHANGEDELETE_CONFLICT,
253 RECORD_PATH_CONFLICT,
254 RECORD_MERGE_DRIVER_MERGE,
255 LEGACY_RECORD_RESOLVED_OTHER,
256 ):
257 bits = record.split(b'\0')
258 # merge entry type MERGE_RECORD_MERGED_OTHER is deprecated
259 # and we now store related information in _stateextras, so
260 # lets write to _stateextras directly
261 if bits[1] == MERGE_RECORD_MERGED_OTHER:
262 self._stateextras[bits[0]][b'filenode-source'] = b'other'
263 else:
264 self._state[bits[0]] = bits[1:]
265 elif rtype == RECORD_FILE_VALUES:
266 filename, rawextras = record.split(b'\0', 1)
267 extraparts = rawextras.split(b'\0')
268 extras = {}
269 i = 0
270 while i < len(extraparts):
271 extras[extraparts[i]] = extraparts[i + 1]
272 i += 2
273
274 self._stateextras[filename] = extras
275 elif rtype == RECORD_LABELS:
276 labels = record.split(b'\0', 2)
277 self._labels = [l for l in labels if len(l) > 0]
278 elif not rtype.islower():
279 unsupported.add(rtype)
280
281 if unsupported:
282 raise error.UnsupportedMergeRecords(unsupported)
283
284 def _readrecords(self):
285 """Read merge state from disk and return a list of record (TYPE, data)
286
287 We read data from both v1 and v2 files and decide which one to use.
288
289 V1 has been used by version prior to 2.9.1 and contains less data than
290 v2. We read both versions and check if no data in v2 contradicts
291 v1. If there is not contradiction we can safely assume that both v1
292 and v2 were written at the same time and use the extract data in v2. If
293 there is contradiction we ignore v2 content as we assume an old version
294 of Mercurial has overwritten the mergestate file and left an old v2
295 file around.
296
297 returns list of record [(TYPE, data), ...]"""
298 v1records = self._readrecordsv1()
299 v2records = self._readrecordsv2()
300 if self._v1v2match(v1records, v2records):
301 return v2records
302 else:
303 # v1 file is newer than v2 file, use it
304 # we have to infer the "other" changeset of the merge
305 # we cannot do better than that with v1 of the format
306 mctx = self._repo[None].parents()[-1]
307 v1records.append((RECORD_OTHER, mctx.hex()))
308 # add place holder "other" file node information
309 # nobody is using it yet so we do no need to fetch the data
310 # if mctx was wrong `mctx[bits[-2]]` may fails.
311 for idx, r in enumerate(v1records):
312 if r[0] == RECORD_MERGED:
313 bits = r[1].split(b'\0')
314 bits.insert(-2, b'')
315 v1records[idx] = (r[0], b'\0'.join(bits))
316 return v1records
317
318 def _v1v2match(self, v1records, v2records):
319 oldv2 = set() # old format version of v2 record
320 for rec in v2records:
321 if rec[0] == RECORD_LOCAL:
322 oldv2.add(rec)
323 elif rec[0] == RECORD_MERGED:
324 # drop the onode data (not contained in v1)
325 oldv2.add((RECORD_MERGED, _droponode(rec[1])))
326 for rec in v1records:
327 if rec not in oldv2:
328 return False
329 else:
330 return True
331
332 def _readrecordsv1(self):
333 """read on disk merge state for version 1 file
334
335 returns list of record [(TYPE, data), ...]
336
337 Note: the "F" data from this file are one entry short
338 (no "other file node" entry)
339 """
340 records = []
341 try:
342 f = self._repo.vfs(self.statepathv1)
343 for i, l in enumerate(f):
344 if i == 0:
345 records.append((RECORD_LOCAL, l[:-1]))
346 else:
347 records.append((RECORD_MERGED, l[:-1]))
348 f.close()
349 except IOError as err:
350 if err.errno != errno.ENOENT:
351 raise
352 return records
353
354 def _readrecordsv2(self):
355 """read on disk merge state for version 2 file
356
357 This format is a list of arbitrary records of the form:
358
359 [type][length][content]
360
361 `type` is a single character, `length` is a 4 byte integer, and
362 `content` is an arbitrary byte sequence of length `length`.
363
364 Mercurial versions prior to 3.7 have a bug where if there are
365 unsupported mandatory merge records, attempting to clear out the merge
366 state with hg update --clean or similar aborts. The 't' record type
367 works around that by writing out what those versions treat as an
368 advisory record, but later versions interpret as special: the first
369 character is the 'real' record type and everything onwards is the data.
370
371 Returns list of records [(TYPE, data), ...]."""
372 records = []
373 try:
374 f = self._repo.vfs(self.statepathv2)
375 data = f.read()
376 off = 0
377 end = len(data)
378 while off < end:
379 rtype = data[off : off + 1]
380 off += 1
381 length = _unpack(b'>I', data[off : (off + 4)])[0]
382 off += 4
383 record = data[off : (off + length)]
384 off += length
385 if rtype == RECORD_OVERRIDE:
386 rtype, record = record[0:1], record[1:]
387 records.append((rtype, record))
388 f.close()
389 except IOError as err:
390 if err.errno != errno.ENOENT:
391 raise
392 return records
393 204
394 @util.propertycache 205 @util.propertycache
395 def mergedriver(self): 206 def mergedriver(self):
396 # protect against the following: 207 # protect against the following:
397 # - A configures a malicious merge driver in their hgrc, then 208 # - A configures a malicious merge driver in their hgrc, then
504 if self._labels is not None: 315 if self._labels is not None:
505 labels = b'\0'.join(self._labels) 316 labels = b'\0'.join(self._labels)
506 records.append((RECORD_LABELS, labels)) 317 records.append((RECORD_LABELS, labels))
507 return records 318 return records
508 319
509 def _writerecords(self, records):
510 """Write current state on disk (both v1 and v2)"""
511 self._writerecordsv1(records)
512 self._writerecordsv2(records)
513
514 def _writerecordsv1(self, records):
515 """Write current state on disk in a version 1 file"""
516 f = self._repo.vfs(self.statepathv1, b'wb')
517 irecords = iter(records)
518 lrecords = next(irecords)
519 assert lrecords[0] == RECORD_LOCAL
520 f.write(hex(self._local) + b'\n')
521 for rtype, data in irecords:
522 if rtype == RECORD_MERGED:
523 f.write(b'%s\n' % _droponode(data))
524 f.close()
525
526 def _writerecordsv2(self, records):
527 """Write current state on disk in a version 2 file
528
529 See the docstring for _readrecordsv2 for why we use 't'."""
530 # these are the records that all version 2 clients can read
531 allowlist = (RECORD_LOCAL, RECORD_OTHER, RECORD_MERGED)
532 f = self._repo.vfs(self.statepathv2, b'wb')
533 for key, data in records:
534 assert len(key) == 1
535 if key not in allowlist:
536 key, data = RECORD_OVERRIDE, b'%s%s' % (key, data)
537 format = b'>sI%is' % len(data)
538 f.write(_pack(format, key, len(data), data))
539 f.close()
540
541 @staticmethod 320 @staticmethod
542 def getlocalkey(path): 321 def getlocalkey(path):
543 """hash the path of a local file context for storage in the .hg/merge 322 """hash the path of a local file context for storage in the .hg/merge
544 directory.""" 323 directory."""
545 324
546 return hex(hashutil.sha1(path).digest()) 325 return hex(hashutil.sha1(path).digest())
547 326
548 def _make_backup(self, fctx, localkey): 327 def _make_backup(self, fctx, localkey):
549 self._repo.vfs.write(b'merge/' + localkey, fctx.data()) 328 raise NotImplementedError()
550 329
551 def _restore_backup(self, fctx, localkey, flags): 330 def _restore_backup(self, fctx, localkey, flags):
552 with self._repo.vfs(b'merge/' + localkey) as f: 331 raise NotImplementedError()
553 fctx.write(f.read(), flags)
554 332
555 def add(self, fcl, fco, fca, fd): 333 def add(self, fcl, fco, fca, fd):
556 """add a new (potentially?) conflicting file the merge state 334 """add a new (potentially?) conflicting file the merge state
557 fcl: file context for local, 335 fcl: file context for local,
558 fco: file context for remote, 336 fco: file context for remote,
787 565
788 Meant for use by custom merge drivers.""" 566 Meant for use by custom merge drivers."""
789 self._results[f] = 0, ACTION_GET 567 self._results[f] = 0, ACTION_GET
790 568
791 569
570 class mergestate(_mergestate_base):
571
572 statepathv1 = b'merge/state'
573 statepathv2 = b'merge/state2'
574
575 @staticmethod
576 def clean(repo):
577 """Initialize a brand new merge state, removing any existing state on
578 disk."""
579 ms = mergestate(repo)
580 ms.reset()
581 return ms
582
583 @staticmethod
584 def read(repo):
585 """Initialize the merge state, reading it from disk."""
586 ms = mergestate(repo)
587 ms._read()
588 return ms
589
590 def _read(self):
591 """Analyse each record content to restore a serialized state from disk
592
593 This function process "record" entry produced by the de-serialization
594 of on disk file.
595 """
596 self._mdstate = MERGE_DRIVER_STATE_SUCCESS
597 unsupported = set()
598 records = self._readrecords()
599 for rtype, record in records:
600 if rtype == RECORD_LOCAL:
601 self._local = bin(record)
602 elif rtype == RECORD_OTHER:
603 self._other = bin(record)
604 elif rtype == RECORD_MERGE_DRIVER_STATE:
605 bits = record.split(b'\0', 1)
606 mdstate = bits[1]
607 if len(mdstate) != 1 or mdstate not in (
608 MERGE_DRIVER_STATE_UNMARKED,
609 MERGE_DRIVER_STATE_MARKED,
610 MERGE_DRIVER_STATE_SUCCESS,
611 ):
612 # the merge driver should be idempotent, so just rerun it
613 mdstate = MERGE_DRIVER_STATE_UNMARKED
614
615 self._readmergedriver = bits[0]
616 self._mdstate = mdstate
617 elif rtype in (
618 RECORD_MERGED,
619 RECORD_CHANGEDELETE_CONFLICT,
620 RECORD_PATH_CONFLICT,
621 RECORD_MERGE_DRIVER_MERGE,
622 LEGACY_RECORD_RESOLVED_OTHER,
623 ):
624 bits = record.split(b'\0')
625 # merge entry type MERGE_RECORD_MERGED_OTHER is deprecated
626 # and we now store related information in _stateextras, so
627 # lets write to _stateextras directly
628 if bits[1] == MERGE_RECORD_MERGED_OTHER:
629 self._stateextras[bits[0]][b'filenode-source'] = b'other'
630 else:
631 self._state[bits[0]] = bits[1:]
632 elif rtype == RECORD_FILE_VALUES:
633 filename, rawextras = record.split(b'\0', 1)
634 extraparts = rawextras.split(b'\0')
635 extras = {}
636 i = 0
637 while i < len(extraparts):
638 extras[extraparts[i]] = extraparts[i + 1]
639 i += 2
640
641 self._stateextras[filename] = extras
642 elif rtype == RECORD_LABELS:
643 labels = record.split(b'\0', 2)
644 self._labels = [l for l in labels if len(l) > 0]
645 elif not rtype.islower():
646 unsupported.add(rtype)
647
648 if unsupported:
649 raise error.UnsupportedMergeRecords(unsupported)
650
651 def _readrecords(self):
652 """Read merge state from disk and return a list of record (TYPE, data)
653
654 We read data from both v1 and v2 files and decide which one to use.
655
656 V1 has been used by version prior to 2.9.1 and contains less data than
657 v2. We read both versions and check if no data in v2 contradicts
658 v1. If there is not contradiction we can safely assume that both v1
659 and v2 were written at the same time and use the extract data in v2. If
660 there is contradiction we ignore v2 content as we assume an old version
661 of Mercurial has overwritten the mergestate file and left an old v2
662 file around.
663
664 returns list of record [(TYPE, data), ...]"""
665 v1records = self._readrecordsv1()
666 v2records = self._readrecordsv2()
667 if self._v1v2match(v1records, v2records):
668 return v2records
669 else:
670 # v1 file is newer than v2 file, use it
671 # we have to infer the "other" changeset of the merge
672 # we cannot do better than that with v1 of the format
673 mctx = self._repo[None].parents()[-1]
674 v1records.append((RECORD_OTHER, mctx.hex()))
675 # add place holder "other" file node information
676 # nobody is using it yet so we do no need to fetch the data
677 # if mctx was wrong `mctx[bits[-2]]` may fails.
678 for idx, r in enumerate(v1records):
679 if r[0] == RECORD_MERGED:
680 bits = r[1].split(b'\0')
681 bits.insert(-2, b'')
682 v1records[idx] = (r[0], b'\0'.join(bits))
683 return v1records
684
685 def _v1v2match(self, v1records, v2records):
686 oldv2 = set() # old format version of v2 record
687 for rec in v2records:
688 if rec[0] == RECORD_LOCAL:
689 oldv2.add(rec)
690 elif rec[0] == RECORD_MERGED:
691 # drop the onode data (not contained in v1)
692 oldv2.add((RECORD_MERGED, _droponode(rec[1])))
693 for rec in v1records:
694 if rec not in oldv2:
695 return False
696 else:
697 return True
698
699 def _readrecordsv1(self):
700 """read on disk merge state for version 1 file
701
702 returns list of record [(TYPE, data), ...]
703
704 Note: the "F" data from this file are one entry short
705 (no "other file node" entry)
706 """
707 records = []
708 try:
709 f = self._repo.vfs(self.statepathv1)
710 for i, l in enumerate(f):
711 if i == 0:
712 records.append((RECORD_LOCAL, l[:-1]))
713 else:
714 records.append((RECORD_MERGED, l[:-1]))
715 f.close()
716 except IOError as err:
717 if err.errno != errno.ENOENT:
718 raise
719 return records
720
721 def _readrecordsv2(self):
722 """read on disk merge state for version 2 file
723
724 This format is a list of arbitrary records of the form:
725
726 [type][length][content]
727
728 `type` is a single character, `length` is a 4 byte integer, and
729 `content` is an arbitrary byte sequence of length `length`.
730
731 Mercurial versions prior to 3.7 have a bug where if there are
732 unsupported mandatory merge records, attempting to clear out the merge
733 state with hg update --clean or similar aborts. The 't' record type
734 works around that by writing out what those versions treat as an
735 advisory record, but later versions interpret as special: the first
736 character is the 'real' record type and everything onwards is the data.
737
738 Returns list of records [(TYPE, data), ...]."""
739 records = []
740 try:
741 f = self._repo.vfs(self.statepathv2)
742 data = f.read()
743 off = 0
744 end = len(data)
745 while off < end:
746 rtype = data[off : off + 1]
747 off += 1
748 length = _unpack(b'>I', data[off : (off + 4)])[0]
749 off += 4
750 record = data[off : (off + length)]
751 off += length
752 if rtype == RECORD_OVERRIDE:
753 rtype, record = record[0:1], record[1:]
754 records.append((rtype, record))
755 f.close()
756 except IOError as err:
757 if err.errno != errno.ENOENT:
758 raise
759 return records
760
761 def _writerecords(self, records):
762 """Write current state on disk (both v1 and v2)"""
763 self._writerecordsv1(records)
764 self._writerecordsv2(records)
765
766 def _writerecordsv1(self, records):
767 """Write current state on disk in a version 1 file"""
768 f = self._repo.vfs(self.statepathv1, b'wb')
769 irecords = iter(records)
770 lrecords = next(irecords)
771 assert lrecords[0] == RECORD_LOCAL
772 f.write(hex(self._local) + b'\n')
773 for rtype, data in irecords:
774 if rtype == RECORD_MERGED:
775 f.write(b'%s\n' % _droponode(data))
776 f.close()
777
778 def _writerecordsv2(self, records):
779 """Write current state on disk in a version 2 file
780
781 See the docstring for _readrecordsv2 for why we use 't'."""
782 # these are the records that all version 2 clients can read
783 allowlist = (RECORD_LOCAL, RECORD_OTHER, RECORD_MERGED)
784 f = self._repo.vfs(self.statepathv2, b'wb')
785 for key, data in records:
786 assert len(key) == 1
787 if key not in allowlist:
788 key, data = RECORD_OVERRIDE, b'%s%s' % (key, data)
789 format = b'>sI%is' % len(data)
790 f.write(_pack(format, key, len(data), data))
791 f.close()
792
793 def _make_backup(self, fctx, localkey):
794 self._repo.vfs.write(b'merge/' + localkey, fctx.data())
795
796 def _restore_backup(self, fctx, localkey, flags):
797 with self._repo.vfs(b'merge/' + localkey) as f:
798 fctx.write(f.read(), flags)
799
800 def reset(self):
801 shutil.rmtree(self._repo.vfs.join(b'merge'), True)
802
803
792 def recordupdates(repo, actions, branchmerge, getfiledata): 804 def recordupdates(repo, actions, branchmerge, getfiledata):
793 """record merge actions to the dirstate""" 805 """record merge actions to the dirstate"""
794 # remove (must come first) 806 # remove (must come first)
795 for f, args, msg in actions.get(ACTION_REMOVE, []): 807 for f, args, msg in actions.get(ACTION_REMOVE, []):
796 if branchmerge: 808 if branchmerge: