From dae0a93cb721dabec125b5a38fe23bb65d55ae74 Mon Sep 17 00:00:00 2001 From: Justin Swanson Date: Mon, 14 Sep 2020 23:56:02 -0500 Subject: [PATCH 01/23] Moved version property to trickledown --- Directory.Build.props | 1 + Mutagen.Bethesda.Synthesis/Mutagen.Bethesda.Synthesis.csproj | 1 - Synthesis.Bethesda.CLI/Synthesis.Bethesda.CLI.csproj | 1 - .../Synthesis.Bethesda.Execution.csproj | 1 - Synthesis.Bethesda/Synthesis.Bethesda.csproj | 3 +-- 5 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 29a4a6ae..3836a9b3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,5 +2,6 @@ enable nullable + 0.7.0.0 diff --git a/Mutagen.Bethesda.Synthesis/Mutagen.Bethesda.Synthesis.csproj b/Mutagen.Bethesda.Synthesis/Mutagen.Bethesda.Synthesis.csproj index 260ac254..4158a557 100644 --- a/Mutagen.Bethesda.Synthesis/Mutagen.Bethesda.Synthesis.csproj +++ b/Mutagen.Bethesda.Synthesis/Mutagen.Bethesda.Synthesis.csproj @@ -12,7 +12,6 @@ https://github.com/Noggog/Synthesis GPL-3.0-only Mutagen.Bethesda.Synthesis - 0.7.0.0 x64 diff --git a/Synthesis.Bethesda.CLI/Synthesis.Bethesda.CLI.csproj b/Synthesis.Bethesda.CLI/Synthesis.Bethesda.CLI.csproj index 9a6982ec..b9f86e7a 100644 --- a/Synthesis.Bethesda.CLI/Synthesis.Bethesda.CLI.csproj +++ b/Synthesis.Bethesda.CLI/Synthesis.Bethesda.CLI.csproj @@ -8,7 +8,6 @@ Synthesis A lightweight wrapper CLI to run Synthesis pipelines without a GUI x64 - 0.7.0.0 diff --git a/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj b/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj index 2b98db69..1fe363e7 100644 --- a/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj +++ b/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj @@ -12,7 +12,6 @@ Mutagen Synthesis Synthesis.Bethesda.Execution - 0.7.0.0 x64 diff --git a/Synthesis.Bethesda/Synthesis.Bethesda.csproj b/Synthesis.Bethesda/Synthesis.Bethesda.csproj index 6f36c3c9..1b65552b 100644 --- a/Synthesis.Bethesda/Synthesis.Bethesda.csproj +++ b/Synthesis.Bethesda/Synthesis.Bethesda.csproj @@ -1,4 +1,4 @@ - + netstandard2.1 @@ -12,7 +12,6 @@ https://github.com/Noggog/Synthesis GPL-3.0-only Synthesis.Bethesda - 0.7.0.0 x64 From 9e3f45a2f8446688c89b1b083a1164aeadce0406 Mon Sep 17 00:00:00 2001 From: Justin Swanson Date: Tue, 15 Sep 2020 17:12:22 -0500 Subject: [PATCH 02/23] Migrated the path to exe logic from Runner to VM Since it currently cleans the build folder, it's rough to have at the prep stage of a run --- .../Patcher Runs/SolutionPatcherRun.cs | 24 +---- .../Synthesis.Bethesda.Execution.csproj | 4 +- .../Synthesis.Bethesda.GUI.csproj | 6 +- .../ViewModels/Config/ConfigurationStateVM.cs | 11 ++- .../ViewModels/Config/PatcherVM.cs | 2 +- .../ViewModels/Config/SolutionPatcherVM.cs | 87 ++++++++++++++++--- .../Config/PatcherConfigListingView.xaml.cs | 3 + 7 files changed, 99 insertions(+), 38 deletions(-) diff --git a/Synthesis.Bethesda.Execution/Patcher Runs/SolutionPatcherRun.cs b/Synthesis.Bethesda.Execution/Patcher Runs/SolutionPatcherRun.cs index f4faa4e7..3db1b97c 100644 --- a/Synthesis.Bethesda.Execution/Patcher Runs/SolutionPatcherRun.cs +++ b/Synthesis.Bethesda.Execution/Patcher Runs/SolutionPatcherRun.cs @@ -1,5 +1,3 @@ -using Buildalyzer; -using Buildalyzer.Environment; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Emit; @@ -26,6 +24,7 @@ public class SolutionPatcherRun : IPatcherRun public string Name { get; } public string PathToSolution { get; } public string PathToProject { get; } + public string PathToExe { get; } public CliPatcherRun? CliRun { get; private set; } private Subject _output = new Subject(); @@ -34,32 +33,17 @@ public class SolutionPatcherRun : IPatcherRun private Subject _error = new Subject(); public IObservable Error => _error; - public SolutionPatcherRun(string nickname, string pathToSln, string pathToProj) + public SolutionPatcherRun(string nickname, string pathToSln, string pathToProj, string pathToExe) { PathToSolution = pathToSln; PathToProject = pathToProj; + PathToExe = pathToExe; Name = $"{Path.GetFileNameWithoutExtension(pathToSln)} => {Path.GetFileNameWithoutExtension(pathToProj)}"; } public async Task Prep(GameRelease release, CancellationToken? cancel = null) { - // Right now this is slow as it cleans the build results unnecessarily. Need to look into that - var manager = new AnalyzerManager(); - var proj = manager.GetProject(PathToProject); - var opt = new EnvironmentOptions(); - opt.TargetsToBuild.SetTo("Build"); - var build = proj.Build(); - var results = build.Results.ToArray(); - if (results.Length != 1) - { - throw new SynthesisBuildFailure("Unsupported number of build results."); - } - var result = results[0]; - if (!result.Properties.TryGetValue("RunCommand", out var cmd)) - { - throw new SynthesisBuildFailure("Could not find executable to be run"); - } - CliRun = new CliPatcherRun(nickname: Name, pathToExecutable: cmd); + CliRun = new CliPatcherRun(nickname: Name, pathToExecutable: PathToExe); var resp = await CompileWithDotnet(PathToSolution, cancel ?? CancellationToken.None).ConfigureAwait(false); if (!resp.Succeeded) diff --git a/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj b/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj index 1fe363e7..a57a897f 100644 --- a/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj +++ b/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj @@ -22,7 +22,7 @@ - + @@ -30,7 +30,7 @@ - + diff --git a/Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj b/Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj index c7fdbd37..a7290666 100644 --- a/Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj +++ b/Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj @@ -1,4 +1,4 @@ - + WinExe @@ -37,9 +37,9 @@ - + - + diff --git a/Synthesis.Bethesda.GUI/ViewModels/Config/ConfigurationStateVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Config/ConfigurationStateVM.cs index 6e281627..bcee7bdc 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Config/ConfigurationStateVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Config/ConfigurationStateVM.cs @@ -1,4 +1,4 @@ -using Noggog; +using Noggog; using Noggog.WPF; using System; using System.Collections.Generic; @@ -12,5 +12,14 @@ public class ConfigurationStateVM : ViewModel public bool IsHaltingError { get; set; } public ErrorResponse RunnableState { get; set; } = ErrorResponse.Success; + + public static implicit operator ConfigurationStateVM(ErrorResponse err) + { + return new ConfigurationStateVM() + { + IsHaltingError = err.Failed, + RunnableState = err, + }; + } } } diff --git a/Synthesis.Bethesda.GUI/ViewModels/Config/PatcherVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Config/PatcherVM.cs index 8d75852a..183ddcd5 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Config/PatcherVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Config/PatcherVM.cs @@ -1,4 +1,4 @@ -using DynamicData; +using DynamicData; using Synthesis.Bethesda.Execution.Settings; using Noggog; using Noggog.WPF; diff --git a/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs index 96ad4e39..1e9fb233 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs @@ -18,6 +18,9 @@ using System.Windows.Input; using System.Threading.Tasks; using System.Diagnostics; +using Buildalyzer.Environment; +using System.Reactive; +using Microsoft.Build.Evaluation; namespace Synthesis.Bethesda.GUI { @@ -34,6 +37,9 @@ public class SolutionPatcherVM : PatcherVM [Reactive] public string ProjectSubpath { get; set; } = string.Empty; + private readonly ObservableAsPropertyHelper _PathToExe; + public string PathToExe => _PathToExe.Value; + public PathPickerVM SelectedProjectPath { get; } = new PathPickerVM() { ExistCheckOption = PathPickerVM.CheckOptions.On, @@ -94,7 +100,7 @@ public SolutionPatcherVM(ProfileVM parent, SolutionPatcherSettings? settings = n .ObserveOnGui() .ToObservableCollection(this); - this.WhenAnyValue(x => x.ProjectSubpath) + var projPath = this.WhenAnyValue(x => x.ProjectSubpath) .DistinctUntilChanged() .CombineLatest(this.WhenAnyValue(x => x.SolutionPath.TargetPath) .DistinctUntilChanged(), @@ -110,25 +116,83 @@ public SolutionPatcherVM(ProfileVM parent, SolutionPatcherSettings? settings = n return string.Empty; } }) + .Replay(1) + .RefCount(); + + projPath .Subscribe(p => SelectedProjectPath.TargetPath = p) .DisposeWith(this); + var pathToExe = projPath + .CombineLatest( + this.WhenAnyValue(x => x.SolutionPath.TargetPath), + (ProjectPath, SolutionPath) => (ProjectPath, SolutionPath)) + .ObserveOn(RxApp.TaskpoolScheduler) + .SelectReplace(async (t, cancel) => + { + try + { + cancel.ThrowIfCancellationRequested(); + if (!File.Exists(t.ProjectPath)) + { + return GetResponse.Fail("Project path does not exist."); + } + // Right now this is slow as it cleans the build results unnecessarily. Need to look into that + var manager = new AnalyzerManager(); + cancel.ThrowIfCancellationRequested(); + var proj = manager.GetProject(t.ProjectPath); + cancel.ThrowIfCancellationRequested(); + var opt = new EnvironmentOptions(); + opt.TargetsToBuild.SetTo("Build"); + var build = proj.Build(); + cancel.ThrowIfCancellationRequested(); + var results = build.Results.ToArray(); + if (results.Length != 1) + { + return GetResponse.Fail("Unsupported number of build results."); + } + var result = results[0]; + if (!result.Properties.TryGetValue("RunCommand", out var cmd)) + { + return GetResponse.Fail("Could not find executable to be run"); + } + + // Now we want to build, just to prep for run + var resp = await SolutionPatcherRun.CompileWithDotnet(t.SolutionPath, cancel).ConfigureAwait(false); + if (resp.Failed) return resp.BubbleFailure(); + + return GetResponse.Succeed(cmd); + } + catch (Exception ex) + { + return GetResponse.Fail(ex); + } + }) + .Replay(1) + .RefCount(); + + _PathToExe = pathToExe + .Select(r => r.Value) + .ToGuiProperty(this, nameof(PathToExe)); + _State = Observable.CombineLatest( this.WhenAnyValue(x => x.SolutionPath.ErrorState), this.WhenAnyValue(x => x.SelectedProjectPath.ErrorState), - (sln, proj) => + Observable.Merge( + projPath + .Select(_ => new ConfigurationStateVM() + { + IsHaltingError = false, + RunnableState = ErrorResponse.Fail("Building") + }), + pathToExe + .Select(p => (ConfigurationStateVM)(ErrorResponse)p)), + (sln, proj, exe) => { if (sln.Failed) return sln; + if (exe.RunnableState.Failed) return exe; return proj; }) - .Select(err => - { - return new ConfigurationStateVM() - { - IsHaltingError = err.Failed, - RunnableState = err, - }; - }) .ToGuiProperty(this, nameof(State), ConfigurationStateVM.Success); OpenSolutionCommand = ReactiveCommand.Create( @@ -174,7 +238,8 @@ public override PatcherRunVM ToRunner(PatchersRunVM parent) this, new SolutionPatcherRun( nickname: DisplayName, - pathToSln: SolutionPath.TargetPath, + pathToSln: SolutionPath.TargetPath, + pathToExe: PathToExe, pathToProj: SelectedProjectPath.TargetPath)); } diff --git a/Synthesis.Bethesda.GUI/Views/Config/PatcherConfigListingView.xaml.cs b/Synthesis.Bethesda.GUI/Views/Config/PatcherConfigListingView.xaml.cs index 1ac4ab60..3cee7fa5 100644 --- a/Synthesis.Bethesda.GUI/Views/Config/PatcherConfigListingView.xaml.cs +++ b/Synthesis.Bethesda.GUI/Views/Config/PatcherConfigListingView.xaml.cs @@ -37,6 +37,9 @@ public PatcherConfigListingView() .Select(x => x ? Visibility.Collapsed : Visibility.Visible) .BindToStrict(this, x => x.BlockingIssueDisplayCircle.Visibility) .DisposeWith(disposable); + this.WhenAnyValue(x => x.ViewModel.State.RunnableState.Reason) + .BindToStrict(this, x => x.BlockingIssueDisplayCircle.ToolTip) + .DisposeWith(disposable); // ContextMenu this.WhenAnyValue(x => x.ViewModel.DeleteCommand) From 8fdd200c322df7878a98ca670dc6c50019a7b7e4 Mon Sep 17 00:00:00 2001 From: Justin Swanson Date: Tue, 15 Sep 2020 17:13:58 -0500 Subject: [PATCH 03/23] Marked viable version ranges for parent libraries --- Directory.Build.props | 2 +- .../Mutagen.Bethesda.Synthesis.csproj | 6 +++--- .../Synthesis.Bethesda.Execution.csproj | 6 +++--- .../Synthesis.Bethesda.UnitTests.csproj | 2 +- Synthesis.Bethesda/Synthesis.Bethesda.csproj | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 3836a9b3..933a9d43 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,6 +2,6 @@ enable nullable - 0.7.0.0 + 0.7.1.0-dev diff --git a/Mutagen.Bethesda.Synthesis/Mutagen.Bethesda.Synthesis.csproj b/Mutagen.Bethesda.Synthesis/Mutagen.Bethesda.Synthesis.csproj index 4158a557..6dd6bda3 100644 --- a/Mutagen.Bethesda.Synthesis/Mutagen.Bethesda.Synthesis.csproj +++ b/Mutagen.Bethesda.Synthesis/Mutagen.Bethesda.Synthesis.csproj @@ -1,4 +1,4 @@ - + netstandard2.1 @@ -22,8 +22,8 @@ - - + + diff --git a/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj b/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj index a57a897f..542b9f35 100644 --- a/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj +++ b/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj @@ -1,4 +1,4 @@ - + netstandard2.1 @@ -28,9 +28,9 @@ - + - + diff --git a/Synthesis.Bethesda.UnitTests/Synthesis.Bethesda.UnitTests.csproj b/Synthesis.Bethesda.UnitTests/Synthesis.Bethesda.UnitTests.csproj index 71a12f84..cffd1812 100644 --- a/Synthesis.Bethesda.UnitTests/Synthesis.Bethesda.UnitTests.csproj +++ b/Synthesis.Bethesda.UnitTests/Synthesis.Bethesda.UnitTests.csproj @@ -13,7 +13,7 @@ - + all diff --git a/Synthesis.Bethesda/Synthesis.Bethesda.csproj b/Synthesis.Bethesda/Synthesis.Bethesda.csproj index 1b65552b..92c9f6cc 100644 --- a/Synthesis.Bethesda/Synthesis.Bethesda.csproj +++ b/Synthesis.Bethesda/Synthesis.Bethesda.csproj @@ -1,4 +1,4 @@ - + netstandard2.1 @@ -23,7 +23,7 @@ - + From 22f45cbe4d5df755152f80a013bc4403fcbf46b7 Mon Sep 17 00:00:00 2001 From: Justin Swanson Date: Tue, 15 Sep 2020 21:32:49 -0500 Subject: [PATCH 04/23] Swapped solution builds to only build project --- .../Mutagen.Bethesda.Synthesis.csproj | 4 ++-- .../Patcher Runs/SolutionPatcherRun.cs | 6 +++--- .../Synthesis.Bethesda.Execution.csproj | 4 ++-- Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj | 2 +- .../ViewModels/Config/SolutionPatcherVM.cs | 11 ++++------- .../Synthesis.Bethesda.UnitTests.csproj | 4 ++-- Synthesis.Bethesda/Synthesis.Bethesda.csproj | 2 +- 7 files changed, 15 insertions(+), 18 deletions(-) diff --git a/Mutagen.Bethesda.Synthesis/Mutagen.Bethesda.Synthesis.csproj b/Mutagen.Bethesda.Synthesis/Mutagen.Bethesda.Synthesis.csproj index 6dd6bda3..777f88ac 100644 --- a/Mutagen.Bethesda.Synthesis/Mutagen.Bethesda.Synthesis.csproj +++ b/Mutagen.Bethesda.Synthesis/Mutagen.Bethesda.Synthesis.csproj @@ -22,8 +22,8 @@ - - + + diff --git a/Synthesis.Bethesda.Execution/Patcher Runs/SolutionPatcherRun.cs b/Synthesis.Bethesda.Execution/Patcher Runs/SolutionPatcherRun.cs index 3db1b97c..10a1ded2 100644 --- a/Synthesis.Bethesda.Execution/Patcher Runs/SolutionPatcherRun.cs +++ b/Synthesis.Bethesda.Execution/Patcher Runs/SolutionPatcherRun.cs @@ -45,7 +45,7 @@ public async Task Prep(GameRelease release, CancellationToken? cancel = null) { CliRun = new CliPatcherRun(nickname: Name, pathToExecutable: PathToExe); - var resp = await CompileWithDotnet(PathToSolution, cancel ?? CancellationToken.None).ConfigureAwait(false); + var resp = await CompileWithDotnet(PathToProject, cancel ?? CancellationToken.None).ConfigureAwait(false); if (!resp.Succeeded) { throw new SynthesisBuildFailure(resp.Reason); @@ -105,10 +105,10 @@ public void Dispose() return (true, default); } - public static async Task CompileWithDotnet(string solutionUrl, CancellationToken cancel) + public static async Task CompileWithDotnet(string url, CancellationToken cancel) { using var process = ProcessWrapper.Start( - new ProcessStartInfo("dotnet", $"build \"{solutionUrl}\""), + new ProcessStartInfo("dotnet", $"build \"{url}\""), cancel: cancel); string? firstError = null; bool buildFailed = false; diff --git a/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj b/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj index 542b9f35..5f9041fc 100644 --- a/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj +++ b/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj @@ -28,9 +28,9 @@ - + - + diff --git a/Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj b/Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj index a7290666..af4e0788 100644 --- a/Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj +++ b/Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj @@ -39,7 +39,7 @@ - + diff --git a/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs index 1e9fb233..2472171a 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs @@ -124,23 +124,20 @@ public SolutionPatcherVM(ProfileVM parent, SolutionPatcherSettings? settings = n .DisposeWith(this); var pathToExe = projPath - .CombineLatest( - this.WhenAnyValue(x => x.SolutionPath.TargetPath), - (ProjectPath, SolutionPath) => (ProjectPath, SolutionPath)) .ObserveOn(RxApp.TaskpoolScheduler) - .SelectReplace(async (t, cancel) => + .SelectReplace(async (projectPath, cancel) => { try { cancel.ThrowIfCancellationRequested(); - if (!File.Exists(t.ProjectPath)) + if (!File.Exists(projectPath)) { return GetResponse.Fail("Project path does not exist."); } // Right now this is slow as it cleans the build results unnecessarily. Need to look into that var manager = new AnalyzerManager(); cancel.ThrowIfCancellationRequested(); - var proj = manager.GetProject(t.ProjectPath); + var proj = manager.GetProject(projectPath); cancel.ThrowIfCancellationRequested(); var opt = new EnvironmentOptions(); opt.TargetsToBuild.SetTo("Build"); @@ -158,7 +155,7 @@ public SolutionPatcherVM(ProfileVM parent, SolutionPatcherSettings? settings = n } // Now we want to build, just to prep for run - var resp = await SolutionPatcherRun.CompileWithDotnet(t.SolutionPath, cancel).ConfigureAwait(false); + var resp = await SolutionPatcherRun.CompileWithDotnet(projectPath, cancel).ConfigureAwait(false); if (resp.Failed) return resp.BubbleFailure(); return GetResponse.Succeed(cmd); diff --git a/Synthesis.Bethesda.UnitTests/Synthesis.Bethesda.UnitTests.csproj b/Synthesis.Bethesda.UnitTests/Synthesis.Bethesda.UnitTests.csproj index cffd1812..2668594b 100644 --- a/Synthesis.Bethesda.UnitTests/Synthesis.Bethesda.UnitTests.csproj +++ b/Synthesis.Bethesda.UnitTests/Synthesis.Bethesda.UnitTests.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.1 @@ -13,7 +13,7 @@ - + all diff --git a/Synthesis.Bethesda/Synthesis.Bethesda.csproj b/Synthesis.Bethesda/Synthesis.Bethesda.csproj index 92c9f6cc..ccac99bb 100644 --- a/Synthesis.Bethesda/Synthesis.Bethesda.csproj +++ b/Synthesis.Bethesda/Synthesis.Bethesda.csproj @@ -23,7 +23,7 @@ - + From 57d9d6d1e2b6c24be9b42dcc321a0c10b2522b84 Mon Sep 17 00:00:00 2001 From: Justin Swanson Date: Tue, 15 Sep 2020 22:01:13 -0500 Subject: [PATCH 05/23] Fix for project path pickers --- .../ViewModels/Config/SolutionPatcherVM.cs | 2 ++ .../Views/Config/SolutionConfigView.xaml.cs | 8 +++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs index 2472171a..361fa31c 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs @@ -101,6 +101,8 @@ public SolutionPatcherVM(ProfileVM parent, SolutionPatcherSettings? settings = n .ToObservableCollection(this); var projPath = this.WhenAnyValue(x => x.ProjectSubpath) + // Need to throttle, as bindings flip to null quickly, which we want to skip + .Throttle(TimeSpan.FromMilliseconds(150), RxApp.MainThreadScheduler) .DistinctUntilChanged() .CombineLatest(this.WhenAnyValue(x => x.SolutionPath.TargetPath) .DistinctUntilChanged(), diff --git a/Synthesis.Bethesda.GUI/Views/Config/SolutionConfigView.xaml.cs b/Synthesis.Bethesda.GUI/Views/Config/SolutionConfigView.xaml.cs index ee6a9def..7da9addb 100644 --- a/Synthesis.Bethesda.GUI/Views/Config/SolutionConfigView.xaml.cs +++ b/Synthesis.Bethesda.GUI/Views/Config/SolutionConfigView.xaml.cs @@ -1,4 +1,4 @@ -using Noggog.WPF; +using Noggog.WPF; using ReactiveUI; using System.Reactive.Disposables; using System; @@ -49,12 +49,10 @@ public SolutionConfigView() .DisposeWith(disposable); // Bind project picker - // Setting initial values here keeps the VM's property from being bounced to null on - // initial binding - this.ProjectsPickerBox.ItemsSource = this.ViewModel.ProjectsDisplay; - this.ProjectsPickerBox.SelectedItem = this.ViewModel.ProjectSubpath; this.BindStrict(this.ViewModel, vm => vm.ProjectSubpath, view => view.ProjectsPickerBox.SelectedItem) .DisposeWith(disposable); + this.OneWayBindStrict(this.ViewModel, vm => vm.ProjectsDisplay, view => view.ProjectsPickerBox.ItemsSource) + .DisposeWith(disposable); // Set project picker tooltips this.WhenAnyValue(x => x.ViewModel.SelectedProjectPath.ErrorState) From d30245f8c56ca6d92534dd4ebb22df9d1b5778e6 Mon Sep 17 00:00:00 2001 From: Justin Swanson Date: Tue, 15 Sep 2020 23:39:35 -0500 Subject: [PATCH 06/23] Fix for open solution not enabled if there was a build error --- Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs index 361fa31c..da260d81 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs @@ -195,7 +195,7 @@ public SolutionPatcherVM(ProfileVM parent, SolutionPatcherSettings? settings = n .ToGuiProperty(this, nameof(State), ConfigurationStateVM.Success); OpenSolutionCommand = ReactiveCommand.Create( - canExecute: this.WhenAnyValue(x => x.State.IsHaltingError) + canExecute: this.WhenAnyValue(x => x.SolutionPath.InError) .Select(x => !x), execute: () => { From 784fdecd664595e5cdb9c186c233e2dcd58348b4 Mon Sep 17 00:00:00 2001 From: Justin Swanson Date: Wed, 16 Sep 2020 16:58:59 -0500 Subject: [PATCH 07/23] Fix for some settings persistence --- Synthesis.Bethesda.GUI/ViewModels/Config/ConfigurationVM.cs | 1 - Synthesis.Bethesda.GUI/ViewModels/MainVM.cs | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Synthesis.Bethesda.GUI/ViewModels/Config/ConfigurationVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Config/ConfigurationVM.cs index 9fb6dcee..559c4c33 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Config/ConfigurationVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Config/ConfigurationVM.cs @@ -157,7 +157,6 @@ public SynthesisGuiSettings Save() SelectedProfile = SelectedProfile?.ID ?? string.Empty }, ShowHelp = ShowHelp, - MainRepositoryFolder = MainVM.Settings.MainRepositoryFolder, }; } } diff --git a/Synthesis.Bethesda.GUI/ViewModels/MainVM.cs b/Synthesis.Bethesda.GUI/ViewModels/MainVM.cs index a6a23d5f..4f30fcd1 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/MainVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/MainVM.cs @@ -100,6 +100,8 @@ public SynthesisGuiSettings Save() { var ret = Configuration.Save(); ret.Ide = this.Ide; + ret.MainRepositoryFolder = Settings.MainRepositoryFolder; + ret.OpenIdeAfterCreating = Settings.OpenIdeAfterCreating; return ret; } From 2d5c8345a07ae7d123b9c39cd056b2186bfe6610 Mon Sep 17 00:00:00 2001 From: Justin Swanson Date: Wed, 16 Sep 2020 20:00:48 -0500 Subject: [PATCH 08/23] HelpWiring logic refactor to not take in PatcherVM --- .../ViewModels/Config/PatcherVM.cs | 2 -- .../Views/Config/CliConfigView.xaml.cs | 9 +++++++-- .../Initialization/SolutionInitView.xaml.cs | 2 +- .../Views/UtilityBindings.cs | 20 ++++++++----------- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/Synthesis.Bethesda.GUI/ViewModels/Config/PatcherVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Config/PatcherVM.cs index 183ddcd5..cf3b43f9 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Config/PatcherVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Config/PatcherVM.cs @@ -32,7 +32,6 @@ public abstract class PatcherVM : ViewModel public abstract string DisplayName { get; } public ICommand DeleteCommand { get; } - public ICommand ShowHelpCommand { get; } public abstract ConfigurationStateVM State { get; } @@ -58,7 +57,6 @@ public PatcherVM(ProfileVM parent, PatcherSettings? settings) $"Are you sure you want to delete {DisplayName}?", () => parent.Patchers.Remove(this)); }); - ShowHelpCommand = parent.Config.ShowHelpToggleCommand; } public abstract PatcherSettings Save(); diff --git a/Synthesis.Bethesda.GUI/Views/Config/CliConfigView.xaml.cs b/Synthesis.Bethesda.GUI/Views/Config/CliConfigView.xaml.cs index 90ea945a..d02a0bbf 100644 --- a/Synthesis.Bethesda.GUI/Views/Config/CliConfigView.xaml.cs +++ b/Synthesis.Bethesda.GUI/Views/Config/CliConfigView.xaml.cs @@ -1,4 +1,4 @@ -using Noggog.WPF; +using Noggog.WPF; using System; using ReactiveUI; using System.Reactive.Disposables; @@ -32,8 +32,13 @@ public CliConfigView() this.BindStrict(this.ViewModel, vm => vm.PathToExecutable, view => view.ExecutablePathPicker.PickerVM) .DisposeWith(disposable); + var isNewPatcher = this.WhenAnyFallback(x => x.ViewModel.Profile.Config.NewPatcher!.Patcher, default) + .Select(newPatcher => object.ReferenceEquals(newPatcher, this.ViewModel)) + .Replay(1) + .RefCount(); + // Hide help box if not in initialization - UtilityBindings.HelpWiring(this.ViewModel, this.HelpButton, this.HelpText) + UtilityBindings.HelpWiring(this.ViewModel.Profile.Config, this.HelpButton, this.HelpText, isNewPatcher) .DisposeWith(disposable); }); } diff --git a/Synthesis.Bethesda.GUI/Views/Initialization/SolutionInitView.xaml.cs b/Synthesis.Bethesda.GUI/Views/Initialization/SolutionInitView.xaml.cs index 8d344217..009e4479 100644 --- a/Synthesis.Bethesda.GUI/Views/Initialization/SolutionInitView.xaml.cs +++ b/Synthesis.Bethesda.GUI/Views/Initialization/SolutionInitView.xaml.cs @@ -21,7 +21,7 @@ public SolutionInitView() this.WhenActivated(dispose => { // Hide help box if not in initialization - UtilityBindings.HelpWiring(this.ViewModel.Patcher, this.HelpButton, this.HelpText) + UtilityBindings.HelpWiring(this.ViewModel.Patcher.Profile.Config, this.HelpButton, this.HelpText) .DisposeWith(dispose); this.BindStrict(this.ViewModel, vm => vm.SelectedIndex, view => view.TopTab.SelectedIndex) diff --git a/Synthesis.Bethesda.GUI/Views/UtilityBindings.cs b/Synthesis.Bethesda.GUI/Views/UtilityBindings.cs index 6fe320b8..66151866 100644 --- a/Synthesis.Bethesda.GUI/Views/UtilityBindings.cs +++ b/Synthesis.Bethesda.GUI/Views/UtilityBindings.cs @@ -1,4 +1,4 @@ -using Noggog.WPF; +using Noggog.WPF; using ReactiveUI; using System; using System.Collections.Generic; @@ -16,33 +16,29 @@ namespace Synthesis.Bethesda.GUI public static class UtilityBindings { #region Help Sections - public static IDisposable HelpWiring(PatcherVM vm, Button button, TextBlock helpBlock) + public static IDisposable HelpWiring(ConfigurationVM config, Button button, TextBlock helpBlock, IObservable? show = null) { CompositeDisposable ret = new CompositeDisposable(); - var isNewPatcher = vm.WhenAnyFallback(x => x.Profile.Config.NewPatcher!.Patcher, default) - .Select(newPatcher => object.ReferenceEquals(newPatcher, vm)) - .Replay(1) - .RefCount(); + show ??= Observable.Return(true); - isNewPatcher - .Select(x => x ? Visibility.Visible : Visibility.Collapsed) + show.Select(x => x ? Visibility.Visible : Visibility.Collapsed) .Subscribe(x => button.Visibility = x) .DisposeWith(ret); Observable.CombineLatest( - isNewPatcher, - vm.WhenAnyValue(x => x.Profile.Config.ShowHelp), + show, + config.WhenAnyValue(x => x.ShowHelp), (newPatcher, on) => on && newPatcher) .Select(x => x ? Visibility.Visible : Visibility.Collapsed) .Subscribe(x => helpBlock.Visibility = x) .DisposeWith(ret); - vm.WhenAnyValue(x => x.ShowHelpCommand) + config.WhenAnyValue(x => x.ShowHelpToggleCommand) .Subscribe(x => button.Command = x) .DisposeWith(ret); - vm.WhenAnyValue(x => x.Profile.Config.ShowHelp) + config.WhenAnyValue(x => x.ShowHelp) .Select(x => x ? 1.0d : 0.4d) .Subscribe(x => button.Opacity = x) .DisposeWith(ret); From 316dc2e48e7767219560c3e9703f4cfd9d0a17b3 Mon Sep 17 00:00:00 2001 From: Justin Swanson Date: Wed, 16 Sep 2020 21:34:51 -0500 Subject: [PATCH 09/23] Inverted Initialization process --- .../Synthesis.Bethesda.GUI.csproj | 3 +- .../ViewModels/Config/CliPatcherVM.cs | 11 +---- .../ViewModels/Config/CodeSnippetPatcherVM.cs | 5 --- .../ViewModels/Config/ConfigurationVM.cs | 9 ++-- .../ViewModels/Config/GithubPatcherVM.cs | 7 +-- .../ViewModels/Config/PatcherVM.cs | 2 - .../ViewModels/Config/ProfileVM.cs | 23 +++++----- .../ViewModels/Config/SolutionPatcherVM.cs | 5 --- .../Initialization/CliPatcherInitVM.cs | 23 +++++----- .../Initialization/GithubPatcherInitVM.cs | 27 +++++++----- .../Initialization/PatcherInitVM.cs | 17 ++++--- .../Initialization/SolutionPatcherInitVM.cs | 44 ++++++++----------- .../Views/Common/TypeIcon.xaml | 21 +++++++++ .../Views/Config/CliConfigView.xaml.cs | 15 +------ .../InitializationDetailView.xaml.cs | 7 ++- .../Initialization/SolutionInitView.xaml.cs | 2 +- 16 files changed, 107 insertions(+), 114 deletions(-) diff --git a/Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj b/Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj index af4e0788..87ee11e3 100644 --- a/Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj +++ b/Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj @@ -1,4 +1,4 @@ - + WinExe @@ -43,6 +43,7 @@ + diff --git a/Synthesis.Bethesda.GUI/ViewModels/Config/CliPatcherVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Config/CliPatcherVM.cs index 53229da0..a6ab60b6 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Config/CliPatcherVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Config/CliPatcherVM.cs @@ -1,15 +1,11 @@ -using Noggog; +using Noggog; using Noggog.WPF; using ReactiveUI; -using ReactiveUI.Fody.Helpers; using Synthesis.Bethesda.Execution; -using Synthesis.Bethesda.Execution.Patchers; using Synthesis.Bethesda.Execution.Settings; using System; -using System.Collections.Generic; using System.IO; using System.Reactive.Linq; -using System.Text; namespace Synthesis.Bethesda.GUI { @@ -87,10 +83,5 @@ public override PatcherRunVM ToRunner(PatchersRunVM parent) this, new CliPatcherRun(nickname: DisplayName, pathToExecutable: PathToExecutable.TargetPath)); } - - public override PatcherInitVM? CreateInitializer() - { - return new CliPatcherInitVM(this); - } } } diff --git a/Synthesis.Bethesda.GUI/ViewModels/Config/CodeSnippetPatcherVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Config/CodeSnippetPatcherVM.cs index b291c728..7f0e4708 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Config/CodeSnippetPatcherVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Config/CodeSnippetPatcherVM.cs @@ -232,11 +232,6 @@ private ConfigurationStateVM ToState((MemoryStream? AssemblyStream, EmitResult? IsHaltingError = true, }; } - - public override PatcherInitVM? CreateInitializer() - { - return null; - } } public enum CodeCompilationStatus diff --git a/Synthesis.Bethesda.GUI/ViewModels/Config/ConfigurationVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Config/ConfigurationVM.cs index 559c4c33..99745315 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Config/ConfigurationVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Config/ConfigurationVM.cs @@ -67,11 +67,12 @@ public ConfigurationVM(MainVM mvm) { var initializer = this.NewPatcher; if (initializer == null) return; - SelectedProfile?.Patchers.Add(initializer.Patcher); + var patchersToAdd = await initializer.Construct().ToListAsync(); NewPatcher = null; - SelectedPatcher = initializer.Patcher; - initializer.Patcher.IsOn = true; - await initializer.ExecuteChanges(); + if (patchersToAdd.Count == 0) return; + patchersToAdd.ForEach(p => p.IsOn = true); + SelectedProfile?.Patchers.AddRange(patchersToAdd); + SelectedPatcher = patchersToAdd.First(); }, canExecute: this.WhenAnyValue(x => x.NewPatcher) .Select(patcher => diff --git a/Synthesis.Bethesda.GUI/ViewModels/Config/GithubPatcherVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Config/GithubPatcherVM.cs index 8ba07aab..042e1d5a 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Config/GithubPatcherVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Config/GithubPatcherVM.cs @@ -1,4 +1,4 @@ -using Synthesis.Bethesda.Execution.Settings; +using Synthesis.Bethesda.Execution.Settings; using Noggog; using Noggog.WPF; using ReactiveUI; @@ -69,10 +69,5 @@ public override PatcherRunVM ToRunner(PatchersRunVM parent) { throw new NotImplementedException(); } - - public override PatcherInitVM? CreateInitializer() - { - return new GithubPatcherInitVM(this); - } } } diff --git a/Synthesis.Bethesda.GUI/ViewModels/Config/PatcherVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Config/PatcherVM.cs index cf3b43f9..679dc373 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Config/PatcherVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Config/PatcherVM.cs @@ -68,7 +68,5 @@ protected void CopyOverSave(PatcherSettings settings) } public abstract PatcherRunVM ToRunner(PatchersRunVM parent); - - public abstract PatcherInitVM? CreateInitializer(); } } diff --git a/Synthesis.Bethesda.GUI/ViewModels/Config/ProfileVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Config/ProfileVM.cs index 3699651b..8007ef1a 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Config/ProfileVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Config/ProfileVM.cs @@ -57,9 +57,9 @@ public ProfileVM(ConfigurationVM parent, GameRelease? release = null, string? id ID = id ?? Guid.NewGuid().ToString(); Config = parent; Release = release ?? GameRelease.Oblivion; - AddGithubPatcherCommand = ReactiveCommand.Create(() => SetPatcherForInitialConfiguration(new GithubPatcherVM(this))); - AddSolutionPatcherCommand = ReactiveCommand.Create(() => SetPatcherForInitialConfiguration(new SolutionPatcherVM(this))); - AddCliPatcherCommand = ReactiveCommand.Create(() => SetPatcherForInitialConfiguration(new CliPatcherVM(this))); + AddGithubPatcherCommand = ReactiveCommand.Create(() => SetInitializer(new GithubPatcherInitVM(this))); + AddSolutionPatcherCommand = ReactiveCommand.Create(() => SetInitializer(new SolutionPatcherInitVM(this))); + AddCliPatcherCommand = ReactiveCommand.Create(() => SetInitializer(new CliPatcherInitVM(this))); AddSnippetPatcherCommand = ReactiveCommand.Create(() => SetPatcherForInitialConfiguration(new CodeSnippetPatcherVM(this))); _WorkingDirectory = this.WhenAnyValue(x => x.Config.WorkingDirectory) .Select(dir => Path.Combine(dir, ID, "Workspace")) @@ -204,16 +204,13 @@ public SynthesisProfile Save() private void SetPatcherForInitialConfiguration(PatcherVM patcher) { - var initializer = patcher.CreateInitializer(); - if (initializer != null) - { - Config.NewPatcher = initializer; - } - else - { - patcher.Profile.Patchers.Add(patcher); - Config.SelectedPatcher = patcher; - } + patcher.Profile.Patchers.Add(patcher); + Config.SelectedPatcher = patcher; + } + + private void SetInitializer(PatcherInitVM initializer) + { + Config.NewPatcher = initializer; } } } diff --git a/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs index da260d81..fec69176 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs @@ -241,10 +241,5 @@ public override PatcherRunVM ToRunner(PatchersRunVM parent) pathToExe: PathToExe, pathToProj: SelectedProjectPath.TargetPath)); } - - public override PatcherInitVM? CreateInitializer() - { - return new SolutionPatcherInitVM(Profile.Config.MainVM, this); - } } } diff --git a/Synthesis.Bethesda.GUI/ViewModels/Initialization/CliPatcherInitVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Initialization/CliPatcherInitVM.cs index da5fa47e..4a08cd91 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Initialization/CliPatcherInitVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Initialization/CliPatcherInitVM.cs @@ -1,27 +1,30 @@ -using Noggog; +using Noggog; using Noggog.WPF; using ReactiveUI; -using System; using System.Collections.Generic; -using System.Text; +using System.Threading.Tasks; namespace Synthesis.Bethesda.GUI { public class CliPatcherInitVM : PatcherInitVM { - private readonly CliPatcherVM _patcher; - public override PatcherVM Patcher => _patcher; - private readonly ObservableAsPropertyHelper _CanCompleteConfiguration; public override ErrorResponse CanCompleteConfiguration => _CanCompleteConfiguration.Value; - public CliPatcherInitVM(CliPatcherVM patcher) - { - _patcher = patcher; + public CliPatcherVM Patcher { get; } - _CanCompleteConfiguration = _patcher.WhenAnyValue(x => x.PathToExecutable.ErrorState) + public CliPatcherInitVM(ProfileVM profile) + : base(profile) + { + Patcher = new CliPatcherVM(profile); + _CanCompleteConfiguration = Patcher.WhenAnyValue(x => x.PathToExecutable.ErrorState) .Cast() .ToGuiProperty(this, nameof(CanCompleteConfiguration), ErrorResponse.Success); } + + public override async IAsyncEnumerable Construct() + { + yield return Patcher; + } } } diff --git a/Synthesis.Bethesda.GUI/ViewModels/Initialization/GithubPatcherInitVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Initialization/GithubPatcherInitVM.cs index 173e0a6e..2395a3ae 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Initialization/GithubPatcherInitVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Initialization/GithubPatcherInitVM.cs @@ -1,31 +1,30 @@ -using Noggog; +using Noggog; using Noggog.WPF; using ReactiveUI; +using ReactiveUI.Fody.Helpers; using System; using System.Collections.Generic; using System.Reactive.Linq; -using System.Text; namespace Synthesis.Bethesda.GUI { public class GithubPatcherInitVM : PatcherInitVM { - private readonly GithubPatcherVM _patcher; - public override PatcherVM Patcher => _patcher; - private readonly ObservableAsPropertyHelper _CanCompleteConfiguration; public override ErrorResponse CanCompleteConfiguration => _CanCompleteConfiguration.Value; - public GithubPatcherInitVM(GithubPatcherVM patcher) - { - _patcher = patcher; + [Reactive] + public string RepoPath { get; set; } = string.Empty; + public GithubPatcherInitVM(ProfileVM profile) + : base(profile) + { // Whenever we change, mark that we cannot - _CanCompleteConfiguration = _patcher.WhenAnyValue(x => x.RepoPath) + _CanCompleteConfiguration = this.WhenAnyValue(x => x.RepoPath) .DistinctUntilChanged() .Select(x => ErrorResponse.Fail("Checking remote repository correctness.")) // But merge in the work of checking the repo on that same path to get the eventual result - .Merge(_patcher.WhenAnyValue(x => x.RepoPath) + .Merge(this.WhenAnyValue(x => x.RepoPath) .DistinctUntilChanged() .Debounce(TimeSpan.FromMilliseconds(300), RxApp.MainThreadScheduler) .ObserveOn(RxApp.TaskpoolScheduler) @@ -43,5 +42,13 @@ public GithubPatcherInitVM(GithubPatcherVM patcher) .Cast() .ToGuiProperty(this, nameof(CanCompleteConfiguration), ErrorResponse.Success); } + + public override async IAsyncEnumerable Construct() + { + yield return new GithubPatcherVM(Profile) + { + RepoPath = this.RepoPath + }; + } } } diff --git a/Synthesis.Bethesda.GUI/ViewModels/Initialization/PatcherInitVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Initialization/PatcherInitVM.cs index 07871219..1483c2dc 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Initialization/PatcherInitVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Initialization/PatcherInitVM.cs @@ -1,18 +1,25 @@ -using Noggog; +using Noggog; using Noggog.WPF; +using ReactiveUI.Fody.Helpers; using System; using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; namespace Synthesis.Bethesda.GUI { public abstract class PatcherInitVM : ViewModel { + [Reactive] + public string DisplayName { get; set; } = string.Empty; + + public ProfileVM Profile { get; } + public abstract ErrorResponse CanCompleteConfiguration { get; } - public abstract PatcherVM Patcher { get; } - public virtual async Task ExecuteChanges() + + public abstract IAsyncEnumerable Construct(); + + public PatcherInitVM(ProfileVM profile) { + Profile = profile; } } } diff --git a/Synthesis.Bethesda.GUI/ViewModels/Initialization/SolutionPatcherInitVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Initialization/SolutionPatcherInitVM.cs index 855e7533..6fa54486 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Initialization/SolutionPatcherInitVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Initialization/SolutionPatcherInitVM.cs @@ -4,14 +4,8 @@ using ReactiveUI.Fody.Helpers; using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Drawing.Design; -using System.IO; -using System.Reactive; using System.Reactive.Linq; -using System.Text; using System.Threading.Tasks; -using DynamicData.Binding; namespace Synthesis.Bethesda.GUI { @@ -23,9 +17,6 @@ public class SolutionPatcherInitVM : PatcherInitVM public NewSolutionInitVM New { get; } = new NewSolutionInitVM(); public ExistingProjectInitVM ExistingProject { get; } = new ExistingProjectInitVM(); - private readonly SolutionPatcherVM _patcher; - public override PatcherVM Patcher => _patcher; - private readonly ObservableAsPropertyHelper _CanCompleteConfiguration; public override ErrorResponse CanCompleteConfiguration => _CanCompleteConfiguration.Value; @@ -38,12 +29,12 @@ public class SolutionPatcherInitVM : PatcherInitVM [Reactive] public bool OpenCodeAfter { get; set; } - public SolutionPatcherInitVM(MainVM mvm, SolutionPatcherVM patcher) + public SolutionPatcherInitVM(ProfileVM profile) + : base(profile) { - MVM = mvm; - _patcher = patcher; - OpenCodeAfter = _patcher.Profile.Config.MainVM.Settings.OpenIdeAfterCreating; - New.ParentDirPath.TargetPath = _patcher.Profile.Config.MainVM.Settings.MainRepositoryFolder; + MVM = profile.Config.MainVM; + OpenCodeAfter = profile.Config.MainVM.Settings.OpenIdeAfterCreating; + New.ParentDirPath.TargetPath = profile.Config.MainVM.Settings.MainRepositoryFolder; var initializer = this.WhenAnyValue(x => x.SelectedIndex) .Select(x => @@ -68,15 +59,25 @@ public SolutionPatcherInitVM(MainVM mvm, SolutionPatcherVM patcher) .ToGuiProperty(this, nameof(CanCompleteConfiguration), ErrorResponse.Failure); } - public override async Task ExecuteChanges() + public override void Dispose() + { + base.Dispose(); + MVM.Settings.OpenIdeAfterCreating = OpenCodeAfter; + MVM.Settings.MainRepositoryFolder = New.ParentDirPath.TargetPath; + } + + public override async IAsyncEnumerable Construct() { - if (TargetSolutionInitializer == null) return; - await TargetSolutionInitializer(_patcher); + if (TargetSolutionInitializer == null) yield break; + var ret = new SolutionPatcherVM(Profile); + await TargetSolutionInitializer(ret); + yield return ret; + if (OpenCodeAfter) { try { - IdeLocator.OpenSolution(_patcher.SolutionPath.TargetPath, MVM.Ide); + IdeLocator.OpenSolution(ret.SolutionPath.TargetPath, MVM.Ide); } catch (Exception) { @@ -86,13 +87,6 @@ public override async Task ExecuteChanges() } } - public override void Dispose() - { - base.Dispose(); - _patcher.Profile.Config.MainVM.Settings.OpenIdeAfterCreating = OpenCodeAfter; - _patcher.Profile.Config.MainVM.Settings.MainRepositoryFolder = New.ParentDirPath.TargetPath; - } - public enum SolutionInitType { New, diff --git a/Synthesis.Bethesda.GUI/Views/Common/TypeIcon.xaml b/Synthesis.Bethesda.GUI/Views/Common/TypeIcon.xaml index 34de11e9..0e893cf0 100644 --- a/Synthesis.Bethesda.GUI/Views/Common/TypeIcon.xaml +++ b/Synthesis.Bethesda.GUI/Views/Common/TypeIcon.xaml @@ -26,6 +26,13 @@ Kind="Github" ToolTip="Mutagen Github Link Patcher" /> + + + + + + + + + diff --git a/Synthesis.Bethesda.GUI/Views/Config/CliConfigView.xaml.cs b/Synthesis.Bethesda.GUI/Views/Config/CliConfigView.xaml.cs index d02a0bbf..42b298e6 100644 --- a/Synthesis.Bethesda.GUI/Views/Config/CliConfigView.xaml.cs +++ b/Synthesis.Bethesda.GUI/Views/Config/CliConfigView.xaml.cs @@ -2,17 +2,6 @@ using System; using ReactiveUI; using System.Reactive.Disposables; -using System.Collections.Generic; -using System.Text; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; using System.Reactive.Linq; namespace Synthesis.Bethesda.GUI.Views @@ -32,8 +21,8 @@ public CliConfigView() this.BindStrict(this.ViewModel, vm => vm.PathToExecutable, view => view.ExecutablePathPicker.PickerVM) .DisposeWith(disposable); - var isNewPatcher = this.WhenAnyFallback(x => x.ViewModel.Profile.Config.NewPatcher!.Patcher, default) - .Select(newPatcher => object.ReferenceEquals(newPatcher, this.ViewModel)) + var isNewPatcher = this.WhenAnyFallback(x => x.ViewModel.Profile.Config.NewPatcher, default) + .Select(newPatcher => newPatcher != null) .Replay(1) .RefCount(); diff --git a/Synthesis.Bethesda.GUI/Views/Initialization/InitializationDetailView.xaml.cs b/Synthesis.Bethesda.GUI/Views/Initialization/InitializationDetailView.xaml.cs index 784169f4..10e153b2 100644 --- a/Synthesis.Bethesda.GUI/Views/Initialization/InitializationDetailView.xaml.cs +++ b/Synthesis.Bethesda.GUI/Views/Initialization/InitializationDetailView.xaml.cs @@ -1,4 +1,4 @@ -using Noggog.WPF; +using Noggog.WPF; using System.Windows.Controls; using ReactiveUI; using System.Reactive.Disposables; @@ -17,10 +17,9 @@ public InitializationDetailView() InitializeComponent(); this.WhenActivated(disposable => { - this.WhenAnyValue(x => x.ViewModel.Patcher.DisplayName) - .BindToStrict(this, x => x.PatcherDetailName.Text) + this.BindStrict(this.ViewModel, vm => vm.DisplayName, view => view.PatcherDetailName.Text) .DisposeWith(disposable); - this.WhenAnyValue(x => x.ViewModel.Patcher) + this.WhenAnyValue(x => x.ViewModel) .BindToStrict(this, x => x.PatcherIconDisplay.DataContext) .DisposeWith(disposable); this.WhenAnyValue(x => x.ViewModel) diff --git a/Synthesis.Bethesda.GUI/Views/Initialization/SolutionInitView.xaml.cs b/Synthesis.Bethesda.GUI/Views/Initialization/SolutionInitView.xaml.cs index 009e4479..c7f6da9b 100644 --- a/Synthesis.Bethesda.GUI/Views/Initialization/SolutionInitView.xaml.cs +++ b/Synthesis.Bethesda.GUI/Views/Initialization/SolutionInitView.xaml.cs @@ -21,7 +21,7 @@ public SolutionInitView() this.WhenActivated(dispose => { // Hide help box if not in initialization - UtilityBindings.HelpWiring(this.ViewModel.Patcher.Profile.Config, this.HelpButton, this.HelpText) + UtilityBindings.HelpWiring(this.ViewModel.Profile.Config, this.HelpButton, this.HelpText) .DisposeWith(dispose); this.BindStrict(this.ViewModel, vm => vm.SelectedIndex, view => view.TopTab.SelectedIndex) From 77b26dadc764dde079f11c41474bb7613deaaa21 Mon Sep 17 00:00:00 2001 From: Justin Swanson Date: Wed, 16 Sep 2020 21:49:49 -0500 Subject: [PATCH 10/23] Add Existing Solution systems can add multiple projects at once --- .../Initialization/ASolutionInitializer.cs | 3 +- .../Initialization/ExistingProjectInitVM.cs | 83 ++++++++++--------- .../Initialization/ExistingSolutionInitVM.cs | 14 ++-- .../Initialization/NewSolutionInitVM.cs | 16 ++-- .../Initialization/SolutionPatcherInitVM.cs | 19 +++-- .../Initialization/SolutionInitView.xaml | 29 ++++--- .../Initialization/SolutionInitView.xaml.cs | 26 +++++- 7 files changed, 109 insertions(+), 81 deletions(-) diff --git a/Synthesis.Bethesda.GUI/ViewModels/Initialization/ASolutionInitializer.cs b/Synthesis.Bethesda.GUI/ViewModels/Initialization/ASolutionInitializer.cs index c565d657..28f948a0 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Initialization/ASolutionInitializer.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Initialization/ASolutionInitializer.cs @@ -17,7 +17,8 @@ namespace Synthesis.Bethesda.GUI { public abstract class ASolutionInitializer : ViewModel { - public abstract IObservable>> InitializationCall { get; } + public delegate Task> InitializerCall(ProfileVM profile); + public abstract IObservable> InitializationCall { get; } public static GetResponse ValidateProjectPath(string projName, GetResponse sln) { diff --git a/Synthesis.Bethesda.GUI/ViewModels/Initialization/ExistingProjectInitVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Initialization/ExistingProjectInitVM.cs index 25c96d8d..96631d15 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Initialization/ExistingProjectInitVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Initialization/ExistingProjectInitVM.cs @@ -18,14 +18,13 @@ namespace Synthesis.Bethesda.GUI { public class ExistingProjectInitVM : ASolutionInitializer { - public override IObservable>> InitializationCall { get; } + public override IObservable> InitializationCall { get; } public PathPickerVM SolutionPath { get; } = new PathPickerVM(); - public IObservableCollection ProjectsDisplay { get; } + public IObservableCollection AvailableProjects { get; } - [Reactive] - public string ProjectSubpath { get; set; } = string.Empty; + public SourceList SelectedProjects { get; } = new SourceList(); public PathPickerVM SelectedProjectPath { get; } = new PathPickerVM() { @@ -39,11 +38,7 @@ public ExistingProjectInitVM() SolutionPath.ExistCheckOption = PathPickerVM.CheckOptions.On; SolutionPath.Filters.Add(new CommonFileDialogFilter("Solution", ".sln")); - SelectedProjectPath.PathType = PathPickerVM.PathTypeOptions.File; - SelectedProjectPath.ExistCheckOption = PathPickerVM.CheckOptions.On; - SelectedProjectPath.Filters.Add(new CommonFileDialogFilter("Project", ".csproj")); - - ProjectsDisplay = this.WhenAnyValue(x => x.SolutionPath.TargetPath) + AvailableProjects = this.WhenAnyValue(x => x.SolutionPath.TargetPath) .ObserveOn(RxApp.TaskpoolScheduler) .Select(x => { @@ -63,39 +58,49 @@ public ExistingProjectInitVM() .ObserveOnGui() .ToObservableCollection(this); - this.WhenAnyValue(x => x.ProjectSubpath) - .DistinctUntilChanged() - .CombineLatest(this.WhenAnyValue(x => x.SolutionPath.TargetPath) - .DistinctUntilChanged(), - (subPath, slnPath) => + InitializationCall = SelectedProjects.Connect() + .Transform(subPath => + { + if (subPath == null || this.SolutionPath.TargetPath == null) return string.Empty; + try { - if (subPath == null || slnPath == null) return string.Empty; - try - { - return Path.Combine(Path.GetDirectoryName(slnPath)!, subPath); - } - catch (Exception) - { - return string.Empty; - } - }) - .Subscribe(p => SelectedProjectPath.TargetPath = p) - .DisposeWith(this); - - InitializationCall = Observable.CombineLatest( - SolutionPath.PathState(), - SelectedProjectPath.PathState(), - (sln, proj) => (sln, proj)) - .Select(i => + return Path.Combine(Path.GetDirectoryName(SolutionPath.TargetPath)!, subPath); + } + catch (Exception) + { + return string.Empty; + } + }) + .Transform(path => { - if (!i.sln.Succeeded) return i.sln.BubbleFailure>(); - if (!i.proj.Succeeded) return i.proj.BubbleFailure>(); - return GetResponse>.Succeed(async (patcher) => + var pathPicker = new PathPickerVM { - patcher.SolutionPath.TargetPath = i.sln.Value; - // Little delay, just to make sure things populated properly. Might not be needed - await Task.Delay(300); - patcher.ProjectSubpath = i.proj.Value.TrimStart($"{Path.GetDirectoryName(i.sln.Value)}\\"!); + PathType = PathPickerVM.PathTypeOptions.File, + ExistCheckOption = PathPickerVM.CheckOptions.On, + TargetPath = path + }; + pathPicker.Filters.Add(new CommonFileDialogFilter("Project", ".csproj")); + return pathPicker; + }) + .DisposeMany() + .QueryWhenChanged(q => + { + if (q.Count == 0) return GetResponse.Fail("No projects selected"); + var err = q + .Select(p => p.ErrorState) + .Where(e => e.Failed) + .And(ErrorResponse.Success) + .First(); + if (err.Failed) return err.BubbleFailure(); + return GetResponse.Succeed(async (profile) => + { + return q.Select(i => + { + var patcher = new SolutionPatcherVM(profile); + patcher.SolutionPath.TargetPath = SolutionPath.TargetPath; + patcher.ProjectSubpath = i.TargetPath.TrimStart($"{Path.GetDirectoryName(SolutionPath.TargetPath)}\\"!); + return patcher; + }); }); }); } diff --git a/Synthesis.Bethesda.GUI/ViewModels/Initialization/ExistingSolutionInitVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Initialization/ExistingSolutionInitVM.cs index 492ff3fd..d84dc519 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Initialization/ExistingSolutionInitVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Initialization/ExistingSolutionInitVM.cs @@ -1,4 +1,4 @@ -using DynamicData; +using DynamicData; using Microsoft.WindowsAPICodePack.Dialogs; using Mutagen.Bethesda; using Noggog; @@ -16,7 +16,7 @@ namespace Synthesis.Bethesda.GUI { public class ExistingSolutionInitVM : ASolutionInitializer { - public override IObservable>> InitializationCall { get; } + public override IObservable> InitializationCall { get; } public PathPickerVM SolutionPath { get; } = new PathPickerVM(); @@ -46,16 +46,16 @@ public ExistingSolutionInitVM() InitializationCall = validation .Select((i) => { - if (!i.sln.Succeeded) return i.sln.BubbleFailure>(); - if (!i.validation.Succeeded) return i.validation.BubbleFailure>(); - return GetResponse>.Succeed(async (patcher) => + if (!i.sln.Succeeded) return i.sln.BubbleFailure(); + if (!i.validation.Succeeded) return i.validation.BubbleFailure(); + return GetResponse.Succeed(async (profile) => { + var patcher = new SolutionPatcherVM(profile); CreateProject(i.validation.Value, patcher.Profile.Release.ToCategory()); AddProjectToSolution(i.sln.Value, i.validation.Value); patcher.SolutionPath.TargetPath = i.sln.Value; - // Little delay, just to make sure things populated properly. Might not be needed - await Task.Delay(300); patcher.ProjectSubpath = Path.Combine(i.proj, $"{i.proj}.csproj"); + return patcher.AsEnumerable(); }); }); } diff --git a/Synthesis.Bethesda.GUI/ViewModels/Initialization/NewSolutionInitVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Initialization/NewSolutionInitVM.cs index 6d8a2647..0bba9ba4 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Initialization/NewSolutionInitVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Initialization/NewSolutionInitVM.cs @@ -1,4 +1,4 @@ -using Mutagen.Bethesda; +using Mutagen.Bethesda; using Noggog; using Noggog.WPF; using ReactiveUI; @@ -26,7 +26,7 @@ public class NewSolutionInitVM : ASolutionInitializer private readonly ObservableAsPropertyHelper> _SolutionPath; public GetResponse SolutionPath => _SolutionPath.Value; - public override IObservable>> InitializationCall { get; } + public override IObservable> InitializationCall { get; } private readonly ObservableAsPropertyHelper _ProjectError; public ErrorResponse ProjectError => _ProjectError.Value; @@ -94,19 +94,19 @@ public NewSolutionInitVM() InitializationCall = validation .Select((i) => { - if (i.parentDir.Failed) return i.parentDir.BubbleFailure>(); - if (i.sln.Failed) return i.sln.BubbleFailure>(); - if (i.validation.Failed) return i.validation.BubbleFailure>(); - return GetResponse>.Succeed(async (patcher) => + if (i.parentDir.Failed) return i.parentDir.BubbleFailure(); + if (i.sln.Failed) return i.sln.BubbleFailure(); + if (i.validation.Failed) return i.validation.BubbleFailure(); + return GetResponse.Succeed(async (profile) => { + var patcher = new SolutionPatcherVM(profile); CreateSolutionFile(i.sln.Value); CreateProject(i.validation.Value, patcher.Profile.Release.ToCategory()); AddProjectToSolution(i.sln.Value, i.validation.Value); patcher.SolutionPath.TargetPath = i.sln.Value; var projName = Path.GetFileNameWithoutExtension(i.validation.Value); - // Little delay, just to make sure things populated properly. Might not be needed - await Task.Delay(300); patcher.ProjectSubpath = Path.Combine(projName, $"{projName}.csproj"); + return patcher.AsEnumerable(); }); }); diff --git a/Synthesis.Bethesda.GUI/ViewModels/Initialization/SolutionPatcherInitVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Initialization/SolutionPatcherInitVM.cs index 6fa54486..6e361681 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Initialization/SolutionPatcherInitVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Initialization/SolutionPatcherInitVM.cs @@ -4,6 +4,7 @@ using ReactiveUI.Fody.Helpers; using System; using System.Collections.Generic; +using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; @@ -23,8 +24,8 @@ public class SolutionPatcherInitVM : PatcherInitVM [Reactive] public int SelectedIndex { get; set; } - private readonly ObservableAsPropertyHelper?> _TargetSolutionInitializer; - public Func? TargetSolutionInitializer => _TargetSolutionInitializer.Value; + private readonly ObservableAsPropertyHelper _TargetSolutionInitializer; + public ASolutionInitializer.InitializerCall? TargetSolutionInitializer => _TargetSolutionInitializer.Value; [Reactive] public bool OpenCodeAfter { get; set; } @@ -52,7 +53,7 @@ public SolutionPatcherInitVM(ProfileVM profile) .Replay(1) .RefCount(); _TargetSolutionInitializer = initializer - .Select(x => x.Succeeded ? x.Value : default(Func?)) + .Select(x => x.Succeeded ? x.Value : default(ASolutionInitializer.InitializerCall?)) .ToGuiProperty(this, nameof(TargetSolutionInitializer)); _CanCompleteConfiguration = initializer .Select(x => (ErrorResponse)x) @@ -69,15 +70,17 @@ public override void Dispose() public override async IAsyncEnumerable Construct() { if (TargetSolutionInitializer == null) yield break; - var ret = new SolutionPatcherVM(Profile); - await TargetSolutionInitializer(ret); - yield return ret; + var ret = (await TargetSolutionInitializer(Profile)).ToList(); + foreach (var item in ret) + { + yield return item; + } - if (OpenCodeAfter) + if (OpenCodeAfter && ret.Count > 0) { try { - IdeLocator.OpenSolution(ret.SolutionPath.TargetPath, MVM.Ide); + IdeLocator.OpenSolution(ret[0].SolutionPath.TargetPath, MVM.Ide); } catch (Exception) { diff --git a/Synthesis.Bethesda.GUI/Views/Initialization/SolutionInitView.xaml b/Synthesis.Bethesda.GUI/Views/Initialization/SolutionInitView.xaml index a9429ac1..43fd7f9b 100644 --- a/Synthesis.Bethesda.GUI/Views/Initialization/SolutionInitView.xaml +++ b/Synthesis.Bethesda.GUI/Views/Initialization/SolutionInitView.xaml @@ -113,26 +113,25 @@ Text="Solution Path" /> - - + Text="Patcher Projects" /> + + - + - - - - - + + diff --git a/Synthesis.Bethesda.GUI/Views/Initialization/SolutionInitView.xaml.cs b/Synthesis.Bethesda.GUI/Views/Initialization/SolutionInitView.xaml.cs index c7f6da9b..6d2a360f 100644 --- a/Synthesis.Bethesda.GUI/Views/Initialization/SolutionInitView.xaml.cs +++ b/Synthesis.Bethesda.GUI/Views/Initialization/SolutionInitView.xaml.cs @@ -5,6 +5,10 @@ using System.Reactive; using System; using Noggog; +using System.Windows.Controls.Primitives; +using System.Linq; +using DynamicData; +using System.Windows; namespace Synthesis.Bethesda.GUI.Views { @@ -56,9 +60,25 @@ public SolutionInitView() this.WhenAnyValue(x => x.ViewModel.ExistingProject.SolutionPath) .BindToStrict(this, x => x.BothExistingSolutionPathPicker.PickerVM) .DisposeWith(dispose); - this.ProjectsPickerBox.ItemsSource = this.ViewModel.ExistingProject.ProjectsDisplay; - this.ProjectsPickerBox.SelectedItem = this.ViewModel.ExistingProject.ProjectSubpath; - this.BindStrict(this.ViewModel, vm => vm.ExistingProject.ProjectSubpath, view => view.ProjectsPickerBox.SelectedItem) + var vis = this.WhenAnyValue(x => x.ViewModel.ExistingProject.SolutionPath.ErrorState) + .Select(x => x.Succeeded ? Visibility.Visible : Visibility.Collapsed); + vis.BindToStrict(this, x => x.AvailableProjects.Visibility) + .DisposeWith(dispose); + vis.BindToStrict(this, x => x.AvailableProjectsText.Visibility) + .DisposeWith(dispose); + this.WhenAnyValue(x => x.ViewModel.ExistingProject.AvailableProjects) + .BindToStrict(this, x => x.AvailableProjects.ItemsSource) + .DisposeWith(dispose); + this.AvailableProjects.Events().SelectionChanged + .Throttle(TimeSpan.FromMilliseconds(300), RxApp.MainThreadScheduler) + .WithLatestFrom( + this.WhenAnyValue(x => x.ViewModel), + (change, vm) => (change, vm)) + .Subscribe(u => + { + u.vm.ExistingProject.SelectedProjects.Clear(); + u.vm.ExistingProject.SelectedProjects.AddRange(this.AvailableProjects.SelectedItems.Cast()); + }) .DisposeWith(dispose); // Bind open after checkbox From a6e2ede7d3b219bf3613aa3160652898a9bfce62 Mon Sep 17 00:00:00 2001 From: Justin Swanson Date: Sat, 19 Sep 2020 19:13:13 -0500 Subject: [PATCH 11/23] Initial Github patcher setup --- .../Patcher Runs/GithubPatcherRun.cs | 115 +++++ .../Patcher Runs/SolutionPatcherRun.cs | 2 +- .../Settings/GithubPatcherSettings.cs | 24 +- .../Synthesis.Bethesda.Execution.csproj | 3 +- .../Synthesis.Bethesda.GUI.csproj | 7 +- Synthesis.Bethesda.GUI/Utility.cs | 27 ++ .../ViewModels/Config/ConfigurationStateVM.cs | 56 ++- .../ViewModels/Config/ConfigurationVM.cs | 5 +- .../ViewModels/Config/GithubPatcherVM.cs | 429 +++++++++++++++++- .../ViewModels/Config/PatcherVM.cs | 7 +- .../ViewModels/Config/ProfileVM.cs | 10 +- .../ViewModels/Config/SolutionPatcherVM.cs | 191 ++++---- .../Initialization/ExistingProjectInitVM.cs | 14 +- .../Initialization/GithubPatcherInitVM.cs | 42 +- .../Initialization/PatcherInitVM.cs | 4 + .../Views/Config/GithubConfigView.xaml | 165 ++++++- .../Views/Config/GithubConfigView.xaml.cs | 76 +++- .../Views/Config/PatchersConfigView.xaml | 5 +- .../Views/Config/SolutionConfigView.xaml.cs | 4 +- 19 files changed, 1019 insertions(+), 167 deletions(-) create mode 100644 Synthesis.Bethesda.Execution/Patcher Runs/GithubPatcherRun.cs create mode 100644 Synthesis.Bethesda.GUI/Utility.cs diff --git a/Synthesis.Bethesda.Execution/Patcher Runs/GithubPatcherRun.cs b/Synthesis.Bethesda.Execution/Patcher Runs/GithubPatcherRun.cs new file mode 100644 index 00000000..851360cf --- /dev/null +++ b/Synthesis.Bethesda.Execution/Patcher Runs/GithubPatcherRun.cs @@ -0,0 +1,115 @@ +using LibGit2Sharp; +using Mutagen.Bethesda; +using Noggog; +using Synthesis.Bethesda.Execution.Patchers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reactive.Subjects; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Synthesis.Bethesda.Execution +{ + public class GithubPatcherRun : IPatcherRun + { + public string Name { get; } + private readonly string _nickname; + private readonly string _remote; + private readonly string _localDir; + private readonly string _pathToSln; + private readonly string _pathToProj; + private readonly string _pathToExe; + public SolutionPatcherRun? SolutionRun { get; private set; } + + private Subject _output = new Subject(); + public IObservable Output => _output; + + private Subject _error = new Subject(); + public IObservable Error => _error; + + public GithubPatcherRun(string nickname, string remote, string localDir, string pathToSln, string pathToProj, string pathToExe) + { + _nickname = nickname; + _remote = remote; + _localDir = localDir; + _pathToProj = pathToProj; + _pathToSln = pathToSln; + _pathToExe = pathToExe; + Name = $"{nickname} => {remote} => {Path.GetFileNameWithoutExtension(pathToProj)}"; + } + + public void Dispose() + { + } + + public async Task Prep(GameRelease release, CancellationToken? cancel = null) + { + var prepResult = await PrepRepo(GetResponse.Succeed(_remote), _localDir, cancel ?? CancellationToken.None); + if (prepResult.Failed) + { + throw new SynthesisBuildFailure(prepResult.Reason); + } + SolutionRun = new SolutionPatcherRun(_nickname, Path.Combine(_localDir, _pathToSln), Path.Combine(_localDir, _pathToProj), Path.Combine(_localDir, _pathToExe)); + await SolutionRun.Prep(release, cancel).ConfigureAwait(false); + } + + public async Task Run(RunSynthesisPatcher settings, CancellationToken? cancel = null) + { + if (SolutionRun == null) + { + throw new SynthesisBuildFailure("Expected Solution Run object did not exist."); + } + await SolutionRun.Run(settings, cancel).ConfigureAwait(false); + } + + private static bool DeleteOldRepo(string localDir, GetResponse remoteUrl) + { + if (!Directory.Exists(localDir)) return false; + var dirInfo = new DirectoryPath(localDir); + if (remoteUrl.Failed) + { + dirInfo.DeleteEntireFolder(); + return false; + } + try + { + using var repo = new Repository(localDir); + // If it's the same remote repo, don't delete + if (repo.Network.Remotes.FirstOrDefault()?.Url.Equals(remoteUrl.Value) ?? false) return true; + int wer = 23; + wer++; + } + catch (Exception ex) + { + int wer = 23; + wer++; + } + dirInfo.DeleteEntireFolder(); + return false; + } + + public static async Task> PrepRepo(GetResponse remote, string localDir, CancellationToken cancel) + { + try + { + cancel.ThrowIfCancellationRequested(); + if (DeleteOldRepo(localDir: localDir, remoteUrl: remote)) + { + // Short circuiting deletion + return GetResponse<(string Remote, string Local)>.Succeed((remote.Value, localDir), remote.Reason); + } + cancel.ThrowIfCancellationRequested(); + if (remote.Failed) return GetResponse<(string Remote, string Local)>.Fail((remote.Value, string.Empty), remote.Reason); + var clonePath = Repository.Clone(remote.Value, localDir); + return GetResponse<(string Remote, string Local)>.Succeed((remote.Value, clonePath), remote.Reason); + } + catch (Exception ex) + { + return GetResponse<(string Remote, string Local)>.Fail((remote.Value, string.Empty), ex); + } + } + } +} diff --git a/Synthesis.Bethesda.Execution/Patcher Runs/SolutionPatcherRun.cs b/Synthesis.Bethesda.Execution/Patcher Runs/SolutionPatcherRun.cs index 10a1ded2..b3791cd5 100644 --- a/Synthesis.Bethesda.Execution/Patcher Runs/SolutionPatcherRun.cs +++ b/Synthesis.Bethesda.Execution/Patcher Runs/SolutionPatcherRun.cs @@ -38,7 +38,7 @@ public SolutionPatcherRun(string nickname, string pathToSln, string pathToProj, PathToSolution = pathToSln; PathToProject = pathToProj; PathToExe = pathToExe; - Name = $"{Path.GetFileNameWithoutExtension(pathToSln)} => {Path.GetFileNameWithoutExtension(pathToProj)}"; + Name = $"{nickname} => {Path.GetFileNameWithoutExtension(pathToSln)} => {Path.GetFileNameWithoutExtension(pathToProj)}"; } public async Task Prep(GameRelease release, CancellationToken? cancel = null) diff --git a/Synthesis.Bethesda.Execution/Settings/GithubPatcherSettings.cs b/Synthesis.Bethesda.Execution/Settings/GithubPatcherSettings.cs index d30ec171..ac87e85e 100644 --- a/Synthesis.Bethesda.Execution/Settings/GithubPatcherSettings.cs +++ b/Synthesis.Bethesda.Execution/Settings/GithubPatcherSettings.cs @@ -1,11 +1,31 @@ -using System; +using System; using System.Collections.Generic; using System.Text; namespace Synthesis.Bethesda.Execution.Settings { + public enum PatcherVersioningEnum + { + Master, + Tag, + Commit, + } + + public enum MutagenVersioningEnum + { + Match, + Latest, + Manual, + } + public class GithubPatcherSettings : PatcherSettings { - public string RepoPath = string.Empty; + public string ID = string.Empty; + public string RemoteRepoPath = string.Empty; + public string SelectedProjectSubpath = string.Empty; + public PatcherVersioningEnum PatcherVersioning = PatcherVersioningEnum.Master; + public MutagenVersioningEnum MutagenVersioning = MutagenVersioningEnum.Match; + public string TargetTag = string.Empty; + public string TargetCommit = string.Empty; } } diff --git a/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj b/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj index 5f9041fc..99405693 100644 --- a/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj +++ b/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj @@ -23,6 +23,7 @@ + @@ -30,7 +31,7 @@ - + diff --git a/Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj b/Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj index 87ee11e3..0d521365 100644 --- a/Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj +++ b/Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj @@ -38,8 +38,9 @@ + - + @@ -65,4 +66,8 @@ + + + + \ No newline at end of file diff --git a/Synthesis.Bethesda.GUI/Utility.cs b/Synthesis.Bethesda.GUI/Utility.cs new file mode 100644 index 00000000..37204a12 --- /dev/null +++ b/Synthesis.Bethesda.GUI/Utility.cs @@ -0,0 +1,27 @@ +using Buildalyzer; +using Noggog; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Synthesis.Bethesda.GUI +{ + public static class Utility + { + public static IEnumerable AvailableProjectSubpaths(string solutionPath) + { + if (!File.Exists(solutionPath)) return Enumerable.Empty(); + try + { + var manager = new AnalyzerManager(solutionPath); + return manager.Projects.Keys.Select(projPath => projPath.TrimStart($"{Path.GetDirectoryName(solutionPath)}\\"!)); + } + catch (Exception) + { + return Enumerable.Empty(); + } + } + } +} diff --git a/Synthesis.Bethesda.GUI/ViewModels/Config/ConfigurationStateVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Config/ConfigurationStateVM.cs index bcee7bdc..f6d41f19 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Config/ConfigurationStateVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Config/ConfigurationStateVM.cs @@ -2,24 +2,72 @@ using Noggog.WPF; using System; using System.Collections.Generic; +using System.Reactive; using System.Text; namespace Synthesis.Bethesda.GUI { - public class ConfigurationStateVM : ViewModel + public class ConfigurationStateVM : ConfigurationStateVM { public static readonly ConfigurationStateVM Success = new ConfigurationStateVM(); + public ConfigurationStateVM() + : base(Unit.Default) + { + } + + public ConfigurationStateVM(ErrorResponse err) + : base(Unit.Default, err) + { + } + } + + public class ConfigurationStateVM : ViewModel + { public bool IsHaltingError { get; set; } public ErrorResponse RunnableState { get; set; } = ErrorResponse.Success; + public T Item { get; } + + public ConfigurationStateVM(T item) + { + Item = item; + } - public static implicit operator ConfigurationStateVM(ErrorResponse err) + public ConfigurationStateVM(T item, ErrorResponse err) + : this(item) + { + IsHaltingError = err.Failed; + RunnableState = err; + } + + public ConfigurationStateVM(GetResponse resp) + : this(resp.Value) + { + IsHaltingError = resp.Failed; + RunnableState = resp; + } + + public ConfigurationStateVM ToUnit() { return new ConfigurationStateVM() { - IsHaltingError = err.Failed, - RunnableState = err, + IsHaltingError = this.IsHaltingError, + RunnableState = this.RunnableState, + }; + } + + public ConfigurationStateVM BubbleError() + { + return new ConfigurationStateVM(default!) + { + IsHaltingError = this.IsHaltingError, + RunnableState = this.RunnableState }; } + + public GetResponse ToGetResponse() + { + return GetResponse.Create(RunnableState.Succeeded, Item, RunnableState.Reason); + } } } diff --git a/Synthesis.Bethesda.GUI/ViewModels/Config/ConfigurationVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Config/ConfigurationVM.cs index 99745315..dec8cf4d 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Config/ConfigurationVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Config/ConfigurationVM.cs @@ -47,8 +47,7 @@ public class ConfigurationVM : ViewModel private readonly ObservableAsPropertyHelper _CurrentRun; public PatchersRunVM? CurrentRun => _CurrentRun.Value; - [Reactive] - public string WorkingDirectory { get; set; } = Path.Combine(Path.GetTempPath(), "Synthesis"); + public string WorkingDirectory { get; } = Path.Combine(Path.GetTempPath(), "Synthesis"); [Reactive] public bool ShowHelp { get; set; } @@ -86,7 +85,7 @@ public ConfigurationVM(MainVM mvm) CancelConfiguration = ReactiveCommand.Create( () => { - // Just forget about patcher and let it GC + NewPatcher?.Cancel(); NewPatcher = null; }); diff --git a/Synthesis.Bethesda.GUI/ViewModels/Config/GithubPatcherVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Config/GithubPatcherVM.cs index 042e1d5a..28bf1208 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Config/GithubPatcherVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Config/GithubPatcherVM.cs @@ -6,29 +6,85 @@ using System; using System.Collections.Generic; using System.Reactive.Linq; -using System.Text; -using Synthesis.Bethesda.Execution.Patchers; +using LibGit2Sharp; +using System.IO; +using System.Linq; +using Synthesis.Bethesda.Execution; +using DynamicData.Binding; +using DynamicData; +using static Synthesis.Bethesda.GUI.SolutionPatcherVM; +using Microsoft.WindowsAPICodePack.Dialogs; +using System.Threading.Tasks; +using System.Reactive; namespace Synthesis.Bethesda.GUI { public class GithubPatcherVM : PatcherVM { [Reactive] - public string RepoPath { get; set; } = string.Empty; + public string RemoteRepoPath { get; set; } = string.Empty; private readonly ObservableAsPropertyHelper _DisplayName; public override string DisplayName => _DisplayName.Value; - public override ConfigurationStateVM State => new ConfigurationStateVM(); + private readonly ObservableAsPropertyHelper _State; + public override ConfigurationStateVM State => _State.Value; + + public string ID { get; private set; } = string.Empty; + + public string LocalDriverRepoDirectory { get; } + public string LocalRunnerRepoDirectory { get; } + + private readonly ObservableAsPropertyHelper _RepoValidity; + public ErrorResponse RepoValidity => _RepoValidity.Value; + + private readonly ObservableAsPropertyHelper _RepoClonesValid; + public bool RepoClonesValid => _RepoClonesValid.Value; + + public IObservableCollection AvailableProjects { get; } + + [Reactive] + public string ProjectSubpath { get; set; } = string.Empty; + + public PathPickerVM SelectedProjectPath { get; } = new PathPickerVM() + { + ExistCheckOption = PathPickerVM.CheckOptions.On, + PathType = PathPickerVM.PathTypeOptions.File, + }; + + [Reactive] + public PatcherVersioningEnum PatcherVersioning { get; set; } = PatcherVersioningEnum.Master; + + [Reactive] + public MutagenVersioningEnum MutagenVersioning { get; set; } = MutagenVersioningEnum.Match; + + public IObservableCollection AvailableTags { get; } + + [Reactive] + public string TargetTag { get; set; } = string.Empty; + + [Reactive] + public string TargetCommit { get; set; } = string.Empty; + + private readonly ObservableAsPropertyHelper _RunnableData; + public RunnerRepoInfo? RunnableData => _RunnableData.Value; + + private readonly ObservableAsPropertyHelper _ExePath; + public string ExePath => _ExePath.Value; public GithubPatcherVM(ProfileVM parent, GithubPatcherSettings? settings = null) : base(parent, settings) { + SelectedProjectPath.Filters.Add(new CommonFileDialogFilter("Project", ".csproj")); + CopyInSettings(settings); + LocalDriverRepoDirectory = Path.Combine(Profile.ProfileDirectory, "Git", ID, "Driver"); + LocalRunnerRepoDirectory = Path.Combine(Profile.ProfileDirectory, "Git", ID, "Runner"); + _DisplayName = this.WhenAnyValue( x => x.Nickname, - x => x.RepoPath, + x => x.RemoteRepoPath, (nickname, path) => { if (!string.IsNullOrWhiteSpace(nickname)) return nickname; @@ -49,25 +105,378 @@ public GithubPatcherVM(ProfileVM parent, GithubPatcherSettings? settings = null) } }) .ToGuiProperty(this, nameof(DisplayName)); + + // Check to see if remote path points to a reachable git repository + var remoteRepoPath = GetRepoPathValidity(this.WhenAnyValue(x => x.RemoteRepoPath)) + .Replay(1) + .RefCount(); + _RepoValidity = remoteRepoPath + .Select(r => r.RunnableState) + .ToGuiProperty(this, nameof(RepoValidity)); + + // Clone repository to a folder where driving information will be retreived from master. + // This will be where we get available projects + tags, etc. + var driverRepoInfo = remoteRepoPath + .Throttle(TimeSpan.FromMilliseconds(100), RxApp.MainThreadScheduler) + .ObserveOn(RxApp.TaskpoolScheduler) + .SelectReplaceWithIntermediate( + new ConfigurationStateVM(default!) + { + IsHaltingError = false, + RunnableState = ErrorResponse.Fail("Cloning driver repository"), + }, + async (item, cancel) => + { + if (!item.IsHaltingError && item.RunnableState.Failed) return item.BubbleError(); + // Clone and/or double check the clone is correct + var state = await GithubPatcherRun.PrepRepo(item.ToGetResponse(), LocalDriverRepoDirectory, cancel); + if (state.Failed) return new ConfigurationStateVM(default!, (ErrorResponse)state); + cancel.ThrowIfCancellationRequested(); + + // Grab all the interesting metadata + List<(int Index, string Name)> tags; + string masterBranch; + try + { + using var repo = new Repository(LocalDriverRepoDirectory); + var master = repo.Branches.Where(b => b.IsCurrentRepositoryHead).FirstOrDefault(); + masterBranch = master.FriendlyName; + repo.Reset(ResetMode.Hard); + Commands.Checkout(repo, master); + Configuration config = repo.Config; + Signature author = config.BuildSignature(DateTimeOffset.Now); + Commands.Pull(repo, author, null); + tags = repo.Tags.Select(tag => tag.FriendlyName).WithIndex().ToList(); + } + catch (Exception ex) + { + return new ConfigurationStateVM(default!, ErrorResponse.Fail(ex)); + } + + // Try to locate a solution to drive from + var slnPath = GetPathToSolution(LocalDriverRepoDirectory); + if (slnPath == null) return new ConfigurationStateVM(default!, ErrorResponse.Fail("Could not locate solution to run.")); + var availableProjs = Utility.AvailableProjectSubpaths(slnPath).ToList(); + return new ConfigurationStateVM( + new DriverRepoInfo( + slnPath: slnPath, + masterBranchName: masterBranch, + tags: tags, + availableProjects: availableProjs)); + }) + .Replay(1) + .RefCount(); + + // Clone a second repository that we will check out the desired target commit to actually run + var runnerRepoState = remoteRepoPath + .Throttle(TimeSpan.FromMilliseconds(100), RxApp.MainThreadScheduler) + .ObserveOn(RxApp.TaskpoolScheduler) + .SelectReplaceWithIntermediate( + ErrorResponse.Fail("Cloning runner repository"), + async (item, cancel) => + { + if (item.RunnableState.Failed) return item.RunnableState; + return await GithubPatcherRun.PrepRepo(item.ToGetResponse(), LocalRunnerRepoDirectory, cancel); + }) + .Replay(1) + .RefCount(); + + // Expose a lot of the metadata + _RepoClonesValid = Observable.CombineLatest( + driverRepoInfo, + runnerRepoState, + (driver, runner) => driver.RunnableState.Succeeded && runner.Succeeded) + .ToGuiProperty(this, nameof(RepoClonesValid)); + + AvailableProjects = driverRepoInfo + .Select(x => x.Item?.AvailableProjects ?? Enumerable.Empty()) + .Select(x => x.AsObservableChangeSet()) + .Switch() + .ToObservableCollection(this); + + var tagInput = Observable.CombineLatest( + this.WhenAnyValue(x => x.SelectedProjectPath.TargetPath), + this.WhenAnyValue(x => x.AvailableProjects.Count), + (targetPath, count) => (targetPath, count)); + AvailableTags = driverRepoInfo + .Select(x => x.Item?.Tags ?? Enumerable.Empty<(int Index, string Name)>()) + .Select(x => x.AsObservableChangeSet()) + .Switch() + .Filter( + tagInput.Select(x => + { + if (x.count == 0) return new Func<(int Index, string Name), bool>(_ => false); + if (x.count == 1) return new Func<(int Index, string Name), bool>(_ => true); + if (!x.targetPath.EndsWith(".csproj")) return new Func<(int Index, string Name), bool>(_ => false); + var projName = Path.GetFileName(x.targetPath); + return new Func<(int Index, string Name), bool>(i => i.Name.StartsWith(projName, StringComparison.OrdinalIgnoreCase)); + })) + .Sort(SortExpressionComparer<(int Index, string Name)>.Descending(x => x.Index)) + .Transform(x => x.Name) + .ToObservableCollection(this); + + var projPath = SolutionPatcherConfigLogic.ProjectPath( + driverRepoInfo + .Select(x => x.Item?.SolutionPath ?? string.Empty), + this.WhenAnyValue(x => x.ProjectSubpath)); + + projPath + .Subscribe(p => SelectedProjectPath.TargetPath = p) + .DisposeWith(this); + + // Checkout desired patcher commit on the runner repository + var checkoutInput = Observable.CombineLatest( + driverRepoInfo.Select(x => x.RunnableState.Failed ? string.Empty : x.Item.MasterBranchName), + this.WhenAnyValue(x => x.PatcherVersioning), + runnerRepoState, + SelectedProjectPath.PathState() + .Select(x => x.Succeeded ? x : GetResponse.Fail("No patcher project selected.")), + this.WhenAnyValue(x => x.TargetTag), + this.WhenAnyValue(x => x.TargetCommit), + (master, versioning, runnerState, proj, tag, commit) => (master, versioning, runnerState, proj, tag, commit)) + .Replay(1) + .RefCount(); + var runnableState = checkoutInput + .Throttle(TimeSpan.FromMilliseconds(150), RxApp.MainThreadScheduler) + .DistinctUntilChanged() + .ObserveOn(RxApp.TaskpoolScheduler) + .SelectReplaceWithIntermediate( + new ConfigurationStateVM(default!) + { + RunnableState = ErrorResponse.Fail("Checking out the proper commit"), + IsHaltingError = false, + }, + async (item, cancel) => + { + async Task> Execute() + { + if (item.runnerState.Failed) return item.runnerState.BubbleFailure(); + if (item.proj.Failed) return item.proj.BubbleFailure(); + cancel.ThrowIfCancellationRequested(); + try + { + const string RunnerBranch = "SynthesisRunner"; + using var repo = new Repository(LocalRunnerRepoDirectory); + var branch = repo.Branches[RunnerBranch] ?? repo.CreateBranch(RunnerBranch); + repo.Reset(ResetMode.Hard); + Commands.Checkout(repo, branch); + string? targetSha; + switch (item.versioning) + { + case PatcherVersioningEnum.Master: + targetSha = repo.Branches + .Where(b => b.FriendlyName == item.master) + .FirstOrDefault() + ?.Tip.Sha; + if (string.IsNullOrWhiteSpace(targetSha)) return GetResponse.Fail("Could not locate master commit"); + break; + case PatcherVersioningEnum.Tag: + if (string.IsNullOrWhiteSpace(item.tag)) return GetResponse.Fail("No tag selected"); + targetSha = repo.Tags[item.tag]?.Target.Sha; + if (string.IsNullOrWhiteSpace(targetSha)) return GetResponse.Fail("Could not locate tag"); + break; + case PatcherVersioningEnum.Commit: + targetSha = item.commit; + if (string.IsNullOrWhiteSpace(targetSha)) return GetResponse.Fail("Could not locate commit"); + break; + default: + throw new NotImplementedException(); + } + if (!ObjectId.TryParse(targetSha, out var objId)) return GetResponse.Fail("Malformed sha string"); + + var commit = repo.Lookup(objId, ObjectType.Commit) as Commit; + if (commit == null) return GetResponse.Fail("Could not locate commit with given sha"); + + var slnPath = GetPathToSolution(LocalRunnerRepoDirectory); + if (slnPath == null) return GetResponse.Fail("Could not locate solution to run."); + + var projName = Path.GetFileName(item.proj.Value); + + var availableProjs = SolutionPatcherConfigLogic.AvailableProject(slnPath).ToList(); + + var foundProjSubPath = availableProjs + .Where(av => Path.GetFileName(av).Equals(projName)) + .FirstOrDefault(); + + if (foundProjSubPath == null) return GetResponse.Fail($"Could not locate target project file: {projName}."); + + repo.Reset(ResetMode.Hard, commit, new CheckoutOptions()); + return GetResponse.Succeed( + new RunnerRepoInfo( + slnPath: slnPath, + projPath: Path.Combine(LocalDriverRepoDirectory, foundProjSubPath), + commitMsg: commit.Message, + commitDate: commit.Author.When.LocalDateTime)); + } + catch (Exception ex) + { + return GetResponse.Fail(ex); + } + } + return new ConfigurationStateVM(await Execute()); + }) + .Replay(1) + .RefCount(); + + _RunnableData = runnableState + .Select(x => x.RunnableState.Succeeded ? x.Item : default(RunnerRepoInfo?)) + .ToGuiProperty(this, nameof(RunnableData)); + + _ExePath = runnableState + .SelectReplace(async (x, cancel) => + { + if (x.RunnableState.Failed) return string.Empty; + var exePath = await SolutionPatcherConfigLogic.PathToExe(x.Item.ProjPath, cancel); + if (exePath.Failed) return string.Empty; + return exePath.Value; + }) + .ToGuiProperty(this, nameof(ExePath)); + + _State = Observable.CombineLatest( + driverRepoInfo + .Select(x => x.ToUnit()), + runnerRepoState + .Select(x => new ConfigurationStateVM(x)), + runnableState + .Select(x => x.ToUnit()), + (driver, runner, checkout) => + { + if (driver.IsHaltingError) return driver; + if (runner.IsHaltingError) return runner; + return checkout; + }) + .ToGuiProperty(this, nameof(State), ConfigurationStateVM.Success); } public override PatcherSettings Save() { - var ret = new GithubPatcherSettings(); + var ret = new GithubPatcherSettings + { + RemoteRepoPath = this.RemoteRepoPath, + ID = this.ID, + SelectedProjectSubpath = this.ProjectSubpath, + PatcherVersioning = this.PatcherVersioning, + MutagenVersioning = this.MutagenVersioning, + TargetTag = this.TargetTag, + TargetCommit = this.TargetCommit, + }; CopyOverSave(ret); - ret.RepoPath = this.RepoPath; return ret; } private void CopyInSettings(GithubPatcherSettings? settings) { - if (settings == null) return; - this.RepoPath = settings.RepoPath; + if (settings == null) + { + this.ID = Guid.NewGuid().ToString(); + return; + } + this.RemoteRepoPath = settings.RemoteRepoPath; + this.ID = string.IsNullOrWhiteSpace(settings.ID) ? Guid.NewGuid().ToString() : settings.ID; + this.ProjectSubpath = settings.SelectedProjectSubpath; + this.PatcherVersioning = settings.PatcherVersioning; + this.MutagenVersioning = settings.MutagenVersioning; + this.TargetTag = settings.TargetTag; + this.TargetCommit = settings.TargetCommit; } public override PatcherRunVM ToRunner(PatchersRunVM parent) { - throw new NotImplementedException(); + if (RunnableData == null) + { + throw new ArgumentNullException(nameof(RunnableData)); + } + return new PatcherRunVM( + parent, + this, + new SolutionPatcherRun( + nickname: DisplayName, + pathToExe: ExePath, + pathToSln: RunnableData.SolutionPath, + pathToProj: SelectedProjectPath.TargetPath)); + } + + public static IObservable> GetRepoPathValidity(IObservable repoPath) + { + return repoPath + .DistinctUntilChanged() + .Select(x => new ConfigurationStateVM(string.Empty) + { + IsHaltingError = false, + RunnableState = ErrorResponse.Fail("Checking remote repository correctness.") + }) + // But merge in the work of checking the repo on that same path to get the eventual result + .Merge(repoPath + .DistinctUntilChanged() + .Debounce(TimeSpan.FromMilliseconds(300), RxApp.MainThreadScheduler) + .ObserveOn(RxApp.TaskpoolScheduler) + .Select(p => + { + try + { + if (Repository.ListRemoteReferences(p).Any()) return new ConfigurationStateVM(p); + } + catch (Exception) + { + } + return new ConfigurationStateVM(string.Empty, ErrorResponse.Fail("Path does not point to a valid repository.")); + })); + } + + public override void Delete() + { + base.Delete(); + var dir = new DirectoryInfo(this.LocalDriverRepoDirectory); + dir.DeleteEntireFolder(); + dir = new DirectoryInfo(this.LocalRunnerRepoDirectory); + dir.DeleteEntireFolder(); + // ToDo + // Handle failure reporting + } + + private static string GetPathToSolution(string pathToRepo) + { + return Directory.EnumerateFiles(pathToRepo, "*.sln").FirstOrDefault(); + } + + private class DriverRepoInfo + { + public readonly string SolutionPath; + public readonly List<(int Index, string Name)> Tags; + public readonly List AvailableProjects; + public readonly string MasterBranchName; + + public DriverRepoInfo( + string slnPath, + string masterBranchName, + List<(int Index, string Name)> tags, + List availableProjects) + { + SolutionPath = slnPath; + Tags = tags; + MasterBranchName = masterBranchName; + AvailableProjects = availableProjects; + } + } + + public class RunnerRepoInfo + { + public readonly string SolutionPath; + public readonly string ProjPath; + public readonly string CommitMessage; + public readonly DateTime CommitDate; + + public RunnerRepoInfo( + string slnPath, + string projPath, + string commitMsg, + DateTime commitDate) + { + SolutionPath = slnPath; + ProjPath = projPath; + CommitMessage = commitMsg; + CommitDate = commitDate; + } } } } diff --git a/Synthesis.Bethesda.GUI/ViewModels/Config/PatcherVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Config/PatcherVM.cs index 679dc373..684d161f 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Config/PatcherVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Config/PatcherVM.cs @@ -55,7 +55,7 @@ public PatcherVM(ProfileVM parent, PatcherSettings? settings) parent.Config.MainVM.ActiveConfirmation = new ConfirmationActionVM( "Confirm", $"Are you sure you want to delete {DisplayName}?", - () => parent.Patchers.Remove(this)); + Delete); }); } @@ -68,5 +68,10 @@ protected void CopyOverSave(PatcherSettings settings) } public abstract PatcherRunVM ToRunner(PatchersRunVM parent); + + public virtual void Delete() + { + Profile.Patchers.Remove(this); + } } } diff --git a/Synthesis.Bethesda.GUI/ViewModels/Config/ProfileVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Config/ProfileVM.cs index 8007ef1a..cce25ec6 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Config/ProfileVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Config/ProfileVM.cs @@ -35,8 +35,8 @@ public class ProfileVM : ViewModel [Reactive] public string Nickname { get; set; } = string.Empty; - private readonly ObservableAsPropertyHelper _WorkingDirectory; - public string WorkingDirectory => _WorkingDirectory.Value; + public string ProfileDirectory { get; } + public string WorkingDirectory { get; } private readonly ObservableAsPropertyHelper _DataFolder; public string DataFolder => _DataFolder.Value; @@ -61,9 +61,9 @@ public ProfileVM(ConfigurationVM parent, GameRelease? release = null, string? id AddSolutionPatcherCommand = ReactiveCommand.Create(() => SetInitializer(new SolutionPatcherInitVM(this))); AddCliPatcherCommand = ReactiveCommand.Create(() => SetInitializer(new CliPatcherInitVM(this))); AddSnippetPatcherCommand = ReactiveCommand.Create(() => SetPatcherForInitialConfiguration(new CodeSnippetPatcherVM(this))); - _WorkingDirectory = this.WhenAnyValue(x => x.Config.WorkingDirectory) - .Select(dir => Path.Combine(dir, ID, "Workspace")) - .ToGuiProperty(this, nameof(WorkingDirectory)); + + ProfileDirectory = Path.Combine(Config.WorkingDirectory, ID); + WorkingDirectory = Path.Combine(Config.WorkingDirectory, ID, "Workspace"); var dataFolderResult = this.WhenAnyValue(x => x.Release) .ObserveOn(RxApp.TaskpoolScheduler) diff --git a/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs index fec69176..cd0db485 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Config/SolutionPatcherVM.cs @@ -21,6 +21,7 @@ using Buildalyzer.Environment; using System.Reactive; using Microsoft.Build.Evaluation; +using System.Threading; namespace Synthesis.Bethesda.GUI { @@ -32,7 +33,7 @@ public class SolutionPatcherVM : PatcherVM PathType = PathPickerVM.PathTypeOptions.File, }; - public IObservableCollection ProjectsDisplay { get; } + public IObservableCollection AvailableProjects { get; } [Reactive] public string ProjectSubpath { get; set; } = string.Empty; @@ -80,98 +81,42 @@ public SolutionPatcherVM(ProfileVM parent, SolutionPatcherSettings? settings = n }) .ToGuiProperty(this, nameof(DisplayName)); - ProjectsDisplay = this.WhenAnyValue(x => x.SolutionPath.TargetPath) - .ObserveOn(RxApp.TaskpoolScheduler) - .Select(x => - { - if (!File.Exists(x)) return Enumerable.Empty(); - try - { - var manager = new AnalyzerManager(x); - return manager.Projects.Keys.Select(projPath => projPath.TrimStart($"{Path.GetDirectoryName(x)}\\"!)); - } - catch (Exception) - { - return Enumerable.Empty(); - } - }) - .Select(x => x.AsObservableChangeSet()) - .Switch() + AvailableProjects = SolutionPatcherConfigLogic.AvailableProject( + this.WhenAnyValue(x => x.SolutionPath.TargetPath)) .ObserveOnGui() .ToObservableCollection(this); - var projPath = this.WhenAnyValue(x => x.ProjectSubpath) - // Need to throttle, as bindings flip to null quickly, which we want to skip - .Throttle(TimeSpan.FromMilliseconds(150), RxApp.MainThreadScheduler) - .DistinctUntilChanged() - .CombineLatest(this.WhenAnyValue(x => x.SolutionPath.TargetPath) - .DistinctUntilChanged(), - (subPath, slnPath) => - { - if (subPath == null || slnPath == null) return string.Empty; - try - { - return Path.Combine(Path.GetDirectoryName(slnPath)!, subPath); - } - catch (Exception) - { - return string.Empty; - } - }) - .Replay(1) - .RefCount(); - + var projPath = SolutionPatcherConfigLogic.ProjectPath( + solutionPath: this.WhenAnyValue(x => x.SolutionPath.TargetPath), + projectSubpath: this.WhenAnyValue(x => x.ProjectSubpath)); projPath .Subscribe(p => SelectedProjectPath.TargetPath = p) .DisposeWith(this); var pathToExe = projPath .ObserveOn(RxApp.TaskpoolScheduler) - .SelectReplace(async (projectPath, cancel) => - { - try + .SelectReplaceWithIntermediate( + new ConfigurationStateVM(default!) { - cancel.ThrowIfCancellationRequested(); - if (!File.Exists(projectPath)) - { - return GetResponse.Fail("Project path does not exist."); - } - // Right now this is slow as it cleans the build results unnecessarily. Need to look into that - var manager = new AnalyzerManager(); - cancel.ThrowIfCancellationRequested(); - var proj = manager.GetProject(projectPath); - cancel.ThrowIfCancellationRequested(); - var opt = new EnvironmentOptions(); - opt.TargetsToBuild.SetTo("Build"); - var build = proj.Build(); - cancel.ThrowIfCancellationRequested(); - var results = build.Results.ToArray(); - if (results.Length != 1) - { - return GetResponse.Fail("Unsupported number of build results."); - } - var result = results[0]; - if (!result.Properties.TryGetValue("RunCommand", out var cmd)) - { - return GetResponse.Fail("Could not find executable to be run"); - } + IsHaltingError = false, + RunnableState = ErrorResponse.Fail("Locating exe to run.") + }, + async (i, cancel) => + { + var exe = await SolutionPatcherConfigLogic.PathToExe(i, cancel); + if (exe.Failed) return new ConfigurationStateVM(exe.BubbleFailure()); // Now we want to build, just to prep for run - var resp = await SolutionPatcherRun.CompileWithDotnet(projectPath, cancel).ConfigureAwait(false); - if (resp.Failed) return resp.BubbleFailure(); + var build = await SolutionPatcherRun.CompileWithDotnet(i, cancel).ConfigureAwait(false); + if (build.Failed) return new ConfigurationStateVM(build.BubbleFailure()); - return GetResponse.Succeed(cmd); - } - catch (Exception ex) - { - return GetResponse.Fail(ex); - } - }) + return new ConfigurationStateVM(exe); + }) .Replay(1) .RefCount(); _PathToExe = pathToExe - .Select(r => r.Value) + .Select(r => r.Item ?? string.Empty) .ToGuiProperty(this, nameof(PathToExe)); _State = Observable.CombineLatest( @@ -185,12 +130,12 @@ public SolutionPatcherVM(ProfileVM parent, SolutionPatcherSettings? settings = n RunnableState = ErrorResponse.Fail("Building") }), pathToExe - .Select(p => (ConfigurationStateVM)(ErrorResponse)p)), + .Select(i => i.ToUnit())), (sln, proj, exe) => { - if (sln.Failed) return sln; + if (sln.Failed) return new ConfigurationStateVM(sln); if (exe.RunnableState.Failed) return exe; - return proj; + return new ConfigurationStateVM(proj); }) .ToGuiProperty(this, nameof(State), ConfigurationStateVM.Success); @@ -241,5 +186,93 @@ public override PatcherRunVM ToRunner(PatchersRunVM parent) pathToExe: PathToExe, pathToProj: SelectedProjectPath.TargetPath)); } + + public class SolutionPatcherConfigLogic + { + public static IObservable> AvailableProject(IObservable solutionPath) + { + return solutionPath + .ObserveOn(RxApp.TaskpoolScheduler) + .Select(AvailableProject) + .Select(x => x.AsObservableChangeSet()) + .Switch() + .RefCount(); + } + + public static IEnumerable AvailableProject(string solutionPath) + { + if (!File.Exists(solutionPath)) return Enumerable.Empty(); + try + { + var manager = new AnalyzerManager(solutionPath); + return manager.Projects.Keys.Select(projPath => projPath.TrimStart($"{Path.GetDirectoryName(solutionPath)}\\"!)); + } + catch (Exception) + { + return Enumerable.Empty(); + } + } + + public static IObservable ProjectPath(IObservable solutionPath, IObservable projectSubpath) + { + return projectSubpath + // Need to throttle, as bindings flip to null quickly, which we want to skip + .Throttle(TimeSpan.FromMilliseconds(150), RxApp.MainThreadScheduler) + .DistinctUntilChanged() + .CombineLatest(solutionPath.DistinctUntilChanged(), + (subPath, slnPath) => + { + if (subPath == null || slnPath == null) return string.Empty; + try + { + return Path.Combine(Path.GetDirectoryName(slnPath)!, subPath); + } + catch (Exception) + { + return string.Empty; + } + }) + .Replay(1) + .RefCount(); + } + + public static async Task> PathToExe(string projectPath, CancellationToken cancel) + { + try + { + cancel.ThrowIfCancellationRequested(); + if (!File.Exists(projectPath)) + { + return GetResponse.Fail("Project path does not exist."); + } + + // Right now this is slow as it cleans the build results unnecessarily. Need to look into that + var manager = new AnalyzerManager(); + cancel.ThrowIfCancellationRequested(); + var proj = manager.GetProject(projectPath); + cancel.ThrowIfCancellationRequested(); + var opt = new EnvironmentOptions(); + opt.TargetsToBuild.SetTo("Build"); + var build = proj.Build(); + cancel.ThrowIfCancellationRequested(); + var results = build.Results.ToArray(); + if (results.Length != 1) + { + return GetResponse.Fail("Unsupported number of build results."); + } + var result = results[0]; + if (!result.Properties.TryGetValue("RunCommand", out var cmd)) + { + return GetResponse.Fail("Could not find executable to be run"); + } + + return GetResponse.Succeed(cmd); + } + catch (Exception ex) + { + return GetResponse.Fail(ex); + } + } + } } } diff --git a/Synthesis.Bethesda.GUI/ViewModels/Initialization/ExistingProjectInitVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Initialization/ExistingProjectInitVM.cs index 96631d15..c846c247 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Initialization/ExistingProjectInitVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Initialization/ExistingProjectInitVM.cs @@ -40,19 +40,7 @@ public ExistingProjectInitVM() AvailableProjects = this.WhenAnyValue(x => x.SolutionPath.TargetPath) .ObserveOn(RxApp.TaskpoolScheduler) - .Select(x => - { - if (!File.Exists(x)) return Enumerable.Empty(); - try - { - var manager = new AnalyzerManager(x); - return manager.Projects.Keys.Select(projPath => projPath.TrimStart($"{Path.GetDirectoryName(x)}\\"!)); - } - catch (Exception) - { - return Enumerable.Empty(); - } - }) + .Select(x => Utility.AvailableProjectSubpaths(x)) .Select(x => x.AsObservableChangeSet()) .Switch() .ObserveOnGui() diff --git a/Synthesis.Bethesda.GUI/ViewModels/Initialization/GithubPatcherInitVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Initialization/GithubPatcherInitVM.cs index 2395a3ae..f37a81f4 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Initialization/GithubPatcherInitVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Initialization/GithubPatcherInitVM.cs @@ -1,9 +1,11 @@ +using LibGit2Sharp; using Noggog; using Noggog.WPF; using ReactiveUI; using ReactiveUI.Fody.Helpers; using System; using System.Collections.Generic; +using System.Linq; using System.Reactive.Linq; namespace Synthesis.Bethesda.GUI @@ -13,42 +15,28 @@ public class GithubPatcherInitVM : PatcherInitVM private readonly ObservableAsPropertyHelper _CanCompleteConfiguration; public override ErrorResponse CanCompleteConfiguration => _CanCompleteConfiguration.Value; - [Reactive] - public string RepoPath { get; set; } = string.Empty; + public GithubPatcherVM Patcher { get; } public GithubPatcherInitVM(ProfileVM profile) : base(profile) { - // Whenever we change, mark that we cannot - _CanCompleteConfiguration = this.WhenAnyValue(x => x.RepoPath) - .DistinctUntilChanged() - .Select(x => ErrorResponse.Fail("Checking remote repository correctness.")) - // But merge in the work of checking the repo on that same path to get the eventual result - .Merge(this.WhenAnyValue(x => x.RepoPath) - .DistinctUntilChanged() - .Debounce(TimeSpan.FromMilliseconds(300), RxApp.MainThreadScheduler) - .ObserveOn(RxApp.TaskpoolScheduler) - .Select(p => - { - try - { - //if (Repository.ListRemoteReferences(p).Any()) return ErrorResponse.Success; - } - catch (Exception) - { - } - return ErrorResponse.Fail("Path does not point to a valid repository."); - })) - .Cast() + Patcher = new GithubPatcherVM(profile); + this.CompositeDisposable.Add(Patcher); + + _CanCompleteConfiguration = this.WhenAnyValue(x => x.Patcher.State) + .Select(x => x.RunnableState) .ToGuiProperty(this, nameof(CanCompleteConfiguration), ErrorResponse.Success); } public override async IAsyncEnumerable Construct() { - yield return new GithubPatcherVM(Profile) - { - RepoPath = this.RepoPath - }; + yield return Patcher; + } + + public override void Cancel() + { + base.Cancel(); + Patcher.Delete(); } } } diff --git a/Synthesis.Bethesda.GUI/ViewModels/Initialization/PatcherInitVM.cs b/Synthesis.Bethesda.GUI/ViewModels/Initialization/PatcherInitVM.cs index 1483c2dc..032940d1 100644 --- a/Synthesis.Bethesda.GUI/ViewModels/Initialization/PatcherInitVM.cs +++ b/Synthesis.Bethesda.GUI/ViewModels/Initialization/PatcherInitVM.cs @@ -21,5 +21,9 @@ public PatcherInitVM(ProfileVM profile) { Profile = profile; } + + public virtual void Cancel() + { + } } } diff --git a/Synthesis.Bethesda.GUI/Views/Config/GithubConfigView.xaml b/Synthesis.Bethesda.GUI/Views/Config/GithubConfigView.xaml index 50f39552..4ffa0890 100644 --- a/Synthesis.Bethesda.GUI/Views/Config/GithubConfigView.xaml +++ b/Synthesis.Bethesda.GUI/Views/Config/GithubConfigView.xaml @@ -1,9 +1,11 @@ - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Synthesis.Bethesda.GUI/Views/Config/GithubConfigView.xaml.cs b/Synthesis.Bethesda.GUI/Views/Config/GithubConfigView.xaml.cs index c0ebf47c..288707ce 100644 --- a/Synthesis.Bethesda.GUI/Views/Config/GithubConfigView.xaml.cs +++ b/Synthesis.Bethesda.GUI/Views/Config/GithubConfigView.xaml.cs @@ -1,18 +1,10 @@ -using Noggog.WPF; +using Noggog.WPF; using ReactiveUI; using System.Reactive.Disposables; -using System; -using System.Collections.Generic; -using System.Text; using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; +using Synthesis.Bethesda.Execution.Settings; +using System.Reactive.Linq; +using Noggog; namespace Synthesis.Bethesda.GUI.Views { @@ -28,7 +20,65 @@ public GithubConfigView() InitializeComponent(); this.WhenActivated(disposable => { - this.BindStrict(this.ViewModel, vm => vm.RepoPath, view => view.RepositoryPath.Text) + this.BindStrict(this.ViewModel, vm => vm.RemoteRepoPath, view => view.RepositoryPath.Text) + .DisposeWith(disposable); + + var processing = Observable.CombineLatest( + this.WhenAnyValue(x => x.ViewModel.RepoValidity), + this.WhenAnyValue(x => x.ViewModel.State), + (repo, state) => repo.Succeeded && !state.IsHaltingError && state.RunnableState.Failed); + + this.WhenAnyValue(x => x.ViewModel.RepoValidity) + .BindError(this.RepositoryPath) + .DisposeWith(disposable); + + processing + .Select(x => x ? Visibility.Visible : Visibility.Collapsed) + .BindToStrict(this, x => x.CloningRing.Visibility) + .DisposeWith(disposable); + + // Bind project picker + this.BindStrict(this.ViewModel, vm => vm.ProjectSubpath, view => view.ProjectsPickerBox.SelectedItem) + .DisposeWith(disposable); + this.OneWayBindStrict(this.ViewModel, vm => vm.AvailableProjects, view => view.ProjectsPickerBox.ItemsSource) + .DisposeWith(disposable); + this.WhenAnyValue(x => x.ViewModel.RepoClonesValid) + .Select(x => x ? Visibility.Visible : Visibility.Collapsed) + .BindToStrict(this, view => view.ProjectsPickerBox.Visibility) + .DisposeWith(disposable); + this.WhenAnyValue(x => x.ViewModel.RepoClonesValid) + .Select(x => x ? Visibility.Visible : Visibility.Collapsed) + .BindToStrict(this, view => view.ProjectTitle.Visibility) + .DisposeWith(disposable); + + Observable.CombineLatest( + this.WhenAnyValue(x => x.ViewModel.RepoClonesValid), + this.WhenAnyValue(x => x.ViewModel.SelectedProjectPath.ErrorState), + (driver, proj) => driver && proj.Succeeded) + .Select(x => x ? Visibility.Visible : Visibility.Collapsed) + .BindToStrict(this, view => view.VersioningGrid.Visibility) + .DisposeWith(disposable); + + this.BindStrict(this.ViewModel, vm => vm.PatcherVersioning, view => view.PatcherVersioningTab.SelectedIndex, (e) => (int)e, i => (PatcherVersioningEnum)i) + .DisposeWith(disposable); + this.BindStrict(this.ViewModel, vm => vm.MutagenVersioning, view => view.MutagenVersioningTab.SelectedIndex, (e) => (int)e, i => (MutagenVersioningEnum)i) + .DisposeWith(disposable); + + // Bind tag picker + this.BindStrict(this.ViewModel, vm => vm.TargetTag, view => view.TagPickerBox.SelectedItem) + .DisposeWith(disposable); + this.OneWayBindStrict(this.ViewModel, vm => vm.AvailableTags, view => view.TagPickerBox.ItemsSource) + .DisposeWith(disposable); + + this.BindStrict(this.ViewModel, vm => vm.TargetCommit, view => view.CommitShaBox.Text) + .DisposeWith(disposable); + this.WhenAnyValue(x => x.ViewModel.RunnableData) + .Select(x => x == null ? string.Empty : x.CommitDate.ToString()) + .BindToStrict(this, view => view.PatcherVersionDateText.Text) + .DisposeWith(disposable); + this.WhenAnyValue(x => x.ViewModel.RunnableData) + .Select(x => x != null ? Visibility.Visible : Visibility.Hidden) + .BindToStrict(this, view => view.CommitDateText.Visibility) .DisposeWith(disposable); }); } diff --git a/Synthesis.Bethesda.GUI/Views/Config/PatchersConfigView.xaml b/Synthesis.Bethesda.GUI/Views/Config/PatchersConfigView.xaml index ea436955..93868814 100644 --- a/Synthesis.Bethesda.GUI/Views/Config/PatchersConfigView.xaml +++ b/Synthesis.Bethesda.GUI/Views/Config/PatchersConfigView.xaml @@ -1,4 +1,4 @@ - - + @@ -117,8 +122,8 @@ x:Name="BranchNameBox" Width="300" Margin="0,3" - mahapps:TextBoxHelper.Watermark="Remote branch name" HorizontalAlignment="Left" + mahapps:TextBoxHelper.Watermark="Remote branch name" Style="{StaticResource Noggog.Styles.TextBox.ErrorState}" /> @@ -126,8 +131,8 @@ x:Name="CommitShaBox" Width="300" Margin="0,3" - mahapps:TextBoxHelper.Watermark="Commit sha" HorizontalAlignment="Left" + mahapps:TextBoxHelper.Watermark="Commit sha" Style="{StaticResource Noggog.Styles.TextBox.ErrorState}" /> @@ -136,6 +141,7 @@ + + x != null ? Visibility.Visible : Visibility.Hidden) .BindToStrict(this, view => view.CommitDateText.Visibility) .DisposeWith(disposable); + + // Bind github open commands + this.WhenAnyValue(x => x.ViewModel.OpenGithubPageCommand) + .BindToStrict(this, x => x.OpenGithubButton.Command) + .DisposeWith(disposable); + this.WhenAnyValue(x => x.ViewModel.OpenGithubPageToVersionCommand) + .BindToStrict(this, x => x.OpenGithubToVersionButton.Command) + .DisposeWith(disposable); }); } } From ca0d1859275b60c0e32104060146bbab7e06bbba Mon Sep 17 00:00:00 2001 From: Justin Swanson Date: Sat, 26 Sep 2020 23:44:55 -0500 Subject: [PATCH 19/23] Github workflow --- .github/workflows/ci-dev.yml | 23 +++++++++++++ .github/workflows/ci-release.yml | 23 +++++++++++++ .github/workflows/release.yml | 23 +++++++++++++ azure-pipelines-dev.yml | 37 --------------------- azure-pipelines.yml | 57 -------------------------------- 5 files changed, 69 insertions(+), 94 deletions(-) create mode 100644 .github/workflows/ci-dev.yml create mode 100644 .github/workflows/ci-release.yml create mode 100644 .github/workflows/release.yml delete mode 100644 azure-pipelines-dev.yml delete mode 100644 azure-pipelines.yml diff --git a/.github/workflows/ci-dev.yml b/.github/workflows/ci-dev.yml new file mode 100644 index 00000000..2a6b5135 --- /dev/null +++ b/.github/workflows/ci-dev.yml @@ -0,0 +1,23 @@ +name: Dev Build + +on: + push: + branches: [ dev ] + pull_request: + branches: [ dev ] + +jobs: + build: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + - name: Install dependencies + run: dotnet restore + - name: Build + run: dotnet build --configuration Release --no-restore + - name: Test + run: dotnet test diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml new file mode 100644 index 00000000..9c0d947e --- /dev/null +++ b/.github/workflows/ci-release.yml @@ -0,0 +1,23 @@ +name: Release Build + +on: + push: + branches: [ release ] + pull_request: + branches: [ release ] + +jobs: + build: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + - name: Install dependencies + run: dotnet restore + - name: Build + run: dotnet build --configuration Release --no-restore + - name: Test + run: dotnet test --no-restore --verbosity normal diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..f55d30e6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,23 @@ +name: Release + +on: + release: + types: [published] + +jobs: + release: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + - name: Install dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore --configuration Release --no-restore /p:Version=${{ github.event.release.tag_name }} + - name: Test + run: dotnet test --verbosity normal + - name: Create NuGet Packages + run: dotnet pack -c Release -p:PackageVersion=${{ github.event.release.tag_name }} -p:PackageReleaseNotes="See https://github.com/Noggog/Synthesis/releases/tag/${{ github.event.release.tag_name }}" + - name: Publish to Nuget.org + run: dotnet nuget push **/*${{ github.event.release.tag_name }}.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --no-symbols true diff --git a/azure-pipelines-dev.yml b/azure-pipelines-dev.yml deleted file mode 100644 index 1f0b456c..00000000 --- a/azure-pipelines-dev.yml +++ /dev/null @@ -1,37 +0,0 @@ -# .NET Desktop -# Build and run tests for .NET Desktop or Windows classic desktop solutions. -# Add steps that publish symbols, save build artifacts, and more: -# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net - -trigger: -- dev - -pool: - vmImage: 'windows-latest' - -variables: - solution: '**/*.sln' - buildPlatform: 'x64' - buildConfiguration: 'Release' - -steps: -- task: NuGetToolInstaller@1 - -- task: NuGetCommand@2 - displayName: 'NuGet restore' - inputs: - restoreSolution: '$(solution)' - vstsFeed: '3b91d9cc-9c4f-4129-88d6-e77bb8a0cb62/6309bacb-dd0d-47a7-ac81-f23a748d0aec' - -- task: DotNetCoreCLI@2 - displayName: "Build" - inputs: - command: build - arguments: --configuration Release - -- task: DotNetCoreCLI@2 - displayName: "Test Release" - inputs: - command: 'test' - projects: '**/*UnitTests.csproj' - arguments: --configuration Release \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index e71ae310..00000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,57 +0,0 @@ -# .NET Desktop -# Build and run tests for .NET Desktop or Windows classic desktop solutions. -# Add steps that publish symbols, save build artifacts, and more: -# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net - -trigger: -- release - -pool: - vmImage: 'windows-latest' - -variables: - solution: '**/*.sln' - buildPlatform: 'x64' - buildConfiguration: 'Release' - -steps: -- task: NuGetToolInstaller@1 - -- task: NuGetCommand@2 - displayName: 'NuGet restore' - inputs: - restoreSolution: '$(solution)' - vstsFeed: '3b91d9cc-9c4f-4129-88d6-e77bb8a0cb62/6309bacb-dd0d-47a7-ac81-f23a748d0aec' - -- task: DotNetCoreCLI@2 - displayName: "Build" - inputs: - command: build - arguments: --configuration Release - -# - task: DotNetCoreCLI@2 -# displayName: "Test Release" -# inputs: -# command: 'test' -# projects: '**/*UnitTests.csproj' -# arguments: --configuration Release - -- task: DotNetCoreCLI@2 - displayName: "Publish" - inputs: - command: publish - arguments: -r win-x64 -p:PublishSingleFile=True --self-contained false -o $(Build.ArtifactStagingDirectory) --configuration Release - projects: '**/Synthesis.Bethesda.GUI.csproj' - publishWebProjects: false - enabled: true - -- task: DotNetCoreCLI@2 - displayName: "Pack" - inputs: - command: pack - -- task: PublishBuildArtifacts@1 - displayName: 'Publish Artifact: Synthesis' - inputs: - PathtoPublish: '$(build.artifactstagingdirectory)' - condition: succeededOrFailed() \ No newline at end of file From a2ea172103c4e21c07821d4f86dcef0c42588192 Mon Sep 17 00:00:00 2001 From: Justin Swanson Date: Sat, 26 Sep 2020 23:49:52 -0500 Subject: [PATCH 20/23] Nuget bump --- .../Synthesis.Bethesda.Execution.csproj | 2 +- Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj b/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj index 44dda9a7..676fb303 100644 --- a/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj +++ b/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj @@ -31,7 +31,7 @@ - + diff --git a/Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj b/Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj index 4d9fa575..027d8591 100644 --- a/Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj +++ b/Synthesis.Bethesda.GUI/Synthesis.Bethesda.GUI.csproj @@ -1,4 +1,4 @@ - + WinExe @@ -40,7 +40,7 @@ - + From 367d7350e91df470e2c6ac89b8a862a367a0f9d8 Mon Sep 17 00:00:00 2001 From: Justin Swanson Date: Sat, 26 Sep 2020 23:57:22 -0500 Subject: [PATCH 21/23] Fix for BlockAutomaticExit taking effect improperly --- Mutagen.Bethesda.Synthesis/SynthesisPipeline.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Mutagen.Bethesda.Synthesis/SynthesisPipeline.cs b/Mutagen.Bethesda.Synthesis/SynthesisPipeline.cs index 1f5b5367..db0ba9c4 100644 --- a/Mutagen.Bethesda.Synthesis/SynthesisPipeline.cs +++ b/Mutagen.Bethesda.Synthesis/SynthesisPipeline.cs @@ -203,7 +203,8 @@ public async Task Patch( state.PatchMod.WriteToBinaryParallel(path: settings.OutputPath, param: GetWriteParams(state.LoadOrder.Select(i => i.Key))); } catch (Exception ex) - when (userPreferences?.ActionsForEmptyArgs?.BlockAutomaticExit ?? false) + when (Environment.GetCommandLineArgs().Length == 0 + && (userPreferences?.ActionsForEmptyArgs?.BlockAutomaticExit ?? false)) { System.Console.Error.WriteLine(ex); System.Console.Error.WriteLine("Error occurred. Press enter to exit"); @@ -237,7 +238,8 @@ public void Patch( state.PatchMod.WriteToBinaryParallel(path: settings.OutputPath, param: GetWriteParams(state.LoadOrder.Select(i => i.Key))); } catch (Exception ex) - when (userPreferences?.ActionsForEmptyArgs?.BlockAutomaticExit ?? false) + when (Environment.GetCommandLineArgs().Length == 0 + && (userPreferences?.ActionsForEmptyArgs?.BlockAutomaticExit ?? false)) { System.Console.Error.WriteLine(ex); System.Console.Error.WriteLine("Error occurred. Press enter to exit"); From 3801b7d717d9d9fd85d69e4e7dfb89431587ebf0 Mon Sep 17 00:00:00 2001 From: Justin Swanson Date: Sun, 27 Sep 2020 04:09:06 -0500 Subject: [PATCH 22/23] Workflow tweaks --- .github/workflows/ci-dev.yml | 4 +-- .github/workflows/ci-release.yml | 4 +-- .github/workflows/publish.yml | 45 ++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 23 ---------------- 4 files changed, 49 insertions(+), 27 deletions(-) create mode 100644 .github/workflows/publish.yml delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci-dev.yml b/.github/workflows/ci-dev.yml index 2a6b5135..b778eef3 100644 --- a/.github/workflows/ci-dev.yml +++ b/.github/workflows/ci-dev.yml @@ -18,6 +18,6 @@ jobs: - name: Install dependencies run: dotnet restore - name: Build - run: dotnet build --configuration Release --no-restore + run: dotnet build -c Release --no-restore - name: Test - run: dotnet test + run: dotnet test -c Release --no-build diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 9c0d947e..d0a0fb91 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -18,6 +18,6 @@ jobs: - name: Install dependencies run: dotnet restore - name: Build - run: dotnet build --configuration Release --no-restore + run: dotnet build -c Release --no-restore - name: Test - run: dotnet test --no-restore --verbosity normal + run: dotnet test -c Release --no-build diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..f01d89ce --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,45 @@ +name: Release + +on: + release: + types: [published] + +jobs: + release: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + - name: Install dependencies + run: dotnet restore + - name: Build + run: dotnet build -c Release --no-restore /p:Version=${{ github.event.release.tag_name }} -p:PackageReleaseNotes="See https://github.com/Noggog/Synthesis/releases/tag/${{ github.event.release.tag_name }}" + - name: Test + run: dotnet test --no-build -c Release + - name: Publish Synthesis GUI to Github + uses: svenstaro/upload-release-action@v2 + with: + file: Synthesis.Bethesda.GUI/bin/Release/Synthesis.exe + repo_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ github.event.release.tag_name }} + - name: Publish Mutagen Synthesis to Github + uses: svenstaro/upload-release-action@v2 + with: + file: Mutagen.Bethesda.Synthesis/bin/Release/Mutagen.Bethesda.Synthesis.${{ github.event.release.tag_name }}.nupkg + repo_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ github.event.release.tag_name }} + - name: Publish Synthesis Bethesda to Github + uses: svenstaro/upload-release-action@v2 + with: + file: Synthesis.Bethesda/bin/Release/Synthesis.Bethesda.${{ github.event.release.tag_name }}.nupkg + repo_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ github.event.release.tag_name }} + - name: Publish Synthesis Execution to Github + uses: svenstaro/upload-release-action@v2 + with: + file: Synthesis.Bethesda.Execution/bin/Release/Synthesis.Bethesda.Execution.${{ github.event.release.tag_name }}.nupkg + repo_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ github.event.release.tag_name }} + - name: Publish to Nuget.org + run: dotnet nuget push **/*${{ github.event.release.tag_name }}.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --no-symbols true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index f55d30e6..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Release - -on: - release: - types: [published] - -jobs: - release: - runs-on: windows-latest - steps: - - uses: actions/checkout@v2 - - name: Setup .NET Core - uses: actions/setup-dotnet@v1 - - name: Install dependencies - run: dotnet restore - - name: Build - run: dotnet build --no-restore --configuration Release --no-restore /p:Version=${{ github.event.release.tag_name }} - - name: Test - run: dotnet test --verbosity normal - - name: Create NuGet Packages - run: dotnet pack -c Release -p:PackageVersion=${{ github.event.release.tag_name }} -p:PackageReleaseNotes="See https://github.com/Noggog/Synthesis/releases/tag/${{ github.event.release.tag_name }}" - - name: Publish to Nuget.org - run: dotnet nuget push **/*${{ github.event.release.tag_name }}.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --no-symbols true From 08a49e3ab04b4868b804c4393a2fde4d93dbb4ef Mon Sep 17 00:00:00 2001 From: Justin Swanson Date: Sun, 27 Sep 2020 04:14:57 -0500 Subject: [PATCH 23/23] Mutagen bump --- Mutagen.Bethesda.Synthesis/Mutagen.Bethesda.Synthesis.csproj | 4 ++-- .../Synthesis.Bethesda.Execution.csproj | 2 +- .../Synthesis.Bethesda.UnitTests.csproj | 2 +- Synthesis.Bethesda/Synthesis.Bethesda.csproj | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Mutagen.Bethesda.Synthesis/Mutagen.Bethesda.Synthesis.csproj b/Mutagen.Bethesda.Synthesis/Mutagen.Bethesda.Synthesis.csproj index 777f88ac..c2899399 100644 --- a/Mutagen.Bethesda.Synthesis/Mutagen.Bethesda.Synthesis.csproj +++ b/Mutagen.Bethesda.Synthesis/Mutagen.Bethesda.Synthesis.csproj @@ -22,8 +22,8 @@ - - + + diff --git a/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj b/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj index 676fb303..ddeea3f4 100644 --- a/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj +++ b/Synthesis.Bethesda.Execution/Synthesis.Bethesda.Execution.csproj @@ -29,7 +29,7 @@ - + diff --git a/Synthesis.Bethesda.UnitTests/Synthesis.Bethesda.UnitTests.csproj b/Synthesis.Bethesda.UnitTests/Synthesis.Bethesda.UnitTests.csproj index 2668594b..8dd74409 100644 --- a/Synthesis.Bethesda.UnitTests/Synthesis.Bethesda.UnitTests.csproj +++ b/Synthesis.Bethesda.UnitTests/Synthesis.Bethesda.UnitTests.csproj @@ -13,7 +13,7 @@ - + all diff --git a/Synthesis.Bethesda/Synthesis.Bethesda.csproj b/Synthesis.Bethesda/Synthesis.Bethesda.csproj index ccac99bb..e3d66f73 100644 --- a/Synthesis.Bethesda/Synthesis.Bethesda.csproj +++ b/Synthesis.Bethesda/Synthesis.Bethesda.csproj @@ -23,7 +23,7 @@ - +