RED.diff = (function() { var currentDiff = {}; var diffVisible = false; var diffList; function init() { // RED.actions.add("core:show-current-diff",showLocalDiff); RED.actions.add("core:show-remote-diff",showRemoteDiff); // RED.keyboard.add("*","ctrl-shift-l","core:show-current-diff"); // RED.keyboard.add("*","ctrl-shift-r","core:show-remote-diff"); // RED.actions.add("core:show-test-flow-diff-1",function(){showTestFlowDiff(1)}); // RED.keyboard.add("*","ctrl-shift-f 1","core:show-test-flow-diff-1"); // // RED.actions.add("core:show-test-flow-diff-2",function(){showTestFlowDiff(2)}); // RED.keyboard.add("*","ctrl-shift-f 2","core:show-test-flow-diff-2"); // RED.actions.add("core:show-test-flow-diff-3",function(){showTestFlowDiff(3)}); // RED.keyboard.add("*","ctrl-shift-f 3","core:show-test-flow-diff-3"); } function createDiffTable(container,CurrentDiff) { var diffList = $('<ol class="node-dialog-view-diff-diff"></ol>').appendTo(container); diffList.editableList({ addButton: false, height: "auto", scrollOnAdd: false, addItem: function(container,i,object) { var localDiff = object.diff; var remoteDiff = object.remoteDiff; var tab = object.tab.n; var def = object.def; var conflicts = CurrentDiff.conflicts; var tabDiv = $('<div>',{class:"node-diff-tab"}).appendTo(container); tabDiv.addClass('collapsed'); var titleRow = $('<div>',{class:"node-diff-tab-title"}).appendTo(tabDiv); var nodesDiv = $('<div>').appendTo(tabDiv); var originalCell = $('<div>',{class:"node-diff-node-entry-cell"}).appendTo(titleRow); var localCell = $('<div>',{class:"node-diff-node-entry-cell node-diff-node-local"}).appendTo(titleRow); var remoteCell; var selectState; if (remoteDiff) { remoteCell = $('<div>',{class:"node-diff-node-entry-cell node-diff-node-remote"}).appendTo(titleRow); } $('<span class="node-diff-chevron"><i class="fa fa-angle-down"></i></span>').appendTo(originalCell); createNodeIcon(tab,def).appendTo(originalCell); var tabForLabel = (object.newTab || object.tab).n; var titleSpan = $('<span>',{class:"node-diff-tab-title-meta"}).appendTo(originalCell); if (tabForLabel.type === 'tab') { titleSpan.html(tabForLabel.label||tabForLabel.id); } else if (tab.type === 'subflow') { titleSpan.html((tabForLabel.name||tabForLabel.id)); } else { titleSpan.html(RED._("diff.globalNodes")); } var flowStats = { local: { addedCount:0, deletedCount:0, changedCount:0, unchangedCount: 0 }, remote: { addedCount:0, deletedCount:0, changedCount:0, unchangedCount: 0 }, conflicts: 0 } if (object.newTab || object.remoteTab) { var localTabNode = { node: localDiff.newConfig.all[tab.id], all: localDiff.newConfig.all, diff: localDiff } var remoteTabNode; if (remoteDiff) { remoteTabNode = { node:remoteDiff.newConfig.all[tab.id]||null, all: remoteDiff.newConfig.all, diff: remoteDiff } } if (tab.type !== undefined) { var div = $("<div>",{class:"node-diff-node-entry node-diff-node-props collapsed"}).appendTo(nodesDiv); var row = $("<div>",{class:"node-diff-node-entry-header"}).appendTo(div); var originalNodeDiv = $("<div>",{class:"node-diff-node-entry-cell"}).appendTo(row); var localNodeDiv = $("<div>",{class:"node-diff-node-entry-cell node-diff-node-local"}).appendTo(row); var localChanged = false; var remoteChanged = false; if (!localDiff.newConfig.all[tab.id]) { localNodeDiv.addClass("node-diff-empty"); } else if (localDiff.added[tab.id]) { localNodeDiv.addClass("node-diff-node-added"); localChanged = true; $('<span class="node-diff-status"><i class="fa fa-plus-square"></i> <span data-i18n="diff.type.added"></span></span>').appendTo(localNodeDiv); } else if (localDiff.changed[tab.id]) { localNodeDiv.addClass("node-diff-node-changed"); localChanged = true; $('<span class="node-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(localNodeDiv); } else { localNodeDiv.addClass("node-diff-node-unchanged"); $('<span class="node-diff-status"><i class="fa fa-square-o"></i> <span data-i18n="diff.type.unchanged"></span></span>').appendTo(localNodeDiv); } var remoteNodeDiv; if (remoteDiff) { remoteNodeDiv = $("<div>",{class:"node-diff-node-entry-cell node-diff-node-remote"}).appendTo(row); if (!remoteDiff.newConfig.all[tab.id]) { remoteNodeDiv.addClass("node-diff-empty"); if (remoteDiff.deleted[tab.id]) { remoteChanged = true; } } else if (remoteDiff.added[tab.id]) { remoteNodeDiv.addClass("node-diff-node-added"); remoteChanged = true; $('<span class="node-diff-status"><i class="fa fa-plus-square"></i> <span data-i18n="diff.type.added"></span></span>').appendTo(remoteNodeDiv); } else if (remoteDiff.changed[tab.id]) { remoteNodeDiv.addClass("node-diff-node-changed"); remoteChanged = true; $('<span class="node-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(remoteNodeDiv); } else { remoteNodeDiv.addClass("node-diff-node-unchanged"); $('<span class="node-diff-status"><i class="fa fa-square-o"></i> <span data-i18n="diff.type.unchanged"></span></span>').appendTo(remoteNodeDiv); } } $('<span class="node-diff-chevron"><i class="fa fa-angle-down"></i></span>').appendTo(originalNodeDiv); $('<span>').html(RED._("diff.flowProperties")).appendTo(originalNodeDiv); row.click(function(evt) { evt.preventDefault(); $(this).parent().toggleClass('collapsed'); }); createNodePropertiesTable(def,tab,localTabNode,remoteTabNode,conflicts).appendTo(div); selectState = ""; if (conflicts[tab.id]) { flowStats.conflicts++; if (!localNodeDiv.hasClass("node-diff-empty")) { $('<span class="node-diff-node-conflict"><span class="node-diff-status"><i class="fa fa-exclamation"></i></span></span>').prependTo(localNodeDiv); } if (!remoteNodeDiv.hasClass("node-diff-empty")) { $('<span class="node-diff-node-conflict"><span class="node-diff-status"><i class="fa fa-exclamation"></i></span></span>').prependTo(remoteNodeDiv); } div.addClass("node-diff-node-entry-conflict"); } else { selectState = CurrentDiff.resolutions[tab.id]; } // Tab properties row createNodeConflictRadioBoxes(tab,div,localNodeDiv,remoteNodeDiv,true,!conflicts[tab.id],selectState,CurrentDiff); } } // var stats = $('<span>',{class:"node-diff-tab-stats"}).appendTo(titleRow); var localNodeCount = 0; var remoteNodeCount = 0; var seen = {}; object.tab.nodes.forEach(function(node) { seen[node.id] = true; createNodeDiffRow(node,flowStats,CurrentDiff).appendTo(nodesDiv) }); if (object.newTab) { localNodeCount = object.newTab.nodes.length; object.newTab.nodes.forEach(function(node) { if (!seen[node.id]) { seen[node.id] = true; createNodeDiffRow(node,flowStats,CurrentDiff).appendTo(nodesDiv) } }); } if (object.remoteTab) { remoteNodeCount = object.remoteTab.nodes.length; object.remoteTab.nodes.forEach(function(node) { if (!seen[node.id]) { createNodeDiffRow(node,flowStats,CurrentDiff).appendTo(nodesDiv) } }); } titleRow.click(function(evt) { // if (titleRow.parent().find(".node-diff-node-entry:not(.hide)").length > 0) { titleRow.parent().toggleClass('collapsed'); if ($(this).parent().hasClass('collapsed')) { $(this).parent().find('.node-diff-node-entry').addClass('collapsed'); $(this).parent().find('.debug-message-element').addClass('collapsed'); } // } }) if (localDiff.deleted[tab.id]) { $('<span class="node-diff-node-deleted"><span class="node-diff-status"><i class="fa fa-minus-square"></i> <span data-i18n="diff.type.flowDeleted"></span></span></span>').appendTo(localCell); } else if (object.newTab) { if (localDiff.added[tab.id]) { $('<span class="node-diff-node-added"><span class="node-diff-status"><i class="fa fa-plus-square"></i> <span data-i18n="diff.type.flowAdded"></span></span></span>').appendTo(localCell); } else { if (tab.id) { if (localDiff.changed[tab.id]) { flowStats.local.changedCount++; } else { flowStats.local.unchangedCount++; } } var localStats = $('<span>',{class:"node-diff-tab-stats"}).appendTo(localCell); $('<span class="node-diff-status"></span>').html(RED._('diff.nodeCount',{count:localNodeCount})).appendTo(localStats); if (flowStats.conflicts + flowStats.local.addedCount + flowStats.local.changedCount + flowStats.local.deletedCount > 0) { $('<span class="node-diff-status"> [ </span>').appendTo(localStats); if (flowStats.conflicts > 0) { $('<span class="node-diff-node-conflict"><span class="node-diff-status"><i class="fa fa-exclamation"></i> '+flowStats.conflicts+'</span></span>').appendTo(localStats); } if (flowStats.local.addedCount > 0) { $('<span class="node-diff-node-added"><span class="node-diff-status"><i class="fa fa-plus-square"></i> '+flowStats.local.addedCount+'</span></span>').appendTo(localStats); } if (flowStats.local.changedCount > 0) { $('<span class="node-diff-node-changed"><span class="node-diff-status"><i class="fa fa-square"></i> '+flowStats.local.changedCount+'</span></span>').appendTo(localStats); } if (flowStats.local.deletedCount > 0) { $('<span class="node-diff-node-deleted"><span class="node-diff-status"><i class="fa fa-minus-square"></i> '+flowStats.local.deletedCount+'</span></span>').appendTo(localStats); } $('<span class="node-diff-status"> ] </span>').appendTo(localStats); } } } else { localCell.addClass("node-diff-empty"); } if (remoteDiff) { if (remoteDiff.deleted[tab.id]) { $('<span class="node-diff-node-deleted"><span class="node-diff-status"><i class="fa fa-minus-square"></i> <span data-i18n="diff.type.flowDeleted"></span></span></span>').appendTo(remoteCell); } else if (object.remoteTab) { if (remoteDiff.added[tab.id]) { $('<span class="node-diff-node-added"><span class="node-diff-status"><i class="fa fa-plus-square"></i> <span data-i18n="diff.type.flowAdded"></span></span></span>').appendTo(remoteCell); } else { if (tab.id) { if (remoteDiff.changed[tab.id]) { flowStats.remote.changedCount++; } else { flowStats.remote.unchangedCount++; } } var remoteStats = $('<span>',{class:"node-diff-tab-stats"}).appendTo(remoteCell); $('<span class="node-diff-status"></span>').html(RED._('diff.nodeCount',{count:remoteNodeCount})).appendTo(remoteStats); if (flowStats.conflicts + flowStats.remote.addedCount + flowStats.remote.changedCount + flowStats.remote.deletedCount > 0) { $('<span class="node-diff-status"> [ </span>').appendTo(remoteStats); if (flowStats.conflicts > 0) { $('<span class="node-diff-node-conflict"><span class="node-diff-status"><i class="fa fa-exclamation"></i> '+flowStats.conflicts+'</span></span>').appendTo(remoteStats); } if (flowStats.remote.addedCount > 0) { $('<span class="node-diff-node-added"><span class="node-diff-status"><i class="fa fa-plus-square"></i> '+flowStats.remote.addedCount+'</span></span>').appendTo(remoteStats); } if (flowStats.remote.changedCount > 0) { $('<span class="node-diff-node-changed"><span class="node-diff-status"><i class="fa fa-square"></i> '+flowStats.remote.changedCount+'</span></span>').appendTo(remoteStats); } if (flowStats.remote.deletedCount > 0) { $('<span class="node-diff-node-deleted"><span class="node-diff-status"><i class="fa fa-minus-square"></i> '+flowStats.remote.deletedCount+'</span></span>').appendTo(remoteStats); } $('<span class="node-diff-status"> ] </span>').appendTo(remoteStats); } } } else { remoteCell.addClass("node-diff-empty"); } selectState = ""; if (flowStats.conflicts > 0) { titleRow.addClass("node-diff-node-entry-conflict"); } else { selectState = CurrentDiff.resolutions[tab.id]; } if (tab.id) { var hide = !(flowStats.conflicts > 0 &&(localDiff.deleted[tab.id] || remoteDiff.deleted[tab.id])); // Tab parent row createNodeConflictRadioBoxes(tab,titleRow,localCell,remoteCell, false, hide, selectState, CurrentDiff); } } if (tabDiv.find(".node-diff-node-entry").length === 0) { tabDiv.addClass("node-diff-tab-empty"); } container.i18n(); } }); return diffList; } function buildDiffPanel(container,diff,options) { var diffPanel = $('<div class="node-dialog-view-diff-panel"></div>').appendTo(container); var diffHeaders = $('<div class="node-dialog-view-diff-headers"></div>').appendTo(diffPanel); if (options.mode === "merge") { diffPanel.addClass("node-dialog-view-diff-panel-merge"); } var diffList = createDiffTable(diffPanel, diff); var localDiff = diff.localDiff; var remoteDiff = diff.remoteDiff; var conflicts = diff.conflicts; var currentConfig = localDiff.currentConfig; var newConfig = localDiff.newConfig; if (remoteDiff !== undefined) { diffPanel.addClass('node-diff-three-way'); var localTitle = options.oldRevTitle || RED._('diff.local'); var remoteTitle = options.newRevTitle || RED._('diff.remote'); $('<div></div>').text(localTitle).appendTo(diffHeaders); $('<div></div>').text(remoteTitle).appendTo(diffHeaders); } else { diffPanel.removeClass('node-diff-three-way'); } return { list: diffList, finish: function() { var el = { diff: localDiff, def: { category: 'config', color: '#f0f0f0' }, tab: { n: {}, nodes: currentConfig.globals }, newTab: { n: {}, nodes: newConfig.globals } }; if (remoteDiff !== undefined) { el.remoteTab = { n:{}, nodes:remoteDiff.newConfig.globals }; el.remoteDiff = remoteDiff; } diffList.editableList('addItem',el); var seenTabs = {}; currentConfig.tabOrder.forEach(function(tabId) { var tab = currentConfig.tabs[tabId]; var el = { diff: localDiff, def: RED.nodes.getType('tab'), tab:tab }; if (newConfig.tabs.hasOwnProperty(tabId)) { el.newTab = newConfig.tabs[tabId]; } if (remoteDiff !== undefined) { el.remoteTab = remoteDiff.newConfig.tabs[tabId]; el.remoteDiff = remoteDiff; } seenTabs[tabId] = true; diffList.editableList('addItem',el) }); newConfig.tabOrder.forEach(function(tabId) { if (!seenTabs[tabId]) { seenTabs[tabId] = true; var tab = newConfig.tabs[tabId]; var el = { diff: localDiff, def: RED.nodes.getType('tab'), tab:tab, newTab: tab }; if (remoteDiff !== undefined) { el.remoteDiff = remoteDiff; } diffList.editableList('addItem',el) } }); if (remoteDiff !== undefined) { remoteDiff.newConfig.tabOrder.forEach(function(tabId) { if (!seenTabs[tabId]) { var tab = remoteDiff.newConfig.tabs[tabId]; // TODO how to recognise this is a remotely added flow var el = { diff: localDiff, remoteDiff: remoteDiff, def: RED.nodes.getType('tab'), tab:tab, remoteTab:tab }; diffList.editableList('addItem',el) } }); } var subflowId; for (subflowId in currentConfig.subflows) { if (currentConfig.subflows.hasOwnProperty(subflowId)) { seenTabs[subflowId] = true; el = { diff: localDiff, def: { defaults:{}, icon:"subflow.png", category: "subflows", color: "#da9" }, tab:currentConfig.subflows[subflowId] } if (newConfig.subflows.hasOwnProperty(subflowId)) { el.newTab = newConfig.subflows[subflowId]; } if (remoteDiff !== undefined) { el.remoteTab = remoteDiff.newConfig.subflows[subflowId]; el.remoteDiff = remoteDiff; } diffList.editableList('addItem',el) } } for (subflowId in newConfig.subflows) { if (newConfig.subflows.hasOwnProperty(subflowId) && !seenTabs[subflowId]) { seenTabs[subflowId] = true; el = { diff: localDiff, def: { defaults:{}, icon:"subflow.png", category: "subflows", color: "#da9" }, tab:newConfig.subflows[subflowId], newTab:newConfig.subflows[subflowId] } if (remoteDiff !== undefined) { el.remoteDiff = remoteDiff; } diffList.editableList('addItem',el) } } if (remoteDiff !== undefined) { for (subflowId in remoteDiff.newConfig.subflows) { if (remoteDiff.newConfig.subflows.hasOwnProperty(subflowId) && !seenTabs[subflowId]) { el = { diff: localDiff, remoteDiff: remoteDiff, def: { defaults:{}, icon:"subflow.png", category: "subflows", color: "#da9" }, tab:remoteDiff.newConfig.subflows[subflowId], remoteTab: remoteDiff.newConfig.subflows[subflowId] } diffList.editableList('addItem',el) } } } } }; } function formatWireProperty(wires,allNodes) { var result = $("<div>",{class:"node-diff-property-wires"}) var list = $("<ol></ol>"); var c = 0; wires.forEach(function(p,i) { var port = $("<li>").appendTo(list); if (p && p.length > 0) { $("<span>").html(i+1).appendTo(port); var links = $("<ul>").appendTo(port); p.forEach(function(d) { c++; var entry = $("<li>").appendTo(links); var node = allNodes[d]; if (node) { var def = RED.nodes.getType(node.type)||{}; createNode(node,def).appendTo(entry); } else { entry.html(d); } }) } else { port.html('none'); } }) if (c === 0) { result.html("none"); } else { list.appendTo(result); } return result; } function createNodeIcon(node,def) { var nodeDiv = $("<div>",{class:"node-diff-node-entry-node"}); var colour = def.color; var icon_url = RED.utils.getNodeIcon(def,node); if (node.type === 'tab') { colour = "#C0DEED"; } nodeDiv.css('backgroundColor',colour); var iconContainer = $('<div/>',{class:"palette_icon_container"}).appendTo(nodeDiv); $('<div/>',{class:"palette_icon",style:"background-image: url("+icon_url+")"}).appendTo(iconContainer); return nodeDiv; } function createNode(node,def) { var nodeTitleDiv = $("<div>",{class:"node-diff-node-entry-title"}) createNodeIcon(node,def).appendTo(nodeTitleDiv); var contentDiv = $('<div>',{class:"node-diff-node-description"}).appendTo(nodeTitleDiv); var nodeLabel = node.label || node.name || node.id; $('<span>',{class:"node-diff-node-label"}).html(nodeLabel).appendTo(contentDiv); return nodeTitleDiv; } function createNodeDiffRow(node,stats,CurrentDiff) { var localDiff = CurrentDiff.localDiff; var remoteDiff = CurrentDiff.remoteDiff; var conflicted = CurrentDiff.conflicts[node.id]; var hasChanges = false; // exists in original and local/remote but with changes var unChanged = true; // existing in original,local,remote unchanged var localChanged = false; if (localDiff.added[node.id]) { stats.local.addedCount++; unChanged = false; } if (remoteDiff && remoteDiff.added[node.id]) { stats.remote.addedCount++; unChanged = false; } if (localDiff.deleted[node.id]) { stats.local.deletedCount++; unChanged = false; } if (remoteDiff && remoteDiff.deleted[node.id]) { stats.remote.deletedCount++; unChanged = false; } if (localDiff.changed[node.id]) { stats.local.changedCount++; hasChanges = true; unChanged = false; } if (remoteDiff && remoteDiff.changed[node.id]) { stats.remote.changedCount++; hasChanges = true; unChanged = false; } // console.log(node.id,localDiff.added[node.id],remoteDiff.added[node.id],localDiff.deleted[node.id],remoteDiff.deleted[node.id],localDiff.changed[node.id],remoteDiff.changed[node.id]) var def = RED.nodes.getType(node.type); if (def === undefined) { if (/^subflow:/.test(node.type)) { def = { icon:"subflow.png", category: "subflows", color: "#da9", defaults:{name:{value:""}} } } else { def = {}; } } var div = $("<div>",{class:"node-diff-node-entry collapsed"}); var row = $("<div>",{class:"node-diff-node-entry-header"}).appendTo(div); var originalNodeDiv = $("<div>",{class:"node-diff-node-entry-cell"}).appendTo(row); var localNodeDiv = $("<div>",{class:"node-diff-node-entry-cell node-diff-node-local"}).appendTo(row); var remoteNodeDiv; var chevron; if (remoteDiff) { remoteNodeDiv = $("<div>",{class:"node-diff-node-entry-cell node-diff-node-remote"}).appendTo(row); } $('<span class="node-diff-chevron"><i class="fa fa-angle-down"></i></span>').appendTo(originalNodeDiv); if (unChanged) { stats.local.unchangedCount++; createNode(node,def).appendTo(originalNodeDiv); localNodeDiv.addClass("node-diff-node-unchanged"); $('<span class="node-diff-status"><i class="fa fa-square-o"></i> <span data-i18n="diff.type.unchanged"></span></span>').appendTo(localNodeDiv); if (remoteDiff) { stats.remote.unchangedCount++; remoteNodeDiv.addClass("node-diff-node-unchanged"); $('<span class="node-diff-status"><i class="fa fa-square-o"></i> <span data-i18n="diff.type.unchanged"></span></span>').appendTo(remoteNodeDiv); } div.addClass("node-diff-node-unchanged"); } else if (localDiff.added[node.id]) { localNodeDiv.addClass("node-diff-node-added"); if (remoteNodeDiv) { remoteNodeDiv.addClass("node-diff-empty"); } $('<span class="node-diff-status"><i class="fa fa-plus-square"></i> <span data-i18n="diff.type.added"></span></span>').appendTo(localNodeDiv); createNode(node,def).appendTo(originalNodeDiv); } else if (remoteDiff && remoteDiff.added[node.id]) { localNodeDiv.addClass("node-diff-empty"); remoteNodeDiv.addClass("node-diff-node-added"); $('<span class="node-diff-status"><i class="fa fa-plus-square"></i> <span data-i18n="diff.type.added"></span></span>').appendTo(remoteNodeDiv); createNode(node,def).appendTo(originalNodeDiv); } else { createNode(node,def).appendTo(originalNodeDiv); if (localDiff.moved[node.id]) { var localN = localDiff.newConfig.all[node.id]; if (!localDiff.deleted[node.z] && node.z !== localN.z && node.z !== "" && !localDiff.newConfig.all[node.z]) { localNodeDiv.addClass("node-diff-empty"); } else { localNodeDiv.addClass("node-diff-node-moved"); var localMovedMessage = ""; if (node.z === localN.z) { localMovedMessage = RED._("diff.type.movedFrom",{id:(localDiff.currentConfig.all[node.id].z||'global')}); } else { localMovedMessage = RED._("diff.type.movedTo",{id:(localN.z||'global')}); } $('<span class="node-diff-status"><i class="fa fa-caret-square-o-right"></i> '+localMovedMessage+'</span>').appendTo(localNodeDiv); } localChanged = true; } else if (localDiff.deleted[node.z]) { localNodeDiv.addClass("node-diff-empty"); localChanged = true; } else if (localDiff.deleted[node.id]) { localNodeDiv.addClass("node-diff-node-deleted"); $('<span class="node-diff-status"><i class="fa fa-minus-square"></i> <span data-i18n="diff.type.deleted"></span></span>').appendTo(localNodeDiv); localChanged = true; } else if (localDiff.changed[node.id]) { if (localDiff.newConfig.all[node.id].z !== node.z) { localNodeDiv.addClass("node-diff-empty"); } else { localNodeDiv.addClass("node-diff-node-changed"); $('<span class="node-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(localNodeDiv); localChanged = true; } } else { if (localDiff.newConfig.all[node.id].z !== node.z) { localNodeDiv.addClass("node-diff-empty"); } else { stats.local.unchangedCount++; localNodeDiv.addClass("node-diff-node-unchanged"); $('<span class="node-diff-status"><i class="fa fa-square-o"></i> <span data-i18n="diff.type.unchanged"></span></span>').appendTo(localNodeDiv); } } if (remoteDiff) { if (remoteDiff.moved[node.id]) { var remoteN = remoteDiff.newConfig.all[node.id]; if (!remoteDiff.deleted[node.z] && node.z !== remoteN.z && node.z !== "" && !remoteDiff.newConfig.all[node.z]) { remoteNodeDiv.addClass("node-diff-empty"); } else { remoteNodeDiv.addClass("node-diff-node-moved"); var remoteMovedMessage = ""; if (node.z === remoteN.z) { remoteMovedMessage = RED._("diff.type.movedFrom",{id:(remoteDiff.currentConfig.all[node.id].z||'global')}); } else { remoteMovedMessage = RED._("diff.type.movedTo",{id:(remoteN.z||'global')}); } $('<span class="node-diff-status"><i class="fa fa-caret-square-o-right"></i> '+remoteMovedMessage+'</span>').appendTo(remoteNodeDiv); } } else if (remoteDiff.deleted[node.z]) { remoteNodeDiv.addClass("node-diff-empty"); } else if (remoteDiff.deleted[node.id]) { remoteNodeDiv.addClass("node-diff-node-deleted"); $('<span class="node-diff-status"><i class="fa fa-minus-square"></i> <span data-i18n="diff.type.deleted"></span></span>').appendTo(remoteNodeDiv); } else if (remoteDiff.changed[node.id]) { if (remoteDiff.newConfig.all[node.id].z !== node.z) { remoteNodeDiv.addClass("node-diff-empty"); } else { remoteNodeDiv.addClass("node-diff-node-changed"); $('<span class="node-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(remoteNodeDiv); } } else { if (remoteDiff.newConfig.all[node.id].z !== node.z) { remoteNodeDiv.addClass("node-diff-empty"); } else { stats.remote.unchangedCount++; remoteNodeDiv.addClass("node-diff-node-unchanged"); $('<span class="node-diff-status"><i class="fa fa-square-o"></i> <span data-i18n="diff.type.unchanged"></span></span>').appendTo(remoteNodeDiv); } } } } var localNode = { node: localDiff.newConfig.all[node.id], all: localDiff.newConfig.all, diff: localDiff }; var remoteNode; if (remoteDiff) { remoteNode = { node:remoteDiff.newConfig.all[node.id]||null, all: remoteDiff.newConfig.all, diff: remoteDiff } } createNodePropertiesTable(def,node,localNode,remoteNode).appendTo(div); var selectState = ""; if (conflicted) { stats.conflicts++; if (!localNodeDiv.hasClass("node-diff-empty")) { $('<span class="node-diff-node-conflict"><span class="node-diff-status"><i class="fa fa-exclamation"></i></span></span>').prependTo(localNodeDiv); } if (!remoteNodeDiv.hasClass("node-diff-empty")) { $('<span class="node-diff-node-conflict"><span class="node-diff-status"><i class="fa fa-exclamation"></i></span></span>').prependTo(remoteNodeDiv); } div.addClass("node-diff-node-entry-conflict"); } else { selectState = CurrentDiff.resolutions[node.id]; } // Node row createNodeConflictRadioBoxes(node,div,localNodeDiv,remoteNodeDiv,false,!conflicted,selectState,CurrentDiff); row.click(function(evt) { $(this).parent().toggleClass('collapsed'); }); return div; } function createNodePropertiesTable(def,node,localNodeObj,remoteNodeObj) { var propertyElements = {}; var localNode = localNodeObj.node; var remoteNode; if (remoteNodeObj) { remoteNode = remoteNodeObj.node; } var nodePropertiesDiv = $("<div>",{class:"node-diff-node-entry-properties"}); var nodePropertiesTable = $("<table>").appendTo(nodePropertiesDiv); var nodePropertiesTableCols = $('<colgroup><col/><col/></colgroup>').appendTo(nodePropertiesTable); if (remoteNode !== undefined) { $("<col/>").appendTo(nodePropertiesTableCols); } var nodePropertiesTableBody = $("<tbody>").appendTo(nodePropertiesTable); var row; var localCell, remoteCell; var element; var currentValue, localValue, remoteValue; var localChanged = false; var remoteChanged = false; var localChanges = 0; var remoteChanges = 0; var conflict = false; var status; row = $("<tr>").appendTo(nodePropertiesTableBody); $("<td>",{class:"node-diff-property-cell-label"}).html("id").appendTo(row); localCell = $("<td>",{class:"node-diff-property-cell node-diff-node-local"}).appendTo(row); if (localNode) { localCell.addClass("node-diff-node-unchanged"); $('<span class="node-diff-status"></span>').appendTo(localCell); element = $('<span class="node-diff-element"></span>').appendTo(localCell); propertyElements['local.id'] = RED.utils.createObjectElement(localNode.id).appendTo(element); } else { localCell.addClass("node-diff-empty"); } if (remoteNode !== undefined) { remoteCell = $("<td>",{class:"node-diff-property-cell node-diff-node-remote"}).appendTo(row); remoteCell.addClass("node-diff-node-unchanged"); if (remoteNode) { $('<span class="node-diff-status"></span>').appendTo(remoteCell); element = $('<span class="node-diff-element"></span>').appendTo(remoteCell); propertyElements['remote.id'] = RED.utils.createObjectElement(remoteNode.id).appendTo(element); } else { remoteCell.addClass("node-diff-empty"); } } if (node.hasOwnProperty('x')) { if (localNode) { if (localNode.x !== node.x || localNode.y !== node.y) { localChanged = true; localChanges++; } } if (remoteNode) { if (remoteNode.x !== node.x || remoteNode.y !== node.y) { remoteChanged = true; remoteChanges++; } } if ( (remoteChanged && localChanged && (localNode.x !== remoteNode.x || localNode.y !== remoteNode.y)) || (!localChanged && remoteChanged && localNodeObj.diff.deleted[node.id]) || (localChanged && !remoteChanged && remoteNodeObj.diff.deleted[node.id]) ) { conflict = true; } row = $("<tr>").appendTo(nodePropertiesTableBody); $("<td>",{class:"node-diff-property-cell-label"}).html("position").appendTo(row); localCell = $("<td>",{class:"node-diff-property-cell node-diff-node-local"}).appendTo(row); if (localNode) { localCell.addClass("node-diff-node-"+(localChanged?"changed":"unchanged")); $('<span class="node-diff-status">'+(localChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(localCell); element = $('<span class="node-diff-element"></span>').appendTo(localCell); propertyElements['local.position'] = RED.utils.createObjectElement({x:localNode.x,y:localNode.y}, { path: "position", exposeApi: true, ontoggle: function(path,state) { if (propertyElements['remote.'+path]) { propertyElements['remote.'+path].prop('expand')(path,state) } } } ).appendTo(element); } else { localCell.addClass("node-diff-empty"); } if (remoteNode !== undefined) { remoteCell = $("<td>",{class:"node-diff-property-cell node-diff-node-remote"}).appendTo(row); remoteCell.addClass("node-diff-node-"+(remoteChanged?"changed":"unchanged")); if (remoteNode) { $('<span class="node-diff-status">'+(remoteChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(remoteCell); element = $('<span class="node-diff-element"></span>').appendTo(remoteCell); propertyElements['remote.position'] = RED.utils.createObjectElement({x:remoteNode.x,y:remoteNode.y}, { path: "position", exposeApi: true, ontoggle: function(path,state) { if (propertyElements['local.'+path]) { propertyElements['local.'+path].prop('expand')(path,state); } } } ).appendTo(element); } else { remoteCell.addClass("node-diff-empty"); } } } // localChanged = remoteChanged = conflict = false; if (node.hasOwnProperty('wires')) { currentValue = JSON.stringify(node.wires); if (localNode) { localValue = JSON.stringify(localNode.wires); if (currentValue !== localValue) { localChanged = true; localChanges++; } } if (remoteNode) { remoteValue = JSON.stringify(remoteNode.wires); if (currentValue !== remoteValue) { remoteChanged = true; remoteChanges++; } } if ( (remoteChanged && localChanged && (localValue !== remoteValue)) || (!localChanged && remoteChanged && localNodeObj.diff.deleted[node.id]) || (localChanged && !remoteChanged && remoteNodeObj.diff.deleted[node.id]) ){ conflict = true; } row = $("<tr>").appendTo(nodePropertiesTableBody); $("<td>",{class:"node-diff-property-cell-label"}).html("wires").appendTo(row); localCell = $("<td>",{class:"node-diff-property-cell node-diff-node-local"}).appendTo(row); if (localNode) { if (!conflict) { localCell.addClass("node-diff-node-"+(localChanged?"changed":"unchanged")); $('<span class="node-diff-status">'+(localChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(localCell); } else { localCell.addClass("node-diff-node-conflict"); $('<span class="node-diff-status"><i class="fa fa-exclamation"></i></span>').appendTo(localCell); } formatWireProperty(localNode.wires,localNodeObj.all).appendTo(localCell); } else { localCell.addClass("node-diff-empty"); } if (remoteNode !== undefined) { remoteCell = $("<td>",{class:"node-diff-property-cell node-diff-node-remote"}).appendTo(row); if (remoteNode) { if (!conflict) { remoteCell.addClass("node-diff-node-"+(remoteChanged?"changed":"unchanged")); $('<span class="node-diff-status">'+(remoteChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(remoteCell); } else { remoteCell.addClass("node-diff-node-conflict"); $('<span class="node-diff-status"><i class="fa fa-exclamation"></i></span>').appendTo(remoteCell); } formatWireProperty(remoteNode.wires,remoteNodeObj.all).appendTo(remoteCell); } else { remoteCell.addClass("node-diff-empty"); } } } var properties = Object.keys(node).filter(function(p) { return p!='inputLabels'&&p!='outputLabels'&&p!='z'&&p!='wires'&&p!=='x'&&p!=='y'&&p!=='id'&&p!=='type'&&(!def.defaults||!def.defaults.hasOwnProperty(p))}); if (def.defaults) { properties = properties.concat(Object.keys(def.defaults)); } if (node.type !== 'tab') { properties = properties.concat(['inputLabels','outputLabels']); } properties.forEach(function(d) { localChanged = false; remoteChanged = false; conflict = false; currentValue = JSON.stringify(node[d]); if (localNode) { localValue = JSON.stringify(localNode[d]); if (currentValue !== localValue) { localChanged = true; localChanges++; } } if (remoteNode) { remoteValue = JSON.stringify(remoteNode[d]); if (currentValue !== remoteValue) { remoteChanged = true; remoteChanges++; } } if ( (remoteChanged && localChanged && (localValue !== remoteValue)) || (!localChanged && remoteChanged && localNodeObj.diff.deleted[node.id]) || (localChanged && !remoteChanged && remoteNodeObj.diff.deleted[node.id]) ){ conflict = true; } row = $("<tr>").appendTo(nodePropertiesTableBody); var propertyNameCell = $("<td>",{class:"node-diff-property-cell-label"}).html(d).appendTo(row); localCell = $("<td>",{class:"node-diff-property-cell node-diff-node-local"}).appendTo(row); if (localNode) { if (!conflict) { localCell.addClass("node-diff-node-"+(localChanged?"changed":"unchanged")); $('<span class="node-diff-status">'+(localChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(localCell); } else { localCell.addClass("node-diff-node-conflict"); $('<span class="node-diff-status"><i class="fa fa-exclamation"></i></span>').appendTo(localCell); } element = $('<span class="node-diff-element"></span>').appendTo(localCell); propertyElements['local.'+d] = RED.utils.createObjectElement(localNode[d], { path: d, exposeApi: true, ontoggle: function(path,state) { if (propertyElements['remote.'+d]) { propertyElements['remote.'+d].prop('expand')(path,state) } } } ).appendTo(element); } else { localCell.addClass("node-diff-empty"); } if (remoteNode !== undefined) { remoteCell = $("<td>",{class:"node-diff-property-cell node-diff-node-remote"}).appendTo(row); if (remoteNode) { if (!conflict) { remoteCell.addClass("node-diff-node-"+(remoteChanged?"changed":"unchanged")); $('<span class="node-diff-status">'+(remoteChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(remoteCell); } else { remoteCell.addClass("node-diff-node-conflict"); $('<span class="node-diff-status"><i class="fa fa-exclamation"></i></span>').appendTo(remoteCell); } element = $('<span class="node-diff-element"></span>').appendTo(remoteCell); propertyElements['remote.'+d] = RED.utils.createObjectElement(remoteNode[d], { path: d, exposeApi: true, ontoggle: function(path,state) { if (propertyElements['local.'+d]) { propertyElements['local.'+d].prop('expand')(path,state) } } } ).appendTo(element); } else { remoteCell.addClass("node-diff-empty"); } } if (localNode && remoteNode && typeof localNode[d] === "string") { if (/\n/.test(localNode[d]) || /\n/.test(remoteNode[d])) { $('<button class="editor-button editor-button-small node-diff-text-diff-button"><i class="fa fa-file-o"> <i class="fa fa-caret-left"></i> <i class="fa fa-caret-right"></i> <i class="fa fa-file-o"></i></button>').click(function() { showTextDiff(localNode[d],remoteNode[d]); }).appendTo(propertyNameCell); } } }); return nodePropertiesDiv; } function createNodeConflictRadioBoxes(node,row,localDiv,remoteDiv,propertiesTable,hide,state,diff) { var safeNodeId = "node-diff-selectbox-"+node.id.replace(/\./g,'-')+(propertiesTable?"-props":""); var className = ""; if (node.z||propertiesTable) { className = "node-diff-selectbox-tab-"+(propertiesTable?node.id:node.z).replace(/\./g,'-'); } var titleRow = !propertiesTable && (node.type === 'tab' || node.type === 'subflow'); var changeHandler = function(evt) { var className; if (node.type === undefined) { // TODO: handle globals } else if (titleRow) { className = "node-diff-selectbox-tab-"+node.id.replace(/\./g,'-'); $("."+className+"-"+this.value).prop('checked',true); if (this.value === 'local') { $("."+className+"-"+this.value).closest(".node-diff-node-entry").addClass("node-diff-select-local"); $("."+className+"-"+this.value).closest(".node-diff-node-entry").removeClass("node-diff-select-remote"); } else { $("."+className+"-"+this.value).closest(".node-diff-node-entry").removeClass("node-diff-select-local"); $("."+className+"-"+this.value).closest(".node-diff-node-entry").addClass("node-diff-select-remote"); } } else { // Individual node or properties table var parentId = "node-diff-selectbox-"+(propertiesTable?node.id:node.z).replace(/\./g,'-'); $('#'+parentId+"-local").prop('checked',false); $('#'+parentId+"-remote").prop('checked',false); var titleRowDiv = $('#'+parentId+"-local").closest(".node-diff-tab").find(".node-diff-tab-title"); titleRowDiv.removeClass("node-diff-select-local"); titleRowDiv.removeClass("node-diff-select-remote"); } if (this.value === 'local') { row.removeClass("node-diff-select-remote"); row.addClass("node-diff-select-local"); } else if (this.value === 'remote') { row.addClass("node-diff-select-remote"); row.removeClass("node-diff-select-local"); } refreshConflictHeader(diff); } var localSelectDiv = $('<label>',{class:"node-diff-selectbox",for:safeNodeId+"-local"}).click(function(e) { e.stopPropagation();}).appendTo(localDiv); var localRadio = $('<input>',{id:safeNodeId+"-local",type:'radio',value:"local",name:safeNodeId,class:className+"-local"+(titleRow?"":" node-diff-select-node")}).data('node-id',node.id).change(changeHandler).appendTo(localSelectDiv); var remoteSelectDiv = $('<label>',{class:"node-diff-selectbox",for:safeNodeId+"-remote"}).click(function(e) { e.stopPropagation();}).appendTo(remoteDiv); var remoteRadio = $('<input>',{id:safeNodeId+"-remote",type:'radio',value:"remote",name:safeNodeId,class:className+"-remote"+(titleRow?"":" node-diff-select-node")}).data('node-id',node.id).change(changeHandler).appendTo(remoteSelectDiv); if (state === 'local') { localRadio.prop('checked',true); } else if (state === 'remote') { remoteRadio.prop('checked',true); } if (hide||localDiv.hasClass("node-diff-empty") || remoteDiv.hasClass("node-diff-empty")) { localSelectDiv.hide(); remoteSelectDiv.hide(); } } function refreshConflictHeader(currentDiff) { var resolutionCount = 0; $(".node-diff-selectbox>input:checked").each(function() { if (currentDiff.conflicts[$(this).data('node-id')]) { resolutionCount++; } currentDiff.resolutions[$(this).data('node-id')] = $(this).val(); }) var conflictCount = Object.keys(currentDiff.conflicts).length; if (conflictCount - resolutionCount === 0) { $("#node-diff-toolbar-resolved-conflicts").html('<span class="node-diff-node-added"><span class="node-diff-status"><i class="fa fa-check"></i></span></span> '+RED._("diff.unresolvedCount",{count:conflictCount - resolutionCount})); } else { $("#node-diff-toolbar-resolved-conflicts").html('<span class="node-diff-node-conflict"><span class="node-diff-status"><i class="fa fa-exclamation"></i></span></span> '+RED._("diff.unresolvedCount",{count:conflictCount - resolutionCount})); } if (conflictCount === resolutionCount) { $("#node-diff-view-diff-merge").removeClass('disabled'); $("#node-diff-view-resolve-diff").removeClass('disabled'); } } function getRemoteDiff(callback) { $.ajax({ headers: { "Accept":"application/json", }, cache: false, url: 'flows', success: function(nodes) { var localFlow = RED.nodes.createCompleteNodeSet(); var originalFlow = RED.nodes.originalFlow(); var remoteFlow = nodes.flows; var localDiff = generateDiff(originalFlow,localFlow); var remoteDiff = generateDiff(originalFlow,remoteFlow); remoteDiff.rev = nodes.rev; callback(resolveDiffs(localDiff,remoteDiff)) } }); } // function showLocalDiff() { // var nns = RED.nodes.createCompleteNodeSet(); // var originalFlow = RED.nodes.originalFlow(); // var diff = generateDiff(originalFlow,nns); // showDiff(diff); // } function showRemoteDiff(diff) { if (diff === undefined) { getRemoteDiff(showRemoteDiff); } else { showDiff(diff,{mode:'merge'}); } } function parseNodes(nodeList) { var tabOrder = []; var tabs = {}; var subflows = {}; var globals = []; var all = {}; nodeList.forEach(function(node) { all[node.id] = node; if (node.type === 'tab') { tabOrder.push(node.id); tabs[node.id] = {n:node,nodes:[]}; } else if (node.type === 'subflow') { subflows[node.id] = {n:node,nodes:[]}; } }); nodeList.forEach(function(node) { if (node.type !== 'tab' && node.type !== 'subflow') { if (tabs[node.z]) { tabs[node.z].nodes.push(node); } else if (subflows[node.z]) { subflows[node.z].nodes.push(node); } else { globals.push(node); } } }); return { all: all, tabOrder: tabOrder, tabs: tabs, subflows: subflows, globals: globals } } function generateDiff(currentNodes,newNodes) { var currentConfig = parseNodes(currentNodes); var newConfig = parseNodes(newNodes); var added = {}; var deleted = {}; var changed = {}; var moved = {}; Object.keys(currentConfig.all).forEach(function(id) { var node = RED.nodes.workspace(id)||RED.nodes.subflow(id)||RED.nodes.node(id); if (!newConfig.all.hasOwnProperty(id)) { deleted[id] = true; } else if (JSON.stringify(currentConfig.all[id]) !== JSON.stringify(newConfig.all[id])) { changed[id] = true; if (currentConfig.all[id].z !== newConfig.all[id].z) { moved[id] = true; } } }); Object.keys(newConfig.all).forEach(function(id) { if (!currentConfig.all.hasOwnProperty(id)) { added[id] = true; } }); return { currentConfig: currentConfig, newConfig: newConfig, added: added, deleted: deleted, changed: changed, moved: moved } } function resolveDiffs(localDiff,remoteDiff) { var conflicted = {}; var resolutions = {}; var diff = { localDiff: localDiff, remoteDiff: remoteDiff, conflicts: conflicted, resolutions: resolutions } var seen = {}; var id,node; for (id in localDiff.currentConfig.all) { if (localDiff.currentConfig.all.hasOwnProperty(id)) { seen[id] = true; var localNode = localDiff.newConfig.all[id]; if (localDiff.changed[id] && remoteDiff.deleted[id]) { conflicted[id] = true; } else if (localDiff.deleted[id] && remoteDiff.changed[id]) { conflicted[id] = true; } else if (localDiff.changed[id] && remoteDiff.changed[id]) { var remoteNode = remoteDiff.newConfig.all[id]; if (JSON.stringify(localNode) !== JSON.stringify(remoteNode)) { conflicted[id] = true; } } if (!conflicted[id]) { if (remoteDiff.added[id]||remoteDiff.changed[id]||remoteDiff.deleted[id]) { resolutions[id] = 'remote'; } else { resolutions[id] = 'local'; } } } } for (id in localDiff.added) { if (localDiff.added.hasOwnProperty(id)) { node = localDiff.newConfig.all[id]; if (remoteDiff.deleted[node.z]) { conflicted[id] = true; // conflicted[node.z] = true; } else { resolutions[id] = 'local'; } } } for (id in remoteDiff.added) { if (remoteDiff.added.hasOwnProperty(id)) { node = remoteDiff.newConfig.all[id]; if (localDiff.deleted[node.z]) { conflicted[id] = true; // conflicted[node.z] = true; } else { resolutions[id] = 'remote'; } } } // console.log(diff.resolutions); // console.log(conflicted); return diff; } function showDiff(diff,options) { if (diffVisible) { return; } options = options || {}; var mode = options.mode || 'merge'; var localDiff = diff.localDiff; var remoteDiff = diff.remoteDiff; var conflicts = diff.conflicts; // currentDiff = diff; var trayOptions = { title: options.title||"Review Changes", //TODO: nls width: Infinity, overlay: true, buttons: [ { text: RED._((options.mode === 'merge')?"common.label.cancel":"common.label.close"), click: function() { RED.tray.close(); } } ], resize: function(dimensions) { // trayWidth = dimensions.width; }, open: function(tray) { var trayBody = tray.find('.editor-tray-body'); var toolbar = $('<div class="node-diff-toolbar">'+ '<span><span id="node-diff-toolbar-resolved-conflicts"></span></span> '+ '</div>').prependTo(trayBody); var diffContainer = $('<div class="node-diff-container"></div>').appendTo(trayBody); var diffTable = buildDiffPanel(diffContainer,diff,options); diffTable.list.hide(); if (remoteDiff) { $("#node-diff-view-diff-merge").show(); if (Object.keys(conflicts).length === 0) { $("#node-diff-view-diff-merge").removeClass('disabled'); } else { $("#node-diff-view-diff-merge").addClass('disabled'); } } else { $("#node-diff-view-diff-merge").hide(); } refreshConflictHeader(diff); // console.log("--------------"); // console.log(localDiff); // console.log(remoteDiff); setTimeout(function() { diffTable.finish(); diffTable.list.show(); },300); $("#sidebar-shade").show(); }, close: function() { diffVisible = false; $("#sidebar-shade").hide(); }, show: function() { } } if (options.mode === 'merge') { trayOptions.buttons.push( { id: "node-diff-view-diff-merge", text: RED._("deploy.confirm.button.merge"), class: "primary disabled", click: function() { if (!$("#node-diff-view-diff-merge").hasClass('disabled')) { refreshConflictHeader(diff); mergeDiff(diff); RED.tray.close(); } } } ); } RED.tray.show(trayOptions); } function applyDiff(diff) { var currentConfig = diff.localDiff.currentConfig; var localDiff = diff.localDiff; var remoteDiff = diff.remoteDiff; var conflicts = diff.conflicts; var resolutions = diff.resolutions; var id; for (id in conflicts) { if (conflicts.hasOwnProperty(id)) { if (!resolutions.hasOwnProperty(id)) { console.log(diff); throw new Error("No resolution for conflict on node",id); } } } var newConfig = []; var node; var nodeChangedStates = {}; var localChangedStates = {}; for (id in localDiff.newConfig.all) { if (localDiff.newConfig.all.hasOwnProperty(id)) { node = RED.nodes.node(id); if (resolutions[id] === 'local') { if (node) { nodeChangedStates[id] = node.changed; } newConfig.push(localDiff.newConfig.all[id]); } else if (resolutions[id] === 'remote') { if (!remoteDiff.deleted[id] && remoteDiff.newConfig.all.hasOwnProperty(id)) { if (node) { nodeChangedStates[id] = node.changed; } localChangedStates[id] = true; newConfig.push(remoteDiff.newConfig.all[id]); } } else { console.log("Unresolved",id) } } } for (id in remoteDiff.added) { if (remoteDiff.added.hasOwnProperty(id)) { node = RED.nodes.node(id); if (node) { nodeChangedStates[id] = node.changed; } if (!localDiff.added.hasOwnProperty(id)) { localChangedStates[id] = true; newConfig.push(remoteDiff.newConfig.all[id]); } } } return { config: newConfig, nodeChangedStates: nodeChangedStates, localChangedStates: localChangedStates } } function mergeDiff(diff) { var appliedDiff = applyDiff(diff); var newConfig = appliedDiff.config; var nodeChangedStates = appliedDiff.nodeChangedStates; var localChangedStates = appliedDiff.localChangedStates; var historyEvent = { t:"replace", config: RED.nodes.createCompleteNodeSet(), changed: nodeChangedStates, dirty: RED.nodes.dirty(), rev: RED.nodes.version() } RED.history.push(historyEvent); RED.nodes.clear(); var imported = RED.nodes.import(newConfig); imported[0].forEach(function(n) { if (nodeChangedStates[n.id] || localChangedStates[n.id]) { n.changed = true; } }) RED.nodes.version(diff.remoteDiff.rev); RED.view.redraw(true); RED.palette.refresh(); RED.workspaces.refresh(); RED.sidebar.config.refresh(); } function showTestFlowDiff(index) { if (index === 1) { var localFlow = RED.nodes.createCompleteNodeSet(); var originalFlow = RED.nodes.originalFlow(); showTextDiff(JSON.stringify(localFlow,null,4),JSON.stringify(originalFlow,null,4)) } else if (index === 2) { var local = "1\n2\n3\n4\n5\nA\n6\n7\n8\n9\n"; var remote = "1\nA\n2\n3\nD\nE\n6\n7\n8\n9\n"; showTextDiff(local,remote); } else if (index === 3) { var local = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22"; var remote = "1\nTWO\nTHREE\nEXTRA\n4\n5\n6\n7\n8\n9\n10\n11\n12\nTHIRTEEN\n14\n15\n16\n17\n18\n19\n20\n21\n22"; showTextDiff(local,remote); } } function showTextDiff(textA,textB) { var trayOptions = { title: "Compare Changes", //TODO: nls width: Infinity, overlay: true, buttons: [ { text: RED._("common.label.close"), click: function() { RED.tray.close(); } } ], resize: function(dimensions) { // trayWidth = dimensions.width; }, open: function(tray) { var trayBody = tray.find('.editor-tray-body'); var diffPanel = $('<div class="node-text-diff"></div>').appendTo(trayBody); var codeTable = $("<table>",{class:"node-text-diff-content"}).appendTo(diffPanel); $('<colgroup><col width="50"><col width="50%"><col width="50"><col width="50%"></colgroup>').appendTo(codeTable); var codeBody = $('<tbody>').appendTo(codeTable); var diffSummary = diffText(textA||"",textB||""); var aIndex = 0; var bIndex = 0; var diffLength = Math.max(diffSummary.a.length, diffSummary.b.length); var diffLines = []; var diffBlocks = []; var currentBlock; var blockLength = 0; var blockType = 0; for (var i=0;i<diffLength;i++) { var diffLine = diffSummary[i]; var Adiff = (aIndex < diffSummary.a.length)?diffSummary.a[aIndex]:{type:2,line:""}; var Bdiff = (bIndex < diffSummary.b.length)?diffSummary.b[bIndex]:{type:2,line:""}; if (Adiff.type === 0 && Bdiff.type !== 0) { Adiff = {type:2,line:""}; bIndex++; } else if (Bdiff.type === 0 && Adiff.type !== 0) { Bdiff = {type:2,line:""}; aIndex++; } else { aIndex++; bIndex++; } diffLines.push({ a: Adiff, b: Bdiff }); if (currentBlock === undefined) { currentBlock = {start:i,end:i}; blockLength = 0; blockType = (Adiff.type === 0 && Bdiff.type === 0)?0:1; } else { if (Adiff.type === 0 && Bdiff.type === 0) { // Unchanged line if (blockType === 0) { // still unchanged - extend the block currentBlock.end = i; blockLength++; } else if (blockType === 1) { // end of a change currentBlock.end = i; blockType = 2; blockLength = 0; } else if (blockType === 2) { // post-change unchanged currentBlock.end = i; blockLength++; if (blockLength === 8) { currentBlock.end -= 5; // rollback the end diffBlocks.push(currentBlock); currentBlock = {start:i-5,end:i-5}; blockType = 0; blockLength = 0; } } } else { // in a change currentBlock.end = i; blockLength++; if (blockType === 0) { if (currentBlock.end > 3) { currentBlock.end -= 3; currentBlock.empty = true; diffBlocks.push(currentBlock); currentBlock = {start:i-3,end:i-3}; } blockType = 1; } else if (blockType === 2) { // we were in unchanged, but hit a change again blockType = 1; } } } } if (blockType === 0) { currentBlock.empty = true; } currentBlock.end = diffLength; diffBlocks.push(currentBlock); // console.table(diffBlocks); var diffRow; for (var b = 0; b<diffBlocks.length; b++) { currentBlock = diffBlocks[b]; if (currentBlock.empty) { diffRow = createExpandLine(currentBlock.start,currentBlock.end,diffLines).appendTo(codeBody); } else { for (var i=currentBlock.start;i<currentBlock.end;i++) { var row = createDiffLine(diffLines[i]).appendTo(codeBody); if (i === currentBlock.start) { row.addClass("start-block"); } else if (i === currentBlock.end-1) { row.addClass("end-block"); } } } } }, close: function() { diffVisible = false; }, show: function() { } } RED.tray.show(trayOptions); } function createExpandLine(start,end,diffLines) { diffRow = $('<tr class="node-text-diff-header node-text-diff-expand">'); var content = $('<td colspan="4"> <i class="fa fa-arrows-v"></i> </td>').appendTo(diffRow); var label = $('<span></span>').appendTo(content); if (end < diffLines.length-1) { label.text("@@ -"+(diffLines[end-1].a.i+1)+" +"+(diffLines[end-1].b.i+1)); } diffRow.click(function(evt) { // console.log(start,end,diffLines.length); if (end - start > 20) { var startPos = $(this).offset(); // console.log(startPos); if (start > 0) { for (var i=start;i<start+10;i++) { createDiffLine(diffLines[i]).addClass("unchanged").insertBefore($(this)); } start += 10; } if (end < diffLines.length-1) { for (var i=end-1;i>end-11;i--) { createDiffLine(diffLines[i]).addClass("unchanged").insertAfter($(this)); } end -= 10; } if (end < diffLines.length-1) { label.text("@@ -"+(diffLines[end-1].a.i+1)+" +"+(diffLines[end-1].b.i+1)); } var endPos = $(this).offset(); var delta = endPos.top - startPos.top; $(".node-text-diff").scrollTop($(".node-text-diff").scrollTop() + delta); } else { for (var i=start;i<end;i++) { createDiffLine(diffLines[i]).addClass("unchanged").insertBefore($(this)); } $(this).remove(); } }); return diffRow; } function createDiffLine(diffLine) { var diffRow = $('<tr>'); var Adiff = diffLine.a; var Bdiff = diffLine.b; //console.log(diffLine); var cellNo = $('<td class="lineno">').text(Adiff.type === 2?"":Adiff.i).appendTo(diffRow); var cellLine = $('<td class="linetext">').text(Adiff.line).appendTo(diffRow); if (Adiff.type === 2) { cellNo.addClass('blank'); cellLine.addClass('blank'); } else if (Adiff.type === 4) { cellNo.addClass('added'); cellLine.addClass('added'); } else if (Adiff.type === 1) { cellNo.addClass('removed'); cellLine.addClass('removed'); } cellNo = $('<td class="lineno">').text(Bdiff.type === 2?"":Bdiff.i).appendTo(diffRow); cellLine = $('<td class="linetext">').text(Bdiff.line).appendTo(diffRow); if (Bdiff.type === 2) { cellNo.addClass('blank'); cellLine.addClass('blank'); } else if (Bdiff.type === 4) { cellNo.addClass('added'); cellLine.addClass('added'); } else if (Bdiff.type === 1) { cellNo.addClass('removed'); cellLine.addClass('removed'); } return diffRow; } function diffText(string1, string2,ignoreWhitespace) { var lines1 = string1.split(/\r?\n/); var lines2 = string2.split(/\r?\n/); var i = lines1.length; var j = lines2.length; var k; var m; var diffSummary = {a:[],b:[]}; var diffMap = []; for (k = 0; k < i + 1; k++) { diffMap[k] = []; for (m = 0; m < j + 1; m++) { diffMap[k][m] = 0; } } var c = 0; for (k = i - 1; k >= 0; k--) { for (m = j - 1; m >=0; m--) { c++; if (compareLines(lines1[k],lines2[m],ignoreWhitespace) !== 1) { diffMap[k][m] = diffMap[k+1][m+1]+1; } else { diffMap[k][m] = Math.max(diffMap[(k + 1)][m], diffMap[k][(m + 1)]); } } } //console.log(c); k = 0; m = 0; while ((k < i) && (m < j)) { var n = compareLines(lines1[k],lines2[m],ignoreWhitespace); if (n !== 1) { var d = 0; if (n===0) { d = 0; } else if (n==2) { d = 3; } diffSummary.a.push({i:k+1,j:m+1,line:lines1[k],type:d}); diffSummary.b.push({i:m+1,j:k+1,line:lines2[m],type:d}); k++; m++; } else if (diffMap[(k + 1)][m] >= diffMap[k][(m + 1)]) { diffSummary.a.push({i:k+1,line:lines1[k],type:1}); k++; } else { diffSummary.b.push({i:m+1,line:lines2[m],type:4}); m++; } } while ((k < i) || (m < j)) { if (k == i) { diffSummary.b.push({i:m+1,line:lines2[m],type:4}); m++; } else if (m == j) { diffSummary.a.push({i:k+1,line:lines1[k],type:1}); k++; } } return diffSummary; } function compareLines(string1, string2, ignoreWhitespace) { if (ignoreWhitespace) { if (string1 === string2) { return 0; } return string1.trim() === string2.trime() ? 2 : 1; } return string1 === string2 ? 0 : 1; } function createUnifiedDiffTable(files,commitOptions) { var diffPanel = $('<div></div>'); files.forEach(function(file) { var hunks = file.hunks; var isBinary = file.binary; var codeTable = $("<table>",{class:"node-text-diff-content"}).appendTo(diffPanel); $('<colgroup><col width="50"><col width="50"><col width="100%"></colgroup>').appendTo(codeTable); var codeBody = $('<tbody>').appendTo(codeTable); var diffFileRow = $('<tr class="node-text-diff-file-header">').appendTo(codeBody); var content = $('<td colspan="3"></td>').appendTo(diffFileRow); var chevron = $('<i class="node-diff-chevron fa fa-angle-down"></i>').appendTo(content); diffFileRow.click(function(e) { diffFileRow.toggleClass("collapsed"); var isCollapsed = diffFileRow.hasClass("collapsed"); diffFileRow.nextUntil(".node-text-diff-file-header").toggle(!isCollapsed); }) var label = $('<span class="filename"></span>').text(file.file).appendTo(content); var conflictHeader; var unresolvedConflicts = 0; var resolvedConflicts = 0; var conflictResolutions = {}; if (commitOptions.project.files && commitOptions.project.files.flow === file.file) { if (commitOptions.unmerged) { $('<span style="float: right;"><span id="node-diff-toolbar-resolved-conflicts"></span></span>').appendTo(content); } var diffRow = $('<tr class="node-text-diff-header">').appendTo(codeBody); var flowDiffContent = $('<td class="flow-diff" colspan="3"></td>').appendTo(diffRow); var projectName = commitOptions.project.name; var filename = commitOptions.project.files.flow; var commonVersionUrl = "projects/"+projectName+"/files/"+commitOptions.commonRev+"/"+filename; var oldVersionUrl = "projects/"+projectName+"/files/"+commitOptions.oldRev+"/"+filename; var newVersionUrl = "projects/"+projectName+"/files/"+commitOptions.newRev+"/"+filename; var promises = [$.Deferred(),$.Deferred(),$.Deferred()]; if (commitOptions.commonRev) { var commonVersionUrl = "projects/"+projectName+"/files/"+commitOptions.commonRev+"/"+filename; $.ajax({dataType: "json",url: commonVersionUrl}).then(function(data) { promises[0].resolve(data); }).fail(function() { promises[0].resolve(null);}) } else { promises[0].resolve(null); } $.ajax({dataType: "json",url: oldVersionUrl}).then(function(data) { promises[1].resolve(data); }).fail(function() { promises[1].resolve({content:"[]"});}) $.ajax({dataType: "json",url: newVersionUrl}).then(function(data) { promises[2].resolve(data); }).fail(function() { promises[2].resolve({content:"[]"});}) $.when.apply($,promises).always(function(commonVersion, oldVersion,newVersion) { var commonFlow; var oldFlow; var newFlow; if (commonVersion) { try { commonFlow = JSON.parse(commonVersion.content||"[]"); } catch(err) { console.log("Common Version doesn't contain valid JSON:",commonVersionUrl); console.log(err); return; } } try { oldFlow = JSON.parse(oldVersion.content||"[]"); } catch(err) { console.log("Old Version doesn't contain valid JSON:",oldVersionUrl); console.log(err); return; } if (!commonFlow) { commonFlow = oldFlow; } try { newFlow = JSON.parse(newVersion.content||"[]"); } catch(err) { console.log("New Version doesn't contain valid JSON:",newFlow); console.log(err); return; } var localDiff = generateDiff(commonFlow,oldFlow); var remoteDiff = generateDiff(commonFlow,newFlow); commitOptions.currentDiff = resolveDiffs(localDiff,remoteDiff); var diffTable = buildDiffPanel(flowDiffContent,commitOptions.currentDiff,{ title: filename, mode: commitOptions.commonRev?'merge':'view', oldRevTitle: commitOptions.oldRevTitle, newRevTitle: commitOptions.newRevTitle }); diffTable.list.hide(); refreshConflictHeader(commitOptions.currentDiff); setTimeout(function() { diffTable.finish(); diffTable.list.show(); },300); // var flowDiffRow = $("<tr>").insertAfter(diffRow); // var content = $('<td colspan="3"></td>').appendTo(flowDiffRow); // currentDiff = diff; // var diffTable = buildDiffPanel(content,diff,{mode:"view"}).finish(); }); } else if (isBinary) { var diffBinaryRow = $('<tr class="node-text-diff-header">').appendTo(codeBody); var binaryContent = $('<td colspan="3"></td>').appendTo(diffBinaryRow); $('<span></span>').text("Cannot show binary file contents").appendTo(binaryContent); } else { if (commitOptions.unmerged) { conflictHeader = $('<span style="float: right;"><span>'+resolvedConflicts+'</span> of <span>'+unresolvedConflicts+'</span> conflicts resolved</span>').appendTo(content); } hunks.forEach(function(hunk) { var diffRow = $('<tr class="node-text-diff-header">').appendTo(codeBody); var content = $('<td colspan="3"></td>').appendTo(diffRow); var label = $('<span></span>').text(hunk.header).appendTo(content); var isConflict = hunk.conflict; var localLine = hunk.localStartLine; var remoteLine = hunk.remoteStartLine; if (isConflict) { unresolvedConflicts++; } hunk.lines.forEach(function(lineText,lineNumber) { // if (lineText[0] === '\\' || lineText === "") { // // Comment line - bail out of this hunk // break; // } var actualLineNumber = hunk.diffStart + lineNumber; var isMergeHeader = isConflict && /^\+\+(<<<<<<<|=======$|>>>>>>>)/.test(lineText); var diffRow = $('<tr>').appendTo(codeBody); var localLineNo = $('<td class="lineno">').appendTo(diffRow); var remoteLineNo; if (!isMergeHeader) { remoteLineNo = $('<td class="lineno">').appendTo(diffRow); } else { localLineNo.attr('colspan',2); } var line = $('<td class="linetext">').appendTo(diffRow); var prefixStart = 0; var prefixEnd = 1; if (isConflict) { prefixEnd = 2; } if (!isMergeHeader) { var changeMarker = lineText[0]; if (isConflict && !commitOptions.unmerged && changeMarker === ' ') { changeMarker = lineText[1]; } $('<span class="prefix">').text(changeMarker).appendTo(line); var handledlLine = false; if (isConflict && commitOptions.unmerged) { $('<span class="prefix">').text(lineText[1]).appendTo(line); if (lineText[0] === '+') { localLineNo.text(localLine++); handledlLine = true; } if (lineText[1] === '+') { remoteLineNo.text(remoteLine++); handledlLine = true; } } else { if (lineText[0] === '+' || (isConflict && lineText[1] === '+')) { localLineNo.addClass("added"); remoteLineNo.addClass("added"); line.addClass("added"); remoteLineNo.text(remoteLine++); handledlLine = true; } else if (lineText[0] === '-' || (isConflict && lineText[1] === '-')) { localLineNo.addClass("removed"); remoteLineNo.addClass("removed"); line.addClass("removed"); localLineNo.text(localLine++); handledlLine = true; } } if (!handledlLine) { line.addClass("unchanged"); if (localLine > 0 && lineText[0] !== '\\' && lineText !== "") { localLineNo.text(localLine++); } if (remoteLine > 0 && lineText[0] !== '\\' && lineText !== "") { remoteLineNo.text(remoteLine++); } } $('<span>').text(lineText.substring(prefixEnd)).appendTo(line); } else { diffRow.addClass("mergeHeader"); var isSeparator = /^\+\+=======$/.test(lineText); if (!isSeparator) { var isOurs = /^..<<<<<<</.test(lineText); if (isOurs) { $('<span>').text("<<<<<<< Local Changes").appendTo(line); hunk.localChangeStart = actualLineNumber; } else { hunk.remoteChangeEnd = actualLineNumber; $('<span>').text(">>>>>>> Remote Changes").appendTo(line); } diffRow.addClass("mergeHeader-"+(isOurs?"ours":"theirs")); $('<button class="editor-button editor-button-small" style="float: right; margin-right: 20px;"><i class="fa fa-angle-double-'+(isOurs?"down":"up")+'"></i> use '+(isOurs?"local":"remote")+' changes</button>') .appendTo(line) .click(function(evt) { evt.preventDefault(); resolvedConflicts++; var addedRows; var midRow; if (isOurs) { addedRows = diffRow.nextUntil(".mergeHeader-separator"); midRow = addedRows.last().next(); midRow.nextUntil(".mergeHeader").remove(); midRow.next().remove(); } else { addedRows = diffRow.prevUntil(".mergeHeader-separator"); midRow = addedRows.last().prev(); midRow.prevUntil(".mergeHeader").remove(); midRow.prev().remove(); } midRow.remove(); diffRow.remove(); addedRows.find(".linetext").addClass('added'); conflictHeader.empty(); $('<span><span>'+resolvedConflicts+'</span> of <span>'+unresolvedConflicts+'</span> conflicts resolved</span>').appendTo(conflictHeader); conflictResolutions[file.file] = conflictResolutions[file.file] || {}; conflictResolutions[file.file][hunk.localChangeStart] = { changeStart: hunk.localChangeStart, separator: hunk.changeSeparator, changeEnd: hunk.remoteChangeEnd, selection: isOurs?"A":"B" } if (commitOptions.resolveConflict) { commitOptions.resolveConflict({ conflicts: unresolvedConflicts, resolved: resolvedConflicts, resolutions: conflictResolutions }); } }) } else { hunk.changeSeparator = actualLineNumber; diffRow.addClass("mergeHeader-separator"); } } }); }); } }); return diffPanel; } function showCommitDiff(options) { var commit = parseCommitDiff(options.commit); var trayOptions = { title: "View Commit Changes", //TODO: nls width: Infinity, overlay: true, buttons: [ { text: RED._("common.label.close"), click: function() { RED.tray.close(); } } ], resize: function(dimensions) { // trayWidth = dimensions.width; }, open: function(tray) { var trayBody = tray.find('.editor-tray-body'); var diffPanel = $('<div class="node-text-diff"></div>').appendTo(trayBody); var codeTable = $("<table>",{class:"node-text-diff-content"}).appendTo(diffPanel); $('<colgroup><col width="50"><col width="50"><col width="100%"></colgroup>').appendTo(codeTable); var codeBody = $('<tbody>').appendTo(codeTable); var diffRow = $('<tr class="node-text-diff-commit-header">').appendTo(codeBody); var content = $('<td colspan="3"></td>').appendTo(diffRow); $("<h3>").text(commit.title).appendTo(content); $('<div class="commit-body"></div>').text(commit.comment).appendTo(content); var summary = $('<div class="commit-summary"></div>').appendTo(content); $('<div style="float: right">').text("Commit "+commit.sha).appendTo(summary); $('<div>').text((commit.authorName||commit.author)+" - "+options.date).appendTo(summary); if (commit.files) { createUnifiedDiffTable(commit.files,options).appendTo(diffPanel); } }, close: function() { diffVisible = false; }, show: function() { } } RED.tray.show(trayOptions); } function showUnifiedDiff(options) { var diff = options.diff; var title = options.title; var files = parseUnifiedDiff(diff); var currentResolution; if (options.unmerged) { options.resolveConflict = function(results) { currentResolution = results; if (results.conflicts === results.resolved) { $("#node-diff-view-resolve-diff").removeClass('disabled'); } } } var trayOptions = { title: title||"Compare Changes", //TODO: nls width: Infinity, overlay: true, buttons: [ { text: RED._((options.unmerged)?"common.label.cancel":"common.label.close"), click: function() { if (options.oncancel) { options.oncancel(); } RED.tray.close(); } } ], resize: function(dimensions) { // trayWidth = dimensions.width; }, open: function(tray) { var trayBody = tray.find('.editor-tray-body'); var diffPanel = $('<div class="node-text-diff"></div>').appendTo(trayBody); createUnifiedDiffTable(files,options).appendTo(diffPanel); }, close: function() { diffVisible = false; }, show: function() { } } if (options.unmerged) { trayOptions.buttons.push( { id: "node-diff-view-resolve-diff", text: "Save conflict resolution", class: "primary disabled", click: function() { if (!$("#node-diff-view-resolve-diff").hasClass('disabled')) { if (options.currentDiff) { // This is a flow file. Need to apply the diff // and generate the new flow. var result = applyDiff(options.currentDiff); currentResolution = { resolutions:{} }; currentResolution.resolutions[options.project.files.flow] = JSON.stringify(result.config,"",4); } if (options.onresolve) { options.onresolve(currentResolution); } RED.tray.close(); } } } ); } RED.tray.show(trayOptions); } function parseCommitDiff(diff) { var result = {}; var lines = diff.split("\n"); var comment = []; for (var i=0;i<lines.length;i++) { if (/^commit /.test(lines[i])) { result.sha = lines[i].substring(7); } else if (/^Author: /.test(lines[i])) { result.author = lines[i].substring(8); var m = /^(.*) <(.*)>$/.exec(result.author); if (m) { result.authorName = m[1]; result.authorEmail = m[2]; } } else if (/^Date: /.test(lines[i])) { result.date = lines[i].substring(8); } else if (/^ /.test(lines[i])) { if (!result.title) { result.title = lines[i].substring(4); } else { if (lines[i].length !== 4 || comment.length > 0) { comment.push(lines[i].substring(4)); } } } else if (/^diff /.test(lines[i])) { result.files = parseUnifiedDiff(lines.slice(i)); break; } } result.comment = comment.join("\n"); return result; } function parseUnifiedDiff(diff) { var lines; if (Array.isArray(diff)) { lines = diff; } else { lines = diff.split("\n"); } var diffHeader = /^diff (?:(?:--git a\/(.*) b\/(.*))|(?:--cc (.*)))$/; var fileHeader = /^\+\+\+ b\/(.*)\t?/; var binaryFile = /^Binary files /; var hunkHeader = /^@@ -((\d+)(,(\d+))?) \+((\d+)(,(\d+))?) @@ ?(.*)$/; var conflictHunkHeader = /^@+ -((\d+)(,(\d+))?) -((\d+)(,(\d+))?) \+((\d+)(,(\d+))?) @+/; var files = []; var currentFile; var hunks = []; var currentHunk; for (var i=0;i<lines.length;i++) { var line = lines[i]; var diffLine = diffHeader.exec(line); if (diffLine) { if (currentHunk) { currentFile.hunks.push(currentHunk); files.push(currentFile); } currentHunk = null; currentFile = { file: diffLine[1]||diffLine[3], hunks: [] } } else if (binaryFile.test(line)) { if (currentFile) { currentFile.binary = true; } } else { var fileLine = fileHeader.exec(line); if (fileLine) { currentFile.file = fileLine[1]; } else { var hunkLine = hunkHeader.exec(line); if (hunkLine) { if (currentHunk) { currentFile.hunks.push(currentHunk); } currentHunk = { header: line, localStartLine: hunkLine[2], localLength: hunkLine[4]||1, remoteStartLine: hunkLine[6], remoteLength: hunkLine[8]||1, lines: [], conflict: false } continue; } hunkLine = conflictHunkHeader.exec(line); if (hunkLine) { if (currentHunk) { currentFile.hunks.push(currentHunk); } currentHunk = { header: line, localStartLine: hunkLine[2], localLength: hunkLine[4]||1, remoteStartLine: hunkLine[6], remoteLength: hunkLine[8]||1, diffStart: parseInt(hunkLine[10]), lines: [], conflict: true } continue; } if (currentHunk) { currentHunk.lines.push(line); } } } } if (currentHunk) { currentFile.hunks.push(currentHunk); } files.push(currentFile); return files; } return { init: init, getRemoteDiff: getRemoteDiff, showRemoteDiff: showRemoteDiff, showUnifiedDiff: showUnifiedDiff, showCommitDiff: showCommitDiff, mergeDiff: mergeDiff } })();