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)