Skip to content

Commit

Permalink
Add support for defining settings folder (#188)
Browse files Browse the repository at this point in the history
* Add support for defining settings folder

* Docs changes

* Update

* Docs changes

* wip

* update

* fix

* Docs changes

* update

* Docs changes

* update

* Docs changes

* update pipeline

* update test coverage

---------

Co-authored-by: GitHub Action <action@github.com>
  • Loading branch information
coenm and actions-user authored Sep 25, 2024
1 parent 78d5450 commit adeacc4
Show file tree
Hide file tree
Showing 19 changed files with 485 additions and 41 deletions.
1 change: 1 addition & 0 deletions .azuredevops/Pipelines/Templates/create-installer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ steps:
displayName: Remove unneeded files
inputs:
contents: |
_output/**/appsettings.json
_output/**/*.xlm
_output/**/*.config
_output/**/*.pdb
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,31 @@ The repositories shown in RepoM are filtered using the search box in RepoM. But

When RepoM starts for the first time, a configuration file wil be created. Most of the properties can be adjusted using the UI but, at this moment, one property must be altered manually. Read more over [here](docs/_old/Settings.md).

## Multi configuration

By default, RepoM stores all configuration files in `%ADPPDATA%/RepoM`. As a user you can alter this location to support multi configurations which might be useful for different working environments. Also, for development or debug purposes, this might be very useful.

To change the app settings location, you can

- alter the `appsettings.json` file located in the same directory where the RepoM executable lives.

<!-- snippet: appsettings_appsettings_path_relative -->
<a id='snippet-appsettings_appsettings_path_relative'></a>
```cs
{
"App": {
"AppSettingsPath": "MyConfigJson"
}
}
```
<sup><a href='/tests/RepoM.App.Tests/ConfigBasedAppDataPathProviderFactoryTest.cs#L20-L26' title='Snippet source file'>snippet source</a> | <a href='#snippet-appsettings_appsettings_path_relative' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

- start RepoM using the commandline argument `--App:AppSettingsPath '<absolute or relative path here>'`.
- use environment variable `REPOM_App__AppSettingsPath`.

If none is set, the default will be used.

## Plugins

RepoM uses plugins to extend functionality. At this moment, when a plugin is available in the installed directory, it will be found and can be enabled or disabled. This is done in the hamburger menu of RepoM. Enabling or disabling requires a restart of RepoM.
Expand Down
15 changes: 15 additions & 0 deletions README.source.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ The repositories shown in RepoM are filtered using the search box in RepoM. But

When RepoM starts for the first time, a configuration file wil be created. Most of the properties can be adjusted using the UI but, at this moment, one property must be altered manually. Read more over [here](docs/_old/Settings.md).

## Multi configuration

By default, RepoM stores all configuration files in `%ADPPDATA%/RepoM`. As a user you can alter this location to support multi configurations which might be useful for different working environments. Also, for development or debug purposes, this might be very useful.

To change the app settings location, you can

- alter the `appsettings.json` file located in the same directory where the RepoM executable lives.

snippet: appsettings_appsettings_path_relative

- start RepoM using the commandline argument `--App:AppSettingsPath '<absolute or relative path here>'`.
- use environment variable `REPOM_App__AppSettingsPath`.

If none is set, the default will be used.

## Plugins

RepoM uses plugins to extend functionality. At this moment, when a plugin is available in the installed directory, it will be found and can be enabled or disabled. This is done in the hamburger menu of RepoM. Enabling or disabling requires a restart of RepoM.
Expand Down
3 changes: 1 addition & 2 deletions src/RepoM.Api/Bootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ namespace RepoM.Api;

using Microsoft.Extensions.Logging;
using RepoM.Api.Common;
using RepoM.Api.IO;
using RepoM.Api.Plugins;
using SimpleInjector;
using System.Collections.Generic;
Expand Down Expand Up @@ -87,7 +86,7 @@ await container
.RegisterPackagesAsync(
assemblies,
filename => new FileBasedPackageConfiguration(
DefaultAppDataPathProvider.Instance,
_appDataProvider,
_fileSystem,
_loggerFactory.CreateLogger<FileBasedPackageConfiguration>(),
filename))
Expand Down
6 changes: 6 additions & 0 deletions src/RepoM.Api/IO/AppDataPathConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace RepoM.Api.IO;

public record struct AppDataPathConfig
{
public string? AppSettingsPath { get; init; }
}
24 changes: 24 additions & 0 deletions src/RepoM.Api/IO/AppDataPathProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace RepoM.Api.IO;

using System;
using System.IO.Abstractions;
using RepoM.Core.Plugin.Common;

public sealed class AppDataPathProvider : IAppDataPathProvider
{
public AppDataPathProvider(AppDataPathConfig config, IFileSystem fileSystem)
{
ArgumentNullException.ThrowIfNull(config);
ArgumentNullException.ThrowIfNull(fileSystem);

if (!string.IsNullOrWhiteSpace(config.AppSettingsPath))
{
AppDataPath = fileSystem.Path.GetFullPath(config.AppSettingsPath);
return;
}

AppDataPath = fileSystem.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RepoM");
}

public string AppDataPath { get; }
}
18 changes: 0 additions & 18 deletions src/RepoM.Api/IO/DefaultAppDataPathProvider.cs

This file was deleted.

27 changes: 16 additions & 11 deletions src/RepoM.App/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ namespace RepoM.App;
using Container = SimpleInjector.Container;
using RepoM.App.Services.HotKey;
using RepoM.Api;
using RepoM.App.Configuration;

/// <summary>
/// Interaction logic for App.xaml
Expand Down Expand Up @@ -69,22 +70,26 @@ protected override async void OnStartup(StartupEventArgs e)
IHmacService hmacService = new HmacSha256Service();
IPluginFinder pluginFinder = new PluginFinder(fileSystem, hmacService);

IConfiguration config = SetupConfiguration();
var factory = new ConfigBasedAppDataPathProviderFactory(e.Args, fileSystem);
AppDataPathProvider appDataProvider = factory.Create();

IConfiguration config = CreateLoggerConfiguration(appDataProvider);
ILoggerFactory loggerFactory = CreateLoggerFactory(config);

ILogger logger = loggerFactory.CreateLogger(nameof(App));
logger.LogInformation("Started");
Bootstrapper.RegisterLogging(loggerFactory);
Bootstrapper.RegisterServices(fileSystem);
await Bootstrapper.RegisterPlugins(pluginFinder, fileSystem, loggerFactory).ConfigureAwait(true);
Bootstrapper.RegisterServices(fileSystem, appDataProvider);
await Bootstrapper.RegisterPlugins(pluginFinder, fileSystem, loggerFactory, appDataProvider).ConfigureAwait(true);

var ensureStartup = new EnsureStartup(fileSystem, appDataProvider);
await ensureStartup.EnsureFilesAsync().ConfigureAwait(true);

#if DEBUG
Bootstrapper.Container.Verify(SimpleInjector.VerificationOption.VerifyAndDiagnose);
#else
Bootstrapper.Container.Options.EnableAutoVerification = false;
#endif

EnsureStartup ensureStartup = Bootstrapper.Container.GetInstance<EnsureStartup>();
await ensureStartup.EnsureFilesAsync().ConfigureAwait(true);

UseRepositoryMonitor(Bootstrapper.Container);

Expand All @@ -104,7 +109,7 @@ protected override async void OnStartup(StartupEventArgs e)
logger.LogError(exception, "Could not start all modules.");
}
}

