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