diff --git a/src/org/openstreetmap/josm/actions/RepeatAction.java b/src/org/openstreetmap/josm/actions/RepeatAction.java
new file mode 100644
index 0000000..e4ddd74
--- /dev/null
+++ b/src/org/openstreetmap/josm/actions/RepeatAction.java
@@ -0,0 +1,69 @@
+package org.openstreetmap.josm.actions;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.command.RepeatableCommand;
+import org.openstreetmap.josm.data.SelectionChangedListener;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.tools.Shortcut;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.util.Collection;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+public class RepeatAction extends JosmAction implements OsmDataLayer.CommandQueueListener, SelectionChangedListener {
+
+    public RepeatAction() {
+        super(tr("Repeat"), null, tr("Repeat the last action."),
+                Shortcut.registerShortcut("system:repeat", tr("Edit: {0}", tr("Repeat")), KeyEvent.VK_PERIOD, Shortcut.ALT_CTRL), true);
+        setEnabled(false);
+        DataSet.addSelectionListener(this);
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        if (Main.map != null) {
+            Main.map.repaint();
+            Main.main.undoRedo.add(getRepeatableCommand().repeatableFor(Main.main.getCurrentDataSet().getSelected()));
+        }
+    }
+
+    @Override
+    protected void updateEnabledState() {
+        setEnabled(getRepeatableCommand() != null
+                && Main.main.getCurrentDataSet() != null
+                && !Main.main.getCurrentDataSet().getSelected().isEmpty()
+                && getRepeatableCommand().isRepeatableFor(Main.main.getCurrentDataSet().getSelected()));
+        if (getRepeatableCommand() == null) {
+            putValue(NAME, tr("Repeat"));
+            setTooltip(tr("Repeat the last action."));
+        } else {
+            putValue(NAME, tr("Repeat ..."));
+            setTooltip(tr("Repeat {0}", getRepeatableCommand().getDescriptionText()));
+        }
+    }
+
+    @Override
+    public void commandChanged(int queueSize, int redoSize) {
+        updateEnabledState();
+    }
+
+    @Override
+    public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
+        updateEnabledState();
+    }
+
+    protected RepeatableCommand getRepeatableCommand() {
+        if (Main.main != null
+                && !Main.main.undoRedo.commands.isEmpty()
+                && Main.main.undoRedo.commands.getLast() instanceof RepeatableCommand) {
+            return (RepeatableCommand) Main.main.undoRedo.commands.getLast();
+        } else {
+            return null;
+        }
+    }
+}
+
diff --git a/src/org/openstreetmap/josm/command/ChangePropertyCommand.java b/src/org/openstreetmap/josm/command/ChangePropertyCommand.java
index 8a3ac10..a106d8f 100644
--- a/src/org/openstreetmap/josm/command/ChangePropertyCommand.java
+++ b/src/org/openstreetmap/josm/command/ChangePropertyCommand.java
@@ -28,7 +28,7 @@ import org.openstreetmap.josm.tools.ImageProvider;
  *
  * @author imi
  */
-public class ChangePropertyCommand extends Command {
+public class ChangePropertyCommand extends RepeatableCommand {
     /**
      * All primitives that are affected with this command.
      */
@@ -213,4 +213,14 @@ public class ChangePropertyCommand extends Command {
         }
         return children;
     }
+
+    @Override
+    public boolean isRepeatableFor(Collection<OsmPrimitive> primitives) {
+        return true;
+    }
+
+    @Override
+    public RepeatableCommand repeatableFor(Collection<OsmPrimitive> primitives) {
+        return new ChangePropertyCommand(primitives, tags);
+    }
 }
diff --git a/src/org/openstreetmap/josm/command/RepeatableCommand.java b/src/org/openstreetmap/josm/command/RepeatableCommand.java
new file mode 100644
index 0000000..a6f1029
--- /dev/null
+++ b/src/org/openstreetmap/josm/command/RepeatableCommand.java
@@ -0,0 +1,39 @@
+package org.openstreetmap.josm.command;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.tools.Predicate;
+import org.openstreetmap.josm.tools.Utils;
+
+import java.util.Collection;
+
+public abstract class RepeatableCommand extends Command {
+    public RepeatableCommand() {
+    }
+
+    public RepeatableCommand(OsmDataLayer layer) throws IllegalArgumentException {
+        super(layer);
+    }
+
+    public abstract boolean isRepeatableFor(final Collection<OsmPrimitive> primitives);
+
+    public abstract RepeatableCommand repeatableFor(final Collection<OsmPrimitive> primitives);
+
+    public static Predicate<Command> getIsRepeatableForPredicate(final Collection<OsmPrimitive> primitives) {
+        return new Predicate<Command>() {
+            @Override
+            public boolean evaluate(Command c) {
+                return c instanceof RepeatableCommand && ((RepeatableCommand) c).isRepeatableFor(primitives);
+            }
+        };
+    }
+
+    public static Utils.Function<RepeatableCommand, RepeatableCommand> getRepeatForFunction(final Collection<OsmPrimitive> primitives) {
+        return new Utils.Function<RepeatableCommand, RepeatableCommand>() {
+            @Override
+            public RepeatableCommand apply(RepeatableCommand x) {
+                return x.repeatableFor(primitives);
+            }
+        };
+    }
+}
diff --git a/src/org/openstreetmap/josm/command/SequenceCommand.java b/src/org/openstreetmap/josm/command/SequenceCommand.java
index f172f22..2ae6b2a 100644
--- a/src/org/openstreetmap/josm/command/SequenceCommand.java
+++ b/src/org/openstreetmap/josm/command/SequenceCommand.java
@@ -6,11 +6,14 @@ import static org.openstreetmap.josm.tools.I18n.tr;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.List;
 
 import javax.swing.Icon;
 
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.Predicate;
+import org.openstreetmap.josm.tools.Utils;
 
 /**
  * A command consisting of a sequence of other commands. Executes the other commands
@@ -18,7 +21,7 @@ import org.openstreetmap.josm.tools.ImageProvider;
  * @author imi
  * @since 31
  */
