Index: src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java
===================================================================
--- src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java	(revision 5198)
+++ src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java	(working copy)
@@ -372,7 +372,7 @@
      *
      * @param numNewConflicts the number of detected conflicts
      */
-    protected void warnNumNewConflicts(int numNewConflicts) {
+    public void warnNumNewConflicts(int numNewConflicts) {
         if (numNewConflicts == 0) return;
 
         String msg1 = trn(
Index: src/org/openstreetmap/josm/actions/MergeSelectionAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/MergeSelectionAction.java	(revision 5198)
+++ src/org/openstreetmap/josm/actions/MergeSelectionAction.java	(working copy)
@@ -8,10 +8,11 @@
 import java.awt.event.KeyEvent;
 import java.util.Collection;
 import java.util.List;
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.command.MergeCommand;
 
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.visitor.MergeSourceBuildingVisitor;
 import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
 import org.openstreetmap.josm.gui.layer.Layer;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
@@ -36,10 +37,12 @@
         Layer targetLayer = askTargetLayer(targetLayers);
         if (targetLayer == null)
             return;
-        MergeSourceBuildingVisitor builder = new MergeSourceBuildingVisitor(getEditLayer().data);
-        ((OsmDataLayer)targetLayer).mergeFrom(builder.build());
+        
+        MergeCommand cmd = new MergeCommand((OsmDataLayer)targetLayer, getEditLayer().data, true);
+        Main.main.undoRedo.add(cmd);
     }
 
+    @Override
     public void actionPerformed(ActionEvent e) {
         if (getEditLayer() == null || getEditLayer().data.getSelected().isEmpty())
             return;
Index: src/org/openstreetmap/josm/data/osm/DataSetMerger.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/DataSetMerger.java	(revision 5198)
+++ src/org/openstreetmap/josm/data/osm/DataSetMerger.java	(working copy)
@@ -43,6 +43,14 @@
      */
     private final Set<PrimitiveId> objectsWithChildrenToMerge;
     private final Set<OsmPrimitive> objectsToDelete;
+    
+    private final Map<OsmPrimitive, PrimitiveData> changedObjectsMap;
+    private final Set<OsmPrimitive> addedObjects;
+    
+    private enum UndoState {
+        INIT, MERGED, UNDONE, REDONE
+    }
+    private UndoState undoState;
 
     /**
      * constructor
@@ -61,6 +69,9 @@
         mergedMap = new HashMap<PrimitiveId, PrimitiveId>();
         objectsWithChildrenToMerge = new HashSet<PrimitiveId>();
         objectsToDelete = new HashSet<OsmPrimitive>();
+        changedObjectsMap = new HashMap<OsmPrimitive, PrimitiveData>();
+        addedObjects = new HashSet<OsmPrimitive>();
+        undoState = UndoState.INIT;
     }
 
     /**
@@ -77,7 +88,7 @@
      * @param <P>  the type of the other primitive
      * @param source  the other primitive
      */
-    protected void mergePrimitive(OsmPrimitive source, Collection<? extends OsmPrimitive> candidates) {
+        protected void mergePrimitive(OsmPrimitive source, Collection<? extends OsmPrimitive> candidates) {
         if (!source.isNew() ) {
             // try to merge onto a matching primitive with the same
             // defined id
@@ -107,6 +118,7 @@
                     target.setTimestamp(source.getTimestamp());
                     target.setModified(source.isModified());
                     objectsWithChildrenToMerge.add(source.getPrimitiveId());
+                    changedObjectsMap.put(target, source.save());
                     return;
                 }
             }
@@ -126,6 +138,7 @@
         targetDataSet.addPrimitive(target);
         mergedMap.put(source.getPrimitiveId(), target.getPrimitiveId());
         objectsWithChildrenToMerge.add(source.getPrimitiveId());
+        addedObjects.add(target);
     }
 
     protected OsmPrimitive getMergeTarget(OsmPrimitive mergeSource) throws IllegalStateException {
@@ -180,6 +193,7 @@
                 if (referrers.isEmpty()) {
                     target.setDeleted(true);
                     target.mergeFrom(source);
+                    changedObjectsMap.put(target, source.save());
                     it.remove();
                     flag = true;
                 } else {
@@ -208,9 +222,11 @@
                     ((Relation) osm).setMembers(null);
                 }
             }
-            for (OsmPrimitive osm: objectsToDelete) {
-                osm.setDeleted(true);
-                osm.mergeFrom(sourceDataSet.getPrimitiveById(osm.getPrimitiveId()));
+            for (OsmPrimitive target: objectsToDelete) {
+                OsmPrimitive source = sourceDataSet.getPrimitiveById(target.getPrimitiveId());
+                target.setDeleted(true);
+                target.mergeFrom(source);
+                changedObjectsMap.put(target, source.save());
             }
         }
     }
@@ -293,6 +309,7 @@
             // => merge source into target
             //
             target.mergeFrom(source);
+            changedObjectsMap.put(target, source.save());
             objectsWithChildrenToMerge.add(source.getPrimitiveId());
         } else if (!target.isIncomplete() && source.isIncomplete()) {
             // target is complete and source is incomplete
@@ -318,6 +335,7 @@
                 if (targetDataSet.getPrimitiveById(referrer.getPrimitiveId()) == null) {
                     conflicts.add(new Conflict<OsmPrimitive>(target, source, true));
                     target.setDeleted(false);
+                    changedObjectsMap.put(target, source.save());
                     break;
                 }
             }
@@ -330,22 +348,26 @@
             // target not modified. We can assume that source is the most recent version.
             // clone it into target.
             target.mergeFrom(source);
+            changedObjectsMap.put(target, source.save());
             objectsWithChildrenToMerge.add(source.getPrimitiveId());
         } else if (! target.isModified() && !source.isModified() && target.getVersion() == source.getVersion()) {
             // both not modified. Merge nevertheless.
             // This helps when updating "empty" relations, see #4295
             target.mergeFrom(source);
+            changedObjectsMap.put(target, source.save());
             objectsWithChildrenToMerge.add(source.getPrimitiveId());
         } else if (! target.isModified() && !source.isModified() && target.getVersion() < source.getVersion()) {
             // my not modified but other is newer. clone other onto mine.
             //
             target.mergeFrom(source);
+            changedObjectsMap.put(target, source.save());
             objectsWithChildrenToMerge.add(source.getPrimitiveId());
         } else if (target.isModified() && ! source.isModified() && target.getVersion() == source.getVersion()) {
             // target is same as source but target is modified
             // => keep target and reset modified flag if target and source are semantically equal
             if (target.hasEqualSemanticAttributes(source)) {
                 target.setModified(false);
+                changedObjectsMap.put(target, source.save());
             }
         } else if (source.isDeleted() != target.isDeleted()) {
             // target is modified and deleted state differs.
@@ -363,6 +385,7 @@
             // attributes should already be equal if we get here.
             //
             target.mergeFrom(source);
+            changedObjectsMap.put(target, source.save());
             objectsWithChildrenToMerge.add(source.getPrimitiveId());
         }
         return true;
