From fc754c1d2b689a30fe6e6e2f175d188eedd45fe1 Mon Sep 17 00:00:00 2001 From: Darren Hoehna Date: Tue, 13 Aug 2024 12:23:29 -0700 Subject: [PATCH 01/41] Refining Repository Management's UI. (#3557) * Adding the Repository tool * WIP * View is set up. Test ata as well. * Aligning columns. Adding column spacing. * Reverting this change * Update tools/RepositoryManagement/DevHome.RepositoryManagement/Strings/en-us/Resources.resw Co-authored-by: Kristen Schau <47155823+krschau@users.noreply.github.com> * Adressing comments * Revert "Reverting this change" This reverts commit 711dd787a7edee4d38340b649b64a5599555294c. * Moving to experimental * Disabling by default * Removing from experimental. Addressing comments. * Reverting to main * Update tools/RepositoryManagement/DevHome.RepositoryManagement/DevHome.RepositoryManagement.csproj Co-authored-by: Kristen Schau <47155823+krschau@users.noreply.github.com> * Update tools/RepositoryManagement/DevHome.RepositoryManagement/Extensions/ServiceExtensions.cs Co-authored-by: Kristen Schau <47155823+krschau@users.noreply.github.com> * Removing duplicate xaml code * Adding to the mermaid diagram * Got lost in the shuffle * Removing a refrence --------- Co-authored-by: Kristen Schau <47155823+krschau@users.noreply.github.com> --- DevHome.sln | 24 +++ docs/architecture.md | 3 + src/App.xaml.cs | 5 + src/DevHome.csproj | 1 + src/NavConfig.jsonc | 8 + .../DevHome.RepositoryManagement.csproj | 31 ++++ .../Extensions/ServiceExtensions.cs | 19 +++ .../Strings/en-us/Resources.resw | 124 ++++++++++++++ .../RepositoryManagementItemViewModel.cs | 65 ++++++++ .../RepositoryManagementMainPageViewModel.cs | 42 +++++ .../RepositoryManagementMainPageView.xaml | 154 ++++++++++++++++++ .../RepositoryManagementMainPageView.xaml.cs | 21 +++ 12 files changed, 497 insertions(+) create mode 100644 tools/RepositoryManagement/DevHome.RepositoryManagement/DevHome.RepositoryManagement.csproj create mode 100644 tools/RepositoryManagement/DevHome.RepositoryManagement/Extensions/ServiceExtensions.cs create mode 100644 tools/RepositoryManagement/DevHome.RepositoryManagement/Strings/en-us/Resources.resw create mode 100644 tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementItemViewModel.cs create mode 100644 tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs create mode 100644 tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml create mode 100644 tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml.cs diff --git a/DevHome.sln b/DevHome.sln index 72a2d0eafc..734364e704 100644 --- a/DevHome.sln +++ b/DevHome.sln @@ -173,6 +173,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.FileExplorerSourceC EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.FileExplorerSourceControlIntegrationUnitTest", "tools\Customization\DevHome.FileExplorerSourceControlIntegrationUnitTest\DevHome.FileExplorerSourceControlIntegrationUnitTest.csproj", "{A1FAE679-39D4-4278-A8E8-EA351F21A3E7}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RepositoryManagement", "RepositoryManagement", "{414E801F-103E-4A5C-86A0-CD6C2E48199B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.RepositoryManagement", "tools\RepositoryManagement\DevHome.RepositoryManagement\DevHome.RepositoryManagement.csproj", "{B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug_FailFast|arm64 = Debug_FailFast|arm64 @@ -1161,6 +1165,24 @@ Global {A1FAE679-39D4-4278-A8E8-EA351F21A3E7}.Release|x64.Build.0 = Release|x64 {A1FAE679-39D4-4278-A8E8-EA351F21A3E7}.Release|x86.ActiveCfg = Release|x86 {A1FAE679-39D4-4278-A8E8-EA351F21A3E7}.Release|x86.Build.0 = Release|x86 + {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}.Debug_FailFast|arm64.ActiveCfg = Debug_FailFast|arm64 + {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}.Debug_FailFast|arm64.Build.0 = Debug_FailFast|arm64 + {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}.Debug_FailFast|x64.ActiveCfg = Debug_FailFast|x64 + {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}.Debug_FailFast|x64.Build.0 = Debug_FailFast|x64 + {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}.Debug_FailFast|x86.ActiveCfg = Debug_FailFast|x86 + {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}.Debug_FailFast|x86.Build.0 = Debug_FailFast|x86 + {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}.Debug|arm64.ActiveCfg = Debug|arm64 + {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}.Debug|arm64.Build.0 = Debug|arm64 + {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}.Debug|x64.ActiveCfg = Debug|x64 + {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}.Debug|x64.Build.0 = Debug|x64 + {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}.Debug|x86.ActiveCfg = Debug|x86 + {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}.Debug|x86.Build.0 = Debug|x86 + {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}.Release|arm64.ActiveCfg = Release|arm64 + {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}.Release|arm64.Build.0 = Release|arm64 + {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}.Release|x64.ActiveCfg = Release|x64 + {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}.Release|x64.Build.0 = Release|x64 + {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}.Release|x86.ActiveCfg = Release|x86 + {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1228,6 +1250,8 @@ Global {8C1C7BF8-B27B-4F4D-97E5-A16E02C7860E} = {01AB3100-A939-41DD-A67F-1F8C275A307D} {83D12033-364A-45F2-8FCA-9BD8E8322D91} = {623998FD-B0A6-4980-95D5-A5072301CA10} {A1FAE679-39D4-4278-A8E8-EA351F21A3E7} = {623998FD-B0A6-4980-95D5-A5072301CA10} + {414E801F-103E-4A5C-86A0-CD6C2E48199B} = {A972EC5B-FC61-4964-A6FF-F9633EB75DFD} + {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41} = {414E801F-103E-4A5C-86A0-CD6C2E48199B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {030B5641-B206-46BB-BF71-36FF009088FA} diff --git a/docs/architecture.md b/docs/architecture.md index e90a4ad311..1e00ef36ad 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -19,6 +19,7 @@ graph TD; DevHome.Common-->DevHome.Experiments; DevHome.Common-->DevHome.ExtensionLibrary; DevHome.Common-->DevHome.Settings; + DevHome.Common-->DevHome.RepositoryManagement; DevHome.Telemetry-->DevHome.SetupFlow.Common; DevHome.SetupFlow.Common-->DevHome.SetupFlow; DevHome.SetupFlow.Common-->DevHome.SetupFlow.ElevatedComponent; @@ -35,6 +36,7 @@ graph TD; DevHome.ExtensionLibrary-->DevHome; DevHome.Settings-->DevHome; DevHome.SetupFlow-->DevHome; + DevHome.RepositoryManagement-->DevHome DevHome.Services.Core-->DevHome.Services.WindowsPackageManager; DevHome.Services.Core-->DevHome.Services.DesiredStateConfiguration; DevHome.Telemetry-->DevHome.Services.Core; @@ -82,6 +84,7 @@ Dev Home currently has the following tools: - Extensions Library - [Windows customization](../tools/Customization/DevHome.Customization/Customization.md) - Utilities +- Repository Management ## Extensions diff --git a/src/App.xaml.cs b/src/App.xaml.cs index a3463f708c..75b468582e 100644 --- a/src/App.xaml.cs +++ b/src/App.xaml.cs @@ -14,6 +14,8 @@ using DevHome.Dashboard.Extensions; using DevHome.ExtensionLibrary.Extensions; using DevHome.Helpers; +using DevHome.RepositoryManagement.Extensions; +using DevHome.RepositoryManagement.ViewModels; using DevHome.Services; using DevHome.Services.Core.Extensions; using DevHome.Services.DesiredStateConfiguration.Extensions; @@ -160,6 +162,9 @@ public App() // Setup flow services.AddSetupFlow(context); + // Repository Management + services.AddRepositoryManagement(context); + // Dashboard services.AddDashboard(context); diff --git a/src/DevHome.csproj b/src/DevHome.csproj index addacc9cfa..b58fcada48 100644 --- a/src/DevHome.csproj +++ b/src/DevHome.csproj @@ -103,6 +103,7 @@ + diff --git a/src/NavConfig.jsonc b/src/NavConfig.jsonc index d4ac3d7159..2ac3c12cd2 100644 --- a/src/NavConfig.jsonc +++ b/src/NavConfig.jsonc @@ -40,6 +40,14 @@ "viewModelFullName": "DevHome.Utilities.ViewModels.UtilitiesMainPageViewModel", "iconFontFamily": "DevHomeFluentIcons", "icon": "ECED" + }, + { + "identity": "DevHome.RepositoryManagement", + "assembly": "DevHome.RepositoryManagement", + "viewFullName": "DevHome.RepositoryManagement.Views.RepositoryManagementMainPageView", + "viewModelFullName": "DevHome.RepositoryManagement.ViewModels.RepositoryManagementMainPageViewModel", + "iconFontFamily": "DevHomeFluentIcons", + "icon": "F03F" } ] } diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/DevHome.RepositoryManagement.csproj b/tools/RepositoryManagement/DevHome.RepositoryManagement/DevHome.RepositoryManagement.csproj new file mode 100644 index 0000000000..77eefdd45f --- /dev/null +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/DevHome.RepositoryManagement.csproj @@ -0,0 +1,31 @@ + + + + DevHome.RepositoryManagement + x86;x64;arm64 + win-x86;win-x64;win-arm64 + true + Debug;Release;Debug_FailFast + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + MSBuild:Compile + + + \ No newline at end of file diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/Extensions/ServiceExtensions.cs b/tools/RepositoryManagement/DevHome.RepositoryManagement/Extensions/ServiceExtensions.cs new file mode 100644 index 0000000000..456f7c83f7 --- /dev/null +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/Extensions/ServiceExtensions.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.RepositoryManagement.ViewModels; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace DevHome.RepositoryManagement.Extensions; + +public static class ServiceExtensions +{ + public static IServiceCollection AddRepositoryManagement(this IServiceCollection services, HostBuilderContext context) + { + services.AddTransient(); + services.AddTransient(); + + return services; + } +} diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/Strings/en-us/Resources.resw b/tools/RepositoryManagement/DevHome.RepositoryManagement/Strings/en-us/Resources.resw new file mode 100644 index 0000000000..9f091b226d --- /dev/null +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/Strings/en-us/Resources.resw @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Repositories + Name of the tool to help manage repositories on the user's machine. + + \ No newline at end of file diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementItemViewModel.cs b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementItemViewModel.cs new file mode 100644 index 0000000000..6bef48954d --- /dev/null +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementItemViewModel.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace DevHome.RepositoryManagement.ViewModels; + +public partial class RepositoryManagementItemViewModel +{ + public string RepositoryName { get; set; } + + public string ClonePath { get; set; } + + public string LatestCommit { get; set; } + + public string Branch { get; set; } + + [RelayCommand] + public void OpenInFileExplorer() + { + throw new NotImplementedException(); + } + + [RelayCommand] + public void OpenInCMD() + { + throw new NotImplementedException(); + } + + [RelayCommand] + public void MoveRepository() + { + throw new NotImplementedException(); + } + + [RelayCommand] + public void DeleteRepository() + { + throw new NotImplementedException(); + } + + [RelayCommand] + public void MakeConfigurationFileWithThisRepository() + { + throw new NotImplementedException(); + } + + [RelayCommand] + public void OpenFileExplorerToConfigurationsFolder() + { + throw new NotImplementedException(); + } + + [RelayCommand] + public void RemoveThisRepositoryFromTheList() + { + throw new NotImplementedException(); + } +} diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs new file mode 100644 index 0000000000..fd38a5ef0b --- /dev/null +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DevHome.Common.Extensions; +using Microsoft.Extensions.Hosting; + +namespace DevHome.RepositoryManagement.ViewModels; + +public class RepositoryManagementMainPageViewModel +{ + private readonly IHost _host; + + public ObservableCollection Items { get; } = new(); + + public RepositoryManagementMainPageViewModel(IHost host) + { + _host = host; + } + + // Some test data to show off in the Repository Management page. + public void PopulateTestData() + { + Items.Clear(); + for (var x = 0; x < 5; x++) + { + var listItem = _host.GetService(); + listItem.RepositoryName = $"MicrosoftRepository{x}"; + listItem.ClonePath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), x.ToString(CultureInfo.InvariantCulture)); + listItem.LatestCommit = $"dhoehna * author {x} min"; + listItem.Branch = "main"; + Items.Add(listItem); + } + } +} diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml b/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml new file mode 100644 index 0000000000..9d776a97fc --- /dev/null +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml.cs b/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml.cs new file mode 100644 index 0000000000..edf961e4d2 --- /dev/null +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.Common.Extensions; +using DevHome.Common.Views; +using DevHome.RepositoryManagement.ViewModels; +using Microsoft.UI.Xaml; + +namespace DevHome.RepositoryManagement.Views; + +public sealed partial class RepositoryManagementMainPageView : ToolPage +{ + public RepositoryManagementMainPageViewModel ViewModel { get; } + + public RepositoryManagementMainPageView() + { + ViewModel = Application.Current.GetService(); + this.InitializeComponent(); + ViewModel.PopulateTestData(); + } +} From 7d00a024ca3f4e3a975ba15a9d48a3790b690342 Mon Sep 17 00:00:00 2001 From: Darren Hoehna Date: Mon, 26 Aug 2024 15:05:29 -0700 Subject: [PATCH 02/41] Adding EF core, database project, and some usage. (#3674) * WIP * EF works. Can save data. * Can read and write. :) * Repos are added when cloned. * Putting save/load into a different class * Adding default values * Cleaning up names and using statements * More comments * Removing refrence to the public nuget * Restoring the nuget config * Adding a test. Better defining dates * Adding some tests * More comments. * Better path comparison. * Removing unused equals code * Adding more comments. Making new migrations --- DevHome.sln | 20 +++ .../RepositoryManagement/Repository.cs | 29 +++++ .../RepositoryMetadata.cs | 27 +++++ .../DevHome.Database/DevHome.Database.csproj | 18 +++ .../DevHomeDatabaseContext.cs | 54 +++++++++ .../Extensions/ServiceExtensions.cs | 17 +++ ...0240826220057_InitialMigration.Designer.cs | 114 ++++++++++++++++++ .../20240826220057_InitialMigration.cs | 79 ++++++++++++ .../DevHomeDatabaseContextModelSnapshot.cs | 109 +++++++++++++++++ src/App.xaml.cs | 4 + src/DevHome.csproj | 1 + test/Database/RepositoryTests.cs | 68 +++++++++++ test/DevHome.Test.csproj | 1 + .../DevHome.RepositoryManagement.csproj | 1 + .../Extensions/ServiceExtensions.cs | 4 +- .../RepositoryManagementDataAccessService.cs | 109 +++++++++++++++++ .../RepositoryManagementItemViewModel.cs | 7 +- .../RepositoryManagementMainPageViewModel.cs | 38 ++---- .../RepositoryManagementMainPageView.xaml | 27 ++++- .../RepositoryManagementMainPageView.xaml.cs | 1 - .../DevHome.SetupFlow.csproj | 2 + .../DevHome.SetupFlow/Models/CloneRepoTask.cs | 14 ++- 22 files changed, 710 insertions(+), 34 deletions(-) create mode 100644 database/DevHome.Database/DatabaseModels/RepositoryManagement/Repository.cs create mode 100644 database/DevHome.Database/DatabaseModels/RepositoryManagement/RepositoryMetadata.cs create mode 100644 database/DevHome.Database/DevHome.Database.csproj create mode 100644 database/DevHome.Database/DevHomeDatabaseContext.cs create mode 100644 database/DevHome.Database/Extensions/ServiceExtensions.cs create mode 100644 database/DevHome.Database/Migrations/20240826220057_InitialMigration.Designer.cs create mode 100644 database/DevHome.Database/Migrations/20240826220057_InitialMigration.cs create mode 100644 database/DevHome.Database/Migrations/DevHomeDatabaseContextModelSnapshot.cs create mode 100644 test/Database/RepositoryTests.cs create mode 100644 tools/RepositoryManagement/DevHome.RepositoryManagement/Services/RepositoryManagementDataAccessService.cs diff --git a/DevHome.sln b/DevHome.sln index 734364e704..f825da6557 100644 --- a/DevHome.sln +++ b/DevHome.sln @@ -177,6 +177,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RepositoryManagement", "Rep EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.RepositoryManagement", "tools\RepositoryManagement\DevHome.RepositoryManagement\DevHome.RepositoryManagement.csproj", "{B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevHome.Database", "database\DevHome.Database\DevHome.Database.csproj", "{CC4E406E-920A-408C-8CF8-098F680B1E29}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug_FailFast|arm64 = Debug_FailFast|arm64 @@ -1183,6 +1185,24 @@ Global {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}.Release|x64.Build.0 = Release|x64 {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}.Release|x86.ActiveCfg = Release|x86 {B4B2F5EA-BCD3-4F3E-856C-F6C32433BA41}.Release|x86.Build.0 = Release|x86 + {CC4E406E-920A-408C-8CF8-098F680B1E29}.Debug_FailFast|arm64.ActiveCfg = Debug|ARM64 + {CC4E406E-920A-408C-8CF8-098F680B1E29}.Debug_FailFast|arm64.Build.0 = Debug|ARM64 + {CC4E406E-920A-408C-8CF8-098F680B1E29}.Debug_FailFast|x64.ActiveCfg = Debug|x64 + {CC4E406E-920A-408C-8CF8-098F680B1E29}.Debug_FailFast|x64.Build.0 = Debug|x64 + {CC4E406E-920A-408C-8CF8-098F680B1E29}.Debug_FailFast|x86.ActiveCfg = Debug|x86 + {CC4E406E-920A-408C-8CF8-098F680B1E29}.Debug_FailFast|x86.Build.0 = Debug|x86 + {CC4E406E-920A-408C-8CF8-098F680B1E29}.Debug|arm64.ActiveCfg = Debug|ARM64 + {CC4E406E-920A-408C-8CF8-098F680B1E29}.Debug|arm64.Build.0 = Debug|ARM64 + {CC4E406E-920A-408C-8CF8-098F680B1E29}.Debug|x64.ActiveCfg = Debug|x64 + {CC4E406E-920A-408C-8CF8-098F680B1E29}.Debug|x64.Build.0 = Debug|x64 + {CC4E406E-920A-408C-8CF8-098F680B1E29}.Debug|x86.ActiveCfg = Debug|x86 + {CC4E406E-920A-408C-8CF8-098F680B1E29}.Debug|x86.Build.0 = Debug|x86 + {CC4E406E-920A-408C-8CF8-098F680B1E29}.Release|arm64.ActiveCfg = Release|ARM64 + {CC4E406E-920A-408C-8CF8-098F680B1E29}.Release|arm64.Build.0 = Release|ARM64 + {CC4E406E-920A-408C-8CF8-098F680B1E29}.Release|x64.ActiveCfg = Release|x64 + {CC4E406E-920A-408C-8CF8-098F680B1E29}.Release|x64.Build.0 = Release|x64 + {CC4E406E-920A-408C-8CF8-098F680B1E29}.Release|x86.ActiveCfg = Release|x86 + {CC4E406E-920A-408C-8CF8-098F680B1E29}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/database/DevHome.Database/DatabaseModels/RepositoryManagement/Repository.cs b/database/DevHome.Database/DatabaseModels/RepositoryManagement/Repository.cs new file mode 100644 index 0000000000..03d8a64e9a --- /dev/null +++ b/database/DevHome.Database/DatabaseModels/RepositoryManagement/Repository.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.EntityFrameworkCore; + +namespace DevHome.Database.DatabaseModels.RepositoryManagement; + +/// +/// This represents the SQLite model EF has in the database. +/// Any change here needs a corresponding migration. +/// Use FluentAPI in the database context to further customize each column. +/// +[Index(nameof(RepositoryName), nameof(RepositoryClonePath), IsUnique = true)] +public class Repository +{ + public int RepositoryId { get; set; } + + public string? RepositoryName { get; set; } + + public string? RepositoryClonePath { get; set; } + + public DateTime? CreatedUTCDate { get; set; } + + public DateTime? UpdatedUTCDate { get; set; } + + // 1:1 relationship. Repository is the parent and needs only + // the object of the dependant. + public RepositoryMetadata? RepositoryMetadata { get; set; } +} diff --git a/database/DevHome.Database/DatabaseModels/RepositoryManagement/RepositoryMetadata.cs b/database/DevHome.Database/DatabaseModels/RepositoryManagement/RepositoryMetadata.cs new file mode 100644 index 0000000000..3a69ff6257 --- /dev/null +++ b/database/DevHome.Database/DatabaseModels/RepositoryManagement/RepositoryMetadata.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace DevHome.Database.DatabaseModels.RepositoryManagement; + +/// +/// This represents the SQLite model EF has in the database. +/// Any change here needs a corresponding migration. +/// Use FluentAPI in the database context to further customize each column. +/// +public class RepositoryMetadata +{ + // EF uses [ClassName]Id as the primary key + public int RepositoryMetadataId { get; set; } + + public bool IsHiddenFromPage { get; set; } + + public DateTime UtcDateHidden { get; set; } + + public DateTime? CreatedUTCDate { get; set; } + + public DateTime? UpdatedUTCDate { get; set; } + + public int RepositoryId { get; set; } + + public Repository Repository { get; set; } = null!; +} diff --git a/database/DevHome.Database/DevHome.Database.csproj b/database/DevHome.Database/DevHome.Database.csproj new file mode 100644 index 0000000000..de0b04b1cb --- /dev/null +++ b/database/DevHome.Database/DevHome.Database.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/database/DevHome.Database/DevHomeDatabaseContext.cs b/database/DevHome.Database/DevHomeDatabaseContext.cs new file mode 100644 index 0000000000..27dc7aa9df --- /dev/null +++ b/database/DevHome.Database/DevHomeDatabaseContext.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.ComponentModel.DataAnnotations.Schema; +using DevHome.Database.DatabaseModels.RepositoryManagement; +using Microsoft.EntityFrameworkCore; + +namespace DevHome.Database; + +public class DevHomeDatabaseContext : DbContext +{ + public DbSet Repositories { get; set; } + + public DbSet RepositoryMetadatas { get; set; } + + public string DbPath { get; } + + public DevHomeDatabaseContext() + { + // Not the final path. It will change before going into main. + // Needs a configurable location for testing. + var folder = Environment.SpecialFolder.LocalApplicationData; + var path = Environment.GetFolderPath(folder); + DbPath = Path.Join(path, "DevHome.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // If I have time, this should be split like service extensions. + // As more entities are added, the longer this method will get. + var repositoryEntity = modelBuilder.Entity(); + if (repositoryEntity != null) + { + repositoryEntity.Property(x => x.RepositoryClonePath).HasDefaultValue(string.Empty).IsRequired(true); + repositoryEntity.Property(x => x.RepositoryName).HasDefaultValue(string.Empty).IsRequired(true); + repositoryEntity.Property(x => x.CreatedUTCDate).HasDefaultValueSql("datetime()"); + repositoryEntity.Property(x => x.UpdatedUTCDate).HasDefaultValue(new DateTime(1, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + repositoryEntity.ToTable("Repository"); + } + + var repositoryMetadataEntity = modelBuilder.Entity(); + if (repositoryMetadataEntity != null) + { + repositoryMetadataEntity.Property(x => x.IsHiddenFromPage).HasDefaultValue(false).IsRequired(true); + repositoryMetadataEntity.Property(x => x.UtcDateHidden).HasDefaultValue(new DateTime(1, 1, 1, 0, 0, 0, DateTimeKind.Utc)).IsRequired(true); + repositoryMetadataEntity.Property(x => x.CreatedUTCDate).HasDefaultValueSql("datetime()"); + repositoryMetadataEntity.Property(x => x.UpdatedUTCDate).HasDefaultValue(new DateTime(1, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + repositoryMetadataEntity.ToTable("RepositoryMetadata"); + } + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseSqlite($"Data Source={DbPath}"); +} diff --git a/database/DevHome.Database/Extensions/ServiceExtensions.cs b/database/DevHome.Database/Extensions/ServiceExtensions.cs new file mode 100644 index 0000000000..940101440e --- /dev/null +++ b/database/DevHome.Database/Extensions/ServiceExtensions.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace DevHome.Database.Extensions; + +public static class ServiceExtensions +{ + public static IServiceCollection AddDatabaseContext(this IServiceCollection services, HostBuilderContext context) + { + services.AddTransient(); + + return services; + } +} diff --git a/database/DevHome.Database/Migrations/20240826220057_InitialMigration.Designer.cs b/database/DevHome.Database/Migrations/20240826220057_InitialMigration.Designer.cs new file mode 100644 index 0000000000..9f9c05272b --- /dev/null +++ b/database/DevHome.Database/Migrations/20240826220057_InitialMigration.Designer.cs @@ -0,0 +1,114 @@ +// +using System; +using DevHome.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DevHome.Database.Migrations +{ + [DbContext(typeof(DevHomeDatabaseContext))] + [Migration("20240826220057_InitialMigration")] + partial class InitialMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("DevHome.Database.DatabaseModels.RepositoryManagement.Repository", b => + { + b.Property("RepositoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedUTCDate") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime()"); + + b.Property("RepositoryClonePath") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("RepositoryName") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("UpdatedUTCDate") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)); + + b.HasKey("RepositoryId"); + + b.HasIndex("RepositoryName", "RepositoryClonePath") + .IsUnique(); + + b.ToTable("Repository", (string)null); + }); + + modelBuilder.Entity("DevHome.Database.DatabaseModels.RepositoryManagement.RepositoryMetadata", b => + { + b.Property("RepositoryMetadataId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedUTCDate") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime()"); + + b.Property("IsHiddenFromPage") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("RepositoryId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedUTCDate") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)); + + b.Property("UtcDateHidden") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)); + + b.HasKey("RepositoryMetadataId"); + + b.HasIndex("RepositoryId") + .IsUnique(); + + b.ToTable("RepositoryMetadata", (string)null); + }); + + modelBuilder.Entity("DevHome.Database.DatabaseModels.RepositoryManagement.RepositoryMetadata", b => + { + b.HasOne("DevHome.Database.DatabaseModels.RepositoryManagement.Repository", "Repository") + .WithOne("RepositoryMetadata") + .HasForeignKey("DevHome.Database.DatabaseModels.RepositoryManagement.RepositoryMetadata", "RepositoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Repository"); + }); + + modelBuilder.Entity("DevHome.Database.DatabaseModels.RepositoryManagement.Repository", b => + { + b.Navigation("RepositoryMetadata"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/database/DevHome.Database/Migrations/20240826220057_InitialMigration.cs b/database/DevHome.Database/Migrations/20240826220057_InitialMigration.cs new file mode 100644 index 0000000000..3b7826c297 --- /dev/null +++ b/database/DevHome.Database/Migrations/20240826220057_InitialMigration.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DevHome.Database.Migrations; + +/// +public partial class InitialMigration : Migration +{ + private static readonly string[] _columns = new[] { "RepositoryName", "RepositoryClonePath" }; + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Repository", + columns: table => new + { + RepositoryId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + RepositoryName = table.Column(type: "TEXT", nullable: false, defaultValue: string.Empty), + RepositoryClonePath = table.Column(type: "TEXT", nullable: false, defaultValue: string.Empty), + CreatedUTCDate = table.Column(type: "TEXT", nullable: true, defaultValueSql: "datetime()"), + UpdatedUTCDate = table.Column(type: "TEXT", nullable: true, defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)), + }, + constraints: table => + { + table.PrimaryKey("PK_Repository", x => x.RepositoryId); + }); + + migrationBuilder.CreateTable( + name: "RepositoryMetadata", + columns: table => new + { + RepositoryMetadataId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + IsHiddenFromPage = table.Column(type: "INTEGER", nullable: false, defaultValue: false), + UtcDateHidden = table.Column(type: "TEXT", nullable: false, defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)), + CreatedUTCDate = table.Column(type: "TEXT", nullable: true, defaultValueSql: "datetime()"), + UpdatedUTCDate = table.Column(type: "TEXT", nullable: true, defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)), + RepositoryId = table.Column(type: "INTEGER", nullable: false), + }, + constraints: table => + { + table.PrimaryKey("PK_RepositoryMetadata", x => x.RepositoryMetadataId); + table.ForeignKey( + name: "FK_RepositoryMetadata_Repository_RepositoryId", + column: x => x.RepositoryId, + principalTable: "Repository", + principalColumn: "RepositoryId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Repository_RepositoryName_RepositoryClonePath", + table: "Repository", + columns: _columns, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_RepositoryMetadata_RepositoryId", + table: "RepositoryMetadata", + column: "RepositoryId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RepositoryMetadata"); + + migrationBuilder.DropTable( + name: "Repository"); + } +} diff --git a/database/DevHome.Database/Migrations/DevHomeDatabaseContextModelSnapshot.cs b/database/DevHome.Database/Migrations/DevHomeDatabaseContextModelSnapshot.cs new file mode 100644 index 0000000000..9f103c6857 --- /dev/null +++ b/database/DevHome.Database/Migrations/DevHomeDatabaseContextModelSnapshot.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; + +#nullable disable + +namespace DevHome.Database.Migrations; + +[DbContext(typeof(DevHomeDatabaseContext))] +public partial class DevHomeDatabaseContextModelSnapshot : ModelSnapshot +{ + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("DevHome.Database.DatabaseModels.RepositoryManagement.Repository", b => + { + b.Property("RepositoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedUTCDate") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime()"); + + b.Property("RepositoryClonePath") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(string.Empty); + + b.Property("RepositoryName") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(string.Empty); + + b.Property("UpdatedUTCDate") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)); + + b.HasKey("RepositoryId"); + + b.HasIndex("RepositoryName", "RepositoryClonePath") + .IsUnique(); + + b.ToTable("Repository", (string)null); + }); + + modelBuilder.Entity("DevHome.Database.DatabaseModels.RepositoryManagement.RepositoryMetadata", b => + { + b.Property("RepositoryMetadataId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedUTCDate") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime()"); + + b.Property("IsHiddenFromPage") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("RepositoryId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedUTCDate") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)); + + b.Property("UtcDateHidden") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)); + + b.HasKey("RepositoryMetadataId"); + + b.HasIndex("RepositoryId") + .IsUnique(); + + b.ToTable("RepositoryMetadata", (string)null); + }); + + modelBuilder.Entity("DevHome.Database.DatabaseModels.RepositoryManagement.RepositoryMetadata", b => + { + b.HasOne("DevHome.Database.DatabaseModels.RepositoryManagement.Repository", "Repository") + .WithOne("RepositoryMetadata") + .HasForeignKey("DevHome.Database.DatabaseModels.RepositoryManagement.RepositoryMetadata", "RepositoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Repository"); + }); + + modelBuilder.Entity("DevHome.Database.DatabaseModels.RepositoryManagement.Repository", b => + { + b.Navigation("RepositoryMetadata"); + }); +#pragma warning restore 612, 618 + } +} diff --git a/src/App.xaml.cs b/src/App.xaml.cs index 75b468582e..95b2cb427a 100644 --- a/src/App.xaml.cs +++ b/src/App.xaml.cs @@ -12,6 +12,7 @@ using DevHome.Contracts.Services; using DevHome.Customization.Extensions; using DevHome.Dashboard.Extensions; +using DevHome.Database.Extensions; using DevHome.ExtensionLibrary.Extensions; using DevHome.Helpers; using DevHome.RepositoryManagement.Extensions; @@ -98,6 +99,9 @@ public App() }). ConfigureServices((context, services) => { + // Add databse connection + services.AddDatabaseContext(context); + // Add Serilog logging for ILogger. services.AddLogging(lb => lb.AddSerilog(dispose: true)); diff --git a/src/DevHome.csproj b/src/DevHome.csproj index dde7cf3798..da10a8ad11 100644 --- a/src/DevHome.csproj +++ b/src/DevHome.csproj @@ -92,6 +92,7 @@ + diff --git a/test/Database/RepositoryTests.cs b/test/Database/RepositoryTests.cs new file mode 100644 index 0000000000..1aabd6e0df --- /dev/null +++ b/test/Database/RepositoryTests.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.Database; +using DevHome.Database.DatabaseModels.RepositoryManagement; +using Microsoft.EntityFrameworkCore; + +namespace DevHome.Test.Database; + +[TestClass] +public class RepositoryTests +{ + [TestMethod] + [TestCategory("Unit")] + public void ReadAndWriteRepositoryData() + { + var dbContext = new DevHomeDatabaseContext(); + + // Reset the database + // Not the best way to test. I will change the test to a mock database + // in the future. + dbContext.ChangeTracker + .Entries() + .ToList() + .ForEach(e => e.State = EntityState.Detached); + + dbContext.Database.EnsureDeleted(); + dbContext.Database.EnsureCreated(); + + // Insert a new record. + var newRepo = new Repository(); + dbContext.Add(newRepo); + dbContext.SaveChanges(); + + var allRepositories = dbContext.Repositories.ToList(); + Assert.AreEqual(1, allRepositories.Count); + + var savedRepository = allRepositories[0]; + Assert.AreEqual(string.Empty, savedRepository.RepositoryName); + Assert.AreEqual(string.Empty, savedRepository.RepositoryClonePath); + Assert.IsTrue(savedRepository.CreatedUTCDate > DateTime.MinValue); + Assert.AreEqual(new DateTime(1, 1, 1, 0, 0, 0, DateTimeKind.Utc), savedRepository.UpdatedUTCDate); + Assert.IsNull(savedRepository.RepositoryMetadata); + + // Modify the record. + savedRepository.RepositoryName = "MyNewName"; + dbContext.SaveChanges(); + + allRepositories = dbContext.Repositories.ToList(); + Assert.AreEqual(1, allRepositories.Count); + + savedRepository = allRepositories[0]; + Assert.AreEqual("MyNewName", savedRepository.RepositoryName); + Assert.AreEqual(string.Empty, savedRepository.RepositoryClonePath); + Assert.IsTrue(savedRepository.CreatedUTCDate > DateTime.MinValue); + Assert.AreEqual(new DateTime(1, 1, 1, 0, 0, 0, DateTimeKind.Utc), savedRepository.UpdatedUTCDate); + Assert.IsNull(savedRepository.RepositoryMetadata); + + RepositoryMetadata savedRepositoryMetadata = new RepositoryMetadata(); + savedRepositoryMetadata.IsHiddenFromPage = true; + savedRepository.RepositoryMetadata = savedRepositoryMetadata; + dbContext.SaveChanges(); + + allRepositories = dbContext.Repositories.ToList(); + savedRepository = allRepositories[0]; + Assert.IsNotNull(savedRepository.RepositoryMetadata); + } +} diff --git a/test/DevHome.Test.csproj b/test/DevHome.Test.csproj index 6ab5a4a683..562afd5a89 100644 --- a/test/DevHome.Test.csproj +++ b/test/DevHome.Test.csproj @@ -18,6 +18,7 @@ + diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/DevHome.RepositoryManagement.csproj b/tools/RepositoryManagement/DevHome.RepositoryManagement/DevHome.RepositoryManagement.csproj index 77eefdd45f..1f66c24de2 100644 --- a/tools/RepositoryManagement/DevHome.RepositoryManagement/DevHome.RepositoryManagement.csproj +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/DevHome.RepositoryManagement.csproj @@ -21,6 +21,7 @@ + diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/Extensions/ServiceExtensions.cs b/tools/RepositoryManagement/DevHome.RepositoryManagement/Extensions/ServiceExtensions.cs index 456f7c83f7..34cc63d235 100644 --- a/tools/RepositoryManagement/DevHome.RepositoryManagement/Extensions/ServiceExtensions.cs +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/Extensions/ServiceExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using DevHome.RepositoryManagement.Services; using DevHome.RepositoryManagement.ViewModels; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -11,7 +12,8 @@ public static class ServiceExtensions { public static IServiceCollection AddRepositoryManagement(this IServiceCollection services, HostBuilderContext context) { - services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); services.AddTransient(); return services; diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/Services/RepositoryManagementDataAccessService.cs b/tools/RepositoryManagement/DevHome.RepositoryManagement/Services/RepositoryManagementDataAccessService.cs new file mode 100644 index 0000000000..6ef1c09ef4 --- /dev/null +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/Services/RepositoryManagementDataAccessService.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using DevHome.Common.Extensions; +using DevHome.Database; +using DevHome.Database.DatabaseModels.RepositoryManagement; +using DevHome.RepositoryManagement.ViewModels; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.Extensions.Hosting; +using Serilog; + +namespace DevHome.RepositoryManagement.Services; + +public class RepositoryManagementDataAccessService +{ + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(RepositoryManagementDataAccessService)); + + private readonly IHost _host; + + public RepositoryManagementDataAccessService(IHost host) + { + // Store the host to make dbContext and RepositoryManagementItemViewModel + // dbContext can not be passed in because RepositoryManagementDataAccessService is singleton + // and dbContext needs to be scoped. + // RepositoryManagementItemViewModel can not be passed in because multiple are made. + // The best solution is to make factories for DevHomeContext and RepositoryManagementItemViewModel. + // Might be in a future change. + _host = host; + } + + public void AddRepository(string repositoryName, string cloneLocation) + { + // Check if repositoryName and cloneLocation is null or empty and + // return a correct value indicating such. + Repository newRepo = new(); + newRepo.RepositoryName = repositoryName; + newRepo.RepositoryClonePath = cloneLocation; + + RepositoryMetadata newMetadata = new(); + newMetadata.Repository = newRepo; + newMetadata.RepositoryId = newRepo.RepositoryId; + newMetadata.IsHiddenFromPage = false; + + newRepo.RepositoryMetadata = newMetadata; + + var dbContext = _host.GetService(); + dbContext.Add(newRepo); + dbContext.Add(newMetadata); + dbContext.SaveChanges(); + } + + public List GetRepositories(bool removeRepositoriesNotInTheirSavedLocation) + { + // This class should not know about RepositoryManagementItemViewModel. + // This should be moved to RepositoryManagementMainpageViewModel instead. + // Do that in the next PR. + var repos = QueryDatabaseForRepositories(removeRepositoriesNotInTheirSavedLocation); + return ConvertToLineItems(repos); + } + + private IIncludableQueryable QueryDatabaseForRepositories(bool removeRepositoriesNotInTheirSavedLocation) + { + var dbContext = _host.GetService(); + var repositoriesFromDatabase = dbContext.Repositories.Include(x => x.RepositoryMetadata); + + if (!removeRepositoriesNotInTheirSavedLocation) + { + return repositoriesFromDatabase; + } + + // The database can get out of sync. Take this as an example. + // DevHome writes a repository. + // The user moves or deletes the repository. + // Now the database record has the correct name, but the incorrect location. + // Remove records where the directory does not exist. + repositoriesFromDatabase + .ToList() // EF does not understand Directory.Exists. Fetch the data first. + .Where(x => !Directory.Exists(x.RepositoryClonePath)) + .ToList() + .ForEach(x => dbContext.Repositories.Remove(x)); + + dbContext.SaveChanges(); + + return dbContext.Repositories.Include(x => x.RepositoryMetadata); + } + + private List ConvertToLineItems(IIncludableQueryable repositories) + { + List items = new(); + + foreach (var repo in repositories) + { + var lineItem = _host.GetService(); + lineItem.ClonePath = repo.RepositoryClonePath; + lineItem.Branch = "main"; // Test value. Will change in the future. + lineItem.RepositoryName = repo.RepositoryName; + lineItem.LatestCommit = "No commits found"; // Test value. Will change in the future. + + lineItem.IsHiddenFromPage = repo.RepositoryMetadata.IsHiddenFromPage; + items.Add(lineItem); + } + + return items; + } +} diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementItemViewModel.cs b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementItemViewModel.cs index 6bef48954d..f4873dd0a5 100644 --- a/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementItemViewModel.cs +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementItemViewModel.cs @@ -2,11 +2,6 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; namespace DevHome.RepositoryManagement.ViewModels; @@ -21,6 +16,8 @@ public partial class RepositoryManagementItemViewModel public string Branch { get; set; } + public bool IsHiddenFromPage { get; set; } + [RelayCommand] public void OpenInFileExplorer() { diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs index fd38a5ef0b..ac40bad6a3 100644 --- a/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs @@ -2,41 +2,29 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using DevHome.Common.Extensions; -using Microsoft.Extensions.Hosting; +using CommunityToolkit.Mvvm.Input; +using DevHome.RepositoryManagement.Services; +using Serilog; namespace DevHome.RepositoryManagement.ViewModels; -public class RepositoryManagementMainPageViewModel +public partial class RepositoryManagementMainPageViewModel { - private readonly IHost _host; + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(RepositoryManagementMainPageViewModel)); - public ObservableCollection Items { get; } = new(); + private readonly RepositoryManagementDataAccessService _dataAccessService; - public RepositoryManagementMainPageViewModel(IHost host) + public ObservableCollection Items => new(_dataAccessService.GetRepositories(true)); + + [RelayCommand] + public void AddExistingRepository() { - _host = host; + throw new NotImplementedException(); } - // Some test data to show off in the Repository Management page. - public void PopulateTestData() + public RepositoryManagementMainPageViewModel(RepositoryManagementDataAccessService dataAccessService) { - Items.Clear(); - for (var x = 0; x < 5; x++) - { - var listItem = _host.GetService(); - listItem.RepositoryName = $"MicrosoftRepository{x}"; - listItem.ClonePath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), x.ToString(CultureInfo.InvariantCulture)); - listItem.LatestCommit = $"dhoehna * author {x} min"; - listItem.Branch = "main"; - Items.Add(listItem); - } + _dataAccessService = dataAccessService; } } diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml b/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml index 9d776a97fc..fefc10c07a 100644 --- a/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml @@ -6,6 +6,8 @@ xmlns:behaviors="using:DevHome.Common.Behaviors" xmlns:commonCustomControls="using:DevHome.Common.Environments.CustomControls" xmlns:commonviews="using:DevHome.Common.Views" + xmlns:i="using:Microsoft.Xaml.Interactivity" + xmlns:ic="using:Microsoft.Xaml.Interactions.Core" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="using:DevHome.RepositoryManagement.Views" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" @@ -47,13 +49,19 @@ + + - From 2c44c31ad5a69bb14f5aaf12d6e8c0a3c30896a7 Mon Sep 17 00:00:00 2001 From: dahoehna Date: Mon, 9 Sep 2024 13:59:02 -0700 Subject: [PATCH 22/41] Add to winget configuration file. --- .../Extensions/ServiceExtensions.cs | 5 +- .../RepositoryManagementDataAccessService.cs | 12 +-- src/App.xaml.cs | 4 +- .../DevHome.RepositoryManagement.csproj | 5 + .../Extensions/ServiceExtensions.cs | 2 - .../RepositoryManagementItemViewModel.cs | 97 +++++++++++++++++-- .../RepositoryManagementMainPageViewModel.cs | 2 +- .../RepositoryManagementMainPageView.xaml | 2 +- .../DevHome.SetupFlow.Common.csproj | 1 + .../DevHome.SetupFlow.csproj | 1 - .../DevHome.SetupFlow/Models/CloneRepoTask.cs | 2 +- .../Services/ConfigurationFileBuilder.cs | 35 +++++++ 12 files changed, 147 insertions(+), 21 deletions(-) rename {tools/RepositoryManagement/DevHome.RepositoryManagement => database/DevHome.Database}/Services/RepositoryManagementDataAccessService.cs (95%) diff --git a/database/DevHome.Database/Extensions/ServiceExtensions.cs b/database/DevHome.Database/Extensions/ServiceExtensions.cs index bf8bf7c4ad..b46a738e24 100644 --- a/database/DevHome.Database/Extensions/ServiceExtensions.cs +++ b/database/DevHome.Database/Extensions/ServiceExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using DevHome.Database.Factories; +using DevHome.Database.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -9,10 +10,12 @@ namespace DevHome.Database.Extensions; public static class ServiceExtensions { - public static IServiceCollection AddDatabaseContext(this IServiceCollection services, HostBuilderContext context) + public static IServiceCollection AddDatabase(this IServiceCollection services, HostBuilderContext context) { services.AddSingleton(); + services.AddSingleton(); + return services; } } diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/Services/RepositoryManagementDataAccessService.cs b/database/DevHome.Database/Services/RepositoryManagementDataAccessService.cs similarity index 95% rename from tools/RepositoryManagement/DevHome.RepositoryManagement/Services/RepositoryManagementDataAccessService.cs rename to database/DevHome.Database/Services/RepositoryManagementDataAccessService.cs index c26babce52..384f400002 100644 --- a/tools/RepositoryManagement/DevHome.RepositoryManagement/Services/RepositoryManagementDataAccessService.cs +++ b/database/DevHome.Database/Services/RepositoryManagementDataAccessService.cs @@ -11,7 +11,7 @@ using DevHome.Telemetry; using Serilog; -namespace DevHome.RepositoryManagement.Services; +namespace DevHome.Database.Services; public class RepositoryManagementDataAccessService { @@ -79,7 +79,7 @@ public Repository MakeRepository(string repositoryName, string cloneLocation, st "DevHome_Database_Event", LogLevel.Critical, new DevHomeDatabaseEvent(nameof(MakeRepository), ex)); - return null; + return new Repository(); } return newRepo; @@ -115,7 +115,7 @@ public Repository GetRepository(string repositoryName, string cloneLocation) using var dbContext = _databaseContextFactory.GetNewContext(); #pragma warning disable CA1309 // Use ordinal string comparison return dbContext.Repositories.FirstOrDefault(x => x.RepositoryName!.Equals(repositoryName) - && string.Equals(x.RepositoryClonePath, Path.GetFullPath(cloneLocation))); + && string.Equals(x.RepositoryClonePath, Path.GetFullPath(cloneLocation))) ?? new Repository(); #pragma warning restore CA1309 // Use ordinal string comparison } catch (Exception ex) @@ -127,7 +127,7 @@ public Repository GetRepository(string repositoryName, string cloneLocation) new DevHomeDatabaseEvent(nameof(GetRepository), ex)); } - return null; + return new Repository(); } public bool UpdateCloneLocation(Repository repository, string newLocation) @@ -150,8 +150,8 @@ public bool UpdateCloneLocation(Repository repository, string newLocation) var configurationFolder = Path.GetDirectoryName(repository.ConfigurationFileLocation); var configurationFileName = Path.GetFileName(configurationFolder); - repository.ConfigurationFileLocation = Path.Combine(newLocation, configurationFolder, configurationFileName); - maybeRepository.ConfigurationFileLocation = Path.Combine(newLocation, configurationFolder, configurationFileName); + repository.ConfigurationFileLocation = Path.Combine(newLocation, configurationFolder ?? string.Empty, configurationFileName ?? string.Empty); + maybeRepository.ConfigurationFileLocation = Path.Combine(newLocation, configurationFolder ?? string.Empty, configurationFileName ?? string.Empty); } dbContext.SaveChanges(); diff --git a/src/App.xaml.cs b/src/App.xaml.cs index 61d0376d19..e472de2e74 100644 --- a/src/App.xaml.cs +++ b/src/App.xaml.cs @@ -100,13 +100,13 @@ public App() ConfigureServices((context, services) => { // Add databse connection - services.AddDatabaseContext(context); + services.AddDatabase(context); // Add Serilog logging for ILogger. services.AddLogging(lb => lb.AddSerilog(dispose: true)); // Add databse connection - services.AddDatabaseContext(context); + services.AddDatabase(context); // Default Activation Handler services.AddTransient, DefaultActivationHandler>(); diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/DevHome.RepositoryManagement.csproj b/tools/RepositoryManagement/DevHome.RepositoryManagement/DevHome.RepositoryManagement.csproj index 93d3bd9294..6c95ed225d 100644 --- a/tools/RepositoryManagement/DevHome.RepositoryManagement/DevHome.RepositoryManagement.csproj +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/DevHome.RepositoryManagement.csproj @@ -23,6 +23,7 @@ + @@ -30,4 +31,8 @@ MSBuild:Compile + + + + \ No newline at end of file diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/Extensions/ServiceExtensions.cs b/tools/RepositoryManagement/DevHome.RepositoryManagement/Extensions/ServiceExtensions.cs index 34cc63d235..4009ed2d42 100644 --- a/tools/RepositoryManagement/DevHome.RepositoryManagement/Extensions/ServiceExtensions.cs +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/Extensions/ServiceExtensions.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using DevHome.RepositoryManagement.Services; using DevHome.RepositoryManagement.ViewModels; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -12,7 +11,6 @@ public static class ServiceExtensions { public static IServiceCollection AddRepositoryManagement(this IServiceCollection services, HostBuilderContext context) { - services.AddSingleton(); services.AddSingleton(); services.AddTransient(); diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementItemViewModel.cs b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementItemViewModel.cs index 110a609bd9..bb65aefb3c 100644 --- a/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementItemViewModel.cs +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementItemViewModel.cs @@ -8,9 +8,11 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using DevHome.Common.Services; using DevHome.Common.TelemetryEvents.RepositoryManagement; using DevHome.Common.Windows.FileDialog; -using DevHome.RepositoryManagement.Services; +using DevHome.Database.Services; +using DevHome.SetupFlow.Services; using DevHome.Telemetry; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -21,6 +23,10 @@ namespace DevHome.RepositoryManagement.ViewModels; // TODO: Clean up the code. public partial class RepositoryManagementItemViewModel : ObservableObject { + public const string RepoNamePrefix = "Clone "; + + public const string RepoNameSuffix = ": "; + public const string EventName = "DevHome_RepositorySpecific_Event"; public const string ErrorEventName = "DevHome_RepositorySpecificError_Event"; @@ -31,6 +37,10 @@ public partial class RepositoryManagementItemViewModel : ObservableObject private readonly RepositoryManagementDataAccessService _dataAccess; + private readonly IStringResource _stringResource; + + private readonly ConfigurationFileBuilder _configurationFileBuilder; + private string _repositoryName; /// @@ -225,11 +235,30 @@ public async Task DeleteRepository() } [RelayCommand] - public void MakeConfigurationFileWithThisRepository() + public async Task MakeConfigurationFileWithThisRepository() { - // D:\git\dhoehna\devhome\tools\SetupFlow\DevHome.SetupFlow\ViewModels\ReviewViewModel.cs - // DownloadConfigurationAsync has the code to do this. Well, to make it from a configuration file. - throw new NotImplementedException(); + try + { + // Show the save file dialog + using var fileDialog = new WindowSaveFileDialog(); + + // TODO: Needs Localization + fileDialog.AddFileType(_stringResource.GetLocalized("{0} file", "YAML"), ".winget"); + fileDialog.AddFileType(_stringResource.GetLocalized("{0} file", "YAML"), ".dsc.yaml"); + var fileName = fileDialog.Show(_window); + + // If the user selected a file, write the configuration to it + if (!string.IsNullOrEmpty(fileName)) + { + var repositoryToUse = _dataAccess.GetRepository(RepositoryName, ClonePath); + var configFile = _configurationFileBuilder.GetConfigurationFileForRepoAndGit(repositoryToUse); + await File.WriteAllTextAsync(fileName, configFile); + } + } + catch (Exception e) + { + _log.Error(e, $"Failed to download configuration file."); + } } [RelayCommand] @@ -289,10 +318,16 @@ public void RemoveThisRepositoryFromTheList() _dataAccess.SetIsHidden(repository, true); } - public RepositoryManagementItemViewModel(Window window, RepositoryManagementDataAccessService dataAccess) + public RepositoryManagementItemViewModel( + Window window, + RepositoryManagementDataAccessService dataAccess, + IStringResource stringResource, + ConfigurationFileBuilder configurationFileBuilder) { _window = window; _dataAccess = dataAccess; + _stringResource = stringResource; + _configurationFileBuilder = configurationFileBuilder; } private void OpenRepositoryInFileExplorer(string repositoryName, string cloneLocation, string action) @@ -447,4 +482,54 @@ private async Task CloneLocationNotFoundNotifyUser( return; } } + + /* + public WinGetConfigFile DownloadConfigFileFromARepository(Repository repository) + { + List resources = []; + resources.Add(MakeSomethingFromARepository(repository)); + resources.Add(CreateWinGetInstallForGitPreReq()); + + var wingetConfigProperties = new WinGetConfigProperties(); + + // Merge the resources into the Resources property in the properties object + wingetConfigProperties.Resources = resources.ToArray(); + wingetConfigProperties.ConfigurationVersion = DscHelpers.WinGetConfigureVersion; + + // Create the new WinGetConfigFile object and serialize it to yaml + return new WinGetConfigFile() { Properties = wingetConfigProperties }; + } + + private WinGetConfigResource MakeSomethingFromARepository(Repository repository) + { + // WinGet configure uses the Id property to uniquely identify a resource and also to display the resource status in the UI. + // So we add a description to the Id to make it more readable in the UI. These do not need to be localized. + var id = $"{RepoNamePrefix}{repository.RepositoryName}{RepoNameSuffix}{Path.GetFullPath(repository.RepositoryClonePath)}"; + + var gitDependsOnId = DscHelpers.GitWinGetPackageId; + + // TODO: Add clone URL to the database + return new WinGetConfigResource() + { + Resource = DscHelpers.GitCloneDscResource, + Id = id, + Directives = new() { AllowPrerelease = true, Description = $"Cloning: {repository.RepositoryName}" }, + DependsOn = [gitDependsOnId], + Settings = new GitDscSettings() { HttpsUrl = string.Empty, RootDirectory = Path.GetFullPath(repository.RepositoryClonePath) }, + }; + } + + private WinGetConfigResource CreateWinGetInstallForGitPreReq() + { + var id = DscHelpers.GitWinGetPackageId; + + return new WinGetConfigResource() + { + Resource = DscHelpers.WinGetDscResource, + Id = id, + Directives = new() { AllowPrerelease = true, Description = $"Installing {DscHelpers.GitName}" }, + Settings = new WinGetDscSettings() { Id = DscHelpers.GitWinGetPackageId, Source = DscHelpers.DscSourceNameForWinGet }, + }; + } + */ } diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs index 739082c702..e42f936036 100644 --- a/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs @@ -8,7 +8,7 @@ using CommunityToolkit.Mvvm.Input; using DevHome.Common.Extensions; using DevHome.Database.DatabaseModels.RepositoryManagement; -using DevHome.RepositoryManagement.Services; +using DevHome.Database.Services; using Microsoft.Extensions.Hosting; using Serilog; diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml b/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml index 5d2474e0eb..2eab671b6c 100644 --- a/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml @@ -168,7 +168,7 @@ - + diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/DevHome.SetupFlow.Common.csproj b/tools/SetupFlow/DevHome.SetupFlow.Common/DevHome.SetupFlow.Common.csproj index 1199e9bc67..3c42c58c89 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.Common/DevHome.SetupFlow.Common.csproj +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/DevHome.SetupFlow.Common.csproj @@ -15,6 +15,7 @@ all runtime; build; native; contentfiles; analyzers + diff --git a/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj b/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj index a9559150cb..adfc2ec100 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj +++ b/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj @@ -21,7 +21,6 @@ - Projection diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs index 5bec0c703c..462ae3f829 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs @@ -13,7 +13,7 @@ using DevHome.Common.Services; using DevHome.Common.TelemetryEvents; using DevHome.Common.TelemetryEvents.SetupFlow; -using DevHome.RepositoryManagement.Services; +using DevHome.Database.Services; using DevHome.SetupFlow.Common.Helpers; using DevHome.SetupFlow.Services; using DevHome.SetupFlow.ViewModels; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs index b72df561a1..08bc2f6235 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs @@ -3,7 +3,10 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Threading.Tasks; +using DevHome.Database.DatabaseModels.RepositoryManagement; using DevHome.SetupFlow.Common.Helpers; using DevHome.SetupFlow.Models; using DevHome.SetupFlow.Models.WingetConfigure; @@ -118,6 +121,38 @@ public string SerializeWingetFileObjectToString(WinGetConfigFile configFile) return configStringWithHeader; } + public string GetConfigurationFileForRepoAndGit(Repository repository) + { + // WinGet configure uses the Id property to uniquely identify a resource and also to display the resource status in the UI. + // So we add a description to the Id to make it more readable in the UI. These do not need to be localized. + var id = $"{RepoNamePrefix}{repository.RepositoryName}{RepoNameSuffix}{Path.GetFullPath(repository.RepositoryClonePath)}"; + var gitDependsOnId = DscHelpers.GitWinGetPackageId; + + List resources = []; + + resources.Add(new WinGetConfigResource() + { + Resource = DscHelpers.GitCloneDscResource, + Id = id, + Directives = new() { AllowPrerelease = true, Description = $"Cloning: {repository.RepositoryName}" }, + DependsOn = [gitDependsOnId], + Settings = new GitDscSettings() { HttpsUrl = string.Empty, RootDirectory = Path.GetFullPath(repository.RepositoryClonePath) }, + }); + + resources.Add(CreateWinGetInstallForGitPreReq(ConfigurationFileKind.Normal)); + + var wingetConfigProperties = new WinGetConfigProperties(); + + // Merge the resources into the Resources property in the properties object + wingetConfigProperties.Resources = resources.ToArray(); + wingetConfigProperties.ConfigurationVersion = DscHelpers.WinGetConfigureVersion; + + // Create the new WinGetConfigFile object and serialize it to yaml + var myFile = new WinGetConfigFile() { Properties = wingetConfigProperties }; + + return SerializeWingetFileObjectToString(myFile); + } + /// /// Creates a list of WinGetConfigResource objects from the CloneRepoTask objects in the RepoConfigTaskGroup /// From cfa8f9a055597b54e53664d84271615ae9a3fec2 Mon Sep 17 00:00:00 2001 From: dahoehna Date: Mon, 9 Sep 2024 16:29:48 -0700 Subject: [PATCH 23/41] Cleaning up the code --- .../RepositoryManagement/Repository.cs | 10 +- ...240909211958_InitialMigration.Designer.cs} | 5 +- ....cs => 20240909211958_InitialMigration.cs} | 5 +- .../DevHomeDatabaseContextModelSnapshot.cs | 3 + .../RepositoryManagementDataAccessService.cs | 13 +- .../Extensions/ServiceExtensions.cs | 3 +- ...epositoryManagementItemViewModelFactory.cs | 67 +++++ .../RepositoryManagementItemViewModel.cs | 254 ++++++------------ .../RepositoryManagementMainPageViewModel.cs | 19 +- .../RepositoryManagementMainPageView.xaml | 2 +- .../DevHome.SetupFlow/Models/CloneRepoTask.cs | 4 +- 11 files changed, 192 insertions(+), 193 deletions(-) rename database/DevHome.Database/Migrations/{20240906194921_InitialMigration.Designer.cs => 20240909211958_InitialMigration.Designer.cs} (94%) rename database/DevHome.Database/Migrations/{20240906194921_InitialMigration.cs => 20240909211958_InitialMigration.cs} (96%) create mode 100644 tools/RepositoryManagement/DevHome.RepositoryManagement/Factories/RepositoryManagementItemViewModelFactory.cs diff --git a/database/DevHome.Database/DatabaseModels/RepositoryManagement/Repository.cs b/database/DevHome.Database/DatabaseModels/RepositoryManagement/Repository.cs index 3b5171c2c4..6b6bb21042 100644 --- a/database/DevHome.Database/DatabaseModels/RepositoryManagement/Repository.cs +++ b/database/DevHome.Database/DatabaseModels/RepositoryManagement/Repository.cs @@ -20,13 +20,15 @@ public class Repository public string? RepositoryClonePath { get; set; } - public DateTime? CreatedUTCDate { get; set; } - - public DateTime? UpdatedUTCDate { get; set; } - public bool IsHidden { get; set; } public bool HasAConfigurationFile { get; set; } public string? ConfigurationFileLocation { get; set; } + + public Uri? RepositoryUri { get; set; } + + public DateTime? CreatedUTCDate { get; set; } + + public DateTime? UpdatedUTCDate { get; set; } } diff --git a/database/DevHome.Database/Migrations/20240906194921_InitialMigration.Designer.cs b/database/DevHome.Database/Migrations/20240909211958_InitialMigration.Designer.cs similarity index 94% rename from database/DevHome.Database/Migrations/20240906194921_InitialMigration.Designer.cs rename to database/DevHome.Database/Migrations/20240909211958_InitialMigration.Designer.cs index 5ba9af1fd7..b567bed038 100644 --- a/database/DevHome.Database/Migrations/20240906194921_InitialMigration.Designer.cs +++ b/database/DevHome.Database/Migrations/20240909211958_InitialMigration.Designer.cs @@ -11,7 +11,7 @@ namespace DevHome.Database.Migrations { [DbContext(typeof(DevHomeDatabaseContext))] - [Migration("20240906194921_InitialMigration")] + [Migration("20240909211958_InitialMigration")] partial class InitialMigration { /// @@ -52,6 +52,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("TEXT") .HasDefaultValue(""); + b.Property("RepositoryUri") + .HasColumnType("TEXT"); + b.Property("UpdatedUTCDate") .ValueGeneratedOnAdd() .HasColumnType("TEXT") diff --git a/database/DevHome.Database/Migrations/20240906194921_InitialMigration.cs b/database/DevHome.Database/Migrations/20240909211958_InitialMigration.cs similarity index 96% rename from database/DevHome.Database/Migrations/20240906194921_InitialMigration.cs rename to database/DevHome.Database/Migrations/20240909211958_InitialMigration.cs index d96185d49d..0cfad039ff 100644 --- a/database/DevHome.Database/Migrations/20240906194921_InitialMigration.cs +++ b/database/DevHome.Database/Migrations/20240909211958_InitialMigration.cs @@ -24,11 +24,12 @@ protected override void Up(MigrationBuilder migrationBuilder) .Annotation("Sqlite:Autoincrement", true), RepositoryName = table.Column(type: "TEXT", nullable: false, defaultValue: string.Empty), RepositoryClonePath = table.Column(type: "TEXT", nullable: false, defaultValue: string.Empty), - CreatedUTCDate = table.Column(type: "TEXT", nullable: true, defaultValueSql: "datetime()"), - UpdatedUTCDate = table.Column(type: "TEXT", nullable: true, defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)), IsHidden = table.Column(type: "INTEGER", nullable: false), HasAConfigurationFile = table.Column(type: "INTEGER", nullable: false), ConfigurationFileLocation = table.Column(type: "TEXT", nullable: true), + RepositoryUri = table.Column(type: "TEXT", nullable: true), + CreatedUTCDate = table.Column(type: "TEXT", nullable: true, defaultValueSql: "datetime()"), + UpdatedUTCDate = table.Column(type: "TEXT", nullable: true, defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)), }, constraints: table => { diff --git a/database/DevHome.Database/Migrations/DevHomeDatabaseContextModelSnapshot.cs b/database/DevHome.Database/Migrations/DevHomeDatabaseContextModelSnapshot.cs index 1fb373a123..9937e87f96 100644 --- a/database/DevHome.Database/Migrations/DevHomeDatabaseContextModelSnapshot.cs +++ b/database/DevHome.Database/Migrations/DevHomeDatabaseContextModelSnapshot.cs @@ -49,6 +49,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT") .HasDefaultValue(string.Empty); + b.Property("RepositoryUri") + .HasColumnType("TEXT"); + b.Property("UpdatedUTCDate") .ValueGeneratedOnAdd() .HasColumnType("TEXT") diff --git a/database/DevHome.Database/Services/RepositoryManagementDataAccessService.cs b/database/DevHome.Database/Services/RepositoryManagementDataAccessService.cs index 384f400002..9a53584469 100644 --- a/database/DevHome.Database/Services/RepositoryManagementDataAccessService.cs +++ b/database/DevHome.Database/Services/RepositoryManagementDataAccessService.cs @@ -33,12 +33,12 @@ public RepositoryManagementDataAccessService(DevHomeDatabaseContextFactory datab /// The name of the repository to add. /// The local location the repository is cloned to. /// The new repository. Can return null if the database threw an exception. - public Repository MakeRepository(string repositoryName, string cloneLocation) + public Repository MakeRepository(string repositoryName, string cloneLocation, Uri repositoryUri) { - return MakeRepository(repositoryName, cloneLocation, string.Empty); + return MakeRepository(repositoryName, cloneLocation, string.Empty, repositoryUri); } - public Repository MakeRepository(string repositoryName, string cloneLocation, string configurationFileLocationAndName) + public Repository MakeRepository(string repositoryName, string cloneLocation, string configurationFileLocationAndName, Uri repositoryUri) { var existingRepository = GetRepository(repositoryName, cloneLocation); if (existingRepository != null) @@ -51,6 +51,7 @@ public Repository MakeRepository(string repositoryName, string cloneLocation, st { RepositoryName = repositoryName, RepositoryClonePath = cloneLocation, + RepositoryUri = repositoryUri, }; if (!string.IsNullOrEmpty(configurationFileLocationAndName)) @@ -107,7 +108,7 @@ public List GetRepositories() return repositories; } - public Repository GetRepository(string repositoryName, string cloneLocation) + public Repository? GetRepository(string repositoryName, string cloneLocation) { _log.Information("Getting a repository"); try @@ -115,7 +116,7 @@ public Repository GetRepository(string repositoryName, string cloneLocation) using var dbContext = _databaseContextFactory.GetNewContext(); #pragma warning disable CA1309 // Use ordinal string comparison return dbContext.Repositories.FirstOrDefault(x => x.RepositoryName!.Equals(repositoryName) - && string.Equals(x.RepositoryClonePath, Path.GetFullPath(cloneLocation))) ?? new Repository(); + && string.Equals(x.RepositoryClonePath, Path.GetFullPath(cloneLocation))); #pragma warning restore CA1309 // Use ordinal string comparison } catch (Exception ex) @@ -127,7 +128,7 @@ public Repository GetRepository(string repositoryName, string cloneLocation) new DevHomeDatabaseEvent(nameof(GetRepository), ex)); } - return new Repository(); + return null; } public bool UpdateCloneLocation(Repository repository, string newLocation) diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/Extensions/ServiceExtensions.cs b/tools/RepositoryManagement/DevHome.RepositoryManagement/Extensions/ServiceExtensions.cs index 4009ed2d42..4cd055611c 100644 --- a/tools/RepositoryManagement/DevHome.RepositoryManagement/Extensions/ServiceExtensions.cs +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/Extensions/ServiceExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using DevHome.RepositoryManagement.Factories; using DevHome.RepositoryManagement.ViewModels; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -12,7 +13,7 @@ public static class ServiceExtensions public static IServiceCollection AddRepositoryManagement(this IServiceCollection services, HostBuilderContext context) { services.AddSingleton(); - services.AddTransient(); + services.AddSingleton(); return services; } diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/Factories/RepositoryManagementItemViewModelFactory.cs b/tools/RepositoryManagement/DevHome.RepositoryManagement/Factories/RepositoryManagementItemViewModelFactory.cs new file mode 100644 index 0000000000..b3a2ecf6fb --- /dev/null +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/Factories/RepositoryManagementItemViewModelFactory.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DevHome.Common.Services; +using DevHome.Database.Services; +using DevHome.RepositoryManagement.ViewModels; +using DevHome.SetupFlow.Services; +using Microsoft.UI.Xaml; +using Serilog; + +namespace DevHome.RepositoryManagement.Factories; + +public class RepositoryManagementItemViewModelFactory +{ + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(RepositoryManagementItemViewModelFactory)); + + private readonly Window _window; + + private readonly RepositoryManagementDataAccessService _dataAccessService; + + private readonly IStringResource _stringResource; + + private readonly ConfigurationFileBuilder _configurationFileBuilder; + + public RepositoryManagementItemViewModelFactory( + Window window, + RepositoryManagementDataAccessService dataAccess, + IStringResource stringResource, + ConfigurationFileBuilder configurationFileBuilder) + { + _window = window; + _dataAccessService = dataAccess; + _stringResource = stringResource; + _configurationFileBuilder = configurationFileBuilder; + } + + public RepositoryManagementItemViewModel MakeViewModel(string repositoryName, string cloneLocation, bool isHidden) + { + var localIsHidden = isHidden; + var localRepositoryName = repositoryName; + if (string.IsNullOrEmpty(repositoryName)) + { + _log.Warning($"{nameof(repositoryName)} is either null or empty. Hiding repository"); + localRepositoryName = string.Empty; + localIsHidden = true; + } + + var localCloneLocation = cloneLocation; + if (string.IsNullOrEmpty(cloneLocation)) + { + _log.Warning($"{nameof(cloneLocation)} is either null or empty. Hiding repository"); + localCloneLocation = string.Empty; + localIsHidden = true; + } + + var newViewModel = new RepositoryManagementItemViewModel(_window, _dataAccessService, _stringResource, _configurationFileBuilder, localRepositoryName, localCloneLocation); + + newViewModel.IsHiddenFromPage = localIsHidden; + + return newViewModel; + } +} diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementItemViewModel.cs b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementItemViewModel.cs index bb65aefb3c..c96c28c2d7 100644 --- a/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementItemViewModel.cs +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementItemViewModel.cs @@ -11,6 +11,7 @@ using DevHome.Common.Services; using DevHome.Common.TelemetryEvents.RepositoryManagement; using DevHome.Common.Windows.FileDialog; +using DevHome.Database.DatabaseModels.RepositoryManagement; using DevHome.Database.Services; using DevHome.SetupFlow.Services; using DevHome.Telemetry; @@ -41,17 +42,10 @@ public partial class RepositoryManagementItemViewModel : ObservableObject private readonly ConfigurationFileBuilder _configurationFileBuilder; - private string _repositoryName; - /// - /// Gets or sets the name of the repository. Nulls are converted to string.empty. + /// Gets the name of the repository. /// - public string RepositoryName - { - get => _repositoryName ?? string.Empty; - - set => _repositoryName = value ?? string.Empty; - } + public string RepositoryName { get; } [ObservableProperty] private string _clonePath; @@ -84,32 +78,20 @@ public string Branch public bool IsHiddenFromPage { get; set; } + public bool HasAConfigurationFile { get; set; } + [RelayCommand] public async Task OpenInFileExplorer() { - var localClonePath = ClonePath ?? string.Empty; - - // Ask the user if they can point DevHome to the correct location - if (!Directory.Exists(Path.GetFullPath(localClonePath))) - { - await CloneLocationNotFoundNotifyUser(RepositoryName); - } - - OpenRepositoryInFileExplorer(RepositoryName, localClonePath, nameof(OpenInFileExplorer)); + await CheckCloneLocationNotifyUserIfNotFound(); + OpenRepositoryInFileExplorer(RepositoryName, ClonePath, nameof(OpenInFileExplorer)); } [RelayCommand] public async Task OpenInCMD() { - var localClonePath = ClonePath ?? string.Empty; - - // Ask the user if they can point DevHome to the correct location - if (!Directory.Exists(Path.GetFullPath(localClonePath))) - { - await CloneLocationNotFoundNotifyUser(RepositoryName); - } - - OpenRepositoryinCMD(RepositoryName, localClonePath, nameof(OpenInCMD)); + await CheckCloneLocationNotifyUserIfNotFound(); + OpenRepositoryinCMD(RepositoryName, ClonePath, nameof(OpenInCMD)); } [RelayCommand] @@ -118,7 +100,6 @@ public async Task MoveRepository() // TODO: Save to the database before moving the folder. var newLocation = await PickNewLocationForRepositoryAsync(); - // TODO: Warn the user no action will take place if (string.IsNullOrEmpty(newLocation)) { _log.Information("The path from the folder picker is either null or empty. Not updating the clone path"); @@ -127,29 +108,12 @@ public async Task MoveRepository() if (string.Equals(Path.GetFullPath(newLocation), Path.GetFullPath(ClonePath), StringComparison.OrdinalIgnoreCase)) { - _log.Information("The selected path is the same as the current path."); - return; - } - - var localOldClonePath = ClonePath ?? string.Empty; - var repository = _dataAccess.GetRepository(RepositoryName, localOldClonePath); - - // The user clicked on this menu from the repository management page. - // The repository should be in the database. - // Somehow getting the repository returned null. - if (repository is null) - { - _log.Warning($"The repository with name {RepositoryName} and clone location {localOldClonePath} is not in the database when it is expected to be there."); - TelemetryFactory.Get().Log( - EventName, - LogLevel.Critical, - new RepositoryLineItemEvent(nameof(OpenInFileExplorer), RepositoryName)); - + _log.Information("The selected path is the same as the current path. Not updating the clone path"); return; } var newDirectoryInfo = new DirectoryInfo(Path.Join(newLocation, RepositoryName)); - var currentDirectoryInfo = new DirectoryInfo(Path.GetFullPath(localOldClonePath)); + var currentDirectoryInfo = new DirectoryInfo(Path.GetFullPath(ClonePath)); try { @@ -164,8 +128,12 @@ public async Task MoveRepository() new RepositoryLineItemEvent(nameof(MoveRepository), RepositoryName)); } - // The repository exists at the location stored in the Database - // and the new location is set. + var repository = GetRepositoryReportIfNull(nameof(MoveRepository)); + if (repository == null) + { + return; + } + var didUpdate = _dataAccess.UpdateCloneLocation(repository, newDirectoryInfo.FullName); if (!didUpdate) @@ -179,59 +147,54 @@ public async Task MoveRepository() [RelayCommand] public async Task DeleteRepository() { - var repository = _dataAccess.GetRepository(RepositoryName, ClonePath); - - // The user clicked on this menu from the repository management page. - // The repository should be in the database. - // Somehow getting the repository returned null. - if (repository is null) - { - _log.Warning($"The repository with name {RepositoryName} and clone location {ClonePath} is not in the database when it is expected to be there."); - TelemetryFactory.Get().Log( - EventName, - LogLevel.Critical, - new RepositoryLineItemEvent(nameof(DeleteRepository), RepositoryName)); - - return; - } - var cantFindRepositoryDialog = new ContentDialog() { XamlRoot = _window.Content.XamlRoot, Title = $"Would you like to delete this repository?", Content = $"Deleting a repository means it will be permanently removed in File Explorer and from your PC.", - PrimaryButtonText = $"Yes", + PrimaryButtonText = "Yes", CloseButtonText = "Cancel", }; var dialogResult = await cantFindRepositoryDialog.ShowAsync(); - if (dialogResult == ContentDialogResult.Primary) + if (dialogResult != ContentDialogResult.Primary) + { + return; + } + + // Remove the repository. + // TODO: Check if this location is a repository and the name matches the repo name + // in path. + if (!string.IsNullOrEmpty(ClonePath) + && Directory.Exists(ClonePath)) { - // Remove the repository. - // TODO: Check if this location is a repository and the name matches the repo name - // in path. - if (!string.IsNullOrEmpty(ClonePath) - && Directory.Exists(ClonePath)) + // Cumbersome, but needed to remove read-only files. + foreach (var myFile in Directory.EnumerateFiles(ClonePath, "*", SearchOption.AllDirectories)) { - // Cumbersome, but needed to remove read-only files. - foreach (var myFile in Directory.EnumerateFiles(ClonePath, "*", SearchOption.AllDirectories)) - { - File.SetAttributes(myFile, FileAttributes.Normal); - File.Delete(myFile); - } + File.SetAttributes(myFile, FileAttributes.Normal); + File.Delete(myFile); + } - foreach (var myDirectory in Directory.GetDirectories(ClonePath, "*", SearchOption.AllDirectories).Reverse()) - { - Directory.Delete(myDirectory); - } + foreach (var myDirectory in Directory.GetDirectories(ClonePath, "*", SearchOption.AllDirectories).Reverse()) + { + Directory.Delete(myDirectory); + } - File.SetAttributes(ClonePath, FileAttributes.Normal); - Directory.Delete(ClonePath, false); + File.SetAttributes(ClonePath, FileAttributes.Normal); + Directory.Delete(ClonePath, false); + } - _dataAccess.RemoveRepository(repository); - } + var repository = GetRepositoryReportIfNull(nameof(DeleteRepository)); + if (repository == null) + { + // Do not warn the user here. If the repository is not in the database + // the repository management page will not display the repository + // when entities are fetched. + return; } + + _dataAccess.RemoveRepository(repository); } [RelayCommand] @@ -251,6 +214,12 @@ public async Task MakeConfigurationFileWithThisRepository() if (!string.IsNullOrEmpty(fileName)) { var repositoryToUse = _dataAccess.GetRepository(RepositoryName, ClonePath); + var repository = GetRepositoryReportIfNull(nameof(MakeConfigurationFileWithThisRepository)); + if (repository == null) + { + return; + } + var configFile = _configurationFileBuilder.GetConfigurationFileForRepoAndGit(repositoryToUse); await File.WriteAllTextAsync(fileName, configFile); } @@ -264,19 +233,9 @@ public async Task MakeConfigurationFileWithThisRepository() [RelayCommand] public void RunConfigurationFile() { - var repository = _dataAccess.GetRepository(RepositoryName, ClonePath); - - // The user clicked on this menu from the repository management page. - // The repository should be in the database. - // Somehow getting the repository returned null. - if (repository is null) + var repository = GetRepositoryReportIfNull(nameof(RunConfigurationFile)); + if (repository == null) { - _log.Warning($"The repository with name {RepositoryName} and clone location {ClonePath} is not in the database when it is expected to be there."); - TelemetryFactory.Get().Log( - EventName, - LogLevel.Critical, - new RepositoryLineItemEvent(nameof(OpenInFileExplorer), RepositoryName)); - return; } @@ -299,35 +258,29 @@ public void RunConfigurationFile() [RelayCommand] public void RemoveThisRepositoryFromTheList() { - var repository = _dataAccess.GetRepository(RepositoryName, ClonePath); - - // The user clicked on this menu from the repository management page. - // The repository should be in the database. - // Somehow getting the repository returned null. - if (repository is null) + var repository = GetRepositoryReportIfNull(nameof(RemoveThisRepositoryFromTheList)); + if (repository == null) { - _log.Warning($"The repository with name {RepositoryName} and clone location {ClonePath} is not in the database when it is expected to be there."); - TelemetryFactory.Get().Log( - EventName, - LogLevel.Critical, - new RepositoryLineItemEvent(nameof(OpenInFileExplorer), RepositoryName)); - return; } _dataAccess.SetIsHidden(repository, true); } - public RepositoryManagementItemViewModel( + internal RepositoryManagementItemViewModel( Window window, RepositoryManagementDataAccessService dataAccess, IStringResource stringResource, - ConfigurationFileBuilder configurationFileBuilder) + ConfigurationFileBuilder configurationFileBuilder, + string repositoryName, + string cloneLocation) { _window = window; _dataAccess = dataAccess; _stringResource = stringResource; _configurationFileBuilder = configurationFileBuilder; + RepositoryName = repositoryName; + _clonePath = cloneLocation; } private void OpenRepositoryInFileExplorer(string repositoryName, string cloneLocation, string action) @@ -422,8 +375,7 @@ private void SendTelemetryAndLogError(string operation, Exception ex) _log.Error(ex, string.Empty); } - private async Task CloneLocationNotFoundNotifyUser( - string repositoryName) + private async Task CloneLocationNotFoundNotifyUser() { // strings need to be localized var cantFindRepositoryDialog = new ContentDialog() @@ -451,19 +403,9 @@ private async Task CloneLocationNotFoundNotifyUser( return; } - var repository = _dataAccess.GetRepository(RepositoryName, ClonePath); - - // The user clicked on this menu from the repository management page. - // The repository should be in the database. - // Somehow getting the repository returned null. - if (repository is null) + var repository = GetRepositoryReportIfNull(nameof(CloneLocationNotFoundNotifyUser)); + if (repository == null) { - _log.Warning($"The repository with name {RepositoryName} and clone location {ClonePath} is not in the database when it is expected to be there."); - TelemetryFactory.Get().Log( - EventName, - LogLevel.Critical, - new RepositoryLineItemEvent(nameof(OpenInFileExplorer), repositoryName)); - return; } @@ -483,53 +425,33 @@ private async Task CloneLocationNotFoundNotifyUser( } } - /* - public WinGetConfigFile DownloadConfigFileFromARepository(Repository repository) + private Repository GetRepositoryReportIfNull(string action) { - List resources = []; - resources.Add(MakeSomethingFromARepository(repository)); - resources.Add(CreateWinGetInstallForGitPreReq()); - - var wingetConfigProperties = new WinGetConfigProperties(); - - // Merge the resources into the Resources property in the properties object - wingetConfigProperties.Resources = resources.ToArray(); - wingetConfigProperties.ConfigurationVersion = DscHelpers.WinGetConfigureVersion; - - // Create the new WinGetConfigFile object and serialize it to yaml - return new WinGetConfigFile() { Properties = wingetConfigProperties }; - } + var repository = _dataAccess.GetRepository(RepositoryName, ClonePath); - private WinGetConfigResource MakeSomethingFromARepository(Repository repository) - { - // WinGet configure uses the Id property to uniquely identify a resource and also to display the resource status in the UI. - // So we add a description to the Id to make it more readable in the UI. These do not need to be localized. - var id = $"{RepoNamePrefix}{repository.RepositoryName}{RepoNameSuffix}{Path.GetFullPath(repository.RepositoryClonePath)}"; + // The user clicked on this menu from the repository management page. + // The repository should be in the database. + // Somehow getting the repository returned null. + if (repository is null) + { + _log.Warning($"The repository with name {RepositoryName} and clone location {ClonePath} is not in the database when it is expected to be there."); + TelemetryFactory.Get().Log( + EventName, + LogLevel.Critical, + new RepositoryLineItemEvent(action, RepositoryName)); - var gitDependsOnId = DscHelpers.GitWinGetPackageId; + return null; + } - // TODO: Add clone URL to the database - return new WinGetConfigResource() - { - Resource = DscHelpers.GitCloneDscResource, - Id = id, - Directives = new() { AllowPrerelease = true, Description = $"Cloning: {repository.RepositoryName}" }, - DependsOn = [gitDependsOnId], - Settings = new GitDscSettings() { HttpsUrl = string.Empty, RootDirectory = Path.GetFullPath(repository.RepositoryClonePath) }, - }; + return repository; } - private WinGetConfigResource CreateWinGetInstallForGitPreReq() + private async Task CheckCloneLocationNotifyUserIfNotFound() { - var id = DscHelpers.GitWinGetPackageId; - - return new WinGetConfigResource() + if (!Directory.Exists(Path.GetFullPath(ClonePath))) { - Resource = DscHelpers.WinGetDscResource, - Id = id, - Directives = new() { AllowPrerelease = true, Description = $"Installing {DscHelpers.GitName}" }, - Settings = new WinGetDscSettings() { Id = DscHelpers.GitWinGetPackageId, Source = DscHelpers.DscSourceNameForWinGet }, - }; + // Ask the user if they can point DevHome to the correct location + await CloneLocationNotFoundNotifyUser(); + } } - */ } diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs index e42f936036..0fee499129 100644 --- a/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs @@ -6,10 +6,9 @@ using System.Collections.ObjectModel; using System.Linq; using CommunityToolkit.Mvvm.Input; -using DevHome.Common.Extensions; using DevHome.Database.DatabaseModels.RepositoryManagement; using DevHome.Database.Services; -using Microsoft.Extensions.Hosting; +using DevHome.RepositoryManagement.Factories; using Serilog; namespace DevHome.RepositoryManagement.ViewModels; @@ -18,7 +17,7 @@ public partial class RepositoryManagementMainPageViewModel { private readonly ILogger _log = Log.ForContext("SourceContext", nameof(RepositoryManagementMainPageViewModel)); - private readonly IHost _host; + private readonly RepositoryManagementItemViewModelFactory _factory; private readonly RepositoryManagementDataAccessService _dataAccessService; @@ -42,10 +41,12 @@ public void LoadRepositories() _items.Where(x => x.IsHiddenFromPage == false).ToList().ForEach(x => Items.Add(x)); } - public RepositoryManagementMainPageViewModel(IHost host, RepositoryManagementDataAccessService dataAccessService) + public RepositoryManagementMainPageViewModel( + RepositoryManagementItemViewModelFactory factory, + RepositoryManagementDataAccessService dataAccessService) { _dataAccessService = dataAccessService; - _host = host; + _factory = factory; Items = []; } @@ -56,13 +57,11 @@ private List ConvertToLineItems(List(); - lineItem.ClonePath = repo.RepositoryClonePath; + // TODO: get correct values for branch and latest commit information + var lineItem = _factory.MakeViewModel(repo.RepositoryName, repo.RepositoryClonePath, repo.IsHidden); lineItem.Branch = "main"; // Test value. Will change in the future. - lineItem.RepositoryName = repo.RepositoryName; lineItem.LatestCommit = "No commits found"; // Test value. Will change in the future. - - lineItem.IsHiddenFromPage = repo.IsHidden; + lineItem.HasAConfigurationFile = repo.HasAConfigurationFile; items.Add(lineItem); } diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml b/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml index 2eab671b6c..0c58b3ea1e 100644 --- a/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml @@ -169,7 +169,7 @@ - + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs index 462ae3f829..2b219336f4 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs @@ -287,11 +287,11 @@ IAsyncOperation ISetupTask.Execute() // Add the configuration file. if (!string.IsNullOrEmpty(_summaryScreenInformation.FilePathAndName)) { - var newRepo = serviceForMe.MakeRepository(RepositoryName, CloneLocation.FullName, _summaryScreenInformation.FilePathAndName); + var newRepo = serviceForMe.MakeRepository(RepositoryName, CloneLocation.FullName, _summaryScreenInformation.FilePathAndName, RepositoryToClone.RepoUri); } else { - var newRepo = serviceForMe.MakeRepository(RepositoryName, CloneLocation.FullName); + var newRepo = serviceForMe.MakeRepository(RepositoryName, CloneLocation.FullName, RepositoryToClone.RepoUri); } WasCloningSuccessful = true; From 0c00fca79c700dbe37e19ae272665aa04c38610d Mon Sep 17 00:00:00 2001 From: dahoehna Date: Tue, 10 Sep 2024 15:17:25 -0700 Subject: [PATCH 24/41] Icon for a configuration file. UI updates when hiding a repo --- .../RepositoryManagementItemViewModel.cs | 23 +++++----- .../RepositoryManagementMainPageViewModel.cs | 12 ++++++ .../RepositoryManagementMainPageView.xaml | 42 +++++++++++++------ 3 files changed, 52 insertions(+), 25 deletions(-) diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementItemViewModel.cs b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementItemViewModel.cs index c96c28c2d7..906ce51647 100644 --- a/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementItemViewModel.cs +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementItemViewModel.cs @@ -255,18 +255,6 @@ public void RunConfigurationFile() StartProcess(processStartInfo, nameof(RunConfigurationFile)); } - [RelayCommand] - public void RemoveThisRepositoryFromTheList() - { - var repository = GetRepositoryReportIfNull(nameof(RemoveThisRepositoryFromTheList)); - if (repository == null) - { - return; - } - - _dataAccess.SetIsHidden(repository, true); - } - internal RepositoryManagementItemViewModel( Window window, RepositoryManagementDataAccessService dataAccess, @@ -283,6 +271,17 @@ internal RepositoryManagementItemViewModel( _clonePath = cloneLocation; } + public void RemoveThisRepositoryFromTheList() + { + var repository = GetRepositoryReportIfNull(nameof(RemoveThisRepositoryFromTheList)); + if (repository == null) + { + return; + } + + _dataAccess.SetIsHidden(repository, true); + } + private void OpenRepositoryInFileExplorer(string repositoryName, string cloneLocation, string action) { _log.Information($"Showing {repositoryName} in File Explorer at location {cloneLocation}"); diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs index 0fee499129..d1811d607d 100644 --- a/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs @@ -41,6 +41,18 @@ public void LoadRepositories() _items.Where(x => x.IsHiddenFromPage == false).ToList().ForEach(x => Items.Add(x)); } + [RelayCommand] + public void HideRepository(RepositoryManagementItemViewModel repository) + { + if (repository == null) + { + return; + } + + repository.RemoveThisRepositoryFromTheList(); + LoadRepositories(); + } + public RepositoryManagementMainPageViewModel( RepositoryManagementItemViewModelFactory factory, RepositoryManagementDataAccessService dataAccessService) diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml b/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml index 0c58b3ea1e..414b4a43ce 100644 --- a/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml @@ -13,6 +13,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:viewmodels="using:DevHome.RepositoryManagement.ViewModels" behaviors:NavigationViewHeaderBehavior.HeaderMode="Never" + x:Name="RepositoryManagementMainPage" mc:Ignorable="d"> @@ -81,6 +82,7 @@ + @@ -89,7 +91,8 @@ VerticalAlignment="Center" Text="Sort:" /> - + + @@ -104,6 +107,7 @@ ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.VerticalScrollMode="Auto" SelectionMode="None"> + - + + + - + - + - + - + + + - + - + [RelayCommand] - private void StartRepoConfig(string flowTitle) + public void StartRepoConfig(string flowTitle) { _log.Information("Starting flow for repo config"); StartSetupFlowForTaskGroups( diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs index 33bab8ad53..7e98b9668d 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs @@ -18,6 +18,7 @@ using Microsoft.UI.Xaml.Navigation; using Serilog; using Windows.Storage; +using static Microsoft.EntityFrameworkCore.DbLoggerCategory; namespace DevHome.SetupFlow.ViewModels; @@ -29,9 +30,10 @@ public partial class SetupFlowViewModel : ObservableObject private readonly MainPageViewModel _mainPageViewModel; private readonly PackageProvider _packageProvider; - private readonly string _creationFlowNavigationParameter = "StartCreationFlow"; private readonly string _configurationFlowNavigationParameter = "StartConfigurationFlow"; - private readonly string _quickstartNavigationParameter = "StartQuickstartPlayground"; + private readonly string _creationFlowNavigationParameter = "StartCreationFlow"; + + private readonly Dictionary> _navigationTargets = new(); public SetupFlowOrchestrator Orchestrator { get; } @@ -43,6 +45,10 @@ public SetupFlowViewModel( SetupFlowOrchestrator orchestrator, PackageProvider packageProvider) { + _navigationTargets.Add(_creationFlowNavigationParameter, LocalStartCreationFlow); + _navigationTargets.Add("StartQuickstartPlayground", LocalStartQuickTestFlow); + _navigationTargets.Add(KnownPageKeys.RepositoryConfiguration, LocalStartRepositoryConfigurationFlow); + _host = host; _stringResource = stringResource; Orchestrator = orchestrator; @@ -149,38 +155,43 @@ public void OnNavigatedTo(NavigationEventArgs args) { // The setup flow isn't set up to support using the navigation service to navigate to specific // pages. Instead we need to navigate to the main page and then start the creation flow template manually. - var parameter = args.Parameter?.ToString(); + var parameter = args.Parameter?.ToString() ?? string.Empty; - if ((!string.IsNullOrEmpty(parameter)) && - parameter.Contains(_creationFlowNavigationParameter, StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(parameter)) { - // We expect that when navigating from anywhere in Dev Home to the create environment page - // that the arg.Parameter variable be semicolon delimited string with the first value being 'StartCreationFlow' - // and the second value being the page name that redirection came from for telemetry purposes. - var parameters = parameter.Split(';'); - Cancel(); - StartCreationFlow(originPage: parameters[1]); + _log.Information("args.Parameters is either null or empty. Not navigating"); + return; } - else if ((!string.IsNullOrEmpty(parameter)) && - parameter.Contains(_quickstartNavigationParameter, StringComparison.OrdinalIgnoreCase)) + + var didNavigate = false; + var actionPair = _navigationTargets.FirstOrDefault(x => parameter.Contains(x.Key, StringComparison.OrdinalIgnoreCase), default); + if (actionPair.Key != default) { - Cancel(); - Orchestrator.FlowPages = [_mainPageViewModel]; - var flowTitle = _stringResource.GetLocalized("MainPage_QuickstartPlayground/Header"); - _mainPageViewModel.StartQuickstart(flowTitle); + didNavigate = true; + actionPair.Value(parameter); } - else if (args.Parameter is object[] configObjs && configObjs.Length == 3) + else { - if (configObjs[0] is string configObj && configObj.Equals(_configurationFlowNavigationParameter, StringComparison.OrdinalIgnoreCase)) + if (args.Parameter is object[] configObjs && configObjs.Length == 3) { - // We expect that when navigating from anywhere in Dev Home to the create environment page - // that the arg.Parameter variable be an object array with the the first value being 'StartCreationFlow', - // the second value being the page name that redirection came from for telemetry purposes, and - // the third value being the ComputeSystemReviewItem to setup. - Cancel(); - StartSetupFlow(originPage: configObjs[1] as string, item: configObjs[2] as ComputeSystemReviewItem); + if (configObjs[0] is string configObj && configObj.Equals(_configurationFlowNavigationParameter, StringComparison.OrdinalIgnoreCase)) + { + didNavigate = true; + + // We expect that when navigating from anywhere in Dev Home to the create environment page + // that the arg.Parameter variable be an object array with the the first value being 'StartCreationFlow', + // the second value being the page name that redirection came from for telemetry purposes, and + // the third value being the ComputeSystemReviewItem to setup. + Cancel(); + StartSetupFlow(originPage: configObjs[1] as string, item: configObjs[2] as ComputeSystemReviewItem); + } } } + + if (!didNavigate) + { + _log.Warning($"Did not navigate with args {parameter}"); + } } public void StartAppManagementFlow(string query = null) @@ -188,4 +199,29 @@ public void StartAppManagementFlow(string query = null) Orchestrator.FlowPages = [_mainPageViewModel]; _mainPageViewModel.StartAppManagementFlow(query); } + + private void LocalStartCreationFlow(string parameter) + { + // We expect that when navigating from anywhere in Dev Home to the create environment page + // that the arg.Parameter variable be semicolon delimited string with the first value being 'StartCreationFlow' + // and the second value being the page name that redirection came from for telemetry purposes. + var parameters = parameter.Split(';'); + Cancel(); + StartCreationFlow(originPage: parameters[1]); + } + + private void LocalStartQuickTestFlow(string parameter) + { + Cancel(); + Orchestrator.FlowPages = [_mainPageViewModel]; + var flowTitle = _stringResource.GetLocalized("MainPage_QuickstartPlayground/Header"); + _mainPageViewModel.StartQuickstart(flowTitle); + } + + private void LocalStartRepositoryConfigurationFlow(string parameter) + { + Cancel(); + Orchestrator.FlowPages = [_mainPageViewModel]; + _mainPageViewModel.StartRepoConfig(_stringResource.GetLocalized("ReposConfigPageTitle")); + } } From 5d0d7d3a9cf7982732a307ae9b39d801ad5a5106 Mon Sep 17 00:00:00 2001 From: dahoehna Date: Tue, 10 Sep 2024 16:58:41 -0700 Subject: [PATCH 26/41] Removing 'local' --- .../RepositoryManagementMainPageViewModel.cs | 5 ++-- .../RepositoryManagementMainPageView.xaml | 1 + .../ViewModels/SetupFlowViewModel.cs | 26 ++++++++----------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs index fb122657ac..13f6ae1114 100644 --- a/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/ViewModels/RepositoryManagementMainPageViewModel.cs @@ -40,6 +40,7 @@ public async Task AddExistingRepository() { try { + // TODO: Use extensions to determine if the selected location is a repository. _log.Information("Opening folder picker to select a new location"); using var folderPicker = new WindowOpenFolderDialog(); var newLocation = await folderPicker.ShowAsync(_window); @@ -52,9 +53,9 @@ public async Task AddExistingRepository() _log.Information("Didn't select a location to clone to"); } } - catch (Exception e) + catch (Exception ex) { - _log.Error(e, "Failed to open folder picker"); + _log.Error(ex, "Failed to open folder picker"); } } diff --git a/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml b/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml index 09f5c49927..556c20cdef 100644 --- a/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml +++ b/tools/RepositoryManagement/DevHome.RepositoryManagement/Views/RepositoryManagementMainPageView.xaml @@ -64,6 +64,7 @@ Style="{ThemeResource SubtitleTextBlockStyle}" Text="Repositories" /> +