private static XmlNode HandleCaseOfNoAncestorChild(XmlMerger merger, XmlNode ours, XmlNode ourChild, XmlNode theirChild) { var mergeSituation = merger.MergeSituation; var pathToFileInRepository = mergeSituation.PathToFileInRepository; var mergeStrategyForChild = merger.MergeStrategies.GetElementStrategy(ourChild ?? theirChild); if (ourChild == null) { // they added child. merger.EventListener.ChangeOccurred(new XmlAdditionChangeReport(pathToFileInRepository, theirChild)); // ReSharper disable once PossibleNullReferenceException -- if ours.OwnerDocument is null, we have other problems ours.AppendChild(ours.OwnerDocument.ImportNode(theirChild, true)); return(ours); } if (theirChild == null) { // We added child. merger.EventListener.ChangeOccurred(new XmlAdditionChangeReport(pathToFileInRepository, ourChild)); return(ours); } // Both added child. if (XmlUtilities.AreXmlElementsEqual(ourChild, theirChild)) { // Both are the same. merger.EventListener.ChangeOccurred(new XmlBothAddedSameChangeReport(pathToFileInRepository, ourChild)); return(ours); } // Both are different. if (XmlUtilities.IsTextLevel(ourChild, theirChild, null)) { if (merger.MergeSituation.ConflictHandlingMode == MergeOrder.ConflictHandlingModeChoices.WeWin) { merger.ConflictOccurred(new XmlTextBothAddedTextConflict(ourChild.Name, ourChild, theirChild, mergeSituation, mergeStrategyForChild, mergeSituation.AlphaUserId)); } else { merger.ConflictOccurred(new XmlTextBothAddedTextConflict(theirChild.Name, theirChild, ourChild, mergeSituation, mergeStrategyForChild, mergeSituation.BetaUserId)); XmlUtilities.ReplaceOursWithTheirs(ours, ref ourChild, theirChild); } } else { if (merger.MergeSituation.ConflictHandlingMode == MergeOrder.ConflictHandlingModeChoices.WeWin) { var ourChildClone = MakeClone(ourChild); var theirChildClone = MakeClone(theirChild); if (XmlUtilities.AreXmlElementsEqual(ourChildClone, theirChildClone)) { merger.EventListener.ChangeOccurred(new XmlAdditionChangeReport(pathToFileInRepository, ourChild)); var ourChildReplacement = ourChild; merger.MergeInner(ours, ref ourChildReplacement, theirChild, null); if (!ReferenceEquals(ourChild, ourChildReplacement)) { XmlUtilities.ReplaceOursWithTheirs(ours, ref ourChild, ourChildReplacement); } } else { merger.ConflictOccurred(new BothAddedMainElementButWithDifferentContentConflict(ourChild.Name, ourChild, theirChild, mergeSituation, mergeStrategyForChild, mergeSituation.AlphaUserId)); } } else { var ourChildClone = MakeClone(ourChild); var theirChildClone = MakeClone(theirChild); if (XmlUtilities.AreXmlElementsEqual(ourChildClone, theirChildClone)) { merger.EventListener.ChangeOccurred(new XmlAdditionChangeReport(pathToFileInRepository, theirChild)); var ourChildReplacement = ourChild; merger.MergeInner(ours, ref ourChildReplacement, theirChild, null); if (!ReferenceEquals(ourChild, ourChildReplacement)) { XmlUtilities.ReplaceOursWithTheirs(ours, ref ourChild, ourChildReplacement); } } else { merger.ConflictOccurred(new BothAddedMainElementButWithDifferentContentConflict(theirChild.Name, theirChild, ourChild, mergeSituation, mergeStrategyForChild, mergeSituation.BetaUserId)); XmlUtilities.ReplaceOursWithTheirs(ours, ref ourChild, theirChild); } } } return(ours); }
/// <summary> /// Remove from ancestorKeepers any node that does not correspond to anything (both deleted) /// Remove from ancestorKeepers and theirKeepers any pair that correspond to each other but not to anything in ours. Report conflict (delete/edit) if pair not identical. /// Remove from ancestorKeepers and ourKeepers any pair that correspond to each other and are identical, but don't correspond to anything in theirs (they deleted) /// Report conflict (edit/delete) on any pair that correspond in ours and ancestor, but nothing in theirs, and that are NOT identical. (but keep them...we win) /// </summary> private void DoDeletions() { // loop over a copy of the list, since we may modify ancestorKeepers. List <XmlNode> loopSource = new List <XmlNode>(_childrenOfAncestorKeepers); var ourChildSet = new HashSet <XmlNode>(_ours == null ? new XmlNode[0] : _ours.ChildNodes.Cast <XmlNode>()); var theirChildSet = new HashSet <XmlNode>(_theirs == null ? new XmlNode[0] : _theirs.ChildNodes.Cast <XmlNode>()); foreach (XmlNode ancestorChild in loopSource) { ElementStrategy mergeStrategy = _merger.MergeStrategies.GetElementStrategy(ancestorChild); IFindNodeToMerge finder = mergeStrategy.MergePartnerFinder; XmlNode ourChild = finder.GetNodeToMerge(ancestorChild, _ours, ourChildSet); XmlNode theirChild = finder.GetNodeToMerge(ancestorChild, _theirs, theirChildSet); var extantNode = ancestorChild ?? ourChild ?? theirChild; if (extantNode is XmlCharacterData) { return; // Already done. } if (XmlUtilities.IsTextLevel(ourChild, theirChild, ancestorChild)) { new MergeTextNodesMethod(_merger, mergeStrategy, _skipInnerMergeFor, ref ourChild, _childrenOfOurKeepers, theirChild, _childrenOfTheirKeepers, ancestorChild, _childrenOfAncestorKeepers).DoDeletions(); } else if (ourChild == null) { // We deleted it. if (theirChild == null) { // We both deleted it. Forget it ever existed. // Route tested: MergeChildrenMethodTests. _merger.EventListener.ChangeOccurred(new XmlBothDeletionChangeReport(_merger.MergeSituation.PathToFileInRepository, ancestorChild)); _childrenOfAncestorKeepers.Remove(ancestorChild); } else { if (!XmlUtilities.AreXmlElementsEqual(ancestorChild, theirChild)) { // We deleted, they modified, report conflict. if (theirChild.NodeType == XmlNodeType.Element) { // Route tested (XmlMergerTests). _merger.ConflictOccurred( new RemovedVsEditedElementConflict(theirChild.Name, null, theirChild, ancestorChild, _merger.MergeSituation, _merger.MergeStrategies.GetElementStrategy(theirChild), _merger.MergeSituation.BetaUserId), theirChild); _skipInnerMergeFor.Add(theirChild); } else { // Never used. But then, there isn't plain text in an xml file. _merger.ConflictOccurred( new RemovedVsEditedTextConflict(null, theirChild, ancestorChild, _merger.MergeSituation, _merger.MergeSituation.BetaUserId)); _skipInnerMergeFor.Add(theirChild); } _childrenOfAncestorKeepers.Remove(ancestorChild); //review hatton added dec 2009, wanting whoever edited it to win (previously "we" always won) } else { //We deleted it, they didn't edit it. So just make it go away. // Route tested in TextElementMergeTests, MergeChildrenMethod_DiffOnlyTests, XmlMergerTests _merger.EventListener.ChangeOccurred(new XmlDeletionChangeReport(_merger.MergeSituation.PathToFileInRepository, ancestorChild, theirChild)); _childrenOfAncestorKeepers.Remove(ancestorChild); _childrenOfTheirKeepers.Remove(theirChild); } } } else if (theirChild == null) { // they deleted it (and we didn't) if (XmlUtilities.AreXmlElementsEqual(ancestorChild, ourChild)) { // We didn't touch it, allow their deletion to go forward, forget it existed. // Route tested (XmlMergerTests). _merger.EventListener.ChangeOccurred(new XmlDeletionChangeReport(_merger.MergeSituation.PathToFileInRepository, ancestorChild, ourChild)); _childrenOfAncestorKeepers.Remove(ancestorChild); _childrenOfOurKeepers.Remove(ourChild); } else { // We changed it, ignore their deletion and report conflict. if (ourChild.NodeType == XmlNodeType.Element) { // Route tested (XmlMergerTests). _merger.ConflictOccurred( new EditedVsRemovedElementConflict(ourChild.Name, ourChild, null, ancestorChild, _merger.MergeSituation, _merger.MergeStrategies.GetElementStrategy(ourChild), _merger.MergeSituation.AlphaUserId), ourChild); _skipInnerMergeFor.Add(ourChild); } else { // Never used. But then, there isn't plain text in an xml file. _merger.ConflictOccurred( new EditedVsRemovedTextConflict(ourChild, null, ancestorChild, _merger.MergeSituation, _merger.MergeSituation.AlphaUserId)); _skipInnerMergeFor.Add(ourChild); } } } } }
public static void Run(XmlMerger merger, ElementStrategy strategy, ref XmlNode ours, XmlNode theirs, XmlNode ancestor) { // All routes tested in this method. Guard.AgainstNull(merger, "merger"); // Route tested. Guard.AgainstNull(strategy, "strategy"); // Route tested. if (ours == null && theirs == null && ancestor == null) { throw new ArgumentNullException(); // Route tested. } if (XmlUtilities.IsTextLevel(ours, theirs, ancestor)) { // Route tested. new MergeTextNodesMethod(merger, merger.MergeStrategies.GetElementStrategy(ours ?? theirs ?? ancestor), new HashSet <XmlNode>(), ref ours, new List <XmlNode>(), theirs, new List <XmlNode>(), ancestor, new List <XmlNode>()).Run(); return; } List <XmlNode> ourChildren; List <XmlNode> theirChildren; List <XmlNode> ancestorChildren; switch (strategy.NumberOfChildren) { default: throw new InvalidOperationException("Using strategy with NumberOfChildren property of NumberOfChildrenAllowed.ZeroOrMore is not legal."); // Route tested. case NumberOfChildrenAllowed.Zero: ourChildren = GetElementChildren(ours).ToList(); if (ourChildren.Any()) { throw new InvalidOperationException("Using strategy with NumberOfChildren property of NumberOfChildrenAllowed.Zero is not legal, when there are child element nodes."); // Route tested. } theirChildren = GetElementChildren(theirs).ToList(); if (theirChildren.Any()) { throw new InvalidOperationException("Using strategy with NumberOfChildren property of NumberOfChildrenAllowed.Zero is not legal, when there are child element nodes."); // Route tested. } ancestorChildren = GetElementChildren(ancestor).ToList(); if (ancestorChildren.Any()) { throw new InvalidOperationException("Using strategy with NumberOfChildren property of NumberOfChildrenAllowed.Zero is not legal, when there are child element nodes."); // Route tested. } // Don't merge deeper than merging the attributes, since there aren't supposed to be any children. // Already done by caller MergeXmlAttributesService.MergeAttributes(merger, ref ours, theirs, ancestor); // Route tested. break; case NumberOfChildrenAllowed.ZeroOrOne: ourChildren = GetElementChildren(ours).ToList(); if (ourChildren.Count > 1) { throw new InvalidOperationException("Using strategy with NumberOfChildren property of NumberOfChildrenAllowed.ZeroOrOne is not legal, when there are multiple child nodes."); // Route tested. } theirChildren = GetElementChildren(theirs).ToList(); if (theirChildren.Count > 1) { throw new InvalidOperationException("Using strategy with NumberOfChildren property of NumberOfChildrenAllowed.ZeroOrOne is not legal, when there are multiple child nodes."); // Route tested. } ancestorChildren = GetElementChildren(ancestor).ToList(); if (ancestorChildren.Count > 1) { throw new InvalidOperationException("Using strategy with NumberOfChildren property of NumberOfChildrenAllowed.ZeroOrOne is not legal, when there are child element nodes."); // Route tested. } // Already done by caller MergeXmlAttributesService.MergeAttributes(merger, ref ours, theirs, ancestor); if (!ourChildren.Any() && !theirChildren.Any() && ancestor != null) { return; // Route tested. } // The return value of Run may be the original 'ours', a replacement for it, or null. ours = Run(merger, ours, theirs, ancestor); // Route tested. break; } }
/// <summary> /// This method does the actual work for the various public entry points of XmlMerge /// and from the various Method-type classes, as it processes child nodes, if any. /// </summary> internal void MergeInner(XmlNode ourParent, ref XmlNode ours, XmlNode theirs, XmlNode ancestor) { SendMergeHeartbeat(); _oursContext = ours; _theirsContext = theirs; _ancestorContext = ancestor; var elementStrat = MergeStrategies.GetElementStrategy(ours ?? theirs ?? ancestor); // Step 0: Do anything special the strategy wants to do before regular merging. This may modify the nodes. // For instance clinets may want to ensure 'our' and 'their' have the latest date stamp available. elementStrat.Premerger.Premerge(EventListener, ref ours, theirs, ancestor); // Step 0.1: Set up a context, if available. // Listeners are set to use NullContextDescriptor as the default context, var contextDescriptorGenerator = elementStrat.ContextDescriptorGenerator; // May be null, which is fine in code, below, that uses it. //review: question: does this not get called at levels below the entry? //this would seem to fail at, say, a sense. I'm confused. (JH 30june09) var descriptor = GetContextDescriptor(ours, contextDescriptorGenerator); EventListener.EnteringContext(descriptor); // TODO: If the context is ever redone as a stack, then this call with push the context onto the stack. _htmlContextGenerator = (contextDescriptorGenerator as IGenerateHtmlContext) ?? new SimpleHtmlGenerator(); // Step 1: If the current set of nodes are immutable, // then make sure no changes took place (among a few other things). if (elementStrat.IsImmutable) { ImmutableElementMergeService.DoMerge(this, ourParent, ref ours, theirs, ancestor); return; // Don't go any further, since it is immutable. } // Step 2: If the current set of elements is 'atomic' // (only one user can make changes to the node, or anything it contains), // then make sure only set of changes have been made. if (elementStrat.IsAtomic) { if (elementStrat.AllowAtomicTextMerge && XmlUtilities.IsTextLevel(ours, theirs, ancestor)) { DoTextMerge(ref ours, theirs, ancestor, elementStrat); return; } MergeAtomicElementService.Run(this, ourParent, ref ours, theirs, ancestor); return; } // Step 3: Go ahead and merge the attributes, as needed. MergeXmlAttributesService.MergeAttributes(this, ref ours, theirs, ancestor); // Step 4: Hmm, trouble is, a node might be required to have one or more child nodes. // I suppose a real validator would have picked up on that, and not allowed // the first commit, so we wouldn't then be doing a merge operation. // So, not to worry here, if the validator doesn't care enough to prevent the first commit. // It could be possible for the elements to have no children, in which case, there is nothing more to merge, so just return. if (ours != null && !ours.HasChildNodes && theirs != null && !theirs.HasChildNodes && ancestor != null && !ancestor.HasChildNodes) { return; } // Step 5: Do some kind of merge on the child node. if (XmlUtilities.IsTextLevel(ours, theirs, ancestor)) { // Step 5A: Merge the text element. DoTextMerge(ref ours, theirs, ancestor, elementStrat); } else { switch (elementStrat.NumberOfChildren) { case NumberOfChildrenAllowed.Zero: case NumberOfChildrenAllowed.ZeroOrOne: // Step 5B: Merge the "special needs" nodes. MergeLimitedChildrenService.Run(this, elementStrat, ref ours, theirs, ancestor); break; case NumberOfChildrenAllowed.ZeroOrMore: // Step 5B: // Q: is this a level of the xml file that would consitute the minimal unit conflict-understanding // from a user perspecitve? // e.g., in a dictionary, this is the lexical entry. In a text, it might be a paragraph. // A (RandyR): Definitely it may not be such a level of node. new MergeChildrenMethod(ours, theirs, ancestor, this).Run(); break; } } // At some point, it may be necessary here to restore the pre-existing values of // _oursContext, _theirsContext, _ancestorContext, and _htmlContextGenerator. // and somehow restore the EventListener's Context. // Currently however no client generates further conflicts after calling MergeChildren. // Step 6: TODO: If the context is ever redone as a stack, then pop the stack here to return to the outer context via some new LeavingContext method on EventListener. }