diff --git a/src/Umbraco.Core/Migrations/MergeBuilder.cs b/src/Umbraco.Core/Migrations/MergeBuilder.cs
new file mode 100644
index 0000000000..f1eeea9dfa
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/MergeBuilder.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+
+namespace Umbraco.Core.Migrations
+{
+ ///
+ /// Represents a migration plan builder for merges.
+ ///
+ public class MergeBuilder
+ {
+ private readonly MigrationPlan _plan;
+ private readonly List _migrations = new List();
+ private string _withLast;
+ private bool _with;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal MergeBuilder(MigrationPlan plan)
+ {
+ _plan = plan;
+ }
+
+ ///
+ /// Adds a transition to a target state through an empty migration.
+ ///
+ public MergeBuilder To(string targetState)
+ => To(targetState);
+
+ ///
+ /// Adds a transition to a target state through a migration.
+ ///
+ public MergeBuilder To(string targetState)
+ where TMigration : IMigration
+ => To(targetState, typeof(TMigration));
+
+ ///
+ /// Adds a transition to a target state through a migration.
+ ///
+ public MergeBuilder To(string targetState, Type migration)
+ {
+ if (_with)
+ {
+ _withLast = targetState;
+ targetState = _plan.CreateRandomState();
+ }
+ else
+ {
+ _migrations.Add(migration);
+ }
+
+ _plan.To(targetState, migration);
+ return this;
+ }
+
+ ///
+ /// Begins the second branch of the merge.
+ ///
+ public MergeBuilder With()
+ {
+ if (_with)
+ throw new InvalidOperationException("Cannot invoke With() twice.");
+ _with = true;
+ return this;
+ }
+
+ ///
+ /// Completes the merge.
+ ///
+ public MigrationPlan As(string targetState)
+ {
+ if (!_with)
+ throw new InvalidOperationException("Cannot invoke As() without invoking With() first.");
+
+ // reach final state
+ _plan.To(targetState);
+
+ // restart at former end of branch2
+ _plan.From(_withLast);
+ // and replay all branch1 migrations
+ foreach (var migration in _migrations)
+ _plan.To(_plan.CreateRandomState(), migration);
+ // reaching final state
+ _plan.To(targetState);
+
+ return _plan;
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/MigrationPlan.cs b/src/Umbraco.Core/Migrations/MigrationPlan.cs
index 2e2bc8b661..85d9c1d2cc 100644
--- a/src/Umbraco.Core/Migrations/MigrationPlan.cs
+++ b/src/Umbraco.Core/Migrations/MigrationPlan.cs
@@ -72,8 +72,8 @@ namespace Umbraco.Core.Migrations
}
///
- /// Adds a transition to a target state through an empty migration.
- ///
+ /// Adds a transition to a target state through an empty migration.
+ ///
public MigrationPlan To(string targetState)
=> To(targetState);
@@ -100,9 +100,39 @@ namespace Umbraco.Core.Migrations
}
///
- /// Adds transitions to a target state by copying transitions from a start state to an end state.
+ /// Adds a transition to a target state through a migration, replacing a previous migration.
///
- public MigrationPlan To(string targetState, string startState, string endState)
+ /// The new migration.
+ /// The migration to use to recover from the previous target state.
+ /// The previous target state, which we need to recover from through .
+ /// The new target state.
+ public MigrationPlan ToWithReplace(string recoverState, string targetState)
+ where TMigrationNew: IMigration
+ where TMigrationRecover : IMigration
+ {
+ To(targetState);
+ From(recoverState).To(targetState);
+ return this;
+ }
+
+ ///
+ /// Adds a transition to a target state through a migration, replacing a previous migration.
+ ///
+ /// The new migration.
+ /// The previous target state, which we can recover from directly.
+ /// The new target state.
+ public MigrationPlan ToWithReplace(string recoverState, string targetState)
+ where TMigrationNew : IMigration
+ {
+ To(targetState);
+ From(recoverState).To(targetState);
+ return this;
+ }
+
+ ///
+ /// Adds transitions to a target state by cloning transitions from a start state to an end state.
+ ///
+ public MigrationPlan ToWithClone(string startState, string endState, string targetState)
{
if (string.IsNullOrWhiteSpace(startState)) throw new ArgumentNullOrEmptyException(nameof(startState));
if (string.IsNullOrWhiteSpace(endState)) throw new ArgumentNullOrEmptyException(nameof(endState));
@@ -128,7 +158,7 @@ namespace Umbraco.Core.Migrations
var newTargetState = transition.TargetState == endState
? targetState
- : Guid.NewGuid().ToString("B").ToUpper();
+ : CreateRandomState();
To(newTargetState, transition.MigrationType);
state = transition.TargetState;
}
@@ -136,6 +166,17 @@ namespace Umbraco.Core.Migrations
return this;
}
+ ///
+ /// Creates a random, unique state.
+ ///
+ public virtual string CreateRandomState()
+ => Guid.NewGuid().ToString("B").ToUpper();
+
+ ///
+ /// Begins a merge.
+ ///
+ public MergeBuilder Merge() => new MergeBuilder(this);
+
///
/// Gets the initial state.
///
@@ -257,13 +298,14 @@ namespace Umbraco.Core.Migrations
/// Follows a path (for tests and debugging).
///
/// Does the same thing Execute does, but does not actually execute migrations.
- internal string FollowPath(string fromState, string toState = null)
+ internal IReadOnlyList FollowPath(string fromState = null, string toState = null)
{
toState = toState.NullOrWhiteSpaceAsNull();
Validate();
var origState = fromState ?? string.Empty;
+ var states = new List { origState };
if (!_transitions.TryGetValue(origState, out var transition))
throw new Exception($"Unknown state \"{origState}\".");
@@ -272,6 +314,7 @@ namespace Umbraco.Core.Migrations
{
var nextState = transition.TargetState;
origState = nextState;
+ states.Add(origState);
if (nextState == toState)
{
@@ -287,7 +330,7 @@ namespace Umbraco.Core.Migrations
if (origState != (toState ?? _finalState))
throw new Exception($"Internal error, reached state {origState} which is not state {toState ?? _finalState}");
- return origState;
+ return states;
}
///
diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
index 4ef4df926c..f7e1ee9921 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
@@ -89,55 +89,27 @@ namespace Umbraco.Core.Migrations.Upgrade
To("{9DF05B77-11D1-475C-A00A-B656AF7E0908}");
To("{6FE3EF34-44A0-4992-B379-B40BC4EF1C4D}");
To("{7F59355A-0EC9-4438-8157-EB517E6D2727}");
- //To("{941B2ABA-2D06-4E04-81F5-74224F1DB037}"); // AddVariationTables1 has been superseded by AddVariationTables2
- To("{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}");
-
- // way out of the commented state
- From("{941B2ABA-2D06-4E04-81F5-74224F1DB037}");
- To("{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}");
- // resume at {76DF5CD7-A884-41A5-8DC6-7860D95B1DF5} ...
-
+ ToWithReplace("{941B2ABA-2D06-4E04-81F5-74224F1DB037}", "{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); // kill AddVariationTable1
To("{A7540C58-171D-462A-91C5-7A9AA5CB8BFD}");
- To("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}"); // shannon added that one - let's keep it as the default path
- //To("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}"); // stephan added that one = merge conflict, remove
- To("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // but add it after shannon's, with a new target state
- // way out of the commented state
- From("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}");
- To("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}");
- // resume at {4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4} ...
+ Merge().To("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}") // shannon added that one
+ .With().To("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}") // stephan added that one
+ .As("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}");
To("{1350617A-4930-4D61-852F-E3AA9E692173}");
To("{39E5B1F7-A50B-437E-B768-1723AEC45B65}"); // from 7.12.0
- //To("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); // andy added that one = merge conflict, remove
- To("{0541A62B-EF87-4CA2-8225-F0EB98ECCC9F}"); // from 7.12.0
- To("{EB34B5DC-BB87-4005-985E-D983EA496C38}"); // from 7.12.0
- To("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}"); // from 7.12.0
- To("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); // from 7.12.0
- //To("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}"); // stephan added that one = merge conflict, remove
- To("{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // add andy's after others, with a new target state
-
- // way out of andy's
- From("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}");
- To("{8B14CEBD-EE47-4AAD-A841-93551D917F11}", "{39E5B1F7-A50B-437E-B768-1723AEC45B65}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}");
- // resume at {8B14CEBD-EE47-4AAD-A841-93551D917F11} ...
-
- To("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // add stephan's after others, with a new target state
-
- // way out of the commented state
- From("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}");
- To("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // to next
- // resume at {5F4597F4-A4E0-4AFE-90B5-6D2F896830EB} ...
-
- //To("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}");
- To("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}");
-
- // way out of the commented state
- From("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}");
- To("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}");
- // resume at {290C18EE-B3DE-4769-84F1-1F467F3F76DA}...
+ Merge()
+ .To("{0541A62B-EF87-4CA2-8225-F0EB98ECCC9F}") // from 7.12.0
+ .To("{EB34B5DC-BB87-4005-985E-D983EA496C38}") // from 7.12.0
+ .To("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}") // from 7.12.0
+ .To("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}") // from 7.12.0
+ .With()
+ .To("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}") // andy added that one
+ .As("{8B14CEBD-EE47-4AAD-A841-93551D917F11}");
+ ToWithReplace("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}", "{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // merge
+ ToWithReplace("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}", "{290C18EE-B3DE-4769-84F1-1F467F3F76DA}"); // merge
To("{6A2C7C1B-A9DB-4EA9-B6AB-78E7D5B722A7}");
To("{77874C77-93E5-4488-A404-A630907CEEF0}");
To("{8804D8E8-FE62-4E3A-B8A2-C047C2118C38}");
@@ -159,16 +131,22 @@ namespace Umbraco.Core.Migrations.Upgrade
From("{init-7.10.2}").To("{init-7.10.0}"); // same as 7.10.0
From("{init-7.10.3}").To("{init-7.10.0}"); // same as 7.10.0
From("{init-7.10.4}").To("{init-7.10.0}"); // same as 7.10.0
+ From("{init-7.10.5}").To("{init-7.10.0}"); // same as 7.10.0
From("{init-7.11.0}").To("{init-7.10.0}"); // same as 7.10.0
From("{init-7.11.1}").To("{init-7.10.0}"); // same as 7.10.0
+ From("{init-7.11.2}").To("{init-7.10.0}"); // same as 7.10.0
// 7.12.0 has migrations, define a custom chain which copies the chain
// going from {init-7.10.0} to former final (1350617A) , and then goes straight to
// main chain, skipping the migrations
//
From("{init-7.12.0}");
- // target copy from copy to (former final)
- To("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}", "{init-7.10.0}", "{1350617A-4930-4D61-852F-E3AA9E692173}");
+ // start stop target
+ ToWithClone("{init-7.10.0}", "{1350617A-4930-4D61-852F-E3AA9E692173}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}");
+
+ From("{init-7.12.1}").To("{init-7.10.0}"); // same as 7.12.0
+ From("{init-7.12.2}").To("{init-7.10.0}"); // same as 7.12.0
+ From("{init-7.12.3}").To("{init-7.10.0}"); // same as 7.12.0
}
}
}
diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj
index ba0cf97bf7..8ef76ade37 100755
--- a/src/Umbraco.Core/Umbraco.Core.csproj
+++ b/src/Umbraco.Core/Umbraco.Core.csproj
@@ -339,6 +339,7 @@
+
diff --git a/src/Umbraco.Tests/Migrations/MigrationPlanTests.cs b/src/Umbraco.Tests/Migrations/MigrationPlanTests.cs
index 9aec42c252..add278f9eb 100644
--- a/src/Umbraco.Tests/Migrations/MigrationPlanTests.cs
+++ b/src/Umbraco.Tests/Migrations/MigrationPlanTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using Moq;
using NPoco;
@@ -68,7 +69,6 @@ namespace Umbraco.Tests.Migrations
// save new state
kvs.SetValue("Umbraco.Tests.MigrationPlan", sourceState, state);
- // fixme - what about post-migrations?
s.Complete();
}
@@ -140,11 +140,11 @@ namespace Umbraco.Tests.Migrations
var plan = new UmbracoPlan();
plan.Validate();
Console.WriteLine(plan.FinalState);
- Assert.IsFalse(string.IsNullOrWhiteSpace(plan.FinalState));
+ Assert.IsFalse(plan.FinalState.IsNullOrWhiteSpace());
}
[Test]
- public void CanCopyChain()
+ public void CanClone()
{
var plan = new MigrationPlan("default");
plan
@@ -157,14 +157,46 @@ namespace Umbraco.Tests.Migrations
plan
.From("xxx")
- .To("yyy", "bbb", "ddd")
+ .ToWithClone("bbb", "ddd", "yyy")
.To("eee");
WritePlanToConsole(plan);
plan.Validate();
- Assert.AreEqual("eee", plan.FollowPath("xxx"));
- Assert.AreEqual("yyy", plan.FollowPath("xxx", "yyy"));
+ Assert.AreEqual("eee", plan.FollowPath("xxx").Last());
+ Assert.AreEqual("yyy", plan.FollowPath("xxx", "yyy").Last());
+ }
+
+ [Test]
+ public void CanMerge()
+ {
+ var plan = new MigrationPlan("default");
+ plan
+ .From(string.Empty)
+ .To("aaa")
+ .Merge()
+ .To("bbb")
+ .To("ccc")
+ .With()
+ .To("ddd")
+ .To("eee")
+ .As("fff")
+ .To("ggg");
+
+ WritePlanToConsole(plan);
+
+ plan.Validate();
+ AssertList(plan.FollowPath(), "", "aaa", "bbb", "ccc", "*", "*", "fff", "ggg");
+ AssertList(plan.FollowPath("ccc"), "ccc", "*", "*", "fff", "ggg");
+ AssertList(plan.FollowPath("eee"), "eee", "*", "*", "fff", "ggg");
+ }
+
+ private void AssertList(IReadOnlyList states, params string[] expected)
+ {
+ Assert.AreEqual(expected.Length, states.Count, string.Join(", ", states));
+ for (var i = 0; i < expected.Length; i++)
+ if (expected[i] != "*")
+ Assert.AreEqual(expected[i], states[i], "at:" + i);
}
private void WritePlanToConsole(MigrationPlan plan)