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