Mercurial > public > mercurial-scm > hg
comparison mercurial/mergestate.py @ 45498: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
45497:e833ff4dd0ea | 45498: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: |