protected override void OnExit(ExitEventArgs e)
{
_windowSizeService?.Unregister();
Expand All @@ -113,19 +118,17 @@ protected override void OnExit(ExitEventArgs e)

_hotKeyService?.Unregister();

// #pragma warning disable CA1416 // Validate platform compatibility
_notifyIcon?.Dispose();
// #pragma warning restore CA1416 // Validate platform compatibility

ReleaseAndDisposeMutex();

base.OnExit(e);
}

private static IConfiguration SetupConfiguration()
private static IConfiguration CreateLoggerConfiguration(AppDataPathProvider appDataProvider)
{
const string FILENAME = "appsettings.serilog.json";
var fullFilename = Path.Combine(DefaultAppDataPathProvider.Instance.AppDataPath, FILENAME);
var fullFilename = Path.Combine(appDataProvider.AppDataPath, FILENAME);

IConfigurationBuilder builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
Expand All @@ -151,6 +154,8 @@ private static ILoggerFactory CreateLoggerFactory(IConfiguration config)
return loggerFactory;
}



private static void UseRepositoryMonitor(Container container)
{
_repositoryMonitor = container.GetInstance<IRepositoryMonitor>();
Expand Down
13 changes: 6 additions & 7 deletions src/RepoM.App/Bootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ internal static class Bootstrapper
{
public static readonly Container Container = new();

public static void RegisterServices(IFileSystem fileSystem)
public static void RegisterServices(IFileSystem fileSystem, IAppDataPathProvider appDataProvider)
{
Container.RegisterInstance<ObjectCache>(MemoryCache.Default);
Container.RegisterSingleton<Window>(() => Container.GetInstance<MainWindow>());
Expand All @@ -51,7 +51,7 @@ public static void RegisterServices(IFileSystem fileSystem)
Container.Register<IRepositoryDetectorFactory, DefaultRepositoryDetectorFactory>(Lifestyle.Singleton);
Container.Register<IRepositoryObserverFactory, DefaultRepositoryObserverFactory>(Lifestyle.Singleton);
Container.Register<IGitRepositoryFinderFactory, GitRepositoryFinderFactory>(Lifestyle.Singleton);
Container.RegisterInstance<IAppDataPathProvider>(DefaultAppDataPathProvider.Instance);
Container.RegisterInstance(appDataProvider);
Container.Register<IRepositoryReader, DefaultRepositoryReader>(Lifestyle.Singleton);
Container.Register<IRepositoryWriter, DefaultRepositoryWriter>(Lifestyle.Singleton);
Container.Register<IRepositoryStore, DefaultRepositoryStore>(Lifestyle.Singleton);
Expand Down Expand Up @@ -106,27 +106,26 @@ public static void RegisterServices(IFileSystem fileSystem)
Container.Register<IRepositoryComparerFactory<SumComparerConfigurationV1>, SumRepositoryComparerFactory>(Lifestyle.Singleton);

Container.RegisterSingleton<ActionExecutor>();
Container.Register(typeof(ICommandExecutor<>), new[] { typeof(CoreBootstrapper).Assembly, }, Lifestyle.Singleton);
Container.Register(typeof(ICommandExecutor<>), [typeof(CoreBootstrapper).Assembly,], Lifestyle.Singleton);
Container.RegisterDecorator(
typeof(ICommandExecutor<>),
typeof(LoggerCommandExecutorDecorator<>),
Lifestyle.Singleton);

Container.RegisterSingleton<HotKeyService>();
Container.RegisterSingleton<WindowSizeService>();

Container.RegisterSingleton<EnsureStartup>();
}

public static async Task RegisterPlugins(
IPluginFinder pluginFinder,
IFileSystem fileSystem,
ILoggerFactory loggerFactory)
ILoggerFactory loggerFactory,
IAppDataPathProvider appDataPathProvider)
{
Container.Register<ModuleService>(Lifestyle.Singleton);
Container.RegisterInstance(pluginFinder);

var coreBootstrapper = new CoreBootstrapper(pluginFinder, fileSystem, DefaultAppDataPathProvider.Instance, loggerFactory);
var coreBootstrapper = new CoreBootstrapper(pluginFinder, fileSystem, appDataPathProvider, loggerFactory);
var baseDirectory = fileSystem.Path.Combine(AppDomain.CurrentDomain.BaseDirectory);
await coreBootstrapper.LoadAndRegisterPluginsAsync(Container, baseDirectory).ConfigureAwait(false);
}
Expand Down
9 changes: 9 additions & 0 deletions src/RepoM.App/Configuration/Config.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace RepoM.App.Configuration;

public class Config
{
/// <summary>
/// Absolute or relative path to the app settings folder.
/// </summary>
public string? AppSettingsPath { get; init; }
}
11 changes: 11 additions & 0 deletions src/RepoM.App/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"profiles": {
"RepoM.App bin config": {
"commandName": "Project",
"commandLineArgs": "--App:AppSettingsPath RepoMConfig"
},
"RepoM.App": {
"commandName": "Project"
}
}
}
8 changes: 8 additions & 0 deletions src/RepoM.App/RepoM.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
Expand Down Expand Up @@ -64,6 +65,13 @@
</Compile>
</ItemGroup>
<ItemGroup>
<None Update="appsettings.Development.json">
<CopyToOutputDirectory Condition="$(Configuration) == 'Release'">Never</CopyToOutputDirectory>
<CopyToOutputDirectory Condition="$(Configuration) == 'Debug'">PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="appsettings.serilog.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
66 changes: 66 additions & 0 deletions src/RepoM.App/Services/ConfigBasedAppDataPathProviderFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
namespace RepoM.App.Services;