@@ -423,6 +446,8 @@
         if (progressMonitor != null) {
             progressMonitor.finishTask();
         }
+        
+        undoState = UndoState.MERGED;
     }
 
     /**
@@ -442,4 +467,59 @@
     public ConflictCollection getConflicts() {
         return conflicts;
     }
+    
+    /**
+     * Undos the merge operation.
+     */
+    public void unmerge() {
+        if (undoState != UndoState.MERGED && undoState != UndoState.REDONE) {
+            throw new AssertionError();
+        }
+        
+        targetDataSet.beginUpdate();
+        
+        for (PrimitiveId osm : addedObjects) {
+            targetDataSet.removePrimitive(osm);
+        }
+        
+        for (Map.Entry<OsmPrimitive, PrimitiveData> e : changedObjectsMap.entrySet()) {
+            // restore previous state and save current state for opposite undo action
+            PrimitiveData old = e.getKey().save();
+            e.getKey().load(e.getValue());
+            e.setValue(old);
+        }
+        
+        targetDataSet.endUpdate();
+        undoState = UndoState.UNDONE;
+    }
+    
+    public void remerge() {
+        if (undoState != UndoState.UNDONE) {
+            throw new AssertionError();
+        }
+        
+        targetDataSet.beginUpdate();
+        
+        for (OsmPrimitive osm : addedObjects) {
+            targetDataSet.addPrimitive(osm);
+        }
+        
+        for (Map.Entry<OsmPrimitive, PrimitiveData> e : changedObjectsMap.entrySet()) {
+            // restore previous state and save current state for opposite undo action
+            PrimitiveData old = e.getKey().save();
+            e.getKey().load(e.getValue());
+            e.setValue(old);
+        }
+        
+        targetDataSet.endUpdate();
+        undoState = UndoState.REDONE;
+    }
+    
+    public Map<OsmPrimitive, PrimitiveData> getChangedObjectsMap() {
+        return changedObjectsMap;
+    }
+    
+    public Set<OsmPrimitive> getAddedObjects() {
+        return addedObjects;
+    }
 }
