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