# -*- coding: utf-8 -*-
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from io import StringIO
from .generic import decide_merge_with_diff
from .decisions import apply_decisions
from ..diffing.notebooks import diff_notebooks
from ..utils import Strategies
from ..prettyprint import (
pretty_print_notebook_diff,
pretty_print_merge_decisions,
pretty_print_notebook,
PrettyPrintConfig
)
import nbdime.log
# Strategies for handling conflicts
generic_conflict_strategies = (
"clear", # Replace value with empty in case of conflict
"remove", # Discard value in case of conflict
"clear-all", # Discard all values on conflict
"fail", # Unexpected: crash and burn in case of conflict (only implemented for leaf nodes)
"inline-cells", # Valid for cell only: use markdown cells as diff markers for conflicting inserts/replace
"inline-source", # Valid for source only: produce new source with inline diff markers
"inline-outputs", # Valid for outputs only: produce new outputs with inline diff markers
"mergetool", # Do not modify decision (but prevent processing at deeper path)
"record-conflict", # Valid for metadata only: produce new metadata with conflicts recorded for external inspection
"take-max", # Take the maximum value in case of conflict
"union", # Join values in case of conflict, don't insert new markers (only applies to sequence types)
"use-base", # Keep base value in case of conflict
"use-local", # Use local value in case of conflict
"use-remote", # Use remote value in case of conflict
)
# Strategies that can be applied to an entire notebook
cli_conflict_strategies = (
"inline", # Inline cells or source and outputs, and record metadata conflicts
"use-base", # Keep base value in case of conflict
"use-local", # Use local value in case of conflict
"use-remote", # Use remote value in case of conflict
#"union", # Take local value, then remote, in case of conflict
)
cli_conflict_strategies_input = cli_conflict_strategies
cli_conflict_strategies_output = cli_conflict_strategies + (
"remove", # Remove conflicting outputs
"clear-all", # Clear all outputs
)
def notebook_merge_strategies(args):
strategies = Strategies({
"/cells/*/id": "remove",
# These fields should never conflict, that would be an internal error:
"/nbformat": "fail",
"/cells/*/cell_type": "fail",
# Pick highest minor format:
"/nbformat_minor": "take-max",
})
ignore_transients = args.ignore_transients if args else True
if ignore_transients:
strategies.transients = [
"/cells/*/execution_count",
#"/cells/*/outputs",
"/cells/*/outputs/*/execution_count",
"/cells/*/metadata/collapsed",
"/cells/*/metadata/autoscroll",
"/cells/*/metadata/scrolled",
]
strategies.update({
"/cells/*/execution_count": "clear",
"/cells/*/outputs/*/execution_count": "clear",
})
# Get args, default to inline for cli tool, intended to produce
# an editable notebook that can be manually edited
merge_strategy = args.merge_strategy if args else "inline"
input_strategy = args.input_strategy if args else None
output_strategy = args.output_strategy if args else None
# Default to merge_strategy
input_strategy = input_strategy or merge_strategy
output_strategy = output_strategy or merge_strategy
metadata_strategy = merge_strategy if merge_strategy != "union" else None
# Set root strategy
if merge_strategy == 'inline':
strategies['/cells'] = "inline-cells"
elif merge_strategy == 'union':
strategies['/cells'] = merge_strategy
else:
strategies["/"] = merge_strategy
# Translate 'inline' to specific strategies for different fields
if input_strategy == 'inline':
source_strategy = "inline-source"
attachments_strategy = "inline-attachments"
else:
source_strategy = input_strategy
attachments_strategy = input_strategy
if output_strategy == 'inline':
outputs_strategy = "inline-outputs"
else:
outputs_strategy = output_strategy
if metadata_strategy == "inline":
metadata_strategy = "record-conflict"
# Set strategies on the main fields
strategies.update({
"/metadata": metadata_strategy,
"/cells/*/metadata": metadata_strategy,
"/cells/*/outputs/*/metadata": metadata_strategy,
"/cells/*/source": source_strategy,
"/cells/*/attachments": attachments_strategy,
"/cells/*/outputs": outputs_strategy
})
return strategies
def decide_notebook_merge(base, local, remote, args=None):
# Build merge strategies for each document path from arguments
strategies = notebook_merge_strategies(args)
# Compute notebook specific diffs
local_diffs = diff_notebooks(base, local)
remote_diffs = diff_notebooks(base, remote)
# Debug outputs
if args and args.log_level == "DEBUG":
# log pretty-print config object:
config = PrettyPrintConfig()
nbdime.log.debug("In merge, base-local diff:")
config.out = StringIO()
pretty_print_notebook_diff("", "", base, local_diffs, config)
nbdime.log.debug(config.out.getvalue())
nbdime.log.debug("In merge, base-remote diff:")
config.out = StringIO()
pretty_print_notebook_diff("", "", base, remote_diffs, config)
nbdime.log.debug(config.out.getvalue())
# Execute a generic merge operation
decisions = decide_merge_with_diff(
base, local, remote,
local_diffs, remote_diffs,
strategies)
# Debug outputs
if args and args.log_level == "DEBUG":
nbdime.log.debug("In merge, decisions:")
config.out = StringIO()
pretty_print_merge_decisions(base, decisions, config)
nbdime.log.debug(config.out.getvalue())
return decisions
def merge_notebooks(base, local, remote, args=None):
"""Merge changes introduced by notebooks local and remote from a shared ancestor base.
Return new (partially) merged notebook and unapplied diffs from the local and remote side.
"""
if args and args.log_level == "DEBUG":
# log pretty-print config object:
config = PrettyPrintConfig()
for (name, nb) in [("base", base), ("local", local), ("remote", remote)]:
nbdime.log.debug("In merge, input %s notebook:", name)
config.out = StringIO()
pretty_print_notebook(nb, config)
nbdime.log.debug(config.out.getvalue())
decisions = decide_notebook_merge(base, local, remote, args)
merged = apply_decisions(base, decisions)
if args and args.log_level == "DEBUG":
nbdime.log.debug("In merge, merged notebook:")
config.out = StringIO()
pretty_print_notebook(merged, config)
nbdime.log.debug(config.out.getvalue())
nbdime.log.debug("End merge")
return merged, decisions