diff FEATURE merge diff
diff --git a/src/diff.c b/src/diff.c
index 01fa8ff..fcb7079 100644
--- a/src/diff.c
+++ b/src/diff.c
@@ -43,6 +43,28 @@
     return NULL;
 }
 
+static enum lyd_diff_op
+lyd_diff_str2op(const char *str)
+{
+    switch (str[0]) {
+    case 'c':
+        assert(!strcmp(str, "create"));
+        return LYD_DIFF_OP_CREATE;
+    case 'd':
+        assert(!strcmp(str, "delete"));
+        return LYD_DIFF_OP_DELETE;
+    case 'r':
+        assert(!strcmp(str, "replace"));
+        return LYD_DIFF_OP_REPLACE;
+    case 'n':
+        assert(!strcmp(str, "none"));
+        return LYD_DIFF_OP_NONE;
+    }
+
+    LOGINT(NULL);
+    return 0;
+}
+
 LY_ERR
 lyd_diff_add(const struct lyd_node *node, enum lyd_diff_op op, const char *orig_default, const char *orig_value,
              const char *key, const char *value, const char *orig_key, struct lyd_node **diff)
@@ -282,7 +304,8 @@
 
     /* orig-default */
     if ((options & LYD_DIFF_WITHDEFAULTS) && (schema->nodetype == LYS_LEAFLIST)
-            && ((*op == LYD_DIFF_OP_REPLACE) || (*op == LYD_DIFF_OP_NONE))) {
+            && ((*op == LYD_DIFF_OP_REPLACE) || (*op == LYD_DIFF_OP_NONE))
+            && ((first->flags & LYD_DEFAULT) != (second->flags & LYD_DEFAULT))) {
         if (first->flags & LYD_DEFAULT) {
             *orig_default = "true";
         } else {
@@ -449,7 +472,8 @@
 
     /* orig-default */
     if ((options & LYD_DIFF_WITHDEFAULTS) && (schema->nodetype & LYD_NODE_TERM)
-            && ((*op == LYD_DIFF_OP_REPLACE) || (*op == LYD_DIFF_OP_NONE))) {
+            && ((*op == LYD_DIFF_OP_REPLACE) || (*op == LYD_DIFF_OP_NONE))
+            && ((first->flags & LYD_DEFAULT) != (second->flags & LYD_DEFAULT))) {
         if (first->flags & LYD_DEFAULT) {
             *orig_default = "true";
         } else {
@@ -698,10 +722,9 @@
  *
  * @param[in] first_node First sibling in the data tree.
  * @param[in] diff_node Diff node to match.
- * @param[out] match_p Matching node.
- * @return LY_ERR value.
+ * @param[out] match_p Matching node, NULL if no found.
  */
-static LY_ERR
+static void
 lyd_diff_find_node(const struct lyd_node *first_node, const struct lyd_node *diff_node, struct lyd_node **match_p)
 {
     if (diff_node->schema->nodetype & (LYS_LIST | LYS_LEAFLIST)) {
@@ -711,9 +734,6 @@
         /* try to simply find the node, there cannot be more instances */
         lyd_find_sibling_val(first_node, diff_node->schema, NULL, 0, match_p);
     }
-    LY_CHECK_ERR_RET(!*match_p, LOGINT(LYD_NODE_CTX(diff_node)), LY_EINT);
-
-    return LY_SUCCESS;
 }
 
 /**
@@ -721,15 +741,14 @@
  *
  * @param[in] diff_node Diff node.
  * @param[out] op Operation.
- * @param[out] key_or_value Optional list instance keys predicate or leaf-list value for move operation.
  * @return LY_ERR value.
  */
 static LY_ERR
-lyd_diff_get_op(const struct lyd_node *diff_node, const char **op, const char **key_or_value)
+lyd_diff_get_op(const struct lyd_node *diff_node, enum lyd_diff_op *op)
 {
     struct lyd_meta *meta = NULL;
     const struct lyd_node *diff_parent;
-    const char *meta_name, *str;
+    const char *str;
     int dynamic;
 
     for (diff_parent = diff_node; diff_parent; diff_parent = (struct lyd_node *)diff_parent->parent) {
@@ -741,7 +760,7 @@
                     /* we do not care about this operation if it's in our parent */
                     continue;
                 }
-                *op = str;
+                *op = lyd_diff_str2op(str);
                 break;
             }
         }
@@ -751,25 +770,6 @@
     }
     LY_CHECK_ERR_RET(!meta, LOGINT(LYD_NODE_CTX(diff_node)), LY_EINT);
 
-    *key_or_value = NULL;
-    if (lysc_is_userordered(diff_node->schema) && (((*op)[0] == 'c') || ((*op)[0] == 'r'))) {
-        if (diff_node->schema->nodetype == LYS_LIST) {
-            meta_name = "key";
-        } else {
-            meta_name = "value";
-        }
-
-        LY_LIST_FOR(diff_node->meta, meta) {
-            if (!strcmp(meta->name, meta_name) && !strcmp(meta->annotation->module->name, "yang")) {
-                str = lyd_meta2str(meta, &dynamic);
-                assert(!dynamic);
-                *key_or_value = str;
-                break;
-            }
-        }
-        LY_CHECK_ERR_RET(!meta, LOGINT(LYD_NODE_CTX(diff_node)), LY_EINT);
-    }
-
     return LY_SUCCESS;
 }
 
@@ -860,6 +860,8 @@
  * @param[in,out] first_node First sibling of the data tree.
  * @param[in] parent_node Parent of the first sibling.
  * @param[in] diff_node Current diff node.
+ * @param[in] diff_cb Optional diff callback.
+ * @param[in] cb_data User data for @p diff_cb.
  * @return LY_ERR value.
  */
 static LY_ERR
@@ -868,33 +870,42 @@
 {
     LY_ERR ret;
     struct lyd_node *match, *diff_child;
-    const char *op, *key_or_value, *str_val;
+    const char *str_val;
+    enum lyd_diff_op op;
+    struct lyd_meta *meta;
     int dynamic;
     const struct ly_ctx *ctx = LYD_NODE_CTX(diff_node);
 
     /* read all the valid attributes */
-    LY_CHECK_RET(lyd_diff_get_op(diff_node, &op, &key_or_value));
+    LY_CHECK_RET(lyd_diff_get_op(diff_node, &op));
 
-    /* handle user-ordered (leaf-)lists separately */
-    if (key_or_value) {
-        assert((op[0] == 'c') || (op[0] == 'r'));
-        if (op[0] == 'r') {
+    /* handle specific user-ordered (leaf-)lists operations separately */
+    if (lysc_is_userordered(diff_node->schema) && ((op == LYD_DIFF_OP_CREATE) || (op == LYD_DIFF_OP_REPLACE))) {
+        if (op == LYD_DIFF_OP_REPLACE) {
             /* find the node (we must have some siblings because the node was only moved) */
-            LY_CHECK_RET(lyd_diff_find_node(*first_node, diff_node, &match));
+            lyd_diff_find_node(*first_node, diff_node, &match);
         } else {
             /* duplicate the node(s) */
             match = lyd_dup(diff_node, NULL, LYD_DUP_NO_META);
             LY_CHECK_RET(!match, LY_EMEM);
         }
 
+        /* get "key" or "value" metadata string value */
+        meta = lyd_find_meta(diff_node->meta, NULL, diff_node->schema->nodetype == LYS_LIST ? "yang:key" : "yang:value");
+        LY_CHECK_ERR_RET(!meta, LOGINT(LYD_NODE_CTX(diff_node)), LY_EINT);
+        str_val = lyd_meta2str(meta, &dynamic);
+
         /* insert/move the node */
-        if (key_or_value[0]) {
-            ret = lyd_diff_insert(first_node, parent_node, match, key_or_value);
+        if (str_val[0]) {
+            ret = lyd_diff_insert(first_node, parent_node, match, str_val);
         } else {
             ret = lyd_diff_insert(first_node, parent_node, match, NULL);
         }
+        if (dynamic) {
+            free((char *)str_val);
+        }
         if (ret) {
-            if (op[0] == 'c') {
+            if (op == LYD_DIFF_OP_CREATE) {
                 lyd_free_tree(match);
             }
             return ret;
@@ -904,10 +915,10 @@
     }
 
     /* apply operation */
-    switch (op[0]) {
-    case 'n':
+    switch (op) {
+    case LYD_DIFF_OP_NONE:
         /* find the node */
-        LY_CHECK_RET(lyd_diff_find_node(*first_node, diff_node, &match));
+        lyd_diff_find_node(*first_node, diff_node, &match);
 
         if (match->schema->nodetype & LYD_NODE_TERM) {
             /* special case of only dflt flag change */
@@ -923,7 +934,7 @@
             }
         }
         break;
-    case 'c':
+    case LYD_DIFF_OP_CREATE:
         /* duplicate the node */
         match = lyd_dup(diff_node, NULL, LYD_DUP_NO_META);
         LY_CHECK_RET(!match, LY_EMEM);
@@ -943,9 +954,9 @@
         }
 
         break;
-    case 'd':
+    case LYD_DIFF_OP_DELETE:
         /* find the node */
-        LY_CHECK_RET(lyd_diff_find_node(*first_node, diff_node, &match));
+        lyd_diff_find_node(*first_node, diff_node, &match);
 
         /* remove it */
         if ((match == *first_node) && !match->parent) {
@@ -957,11 +968,11 @@
 
         /* we are not going recursively in this case, the whole subtree was already deleted */
         return LY_SUCCESS;
-    case 'r':
+    case LYD_DIFF_OP_REPLACE:
         LY_CHECK_ERR_RET(diff_node->schema->nodetype != LYS_LEAF, LOGINT(ctx), LY_EINT);
 
         /* find the node */
-        LY_CHECK_RET(lyd_diff_find_node(*first_node, diff_node, &match));
+        lyd_diff_find_node(*first_node, diff_node, &match);
 
         /* update its value */
         str_val = lyd_value2str((struct lyd_node_term *)diff_node, &dynamic);
@@ -1018,3 +1029,518 @@
 {
     return lyd_diff_apply_module(data, diff, NULL, NULL, NULL);
 }
+
+/**
+ * @brief Update operations on a diff node when the new operation is NONE.
+ *
+ * @param[in] diff_match Node from the diff.
+ * @param[in] cur_op Current operation of the diff node.
+ * @param[in] src_diff Current source diff node.
+ * @return LY_ERR value.
+ */
+static LY_ERR
+lyd_diff_merge_none(struct lyd_node *diff_match, enum lyd_diff_op cur_op, const struct lyd_node *src_diff)
+{
+    switch (cur_op) {
+    case LYD_DIFF_OP_NONE:
+    case LYD_DIFF_OP_CREATE:
+    case LYD_DIFF_OP_REPLACE:
+        if (src_diff->schema->nodetype & LYD_NODE_TERM) {
+            /* NONE on a term means only its dflt flag was changed */
+            diff_match->flags &= ~LYD_DEFAULT;
+            diff_match->flags |= src_diff->flags & LYD_DEFAULT;
+        }
+        break;
+    default:
+        /* delete operation is not valid */
+        LOGINT_RET(LYD_NODE_CTX(src_diff));
+    }
+
+    return LY_SUCCESS;
+}
+
+/**
+ * @brief Remove an attribute from a node.
+ *
+ * @param[in] node Node with the metadata.
+ * @param[in] name Metadata name.
+ */
+static void
+lyd_diff_del_meta(struct lyd_node *node, const char *name)
+{
+    struct lyd_meta *meta;
+
+    LY_LIST_FOR(node->meta, meta) {
+        if (!strcmp(meta->name, name) && !strcmp(meta->annotation->module->name, "yang")) {
+            lyd_free_meta(LYD_NODE_CTX(node), meta, 0);
+            return;
+        }
+    }
+
+    assert(0);
+}
+
+/**
+ * @brief Set a specific operation of a node. Delete the previous operation, if any.
+ *
+ * @param[in] node Node to change.
+ * @param[in] op Operation to set.
+ * @return LY_ERR value.
+ */
+static LY_ERR
+lyd_diff_change_op(struct lyd_node *node, enum lyd_diff_op op)
+{
+    struct lyd_meta *meta;
+
+    LY_LIST_FOR(node->meta, meta) {
+        if (!strcmp(meta->name, "operation") && !strcmp(meta->annotation->module->name, "yang")) {
+            lyd_free_meta(LYD_NODE_CTX(node), meta, 0);
+            break;
+        }
+    }
+
+    if (!lyd_new_meta(node, NULL, "yang:operation", lyd_diff_op2str(op))) {
+        return LY_EINT;
+    }
+    return LY_SUCCESS;
+}
+
+/**
+ * @brief Update operations on a diff node when the new operation is REPLACE.
+ *
+ * @param[in] diff_match Node from the diff.
+ * @param[in] cur_op Current operation of the diff node.
+ * @param[in] src_diff Current source diff node.
+ * @return LY_ERR value.
+ */
+static LY_ERR
+lyd_diff_merge_replace(struct lyd_node *diff_match, enum lyd_diff_op cur_op, const struct lyd_node *src_diff)
+{
+    LY_ERR ret;
+    int dynamic;
+    const char *str_val, *meta_name;
+    struct lyd_meta *meta;
+    const struct lys_module *mod;
+    const struct lyd_node_any *any;
+
+    /* get "yang" module for the metadata */
+    mod = ly_ctx_get_module_latest(LYD_NODE_CTX(diff_match), "yang");
+    assert(mod);
+
+    switch (cur_op) {
+    case LYD_DIFF_OP_REPLACE:
+    case LYD_DIFF_OP_CREATE:
+        switch (diff_match->schema->nodetype) {
+        case LYS_LIST:
+        case LYS_LEAFLIST:
+            /* it was created/moved somewhere somewhere, but now it will be created/moved somewhere else,
+             * keep orig_key/orig_value (only replace oper) and replace key/value */
+            assert(lysc_is_userordered(diff_match->schema));
+            meta_name = (diff_match->schema->nodetype == LYS_LIST ? "key" : "value");
+
+            lyd_diff_del_meta(diff_match, meta_name);
+            meta = lyd_find_meta(src_diff->meta, mod, meta_name);
+            LY_CHECK_ERR_RET(!meta, LOGINT(LYD_NODE_CTX(src_diff)), LY_EINT);
+            if (!lyd_dup_meta(meta, diff_match)) {
+                return LY_EINT;
+            }
+            break;
+        case LYS_LEAF:
+            /* replaced with the exact same value, impossible */
+            if (!lyd_compare(diff_match, src_diff, 0)) {
+                LOGINT_RET(LYD_NODE_CTX(src_diff));
+            }
+
+            /* get leaf value */
+            str_val = lyd_value2str((struct lyd_node_term *)src_diff, &dynamic);
+
+            /* modify the node value */
+            ret = lyd_change_term(diff_match, str_val);
+            if (dynamic) {
+                free((char *)str_val);
+            }
+            if (ret) {
+                LOGINT_RET(LYD_NODE_CTX(src_diff));
+            }
+
+            /* compare values whether there is any change at all */
+            meta = lyd_find_meta(diff_match->meta, mod, "orig-value");
+            LY_CHECK_ERR_RET(!meta, LOGINT(LYD_NODE_CTX(diff_match)), LY_EINT);
+            str_val = lyd_meta2str(meta, &dynamic);
+            ret = lyd_value_compare((struct lyd_node_term *)diff_match, str_val, strlen(str_val), lydjson_resolve_prefix,
+                                    NULL, LYD_JSON, NULL);
+            if (dynamic) {
+                free((char *)str_val);
+            }
+            if (!ret) {
+                /* values are the same, remove orig-value meta and set oper to NONE */
+                lyd_free_meta(LYD_NODE_CTX(diff_match), meta, 0);
+                LY_CHECK_RET(lyd_diff_change_op(diff_match, LYD_DIFF_OP_NONE));
+            }
+
+            /* modify the default flag */
+            diff_match->flags &= ~LYD_DEFAULT;
+            diff_match->flags |= src_diff->flags & LYD_DEFAULT;
+            break;
+        case LYS_ANYXML:
+        case LYS_ANYDATA:
+            if (!lyd_compare(diff_match, src_diff, 0)) {
+                /* replaced with the exact same value, impossible */
+                LOGINT_RET(LYD_NODE_CTX(src_diff));
+            }
+
+            /* modify the node value */
+            any = (struct lyd_node_any *)src_diff;
+            LY_CHECK_RET(lyd_any_copy_value(diff_match, &any->value, any->value_type));
+            break;
+        default:
+            LOGINT_RET(LYD_NODE_CTX(src_diff));
+        }
+        break;
+    case LYD_DIFF_OP_NONE:
+        /* it is moved now */
+        assert(lysc_is_userordered(diff_match->schema) && (diff_match->schema->nodetype == LYS_LIST));
+
+        /* change the operation */
+        LY_CHECK_RET(lyd_diff_change_op(diff_match, LYD_DIFF_OP_REPLACE));
+
+        /* set orig-key and key metadata */
+        meta = lyd_find_meta(src_diff->meta, mod, "orig-key");
+        LY_CHECK_ERR_RET(!meta, LOGINT(LYD_NODE_CTX(src_diff)), LY_EINT);
+        if (!lyd_dup_meta(meta, diff_match)) {
+            return LY_EINT;
+        }
+
+        meta = lyd_find_meta(src_diff->meta, mod, "key");
+        LY_CHECK_ERR_RET(!meta, LOGINT(LYD_NODE_CTX(src_diff)), LY_EINT);
+        if (!lyd_dup_meta(meta, diff_match)) {
+            return LY_EINT;
+        }
+        break;
+    default:
+        /* delete operation is not valid */
+        LOGINT_RET(LYD_NODE_CTX(src_diff));
+    }
+
+    return LY_SUCCESS;
+}
+
+/**
+ * @brief Update operations in a diff node when the new operation is CREATE.
+ *
+ * @param[in] diff_match Node from the diff.
+ * @param[in] cur_op Current operation of the diff node.
+ * @param[in] src_diff Current source diff node.
+ * @return LY_ERR value.
+ */
+static LY_ERR
+lyd_diff_merge_create(struct lyd_node *diff_match, enum lyd_diff_op cur_op, const struct lyd_node *src_diff)
+{
+    struct lyd_node *child;
+    struct lyd_meta *meta;
+    const char *str_val;
+    int dynamic;
+    LY_ERR ret;
+
+    switch (cur_op) {
+    case LYD_DIFF_OP_DELETE:
+        /* delete operation, if any */
+        if (!lyd_compare(diff_match, src_diff, 0)) {
+            /* deleted + created -> operation NONE */
+            LY_CHECK_RET(lyd_diff_change_op(diff_match, LYD_DIFF_OP_NONE));
+
+            if (diff_match->schema->nodetype & LYD_NODE_TERM) {
+                /* add orig-dflt metadata */
+                if (!lyd_new_meta(diff_match, NULL, "yang:orig-default", diff_match->flags & LYD_DEFAULT ? "true" : "false")) {
+                    return LY_EINT;
+                }
+            }
+        } else {
+            assert(diff_match->schema->nodetype == LYS_LEAF);
+            /* we deleted it, but it was created with a different value -> operation REPLACE */
+            LY_CHECK_RET(lyd_diff_change_op(diff_match, LYD_DIFF_OP_REPLACE));
+
+            /* current value is the previous one (meta) */
+            str_val = lyd_value2str((struct lyd_node_term *)diff_match, &dynamic);
+            meta = lyd_new_meta(diff_match, NULL, "yang:orig-value", str_val);
+            if (dynamic) {
+                free((char *)str_val);
+            }
+            LY_CHECK_RET(!meta, LY_EINT);
+
+            /* update the value itself */
+            str_val = lyd_value2str((struct lyd_node_term *)src_diff, &dynamic);
+            ret = lyd_change_term(diff_match, str_val);
+            if (dynamic) {
+                free((char *)str_val);
+            }
+            LY_CHECK_RET(ret);
+        }
+
+        if (diff_match->schema->nodetype & LYD_NODE_TERM) {
+            /* update dflt flag itself */
+            diff_match->flags &= ~LYD_DEFAULT;
+            diff_match->flags |= src_diff->flags & LYD_DEFAULT;
+        } else {
+            /* but the operation of its children should remain DELETE */
+            LY_LIST_FOR(LYD_CHILD(diff_match), child) {
+                LY_CHECK_RET(lyd_diff_change_op(child, LYD_DIFF_OP_DELETE));
+            }
+        }
+        break;
+    default:
+        /* create and replace operations are not valid */
+        LOGINT_RET(LYD_NODE_CTX(src_diff));
+    }
+
+    return LY_SUCCESS;
+}
+
+/**
+ * @brief Update operations on a diff node when the new operation is DELETE.
+ *
+ * @param[in] diff_match Node from the diff.
+ * @param[in] cur_op Current operation of the diff node.
+ * @param[in] src_diff Current source diff node.
+ * @return LY_ERR value.
+ */
+static LY_ERR
+lyd_diff_merge_delete(struct lyd_node *diff_match, enum lyd_diff_op cur_op, const struct lyd_node *src_diff)
+{
+    struct lyd_node *next, *child;
+
+    /* we can delete only exact existing nodes */
+    LY_CHECK_ERR_RET(lyd_compare(diff_match, src_diff, 0), LOGINT(LYD_NODE_CTX(src_diff)), LY_EINT);
+
+    switch (cur_op) {
+    case LYD_DIFF_OP_CREATE:
+        /* it was created, but then deleted -> set NONE operation */
+        LY_CHECK_RET(lyd_diff_change_op(diff_match, LYD_DIFF_OP_NONE));
+
+        if (diff_match->schema->nodetype & LYD_NODE_TERM) {
+            /* add orig-default meta because it is expected */
+            if (!lyd_new_meta(diff_match, NULL, "yang:orig-default", diff_match->flags & LYD_DEFAULT ? "true" : "false")) {
+                return LY_EINT;
+            }
+        } else {
+            /* keep operation for all descendants (for now) */
+            LY_LIST_FOR(LYD_CHILD(diff_match), child) {
+                LY_CHECK_RET(lyd_diff_change_op(child, cur_op));
+            }
+        }
+        break;
+    case LYD_DIFF_OP_REPLACE:
+        /* similar to none operation but also remove the redundant attribute */
+        lyd_diff_del_meta(diff_match, "orig-value");
+        /* fallthrough */
+    case LYD_DIFF_OP_NONE:
+        /* it was not modified, but should be deleted -> set DELETE operation */
+        LY_CHECK_RET(lyd_diff_change_op(diff_match, LYD_DIFF_OP_DELETE));
+
+        /* all descendants will be deleted even without being in the diff, so remove them */
+        LY_LIST_FOR_SAFE(LYD_CHILD(diff_match), next, child) {
+            lyd_free_tree(child);
+        }
+        break;
+    default:
+        /* delete operation is not valid */
+        LOGINT_RET(LYD_NODE_CTX(src_diff));
+    }
+
+    return LY_SUCCESS;
+}
+
+/**
+ * @brief Check whether this diff node is redundant (does not change data).
+ *
+ * @param[in] diff Diff node.
+ * @return 0 if not, non-zero if it is.
+ */
+static int
+lyd_diff_is_redundant(struct lyd_node *diff)
+{
+    enum lyd_diff_op op;
+    struct lyd_meta *meta, *orig_val_meta = NULL, *val_meta = NULL;
+    struct lyd_node *child;
+    const struct lys_module *mod;
+    const char *str;
+    int dynamic;
+
+    assert(diff);
+
+    child = LYD_CHILD(diff);
+    mod = ly_ctx_get_module_latest(LYD_NODE_CTX(diff), "yang");
+    assert(mod);
+
+    /* get node operation */
+    lyd_diff_get_op(diff, &op);
+
+    if ((op == LYD_DIFF_OP_REPLACE) && lysc_is_userordered(diff->schema)) {
+        /* check for redundant move */
+        orig_val_meta = lyd_find_meta(diff->meta, mod, (diff->schema->nodetype == LYS_LIST ? "orig-key" : "orig-value"));
+        val_meta = lyd_find_meta(diff->meta, mod, (diff->schema->nodetype == LYS_LIST ? "key" : "value"));
+        assert(orig_val_meta && val_meta);
+
+        if (!lyd_compare_meta(orig_val_meta, val_meta)) {
+            /* there is actually no move */
+            lyd_free_meta(LYD_NODE_CTX(diff), orig_val_meta, 0);
+            lyd_free_meta(LYD_NODE_CTX(diff), val_meta, 0);
+            if (child) {
+                /* change operation to NONE, we have siblings */
+                lyd_diff_change_op(diff, LYD_DIFF_OP_NONE);
+                return 0;
+            }
+
+            /* redundant node, BUT !!
+             * In diff the move operation is always converted to be INSERT_AFTER, which is fine
+             * because the data that this is applied on should not change for the diff lifetime.
+             * However, when we are merging 2 diffs, this conversion is actually lossy because
+             * if the data change, the move operation can also change its meaning. In this specific
+             * case the move operation will be lost. But it can be considered a feature, it is not supported.
+             */
+            return 1;
+        }
+    } else if ((op == LYD_DIFF_OP_NONE) && (diff->schema->nodetype & LYD_NODE_TERM)) {
+        /* check whether at least the default flags are different */
+        meta = lyd_find_meta(diff->meta, mod, "orig-default");
+        assert(meta);
+        str = lyd_meta2str(meta, &dynamic);
+        assert(!dynamic);
+
+        /* if previous and current dflt flags are the same, this node is redundant */
+        if ((!strcmp(str, "true") && (diff->flags & LYD_DEFAULT)) || (!strcmp(str, "false") && !(diff->flags & LYD_DEFAULT))) {
+            return 1;
+        }
+        return 0;
+    }
+
+    if (!child && (op == LYD_DIFF_OP_NONE)) {
+        return 1;
+    }
+
+    return 0;
+}
+
+/**
+ * @brief Merge sysrepo diff with another diff, recursively.
+ *
+ * @param[in] src_diff Source diff node.
+ * @param[in] diff_parent Current sysrepo diff parent.
+ * @param[in] diff_cb Optional diff callback.
+ * @param[in] cb_data User data for @p diff_cb.
+ * @param[in,out] diff Diff root node.
+ * @return LY_ERR value.
+ */
+static LY_ERR
+lyd_diff_merge_r(const struct lyd_node *src_diff, struct lyd_node *diff_parent, lyd_diff_cb diff_cb, void *cb_data,
+                 struct lyd_node **diff)
+{
+    LY_ERR ret = LY_SUCCESS;
+    struct lyd_node *child, *diff_node = NULL;
+    enum lyd_diff_op src_op, cur_op;
+
+    /* get source node operation */
+    LY_CHECK_RET(lyd_diff_get_op(src_diff, &src_op));
+
+    /* find an equal node in the current diff */
+    lyd_diff_find_node(diff_parent ? LYD_CHILD(diff_parent) : *diff, src_diff, &diff_node);
+
+    if (diff_node) {
+        /* get target (current) operation */
+        LY_CHECK_RET(lyd_diff_get_op(diff_node, &cur_op));
+
+        /* merge operations */
+        switch (src_op) {
+        case LYD_DIFF_OP_REPLACE:
+            ret = lyd_diff_merge_replace(diff_node, cur_op, src_diff);
+            break;
+        case LYD_DIFF_OP_CREATE:
+            ret = lyd_diff_merge_create(diff_node, cur_op, src_diff);
+            break;
+        case LYD_DIFF_OP_DELETE:
+            ret = lyd_diff_merge_delete(diff_node, cur_op, src_diff);
+            break;
+        case LYD_DIFF_OP_NONE:
+            ret = lyd_diff_merge_none(diff_node, cur_op, src_diff);
+            break;
+        default:
+            LOGINT_RET(LYD_NODE_CTX(src_diff));
+        }
+        if (ret) {
+            LOGERR(LYD_NODE_CTX(src_diff), LY_EOTHER, "Merging operation \"%s\" failed.", lyd_diff_op2str(src_op));
+            return ret;
+        }
+
+        if (diff_cb) {
+            /* call callback */
+            LY_CHECK_RET(diff_cb(diff_node, NULL, cb_data));
+        }
+
+        /* update diff parent */
+        diff_parent = diff_node;
+
+        /* merge src_diff recursively */
+        LY_LIST_FOR(LYD_CHILD(src_diff), child) {
+            LY_CHECK_RET(lyd_diff_merge_r(child, diff_parent, diff_cb, cb_data, diff));
+        }
+    } else {
+        /* add new diff node with all descendants */
+        diff_node = lyd_dup(src_diff, (struct lyd_node_inner *)diff_parent, LYD_DUP_RECURSIVE);
+        LY_CHECK_RET(!diff_node, LY_EINT);
+
+        /* insert node into diff if not already */
+        if (!diff_parent) {
+            if (*diff) {
+                lyd_insert_sibling(*diff, diff_node);
+            } else {
+                *diff = diff_node;
+            }
+        }
+
+        /* update operation */
+        LY_CHECK_RET(lyd_diff_change_op(diff_node, src_op));
+
+        if (diff_cb) {
+            /* call callback */
+            LY_CHECK_RET(diff_cb(diff_node, NULL, cb_data));
+        }
+
+        /* update diff parent */
+        diff_parent = diff_node;
+    }
+
+    /* remove any redundant nodes */
+    if (diff_parent && lyd_diff_is_redundant(diff_parent)) {
+        if (diff_parent == *diff) {
+            *diff = (*diff)->next;
+        }
+        lyd_free_tree(diff_parent);
+    }
+
+    return LY_SUCCESS;
+}
+
+API LY_ERR
+lyd_diff_merge_module(const struct lyd_node *src_diff, const struct lys_module *mod, lyd_diff_cb diff_cb, void *cb_data,
+                      struct lyd_node **diff)
+{
+    const struct lyd_node *src_root;
+
+    LY_LIST_FOR(src_diff, src_root) {
+        if (mod && (lyd_owner_module(src_root) != mod)) {
+            /* skip data nodes from different modules */
+            continue;
+        }
+
+        /* apply relevant nodes from the diff datatree */
+        LY_CHECK_RET(lyd_diff_merge_r(src_root, NULL, diff_cb, cb_data, diff));
+    }
+
+    return LY_SUCCESS;
+}
+
+API LY_ERR
+lyd_diff_merge(const struct lyd_node *src_diff, struct lyd_node **diff)
+{
+    return lyd_diff_merge_module(src_diff, NULL, NULL, NULL, diff);
+}
diff --git a/src/tree_data.h b/src/tree_data.h
index f58b523..553f4cc 100644
--- a/src/tree_data.h
+++ b/src/tree_data.h
@@ -1017,6 +1017,40 @@
 LY_ERR lyd_diff_apply(struct lyd_node **data, const struct lyd_node *diff);
 
 /**
+ * @brief Merge 2 diffs into each other but restrict the operation to one module.
+ *
+ * The diffs must be possible to be merged, which is guaranteed only if the source diff was
+ * created on data that had the target diff applied on them. In other words, this sequence is legal
+ *
+ * diff1 from data1 and data2 -> data11 from apply diff1 on data1 -> diff2 from data11 and data3 ->
+ * -> data 33 frm apply diff2 on data1
+ *
+ * and reusing these diffs
+ *
+ * diff11 from merge diff1 and diff2 -> data33 from apply diff11 on data1
+ *
+ * @param[in] src_diff Source diff.
+ * @param[in] mod Module, whose diff only to consider, NULL for all modules.
+ * @param[in] diff_cb Optional diff callback that will be called for every changed node.
+ * @param[in] cb_data Arbitrary callback data.
+ * @param[in,out] diff Target diff to merge into.
+ * @return LY_SUCCESS on success,
+ * @return LY_ERR on error.
+ */
+LY_ERR lyd_diff_merge_module(const struct lyd_node *src_diff, const struct lys_module *mod, lyd_diff_cb diff_cb,
+                             void *cb_data, struct lyd_node **diff);
+
+/**
+ * @brief Merge 2 diffs into each other.
+ *
+ * @param[in] src_diff Source diff.
+ * @param[in,out] diff Target diff to merge into.
+ * @return LY_SUCCESS on success,
+ * @return LY_ERR on error.
+ */
+LY_ERR lyd_diff_merge(const struct lyd_node *src_diff, struct lyd_node **diff);
+
+/**
  * @brief Find the target in data of a compiled ly_path structure (instance-identifier).
  *
  * @param[in] path Compiled path structure.