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