Index: src/org/openstreetmap/josm/command/MergeCommand.java
===================================================================
--- src/org/openstreetmap/josm/command/MergeCommand.java	(revision 0)
+++ src/org/openstreetmap/josm/command/MergeCommand.java	(working copy)
@@ -0,0 +1,187 @@
+// License: GPL. Copyright 2012 by Josh Doe and others
+package org.openstreetmap.josm.command;
+
+import java.awt.geom.Area;
+import java.util.Collection;
+import java.util.HashSet;
+import javax.swing.Icon;
+import javax.swing.JOptionPane;
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.conflict.Conflict;
+import org.openstreetmap.josm.data.osm.*;
+import org.openstreetmap.josm.data.osm.visitor.MergeSourceBuildingVisitor;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor;
+import org.openstreetmap.josm.tools.CheckParameterUtil;
+import static org.openstreetmap.josm.tools.I18n.marktr;
+import static org.openstreetmap.josm.tools.I18n.tr;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * A command that merges objects from one layer to another.
+ *
+ * @author joshdoe
+ */
+public class MergeCommand extends Command {
+
+    private DataSetMerger merger;
+    private DataSet sourceDataSet;
+    private DataSet targetDataSet;
+    private OsmDataLayer targetLayer;
+    private Collection<DataSource> addedDataSources;
+    private String otherVersion;
+    private Collection<Conflict> addedConflicts;
+
+    /**
+     * Create command to merge all or only currently selected objects from
+     * sourceDataSet to targetLayer.
+     *
+     * @param targetLayer
+     * @param sourceDataSet
+     * @param onlySelected true to only merge objects selected in the
+     * sourceDataSet
+     */
+    public MergeCommand(OsmDataLayer targetLayer, DataSet sourceDataSet, boolean onlySelected) {
+        this(targetLayer, sourceDataSet, onlySelected ? sourceDataSet.getSelected() : null);
+    }
+
+    /**
+     * Create command to merge the selection from the sourceDataSet to the
+     * targetLayer.
+     *
+     * @param targetLayer
+     * @param sourceDataSet
+     * @param selection
+     */
+    public MergeCommand(OsmDataLayer targetLayer, DataSet sourceDataSet, Collection<OsmPrimitive> selection) {
+        CheckParameterUtil.ensureParameterNotNull(targetLayer, "targetLayer");
+        CheckParameterUtil.ensureParameterNotNull(sourceDataSet, "sourceDataSet");
+        this.targetLayer = targetLayer;
+        this.targetDataSet = targetLayer.data;
+
+        // if selection present, create new dataset with just selected objects
+        // and their "hull" (otherwise use entire dataset)
+        if (selection != null && !selection.isEmpty()) {
+            Collection<OsmPrimitive> origSelection = sourceDataSet.getSelected();
+            sourceDataSet.setSelected(selection);
+            MergeSourceBuildingVisitor builder = new MergeSourceBuildingVisitor(sourceDataSet);
+            this.sourceDataSet = builder.build();
+            sourceDataSet.setSelected(origSelection);
+        }
+
+        addedConflicts = new HashSet<Conflict>();
+        addedDataSources = new HashSet<DataSource>();
+    }
+
+    @Override
+    public boolean executeCommand() {
+        PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor(tr("Merging data"));
+        monitor.setCancelable(false);
+        if (merger == null) {
+            //first time command is executed
+            merger = new DataSetMerger(targetDataSet, sourceDataSet);
+            try {
+                merger.merge(monitor);
+            } catch (DataIntegrityProblemException e) {
+                JOptionPane.showMessageDialog(
+                        Main.parent,
+                        e.getHtmlMessage() != null ? e.getHtmlMessage() : e.getMessage(),
+                        tr("Error"),
+                        JOptionPane.ERROR_MESSAGE);
+                return false;
+
+            }
+
+            Area a = targetDataSet.getDataSourceArea();
+
+            // copy the merged layer's data source info;
+            // only add source rectangles if they are not contained in the
+            // layer already.
+            for (DataSource src : sourceDataSet.dataSources) {
+                if (a == null || !a.contains(src.bounds.asRect())) {
+                    targetDataSet.dataSources.add(src);
+                    addedDataSources.add(src);
+                }
+            }
+
+            otherVersion = targetDataSet.getVersion();
+            // copy the merged layer's API version, downgrade if required
+            if (targetDataSet.getVersion() == null) {
+                targetDataSet.setVersion(sourceDataSet.getVersion());
+            } else if ("0.5".equals(targetDataSet.getVersion()) ^ "0.5".equals(sourceDataSet.getVersion())) {
+                System.err.println(tr("Warning: mixing 0.6 and 0.5 data results in version 0.5"));
+                targetDataSet.setVersion("0.5");
+            }
+
+
+            // FIXME: allow conflicts to be retrieved rather than added to layer?
+            if (targetLayer != null) {
+                for (Conflict<?> c : merger.getConflicts()) {
+                    if (!targetLayer.getConflicts().hasConflict(c)) {
+                        targetLayer.getConflicts().add(c);
+                        addedConflicts.add(c);
+                    }
+                }
+            }
+        } else {
+            // command is being "redone"
+            
+            merger.remerge();
+            targetDataSet.dataSources.addAll(addedDataSources);
+
+            String version = otherVersion;
+            otherVersion = targetDataSet.getVersion();
+            targetDataSet.setVersion(version);
+
+            for (Conflict c : addedConflicts) {
+                targetLayer.getConflicts().add(c);
+            }
+        }
+        
+        if (addedConflicts.size() > 0) {
+            targetLayer.warnNumNewConflicts(addedConflicts.size());
+        }
+        
+        // repaint to make sure new data is displayed properly.
+        Main.map.mapView.repaint();
+        
+        monitor.close();
+        
+        return true;
+    }
+
+    @Override
+    public void undoCommand() {
+        merger.unmerge();
+
+        // restore data source area
+        targetDataSet.dataSources.removeAll(addedDataSources);
+
+        String version = otherVersion;
+        otherVersion = targetDataSet.getVersion();
+        targetDataSet.setVersion(version);
+
+        for (Conflict c : addedConflicts) {
+            targetLayer.getConflicts().remove(c);
+        }
+
+        Main.map.mapView.repaint();
+    }
+
+    @Override
+    public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    @Override
+    public String getDescriptionText() {
+        return tr(marktr("Merge objects, {0} added, {1} modified"),
+                merger.getAddedObjects().size(),
+                merger.getChangedObjectsMap().size());
+    }
+
+    @Override
+    public Icon getDescriptionIcon() {
+        return ImageProvider.get("dialogs", "mergedown");
+    }
+}
\ No newline at end of file