using System;
using System.IO.Abstractions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders;
using RepoM.Api.IO;
using RepoM.App.Configuration;

internal class ConfigBasedAppDataPathProviderFactory
{
private readonly string[] _args;
private readonly IFileProvider? _fileProvider;
private readonly IFileSystem _fileSystem;

public ConfigBasedAppDataPathProviderFactory(string[] args, IFileSystem fileSystem)
{
_args = args ?? throw new ArgumentNullException(nameof(args));
_fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
}

public ConfigBasedAppDataPathProviderFactory(string[] args, IFileSystem fileSystem, IFileProvider fileProvider)
:this(args, fileSystem)
{
_fileProvider = fileProvider ?? throw new ArgumentNullException(nameof(fileProvider));
}

public AppDataPathProvider Create()
{
IConfiguration configuration = CreateConfiguration(_args);
return CreateAppDataPathProvider(configuration);
}

private IConfiguration CreateConfiguration(string[] args)
{
var builder = new ConfigurationBuilder();
if (_fileProvider != null)
{
builder.SetFileProvider(_fileProvider);
}

builder.AddEnvironmentVariables("REPOM_");

builder.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false);

#if DEBUG
builder.AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: false);
#endif

builder.AddCommandLine(args);

return builder.Build();
}

private AppDataPathProvider CreateAppDataPathProvider(IConfiguration appDataPathConfiguration)
{
var appConfig = new Config();
appDataPathConfiguration.Bind("App", appConfig);
var appDataPathConfig = new AppDataPathConfig
{
AppSettingsPath = appConfig.AppSettingsPath,
};

return new AppDataPathProvider(appDataPathConfig, _fileSystem);
}
}
7 changes: 7 additions & 0 deletions src/RepoM.App/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"App": {
// Absolute or relative path to the configuration directory.
// like: "AppSettingsPath": "C:/my-config/",
"AppSettingsPath": "MyConfig"
}
}
5 changes: 5 additions & 0 deletions src/RepoM.App/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"App": {
"AppSettingsPath": null
}
}
Loading

0 comments on commit adeacc4

Please sign in to comment.