1# coding: utf-8 2 3# Copyright (c) Jupyter Development Team. 4# Distributed under the terms of the Modified BSD License. 5 6 7 8from collections import defaultdict 9from itertools import chain 10 11import nbformat 12 13import nbdime.log 14from .chunks import chunk_typename 15from .decisions import MergeDecisionBuilder, push_patch_decision 16from ..diff_format import ( 17 DiffOp, ParentDeleted, 18 op_patch, op_addrange, op_removerange, op_add, op_replace) 19from ..patching import patch 20from ..prettyprint import merge_render 21from ..utils import join_path 22 23 24# ============================================================================= 25# 26# Utility code follows 27# 28# ============================================================================= 29 30def combine_patches(diffs): 31 """Rewrite diffs in canonical form where only one patch 32 applies to one key and diff entries are sorted by key.""" 33 patches = {} 34 newdiffs = [] 35 for d in diffs: 36 if d.op == DiffOp.PATCH: 37 p = patches.get(d.key) 38 if p is None: 39 p = op_patch(d.key, combine_patches(d.diff)) 40 newdiffs.append(p) 41 patches[d.key] = p 42 else: 43 p.diff = combine_patches(p.diff + d.diff) 44 else: 45 newdiffs.append(d) 46 return sorted(newdiffs, key=lambda x: x.key) 47 48 49def adjust_patch_level(target_path, common_path, diff): 50 n = len(target_path) 51 assert common_path[:n] == target_path 52 if n == len(target_path): 53 return diff 54 remainder_path = tuple(reversed(common_path[n:])) 55 newdiff = [] 56 for d in diff: 57 nd = d 58 assert nd is not None 59 for key in remainder_path: 60 nd = op_patch(key, nd) 61 newdiff.append(nd) 62 return newdiff 63 64 65 66def collect_diffs(path, decisions): 67 local_diff = [] 68 remote_diff = [] 69 for d in decisions: 70 ld = adjust_patch_level(path, d.common_path, d.local_diff) 71 rd = adjust_patch_level(path, d.common_path, d.remote_diff) 72 local_diff.extend(ld) 73 remote_diff.extend(rd) 74 local_diff = combine_patches(local_diff) 75 remote_diff = combine_patches(remote_diff) 76 return local_diff, remote_diff 77 78 79def collect_conflicting_diffs(path, decisions): 80 local_conflict_diffs = [] 81 remote_conflict_diffs = [] 82 for d in decisions: 83 if d.conflict: 84 ld = adjust_patch_level(path, d.common_path, d.local_diff) 85 rd = adjust_patch_level(path, d.common_path, d.remote_diff) 86 local_conflict_diffs.extend(ld) 87 remote_conflict_diffs.extend(rd) 88 return local_conflict_diffs, remote_conflict_diffs 89 90 91def collect_unresolved_diffs(base_path, unresolved_conflicts): 92 """Collect local and remote diffs from conflict tuples. 93 94 Assumed to be on the same path.""" 95 96 # Collect diffs 97 local_conflict_diffs = [] 98 remote_conflict_diffs = [] 99 for conf in unresolved_conflicts: 100 (path, ld, rd, strategy) = conf 101 102 assert base_path == path 103 #assert base_path == path[:len(base_path)] 104 #relative_path = path[len(base_path):] 105 #assert not relative_path 106 107 assert isinstance(ld, list) 108 assert isinstance(rd, list) 109 110 for d in ld: 111 local_conflict_diffs.append(d) 112 for d in rd: 113 remote_conflict_diffs.append(d) 114 115 # Combine patches into canonical tree again 116 local_conflict_diffs = combine_patches(local_conflict_diffs) 117 remote_conflict_diffs = combine_patches(remote_conflict_diffs) 118 return local_conflict_diffs, remote_conflict_diffs 119 120 121 122def bundle_decisions_by_index(base_path, decisions): 123 """""" 124 decisions_by_index = defaultdict(list) 125 level = len(base_path) 126 for d in decisions: 127 assert base_path == d.common_path[:level], ( 128 'decision has incorrect base path: %r vs %r' % (d.common_path, base_path)) 129 if len(d.common_path) > level: 130 # At least patch/patch will have common_path on a particular item 131 key = d.common_path[level] 132 # Wrap decision diffs in patches so common_path points to list 133 prefix = d.common_path[level:] 134 d = push_patch_decision(d, prefix) 135 else: 136 # Removerange or addrange will have common_path 137 # on list and key only in the diff entries 138 keys = set(e.key for e in chain(d.local_diff, d.remote_diff, d.get("custom_diff", ()))) 139 assert len(keys) == 1 140 key, = keys 141 decisions_by_index[key].append(d) 142 return decisions_by_index 143 144 145 146# ============================================================================= 147# 148# Autoresolve code follows 149# 150# ============================================================================= 151 152# Thinking through conflict resolution: 153# - when conflict first encountered, it's registered as dec.conflict(path, ..., strategy) 154# - strategy is used in tryresolve and eventually dropped if a conflict is registered 155# - when recursing and subdecisions are returned, they're added to decisions at this level 156# - i.e. decisions at this point contains all conflicts not resolved for subdocument, 157# as well as unresolved conflicts for this level 158# - if we have a strategy at this level, we can try to resolve all 159# conflicts of subdocument as well as those at this level 160 161def resolve_strategy_inline_attachments(base_path, attachments, decisions): 162 strategy = "inline-attachments" 163 164 local_conflict_diffs, remote_conflict_diffs = collect_conflicting_diffs(base_path, decisions) 165 166 # Drop conflict decisions 167 decisions.decisions = [d for d in decisions if not d.conflict] 168 169 # FIXME: Review this code. 170 171 ldiffs_by_key = {d.key: d for d in local_conflict_diffs} 172 rdiffs_by_key = {d.key: d for d in remote_conflict_diffs} 173 conflict_keys = sorted(set(ldiffs_by_key) | set(rdiffs_by_key)) 174 175 for key in conflict_keys: 176 # key is the attachment filename 177 ld = ldiffs_by_key[key] 178 rd = rdiffs_by_key[key] 179 180 if ld.op == DiffOp.REMOVE: 181 # If one side is removing and we have a conflict, 182 # the other side did an edit and we keep that 183 # but flag a conflict (TODO: Or don't flag conflict?) 184 assert rd.op != DiffOp.REMOVE 185 decisions.remote(base_path, ld, rd, conflict=True, strategy=strategy) 186 elif rd.op == DiffOp.REMOVE: 187 decisions.local(base_path, ld, rd, conflict=True, strategy=strategy) 188 else: 189 # Not merging attachment contents, but adding attachments 190 # with new names LOCAL_oldname and REMOTE_oldname instead. 191 192 base = attachments[key] 193 194 if ld.op == DiffOp.ADD: 195 assert rd.op == DiffOp.ADD 196 local = ld.value 197 elif ld.op == DiffOp.REPLACE: 198 local = ld.value 199 else: 200 assert ld.op == DiffOp.PATCH 201 local = patch(base, ld.diff) 202 203 if rd.op == DiffOp.ADD: 204 remote = rd.value 205 elif rd.op == DiffOp.REPLACE: 206 remote = rd.value 207 else: 208 assert rd.op == DiffOp.PATCH 209 remote = patch(base, rd.diff) 210 211 local_name = "LOCAL_" + key 212 remote_name = "REMOTE_" + key 213 214 custom_diff = [] 215 216 if local_name in attachments: 217 nbdime.log.warning( 218 "Replacing previous conflicted attachment with filename %r", local_name) 219 custom_diff += [op_replace(local_name, local)] 220 else: 221 custom_diff += [op_add(local_name, local)] 222 223 if remote_name in attachments: 224 nbdime.log.warning( 225 "Replacing previous conflicted attachment with filename %r", remote_name) 226 custom_diff += [op_replace(remote_name, remote)] 227 else: 228 custom_diff += [op_add(remote_name, remote)] 229 230 decisions.custom(base_path, ld, rd, custom_diff, conflict=True, strategy=strategy) 231 232 233def output_marker(text): 234 return nbformat.v4.new_output("stream", name="stderr", text=text) 235 236 237def _cell_marker_format(text): 238 return '<span style="color:red">**{0}**</span>'.format(text) 239 240 241def cell_marker(text): 242 return nbformat.v4.new_markdown_cell(source=_cell_marker_format(text)) 243 244 245def get_outputs_and_note(base, removes, patches): 246 if removes: 247 note = " <removed>" 248 suboutputs = [] 249 elif patches: 250 e, = patches # 0 or 1 item 251 252 # Collect which mime types are modified 253 mkeys = set() 254 keys = set() 255 for d in e.diff: 256 if d.key == "data": 257 assert d.op == DiffOp.PATCH 258 for f in d.diff: 259 mkeys.add(f.key) 260 else: 261 keys.add(d.key) 262 data = base.get("data") 263 if data: 264 ukeys = set(data.keys()) - mkeys 265 else: 266 ukeys = () 267 268 notes = [] 269 if mkeys or keys: 270 notes.append("modified: {}".format(", ".join(sorted(mkeys)))) 271 if ukeys: 272 notes.append("unchanged: {}".format(", ".join(sorted(ukeys)))) 273 if notes: 274 note = " <" + "; ".join(notes) + ">" 275 else: 276 note = "" 277 278 suboutputs = [patch(base, e.diff)] 279 else: 280 note = " <unchanged>" 281 suboutputs = [base] 282 return suboutputs, note 283 284 285def make_inline_output_conflict(base_output, local_diff, remote_diff): 286 """Make a list of outputs with conflict markers from conflicting 287 local and remote diffs applying to a single base_output""" 288 # Styling details 289 local_title = "local" 290 remote_title = "remote" 291 marker_size = 7 # default in git 292 m0 = "<"*marker_size 293 m1 = "="*marker_size 294 m2 = ">"*marker_size 295 296 # Split diffs by type 297 d0 = local_diff 298 d1 = remote_diff 299 lpatches = [e for e in d0 if e.op == DiffOp.PATCH] 300 rpatches = [e for e in d1 if e.op == DiffOp.PATCH] 301 linserts = [e for e in d0 if e.op == DiffOp.ADDRANGE] 302 rinserts = [e for e in d1 if e.op == DiffOp.ADDRANGE] 303 lremoves = [e for e in d0 if e.op == DiffOp.REMOVERANGE] 304 rremoves = [e for e in d1 if e.op == DiffOp.REMOVERANGE] 305 assert len(lpatches) + len(linserts) + len(lremoves) == len(d0) 306 assert len(rpatches) + len(rinserts) + len(rremoves) == len(d1) 307 308 # Collect new outputs with surrounding markers 309 outputs = [] 310 311 # Collect inserts from both sides separately 312 if linserts or rinserts: 313 lnote = "" 314 loutputs = [] 315 for e in linserts: # 0 or 1 item 316 loutputs.extend(e.valuelist) 317 rnote = "" 318 routputs = [] 319 for e in rinserts: # 0 or 1 item 320 routputs.extend(e.valuelist) 321 322 outputs.append(output_marker("%s %s%s\n" % (m0, local_title, lnote))) 323 outputs.extend(loutputs) 324 outputs.append(output_marker("%s\n" % (m1,))) 325 outputs.extend(routputs) 326 outputs.append(output_marker("%s %s%s\n" % (m2, remote_title, rnote))) 327 328 # Keep base output if untouched (only inserts) 329 keep_base = not (lremoves or rremoves or lpatches or rpatches) 330 if lremoves and rremoves: 331 # Don't add anything 332 pass 333 elif not keep_base: 334 assert not (lremoves and lpatches) 335 assert not (rremoves and rpatches) 336 lnote = "" 337 rnote = "" 338 339 # Insert changed output with surrounding markers 340 loutputs, lnote = get_outputs_and_note(base_output, lremoves, lpatches) 341 routputs, rnote = get_outputs_and_note(base_output, rremoves, rpatches) 342 343 outputs.append(output_marker("%s %s%s\n" % (m0, local_title, lnote))) 344 outputs.extend(loutputs) 345 outputs.append(output_marker("%s\n" % (m1,))) 346 outputs.extend(routputs) 347 outputs.append(output_marker("%s %s%s\n" % (m2, remote_title, rnote))) 348 349 # Return marked up output 350 return outputs, keep_base 351 352 353 354def make_inline_cell_conflict(base_cells, local_diff, remote_diff): 355 """Make a list of outputs with conflict markers from conflicting 356 local and remote diffs. 357 358 base_cells should be the entire cells array of the base notebook. 359 """ 360 # Note: This is currently only used for conflicting, *non-similar* 361 # insertions/replacements. 362 assert 1 <= len(local_diff) <= 2 363 assert 1 <= len(remote_diff) <= 2 364 assert local_diff[0].op == DiffOp.ADDRANGE and remote_diff[0].op == DiffOp.ADDRANGE 365 assert len(local_diff) == 1 or local_diff[1].op == DiffOp.REMOVERANGE 366 assert len(remote_diff) == 1 or remote_diff[1].op == DiffOp.REMOVERANGE 367 368 # Styling details 369 local_title = "local" 370 remote_title = "remote" 371 marker_size = 7 # default in git 372 m0 = "<"*marker_size 373 m1 = "="*marker_size 374 m2 = ">"*marker_size 375 376 lremove = local_diff[1].length if len(local_diff) > 1 else 0 377 rremove = remote_diff[1].length if len(remote_diff) > 1 else 0 378 379 start = local_diff[0].key 380 lkeep = max(0, lremove - rremove) 381 rkeep = max(0, rremove - lremove) 382 383 lcells = local_diff[0].valuelist + base_cells[start : start + lkeep] 384 rcells = remote_diff[0].valuelist + base_cells[start : start + rkeep] 385 386 cells = [] 387 cells.append(cell_marker("%s %s" % (m0, local_title))) 388 cells.extend(lcells) 389 cells.append(cell_marker("%s" % (m1,))) 390 cells.extend(rcells) 391 cells.append(cell_marker("%s %s" % (m2, remote_title))) 392 393 # Return marked up cells 394 return cells 395 396 397def resolve_strategy_remove_outputs(base_path, outputs, decisions): 398 strategy = "remove" 399 400 decisions_by_index = bundle_decisions_by_index(base_path, decisions) 401 decisions.decisions = [] 402 for key, decs in sorted(decisions_by_index.items()): 403 if not any(d.conflict for d in decs): 404 decisions.decisions.extend(decs) 405 else: 406 # Replace all decisions affecting key with resolution 407 local_diff, remote_diff = collect_diffs(base_path, decs) 408 if ( 409 len(local_diff) == len(remote_diff) == 1 and 410 local_diff[0].op == remote_diff[0].op == DiffOp.ADDRANGE 411 ): 412 # remove in add vs add is a no-op 413 custom_diff = [] 414 else: 415 custom_diff = [op_removerange(key, 1)] 416 decisions.custom(base_path, local_diff, remote_diff, 417 custom_diff, conflict=False, strategy=strategy) 418 419 420def resolve_strategy_inline_outputs(base_path, outputs, decisions): 421 strategy = "inline-outputs" 422 423 decisions_by_index = bundle_decisions_by_index(base_path, decisions) 424 decisions.decisions = [] 425 for key, decs in sorted(decisions_by_index.items()): 426 if not any(d.conflict for d in decs): 427 decisions.decisions.extend(decs) 428 else: 429 # Replace all decisions affecting key with resolution 430 local_diff, remote_diff = collect_diffs(base_path, decs) 431 inlined_conflict, keep_base = make_inline_output_conflict( 432 outputs[key] if outputs else None, local_diff, remote_diff) 433 custom_diff = [] 434 custom_diff += [op_addrange(key, inlined_conflict)] 435 if not keep_base: 436 custom_diff += [op_removerange(key, 1)] 437 decisions.custom(base_path, local_diff, remote_diff, custom_diff, conflict=True, strategy=strategy) 438 439 440def resolve_strategy_record_conflicts(base_path, base, decisions): 441 strategy = "record-conflict" 442 443 decisions.decisions = [push_patch_decision(d, d.common_path[len(base_path):]) for d in decisions] 444 445 local_conflict_diffs, remote_conflict_diffs = collect_conflicting_diffs(base_path, decisions) 446 #local_diff, remote_diff = collect_diffs(base_path, decisions) 447 448 local_conflict_diffs = combine_patches(local_conflict_diffs) 449 remote_conflict_diffs = combine_patches(remote_conflict_diffs) 450 451 # Drop conflict decisions 452 #conflict_decisions = [d for d in decisions if d.conflict] 453 decisions.decisions = [d for d in decisions if not d.conflict] 454 455 # Record remaining conflicts in field nbdime-conflicts 456 conflicts_dict = { 457 "local_diff": local_conflict_diffs, 458 "remote_diff": remote_conflict_diffs, 459 # TODO: Record local and remote versions of full metadata, easier to manually select one? 460 #"base_metadata": base, 461 #"local_metadata": patch(base, local_diff), 462 #"remote_metadata": patch(base, remote_diff), 463 } 464 if "nbdime-conflicts" in base: 465 nbdime.log.warning("Replacing previous nbdime-conflicts field from base notebook.") 466 op = op_replace("nbdime-conflicts", conflicts_dict) 467 else: 468 nbdime.log.warning( 469 "Recording unresolved conflicts in %s/nbdime-conflicts.", join_path(base_path)) 470 op = op_add("nbdime-conflicts", conflicts_dict) 471 custom_diff = [op] 472 473 decisions.custom( 474 base_path, local_conflict_diffs, remote_conflict_diffs, custom_diff, 475 conflict=True, strategy=strategy) 476 477 478def resolve_strategy_inline_source(path, base, local_diff, remote_diff): 479 strategy = "inline-source" 480 481 decisions = MergeDecisionBuilder() 482 483 if local_diff is ParentDeleted: 484 # Add marker at top of cell, easier to clean up manually 485 local_diff = [op_addrange(0, ["<<<<<<< LOCAL CELL DELETED >>>>>>>\n"])] 486 decisions.local_then_remote(path, local_diff, remote_diff, conflict=True, strategy=strategy) 487 elif remote_diff is ParentDeleted: 488 # Add marker at top of cell, easier to clean up manually 489 remote_diff = [op_addrange(0, ["<<<<<<< REMOTE CELL DELETED >>>>>>>\n"])] 490 decisions.remote_then_local(path, local_diff, remote_diff, conflict=True, strategy=strategy) 491 else: 492 # This is another approach, replacing content with markers 493 # if local_diff is ParentDeleted: 494 # local = "<CELL DELETED>" 495 # else: 496 # local = patch(base, local_diff) 497 # if remote_diff is ParentDeleted: 498 # remote = "<CELL DELETED>" 499 # else: 500 # remote = patch(base, remote_diff) 501 502 # Run merge renderer on full sources 503 local = patch(base, local_diff) 504 remote = patch(base, remote_diff) 505 merged, status = merge_render(base, local, remote, None) 506 conflict = status != 0 507 508 assert path[-1] == "source" 509 custom_diff = [op_replace(path[-1], merged)] 510 decisions.custom(path[:-1], 511 [op_patch(path[-1], local_diff)], 512 [op_patch(path[-1], remote_diff)], 513 custom_diff, conflict=conflict, strategy=strategy) 514 515 return decisions 516 517 518def resolve_strategy_inline_recurse(path, base, decisions): 519 strategy = "inline-cells" 520 521 old_decisions = decisions.decisions 522 decisions.decisions = [] 523 524 for d in old_decisions: 525 if not d.conflict: 526 decisions.decisions.append(d) 527 continue 528 assert d.local_diff and d.remote_diff 529 laname, lpname = chunk_typename(d.local_diff) 530 raname, rpname = chunk_typename(d.remote_diff) 531 chunktype = laname + lpname + "/" + raname + rpname 532 if (chunktype not in ('AR/A', 'A/AR', 'A/A', 'AR/AR') or 533 d.common_path != ('cells',)): 534 decisions.decisions.append(d) 535 continue 536 if d.get('similar_insert', None) is None: 537 # Inserts not similar, cannot recurse. Markup block 538 cells = make_inline_cell_conflict(base, d.local_diff, d.remote_diff) 539 rdiff = [] 540 if len(d.local_diff) > 1: 541 rdiff.append(d.local_diff[1]) 542 elif len(d.remote_diff) > 1: 543 rdiff.append(d.remote_diff[1]) 544 545 decisions.custom(path, 546 d.local_diff, 547 d.remote_diff, 548 [op_addrange(d.local_diff[0].key, cells)] + rdiff, 549 conflict=True, 550 strategy=strategy) 551 continue 552 # Should only be insert range vs insertion range here now: 553 # FIXME: Remove asserts when sure there are no missed corner cases: 554 assert chunktype == 'A/A', 'Unexpected chunk type: %r' % chunktype 555 556 # We have conflicting, similar inserts, should only be one cell per decision 557 if not (len(d.local_diff[0].valuelist) == len(d.remote_diff[0].valuelist) == 1): 558 raise AssertionError('Unexpected diff length. Expected both local and remote ' 559 'inserts to have length 1, as they are assumed similar.') 560 561 custom_diff = [] 562 # Diff of local to remote: 563 lr_diff = d.similar_insert 564 assert lr_diff[0].op == 'patch' 565 lcell = d.local_diff[0].valuelist[0] 566 rcell = d.remote_diff[0].valuelist[0] 567 568 assert lcell['cell_type'] == rcell['cell_type'], 'cell types cannot differ' 569 570 cell = {} 571 keys = [e.key for e in lr_diff[0].diff] 572 573 for k in lcell.keys(): 574 if k not in keys: 575 # either identical, or one-way from local 576 cell[k] = lcell[k] 577 for k in rcell.keys(): 578 if k not in keys and k not in lcell: 579 # one-way from remote 580 cell[k] = rcell[k] 581 582 for k in keys: 583 if k == 'source': 584 cell[k] = merge_render('', lcell[k], rcell[k], None)[0] 585 586 elif k == 'metadata': 587 cell[k] = { 588 "local_metadata": lcell[k], 589 "remote_metadata": rcell[k], 590 } 591 592 elif k == 'id': 593 cell[k] = { 594 "local_id": lcell[k], 595 "remote_id": rcell[k], 596 } 597 598 elif k == 'execution_count': 599 cell[k] = None # Clear 600 601 elif k == 'outputs': 602 cell[k] = [] 603 # TODO: Do inline merge 604 pass 605 606 else: 607 raise ValueError('Conflict on unrecognized key: %r' % (k,)) 608 609 custom_diff = [op_addrange(d.local_diff[0].key, [cell])] 610 611 decisions.custom(path, 612 d.local_diff, 613 d.remote_diff, 614 custom_diff, 615 conflict=True, 616 strategy=strategy) 617 618 619def resolve_strategy_generic(path, decisions, strategy): 620 if not (strategy and strategy != "mergetool" and decisions.has_conflicted()): 621 return 622 623 if strategy.startswith("use-"): 624 # Example: /cells strategy use-local, 625 # patch/remove conflict on /cells/3, 626 # decision is changed to action=local 627 action = strategy.replace("use-", "") 628 for d in decisions: 629 # Resolve conflicts that aren't marked with an 630 # already applied strategy. This applies to 631 # at least the inline conflict strategies. 632 if d.conflict and not d.get("strategy"): 633 d.action = action 634 d.conflict = False 635 else: 636 msg = "Unexpected strategy {} on {}.".format( 637 strategy, join_path(path)) 638 nbdime.log.error(msg) 639 640 641def resolve_conflicted_decisions_list(path, base, decisions, strategy): 642 if not (strategy and strategy != "mergetool" and decisions.has_conflicted()): 643 return 644 645 if strategy == "inline-outputs": 646 # Affects conflicts on output dicts at /cells/*/outputs 647 resolve_strategy_inline_outputs(path, base, decisions) 648 649 elif strategy == "inline-cells": 650 # Affects conflicts on cell dicts at /cells/* 651 resolve_strategy_inline_recurse(path, base, decisions) 652 653 elif strategy == "remove": 654 # Affects conflicts on output dicts at /cells/*/outputs 655 resolve_strategy_remove_outputs(path, base, decisions) 656 657 elif strategy == "clear-all": 658 # Old approach that relies on special handling in apply_decisions 659 # to deal with clear-all overriding other decisions: 660 # Collect local diffs and remote diffs from unresolved_conflicts 661 #local_conflict_diffs, remote_conflict_diffs = collect_conflicting_diffs(path, decisions) 662 #decisions.add_decision(path, "clear_all", local_conflict_diffs, remote_conflict_diffs, conflict=False) 663 664 # Just drop all decisions and add a decision to remove the entire range 665 local_diff, remote_diff = collect_diffs(path, decisions) 666 custom_diff = [op_removerange(0, len(base))] 667 decisions.decisions = [] 668 decisions.custom(path, local_diff, remote_diff, custom_diff, conflict=False, strategy=strategy) 669 670 elif strategy == "clear": 671 # FIXME: Only getting here when base is a list of the lines in a string, 672 # actually resolved in resolve_conflicted_decisions_strings 673 # Avoid error message in generic 674 pass 675 676 else: 677 resolve_strategy_generic(path, decisions, strategy) 678 679 680def resolve_conflicted_decisions_dict(path, base, decisions, strategy): 681 if not (strategy and strategy != "mergetool" and decisions.has_conflicted()): 682 return 683 684 if strategy == "record-conflict": 685 # affects conflicts on dicts at /***/metadata or below 686 resolve_strategy_record_conflicts(path, base, decisions) 687 688 elif strategy == "inline-attachments": 689 # affects conflicts on string at /cells/*/attachments or below 690 resolve_strategy_inline_attachments(path, base, decisions) 691 692 else: 693 resolve_strategy_generic(path, decisions, strategy) 694 695 696def resolve_conflicted_decisions_strings(path, decisions, strategy): 697 if not (strategy and strategy != "mergetool" and decisions.has_conflicted()): 698 return 699 700 if strategy == "clear": 701 for d in decisions: 702 if d.conflict and not d.get("strategy"): 703 d.action = "clear" 704 d.conflict = False 705 elif strategy == "inline-source": 706 # Avoid error message in generic 707 pass 708 else: 709 resolve_strategy_generic(path, decisions, strategy) 710