-public class SequenceCommand extends Command {
+public class SequenceCommand extends RepeatableCommand {
 
     /** The command sequence to be executed. */
     private Command[] sequence;
@@ -32,7 +35,7 @@ public class SequenceCommand extends Command {
      * @param name The description text
      * @param sequenz The sequence that should be executed.
      */
-    public SequenceCommand(String name, Collection<Command> sequenz) {
+    public SequenceCommand(String name, Collection<? extends Command> sequenz) {
         super();
         this.name = name;
         this.sequence = sequenz.toArray(new Command[sequenz.size()]);
@@ -121,4 +124,16 @@ public class SequenceCommand extends Command {
     protected final void setSequenceComplete(boolean sequenceComplete) {
         this.sequenceComplete = sequenceComplete;
     }
+
+    @Override
+    public boolean isRepeatableFor(final Collection<OsmPrimitive> primitives) {
+        return Utils.forAll(Arrays.asList(sequence), getIsRepeatableForPredicate(primitives));
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public RepeatableCommand repeatableFor(final Collection<OsmPrimitive> primitives) {
+        final List commands = Arrays.asList(sequence);
+        return new SequenceCommand(name, Utils.transform(commands, getRepeatForFunction(primitives)));
+    }
 }
diff --git a/src/org/openstreetmap/josm/gui/MainMenu.java b/src/org/openstreetmap/josm/gui/MainMenu.java
index a4d6653..886dcff 100644
--- a/src/org/openstreetmap/josm/gui/MainMenu.java
+++ b/src/org/openstreetmap/josm/gui/MainMenu.java
@@ -22,80 +22,8 @@ import javax.swing.event.MenuEvent;
 import javax.swing.event.MenuListener;
 
 import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.actions.AboutAction;
-import org.openstreetmap.josm.actions.AddNodeAction;
-import org.openstreetmap.josm.actions.AlignInCircleAction;
-import org.openstreetmap.josm.actions.AlignInLineAction;
-import org.openstreetmap.josm.actions.AutoScaleAction;
-import org.openstreetmap.josm.actions.ChangesetManagerToggleAction;
-import org.openstreetmap.josm.actions.CloseChangesetAction;
-import org.openstreetmap.josm.actions.CombineWayAction;
-import org.openstreetmap.josm.actions.CopyAction;
-import org.openstreetmap.josm.actions.CopyCoordinatesAction;
-import org.openstreetmap.josm.actions.CreateCircleAction;
-import org.openstreetmap.josm.actions.CreateMultipolygonAction;
-import org.openstreetmap.josm.actions.DeleteAction;
-import org.openstreetmap.josm.actions.DialogsToggleAction;
-import org.openstreetmap.josm.actions.DistributeAction;
-import org.openstreetmap.josm.actions.DownloadAction;
-import org.openstreetmap.josm.actions.DownloadPrimitiveAction;
-import org.openstreetmap.josm.actions.DownloadReferrersAction;
-import org.openstreetmap.josm.actions.DuplicateAction;
-import org.openstreetmap.josm.actions.ExitAction;
-import org.openstreetmap.josm.actions.ExpertToggleAction;
-import org.openstreetmap.josm.actions.FollowLineAction;
-import org.openstreetmap.josm.actions.FullscreenToggleAction;
-import org.openstreetmap.josm.actions.GpxExportAction;
-import org.openstreetmap.josm.actions.HelpAction;
-import org.openstreetmap.josm.actions.HistoryInfoAction;
-import org.openstreetmap.josm.actions.HistoryInfoWebAction;
-import org.openstreetmap.josm.actions.InfoAction;
-import org.openstreetmap.josm.actions.InfoWebAction;
-import org.openstreetmap.josm.actions.JoinAreasAction;
-import org.openstreetmap.josm.actions.JoinNodeWayAction;
-import org.openstreetmap.josm.actions.JosmAction;
-import org.openstreetmap.josm.actions.JumpToAction;
-import org.openstreetmap.josm.actions.MergeLayerAction;
-import org.openstreetmap.josm.actions.MergeNodesAction;
-import org.openstreetmap.josm.actions.MergeSelectionAction;
-import org.openstreetmap.josm.actions.MirrorAction;
-import org.openstreetmap.josm.actions.MoveAction;
-import org.openstreetmap.josm.actions.MoveNodeAction;
-import org.openstreetmap.josm.actions.NewAction;
-import org.openstreetmap.josm.actions.OpenFileAction;
-import org.openstreetmap.josm.actions.OpenLocationAction;
-import org.openstreetmap.josm.actions.OrthogonalizeAction;
+import org.openstreetmap.josm.actions.*;
 import org.openstreetmap.josm.actions.OrthogonalizeAction.Undo;
-import org.openstreetmap.josm.actions.PasteAction;
-import org.openstreetmap.josm.actions.PasteTagsAction;
-import org.openstreetmap.josm.actions.PreferenceToggleAction;
-import org.openstreetmap.josm.actions.PreferencesAction;
-import org.openstreetmap.josm.actions.PurgeAction;
-import org.openstreetmap.josm.actions.RedoAction;
-import org.openstreetmap.josm.actions.RestartAction;
-import org.openstreetmap.josm.actions.ReverseWayAction;
-import org.openstreetmap.josm.actions.SaveAction;
-import org.openstreetmap.josm.actions.SaveAsAction;
-import org.openstreetmap.josm.actions.SelectAllAction;
-import org.openstreetmap.josm.actions.SessionLoadAction;
-import org.openstreetmap.josm.actions.SessionSaveAsAction;
-import org.openstreetmap.josm.actions.ShowStatusReportAction;
-import org.openstreetmap.josm.actions.SimplifyWayAction;
-import org.openstreetmap.josm.actions.SplitWayAction;
-import org.openstreetmap.josm.actions.ToggleGPXLinesAction;
-import org.openstreetmap.josm.actions.UnGlueAction;
-import org.openstreetmap.josm.actions.UnJoinNodeWayAction;
-import org.openstreetmap.josm.actions.UndoAction;
-import org.openstreetmap.josm.actions.UnselectAllAction;
-import org.openstreetmap.josm.actions.UpdateDataAction;
-import org.openstreetmap.josm.actions.UpdateModifiedAction;
-import org.openstreetmap.josm.actions.UpdateSelectionAction;
-import org.openstreetmap.josm.actions.UploadAction;
-import org.openstreetmap.josm.actions.UploadSelectionAction;
-import org.openstreetmap.josm.actions.ViewportFollowToggleAction;
-import org.openstreetmap.josm.actions.WireframeToggleAction;
-import org.openstreetmap.josm.actions.ZoomInAction;
-import org.openstreetmap.josm.actions.ZoomOutAction;
 import org.openstreetmap.josm.actions.audio.AudioBackAction;
 import org.openstreetmap.josm.actions.audio.AudioFasterAction;
 import org.openstreetmap.josm.actions.audio.AudioFwdAction;
@@ -172,6 +100,8 @@ public class MainMenu extends JMenuBar {
     public final UndoAction undo = new UndoAction();
     /** Edit -> Redo */
     public final RedoAction redo = new RedoAction();
+    /** Edit -> Repeat */
+    public final RepeatAction repeat = new RepeatAction();
     /** Edit -> Copy */
     public final CopyAction copy = new CopyAction();
     /** Edit -> Copy Coordinates */
@@ -621,6 +551,8 @@ public class MainMenu extends JMenuBar {
         Main.main.undoRedo.addCommandQueueListener(undo);
         add(editMenu, redo);
         Main.main.undoRedo.addCommandQueueListener(redo);
+        add(editMenu, repeat);
+        Main.main.undoRedo.addCommandQueueListener(repeat);
         editMenu.addSeparator();
         add(editMenu, copy);
         add(editMenu, copyCoordinates, true);
diff --git a/src/org/openstreetmap/josm/tools/Utils.java b/src/org/openstreetmap/josm/tools/Utils.java
index 48149d6..92ff0cc 100644
--- a/src/org/openstreetmap/josm/tools/Utils.java
+++ b/src/org/openstreetmap/josm/tools/Utils.java
@@ -100,6 +100,19 @@ public final class Utils {
         return null;
     }
 
+    public static <T> Predicate<T> not(final Predicate<T> predicate) {
+        return new Predicate<T>() {
+            @Override
+            public boolean evaluate(T object) {
+                return !predicate.evaluate(object);
+            }
+        };
+    }
+
+    public static <T> boolean forAll(Iterable<? extends T> collection, Predicate<? super T> predicate) {
+        return !exists(collection, not(predicate));
+    }
+
     /**
      * Filter a collection by (sub)class.
      * This is an efficient read-only implementation.
