Compare commits

..
Author SHA1 Message Date
5c09b7c5a8 feat: Add GitRepoRegistration cmdlets (#5)
- adds GitRepoRegistration cmdlets
- adds basic tests for GitRepoRegistration implementations
- adds initial build project and scripts

Refs: #5, #6
2026-09-06 09:33:35 +10:00
56 changed files with 371 additions and 192 deletions

4
.gitattributes vendored Normal file
View file

@ -0,0 +1,4 @@
*.verified.txt text eol=lf working-tree-encoding=UTF-8
*.verified.xml text eol=lf working-tree-encoding=UTF-8
*.verified.json text eol=lf working-tree-encoding=UTF-8
*.verified.bin binary

View file

@ -1,6 +1,7 @@
using Cake.Common.IO; using Cake.Common.IO;
using Cake.Common.IO.Paths; using Cake.Common.IO.Paths;
using Cake.Core; using Cake.Core;
using Cake.Core.IO;
using Cake.Frosting; using Cake.Frosting;
namespace Build; namespace Build;
@ -10,7 +11,7 @@ public class BuildContext : FrostingContext
/// <summary> /// <summary>
/// Base source directory /// Base source directory
/// </summary> /// </summary>
public ConvertableDirectoryPath BaseSourceLocation { get; set; } public DirectoryPath BaseSourceLocation { get; set; }
/// <summary> /// <summary>
/// Powershell module project folder /// Powershell module project folder
@ -32,19 +33,29 @@ public class BuildContext : FrostingContext
/// </summary> /// </summary>
public ConvertableFilePath CreateModuleManifestScript { get; set; } public ConvertableFilePath CreateModuleManifestScript { get; set; }
/// <summary>
/// Suffix to tag the build with, defaults to pre-release.
/// <para>
/// Immediately follows the version number and before the commit hash.
/// </para>
/// </summary>
public string BuildSuffix { get; set; } public string BuildSuffix { get; set; }
/// <summary>
/// Disable the commit hash from being added
/// </summary>
public bool DisableCommitHash { get; set; } public bool DisableCommitHash { get; set; }
public BuildContext(ICakeContext context) public BuildContext(ICakeContext context)
: base(context) : base(context)
{ {
BaseSourceLocation = context.Directory("../src"); BaseSourceLocation = context.Directory("../src").Path.MakeAbsolute(context.Environment);
PowershellModuleProjectDirectory = BaseSourceLocation + context.Directory("PowershellModule"); PowershellModuleProjectDirectory = BaseSourceLocation + context.Directory("PowershellModule");
PowershellModuleCsproj = PowershellModuleProjectDirectory + context.File("PowershellModule.csproj"); PowershellModuleCsproj = PowershellModuleProjectDirectory + context.File("PowershellModule.csproj");
PowershellModuleOutputDir = context.Directory("../") + context.Directory("output") + context.Directory("PowershellModule"); PowershellModuleOutputDir = context.Directory("../") + context.Directory("output") + context.Directory("PowershellModule");
var buildScriptDirectory = context.Directory("./Scripts"); var buildScriptDirectory = context.Directory("./Scripts");
CreateModuleManifestScript = buildScriptDirectory + context.File("CreateModuleManifest.ps1"); CreateModuleManifestScript = buildScriptDirectory + context.File("CreateModuleManifest.ps1");
BuildSuffix = context.Configuration.GetValue(nameof(BuildSuffix)); BuildSuffix = context.Configuration.GetValue(nameof(BuildSuffix)) ?? "pre-release";
DisableCommitHash = context.Configuration.GetBoolValue(nameof(DisableCommitHash)); DisableCommitHash = context.Configuration.GetBoolValue(nameof(DisableCommitHash));
} }
} }

48
build/Helpers.cs Normal file
View file

@ -0,0 +1,48 @@
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
namespace Build;
public class Helpers
{
/// <summary>
/// Returns a formatted version of the assembly that contains PowerShell cmdlets
/// </summary>
/// <param name="powershelModuleOutputLocation"></param>
/// <returns></returns>
public static string GetPowershellModuleVersion(string powershelModuleOutputLocation)
{
// When the project name changes, this dll will also need to be updated
var moduleVersionInfo = FileVersionInfo.GetVersionInfo(Path.Combine(powershelModuleOutputLocation, "PowershellModule.dll"));
// It's (almost) impossible to not have a product version tag here. There is a reason why the implementation
// returns a string? but I can't find it and I don't really care too much. If we have null return an empty string
// and append no version to the archive bundle.
if (moduleVersionInfo.ProductVersion == null)
{
return string.Empty;
}
var versionRegex = new Regex(@"(\d+\.\d+\.\d+)(?:\-?([\w\-]+)\+?(\w+)?)?");
var match = versionRegex.Match(moduleVersionInfo.ProductVersion);
if (match.Success)
{
// If we have 4 groups, we've got a version number, suffix, and commit hash, so we return the first 2 as is
// and the commit has capped to 8 characters
if (match.Groups.Count == 4)
{
return $"{match.Groups[1]}-{match.Groups[2]}+{match.Groups[3].Value.Substring(0, 8)}";
}
if (match.Groups.Count == 3)
{
return $"{match.Groups[1]}-{match.Groups[2]}";
}
return $"{match.Groups[1]}";
}
return string.Empty;
}
}

View file

@ -8,8 +8,6 @@ public static class Program
{ {
return new CakeHost() return new CakeHost()
.UseContext<BuildContext>() .UseContext<BuildContext>()
// Uncomment this if you don't want to set the suffix via the run profile program arguments
//.UseCakeSetting(nameof(BuildContext.BuildSuffix), "<your suffix here>")
.Run(args); .Run(args);
} }
} }

View file

@ -1,4 +1,4 @@
param ( param (
[string]$powershellModuleFileLocation, [string]$powershellModuleFileLocation,
[string]$guid, [string]$guid,
[string]$author, [string]$author,

View file

@ -1,4 +1,4 @@
using Cake.Common.Tools.DotNet; using Cake.Common.Tools.DotNet;
using Cake.Common.Tools.DotNet.Build; using Cake.Common.Tools.DotNet.Build;
using Cake.Common.Tools.DotNet.MSBuild; using Cake.Common.Tools.DotNet.MSBuild;
using Cake.Core.Diagnostics; using Cake.Core.Diagnostics;

View file

@ -1,4 +1,4 @@
using Cake.Common; using Cake.Common;
using Cake.Common.Tools.DotNet; using Cake.Common.Tools.DotNet;
using Cake.Core.Diagnostics; using Cake.Core.Diagnostics;
using Cake.Frosting; using Cake.Frosting;

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Cake.Core.Diagnostics; using Cake.Core.Diagnostics;
@ -15,8 +15,8 @@ public class CopyOutputTask : FrostingTask<BuildContext>
public override void Run(BuildContext context) public override void Run(BuildContext context)
{ {
var powershellModuleName = "PowershellModule"; var powershellModuleName = "PowershellModule";
// TODO: probably don't create full file locations when I can pass the output dir in and have the script // TODO: [#10] Refactor file paths used in build project to be more explict and easier to understand
// make the path // Probably don't create full file locations when I can pass the output dir in and have the script make the path
var scriptParams = new var scriptParams = new
{ {
PowershellModuleFileLocation = $"{context.PowershellModuleOutputDir}/{powershellModuleName}.psd1", PowershellModuleFileLocation = $"{context.PowershellModuleOutputDir}/{powershellModuleName}.psd1",

View file

@ -1,4 +1,5 @@
using System.Diagnostics; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -79,7 +80,7 @@ public class CreateBundleArchiveTask : FrostingTask<BuildContext>
context.Log.Information($"Bundle files copied"); context.Log.Information($"Bundle files copied");
GenerateModuleImportScript(moduleFolderLocation, context); GenerateModuleImportScript(moduleFolderLocation, context);
var moduleVersionForFilename = GetPowershellModuleVersion(powershelModuleOutputLocation); var moduleVersionForFilename = Helpers.GetPowershellModuleVersion(powershelModuleOutputLocation);
CreateBundleZip(bundleRootLocation, moduleFolderLocation, moduleVersionForFilename, context); CreateBundleZip(bundleRootLocation, moduleFolderLocation, moduleVersionForFilename, context);
} }
@ -132,38 +133,4 @@ public class CreateBundleArchiveTask : FrostingTask<BuildContext>
ZipFile.CreateFromDirectory(moduleFolderLocation, bundleZipFileLocaiton, CompressionLevel.Fastest, true); ZipFile.CreateFromDirectory(moduleFolderLocation, bundleZipFileLocaiton, CompressionLevel.Fastest, true);
context.Log.Information($"Archive created at '{bundleZipFileLocaiton}'"); context.Log.Information($"Archive created at '{bundleZipFileLocaiton}'");
} }
private static string GetPowershellModuleVersion(string powershelModuleOutputLocation)
{
var moduleVersionInfo = FileVersionInfo.GetVersionInfo(Path.Combine(powershelModuleOutputLocation, "PowershellModule.dll"));
// It's (almost) impossible to not have a product version tag here. There is a reason why the implementation
// returns a string? but I can't find it and I don't really care too much. If we have null return an empty string
// and append no version to the archive bundle.
if (moduleVersionInfo.ProductVersion == null)
{
return string.Empty;
}
var versionRegex = new Regex(@"(\d+\.\d+\.\d+)(?:\-?([\w\-]+)\+?(\w+)?)?");
var match = versionRegex.Match(moduleVersionInfo.ProductVersion);
if (match.Success)
{
// If we have 4 groups, we've got a version number, suffix, and commit hash, so we return the first 2 as is
// and the commit has capped to 8 characters
if (match.Groups.Count == 4)
{
return $"{match.Groups[1]}-{match.Groups[2]}+{match.Groups[3].Value.Substring(0, 8)}";
}
if (match.Groups.Count == 3)
{
return $"{match.Groups[1]}-{match.Groups[2]}";
}
return $"{match.Groups[1]}";
}
return string.Empty;
}
} }

View file

@ -1,4 +1,4 @@
using Cake.Core; using Cake.Core;
using Cake.Core.Diagnostics; using Cake.Core.Diagnostics;
using Cake.Frosting; using Cake.Frosting;
@ -7,6 +7,7 @@ namespace Build.Tasks;
// Consider this the "entry" point for builds, task order is defined by a chain of IsDependentOn // Consider this the "entry" point for builds, task order is defined by a chain of IsDependentOn
[TaskName("Default")] [TaskName("Default")]
[IsDependentOn(typeof(BuildTask))] [IsDependentOn(typeof(BuildTask))]
[IsDependentOn(typeof(TagCommitTask))]
[IsDependentOn(typeof(CopyOutputTask))] [IsDependentOn(typeof(CopyOutputTask))]
[IsDependentOn(typeof(CreateBundleArchiveTask))] [IsDependentOn(typeof(CreateBundleArchiveTask))]
public class DefaultTask : FrostingTask public class DefaultTask : FrostingTask

View file

@ -0,0 +1,54 @@
using System;
using System.Diagnostics;
using Cake.Core.Diagnostics;
using Cake.Frosting;
namespace Build.Tasks;
[TaskName("TagCurrentCommitWithVersion")]
[IsDependentOn(typeof(BuildTask))]
public class TagCommitTask : FrostingTask<BuildContext>
{
public override void Run(BuildContext context)
{
// This task only really exists to create pre-release builds for pull requests.
// It'll tag the current commit with the same version that'll be used for the bundle archive filename.
// TODO: update build.ps1 in the solution root so it doesn't do this tagging task
TagCommit(context);
base.Run(context);
}
private static void TagCommit(BuildContext context)
{
var moduleVersionForFilename = Helpers.GetPowershellModuleVersion(context.PowershellModuleOutputDir.Path.MakeAbsolute(context.Environment).FullPath);
var ps = new ProcessStartInfo("git",
["-C", context.BaseSourceLocation.FullPath, "tag", moduleVersionForFilename])
{
RedirectStandardOutput = true,
RedirectStandardError = true,
};
// If the user doesn't have git on their path, this will throw an exception that I don't have to do anything
// special with, it'll be unhandled and powershell will handle it
var gitProcess = Process.Start(ps);
// This probably shouldn't be possible?
if (gitProcess is null)
{
throw new Exception("git failed to start")
{
Source = "git-process",
};
}
gitProcess.WaitForExit();
if (!gitProcess.StandardError.EndOfStream)
{
throw new Exception($"Unable to tag: {gitProcess.StandardError.ReadToEnd().Trim()}. Either remove the existing tag, or commit your changes before running a build");
}
context.Log.Information($"Tagged commit with {moduleVersionForFilename}");
}
}

View file

@ -1,4 +1,4 @@
# Git Repo Registration # Git Repo Registration
- all commands support `-debug` - all commands support `-debug`
- at its simplest level these commands allow you to register a git repo against a simple name, and provide the ability to quickly pushd to a location - at its simplest level these commands allow you to register a git repo against a simple name, and provide the ability to quickly pushd to a location

View file

@ -1,4 +1,4 @@
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
namespace ModuleCore.Calendar; namespace ModuleCore.Calendar;

View file

@ -1,4 +1,4 @@
using SQLite; using SQLite;
namespace ModuleCore.Database; namespace ModuleCore.Database;
@ -26,12 +26,22 @@ public class DatabaseManager
} }
} }
/// <summary>
/// Runs the given action within a new database connection
/// </summary>
/// <param name="dbAction"></param>
public void InConnection(Action<SQLiteConnection> dbAction) public void InConnection(Action<SQLiteConnection> dbAction)
{ {
using var conn = new SQLiteConnection(_databaseLocation.FullName); using var conn = new SQLiteConnection(_databaseLocation.FullName);
dbAction(conn); dbAction(conn);
} }
/// <summary>
/// Runs the given func in a new database connection, returning the result
/// </summary>
/// <param name="dbAction"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T InConnection<T>(Func<SQLiteConnection, T> dbAction) public T InConnection<T>(Func<SQLiteConnection, T> dbAction)
{ {
using var conn = new SQLiteConnection(_databaseLocation.FullName); using var conn = new SQLiteConnection(_databaseLocation.FullName);
@ -51,6 +61,18 @@ public class DatabaseManager
return exists ?? false; return exists ?? false;
} }
/// <summary>
/// Deletes the current database file. This will cause any future instance methods to fail on database action if
/// a new instance is not created.
/// <para>
/// This method should be avoided unless calling from a test.
/// </para>
/// </summary>
internal void DeleteDatabase()
{
_databaseLocation.Delete();
}
/// <summary> /// <summary>
/// Removes double dots from the filename and removes the file extension /// Removes double dots from the filename and removes the file extension
/// </summary> /// </summary>

View file

@ -1,4 +1,4 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<VersionPrefix>0.0.1</VersionPrefix> <VersionPrefix>0.0.1</VersionPrefix>
<VersionSuffix>dev</VersionSuffix> <VersionSuffix>dev</VersionSuffix>

View file

@ -1,4 +1,4 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics; using System.Diagnostics;
using ModuleCore.Database; using ModuleCore.Database;
using ModuleCore.Git.Models; using ModuleCore.Git.Models;
@ -6,28 +6,44 @@ using SQLite;
namespace ModuleCore.Git; namespace ModuleCore.Git;
// TODO: better name for this /// <summary>
public class GitManager /// Manages git repo registration, including creating any registration persistence via a backing <see cref="DatabaseManager"/>
/// </summary>
public class GitRepoRegistrationManager
{ {
private static readonly Lazy<GitManager> GitManagerInstance = new(() => new GitManager()); private static readonly Lazy<GitRepoRegistrationManager> GitManagerInstance = new(() => new GitRepoRegistrationManager());
private static Action<string>? _debugWriterDelegate; private static Action<string>? _debugWriterDelegate;
private readonly DatabaseManager _db; private readonly DatabaseManager _db;
private readonly ConcurrentDictionary<string, InternalGitRegistration> _registrations; private readonly ConcurrentDictionary<string, InternalGitRegistration> _registrations;
private GitManager() private GitRepoRegistrationManager(string? databaseName = null)
{ {
_registrations = new ConcurrentDictionary<string, InternalGitRegistration>(); _registrations = new ConcurrentDictionary<string, InternalGitRegistration>();
_db = new DatabaseManager("git.db"); // Regular usage of this constructor will never pass a database name in. Currently only tests should be hitting
// a code path that has a different database name
_db = new DatabaseManager(databaseName ?? "git.db");
InitialiseRegistrations(); InitialiseRegistrations();
} }
public static GitManager Instance => GitManagerInstance.Value; /// <summary>
/// Returns the current <see cref="GitRepoRegistrationManager"/> instance. If no instance has been created, returns a new instance
/// and then the same instance every call after.
/// </summary>
public static GitRepoRegistrationManager Instance => GitManagerInstance.Value;
/// <summary> /// <summary>
/// Always returns a new clean instance of GitManager /// Always returns a new clean instance of GitManager
/// </summary> /// </summary>
internal static GitManager InternalFreshInstance => new(); internal static GitRepoRegistrationManager InternalFreshInstance(string databaseName) => new(databaseName);
/// <summary>
/// Deletes the underlying database file.
/// <para>
/// Avoid calling this outside of tests.
/// </para>
/// </summary>
internal void DeleteDatabase() => _db.DeleteDatabase();
/// <summary> /// <summary>
/// Creates up any database tables and loads all previously saved git registrations. /// Creates up any database tables and loads all previously saved git registrations.
@ -118,6 +134,11 @@ public class GitManager
}); });
} }
/// <summary>
/// Unregisters a git repo registration by name. If no registration exists an exception will be thrown.
/// </summary>
/// <param name="registrationName"></param>
/// <exception cref="Exception"></exception>
public void UnregisterRepo(string registrationName) public void UnregisterRepo(string registrationName)
{ {
_db.InConnection(conn => _db.InConnection(conn =>
@ -185,7 +206,13 @@ public class GitManager
.ToList(); .ToList();
} }
public string GetRepo(string? registeredName) /// <summary>
/// Returns the file location for a git repo registration by name.
/// </summary>
/// <param name="registeredName"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public string GetDirectoryForRegisteredRepo(string? registeredName)
{ {
if (string.IsNullOrEmpty(registeredName)) if (string.IsNullOrEmpty(registeredName))
{ {
@ -312,7 +339,7 @@ public class GitManager
/// </summary> /// </summary>
public string CurrentBranch => GetCurrentBranch(); public string CurrentBranch => GetCurrentBranch();
// TODO: not fully decided on if I want this feature or not, but keeping it in for now // TODO: [#13] Create GitManager to centralise calls to git process
private string GetCurrentBranch() private string GetCurrentBranch()
{ {
var now = DateTime.Now; var now = DateTime.Now;
@ -364,19 +391,3 @@ public class GitManager
} }
} }
} }
/// <summary>
/// The directory details of the directory returned from git rev-parse --show-toplevel
/// </summary>
public class ParsedGitFolderDetails
{
/// <summary>
/// The full path to the top level folder containing a git repository
/// </summary>
public string Directory { get; init; } = null!;
/// <summary>
/// The last folder name of the directory
/// </summary>
public string Folder { get; init; } = null!;
}

View file

@ -1,8 +1,21 @@
namespace ModuleCore.Git.Models; namespace ModuleCore.Git.Models;
/// <summary>
/// Details for a registered git repo
/// </summary>
public class GitRegistration public class GitRegistration
{ {
/// <summary>
/// Display name for the registration. Can either be the name of the repo retrieved from git, or a user supplied
/// alias
/// </summary>
public required string Name { get; set; } public required string Name { get; set; }
/// <summary>
/// Location on disk for the top level of the registered git repo. Not guaranteed to exist on disk
/// </summary>
public required string Location { get; set; } public required string Location { get; set; }
/// <summary>
/// Current branch for the git repo
/// </summary>
public required string CurrentBranch { get; set; } public required string CurrentBranch { get; set; }
} }

View file

@ -0,0 +1,17 @@
namespace ModuleCore.Git.Models;
/// <summary>
/// The directory details of the directory returned from git rev-parse --show-toplevel
/// </summary>
public class ParsedGitFolderDetails
{
/// <summary>
/// The full path to the top level folder containing a git repository
/// </summary>
public string Directory { get; init; } = null!;
/// <summary>
/// The last folder name of the directory
/// </summary>
public string Folder { get; init; } = null!;
}

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
@ -14,7 +14,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="sqlite-net-pcl" /> <PackageReference Include="sqlite-net-pcl"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Management.Automation; using System.Management.Automation;
using ModuleCore.Calendar; using ModuleCore.Calendar;

View file

@ -1,4 +1,4 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<VersionPrefix>0.0.1</VersionPrefix> <VersionPrefix>0.0.1</VersionPrefix>
<VersionSuffix>dev</VersionSuffix> <VersionSuffix>dev</VersionSuffix>

View file

@ -1,4 +1,4 @@
using System.Management.Automation; using System.Management.Automation;
using ModuleCore.Git; using ModuleCore.Git;
using ModuleCore.Git.Models; using ModuleCore.Git.Models;
@ -13,7 +13,7 @@ public class GetGitRepoRegistrationCommand : PSCmdlet
{ {
protected override void BeginProcessing() protected override void BeginProcessing()
{ {
var repos = GitManager.Instance.ListRepos(); var repos = GitRepoRegistrationManager.Instance.ListRepos();
WriteObject(repos); WriteObject(repos);

View file

@ -1,4 +1,4 @@
namespace PowershellModule.Git.Commands; namespace PowershellModule.Git.Commands;
public class GitCommands public class GitCommands
{ {

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Management.Automation; using System.Management.Automation;
using ModuleCore.Git; using ModuleCore.Git;
@ -17,19 +17,19 @@ public sealed class NewGitRepoRegistrationCommand : PSCmdlet
{ {
try try
{ {
GitManager.SetDebugWriter(WriteDebug); GitRepoRegistrationManager.SetDebugWriter(WriteDebug);
// Test that we're in a git repo first. If we aren't (or git isn't available), this method will throw // Test that we're in a git repo first. If we aren't (or git isn't available), this method will throw
// so we don't need to handle for null (yet). // so we don't need to handle for null (yet).
var repoFolder = GitManager.IsGitRepo(SessionState.Path.CurrentLocation.Path); var repoFolder = GitRepoRegistrationManager.IsGitRepo(SessionState.Path.CurrentLocation.Path);
if (string.IsNullOrWhiteSpace(Name)) if (string.IsNullOrWhiteSpace(Name))
{ {
WriteDebug("No name given for registration, defaulting to git folder root."); WriteDebug("No name given for registration, defaulting to git folder root.");
} }
GitManager.Instance.RegisterRepo(repoFolder.Directory, Name ?? repoFolder.Folder); GitRepoRegistrationManager.Instance.RegisterRepo(repoFolder.Directory, Name ?? repoFolder.Folder);
GitManager.ClearDebugWriter(); GitRepoRegistrationManager.ClearDebugWriter();
base.BeginProcessing(); base.BeginProcessing();
} }

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Management.Automation; using System.Management.Automation;
using ModuleCore.Git; using ModuleCore.Git;
@ -17,21 +17,21 @@ public class RemoveGitRepoRegistrationCommand : PSCmdlet
{ {
try try
{ {
GitManager.SetDebugWriter(WriteDebug); GitRepoRegistrationManager.SetDebugWriter(WriteDebug);
// If we aren't given a value for the Name argument, default behaviour is to attempt to remove a registration // If we aren't given a value for the Name argument, default behaviour is to attempt to remove a registration
// by the current git repo folder name for the current location. // by the current git repo folder name for the current location.
// If we have a name, don't bother testing for a git repo, just attempt to remove the registration by name // If we have a name, don't bother testing for a git repo, just attempt to remove the registration by name
// regardless of where we're being called from // regardless of where we're being called from
var registrationNameToRemove = string.IsNullOrEmpty(Name) var registrationNameToRemove = string.IsNullOrEmpty(Name)
? GitManager.IsGitRepo(SessionState.Path.CurrentLocation.Path).Folder ? GitRepoRegistrationManager.IsGitRepo(SessionState.Path.CurrentLocation.Path).Folder
: Name; : Name;
GitManager.Instance.UnregisterRepo(registrationNameToRemove); GitRepoRegistrationManager.Instance.UnregisterRepo(registrationNameToRemove);
// Removing a registration works similar to registering a new one - we either remove by exact name, or by // Removing a registration works similar to registering a new one - we either remove by exact name, or by
// the folder if no name is given (so a user can remove a registration from a git repo they're currently in) // the folder if no name is given (so a user can remove a registration from a git repo they're currently in)
GitManager.ClearDebugWriter(); GitRepoRegistrationManager.ClearDebugWriter();
base.BeginProcessing(); base.BeginProcessing();
} }

View file

@ -1,4 +1,4 @@
using System.Management.Automation; using System.Management.Automation;
using ModuleCore.Git; using ModuleCore.Git;
namespace PowershellModule.Git.Commands; namespace PowershellModule.Git.Commands;
@ -21,7 +21,7 @@ public class ShowGitRepoRegistrationCommand : PSCmdlet
protected override void BeginProcessing() protected override void BeginProcessing()
{ {
var location = GitManager.Instance.GetRepo(Name); var location = GitRepoRegistrationManager.Instance.GetDirectoryForRegisteredRepo(Name);
// By default instead of doing the same as cd, we instead do pushd so a user can popd straight back to where // By default instead of doing the same as cd, we instead do pushd so a user can popd straight back to where
// they came from. // they came from.

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Management.Automation; using System.Management.Automation;
using System.Management.Automation.Provider; using System.Management.Automation.Provider;

View file

@ -1,4 +1,4 @@
using System.Management.Automation; using System.Management.Automation;
namespace PowershellModule.Git; namespace PowershellModule.Git;

View file

@ -1,4 +1,4 @@
<# <#
.SYNOPSIS .SYNOPSIS
Removes all non-core files from build output Removes all non-core files from build output
#> #>

View file

@ -9,17 +9,17 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="PowerShellStandard.Library" > <PackageReference Include="PowerShellStandard.Library">
<PrivateAssets>All</PrivateAssets> <PrivateAssets>All</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="System.Management.Automation" /> <PackageReference Include="System.Management.Automation"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\ModuleCore\ModuleCore.csproj" /> <ProjectReference Include="..\ModuleCore\ModuleCore.csproj"/>
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="pwsh.exe -file &quot;$(ProjectDir)PostBuild.ps1&quot; $(TargetDir)" /> <Exec Command="pwsh.exe -file &quot;$(ProjectDir)PostBuild.ps1&quot; $(TargetDir)"/>
</Target> </Target>
</Project> </Project>

View file

@ -1,4 +1,4 @@
using ModuleCore.Calendar; using ModuleCore.Calendar;
using ModuleTests.Calendar.TestData; using ModuleTests.Calendar.TestData;
using System.Globalization; using System.Globalization;

View file

@ -1,4 +1,4 @@
using System.Text; using System.Text;
using ModuleCore.Calendar; using ModuleCore.Calendar;
namespace ModuleTests.Calendar; namespace ModuleTests.Calendar;

View file

@ -1,4 +1,4 @@
┌─────────────────────────────────────────┐ ┌─────────────────────────────────────────┐
│ June 2026 │ │ June 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤ ├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │ │ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │

View file

@ -1,4 +1,4 @@
┌─────────────────────────────────────────┐ ┌─────────────────────────────────────────┐
│ July 2026 │ │ July 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤ ├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │ │ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │

View file

@ -1,4 +1,4 @@
┌─────────────────────────────────────────┐ ┌─────────────────────────────────────────┐
│ August 2026 │ │ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤ ├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │ │ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │

View file

@ -1,4 +1,4 @@
┌──────────────────────────────────┐ ┌──────────────────────────────────┐
│ srpen 2026 │ │ srpen 2026 │
├────┬────┬────┬────┬────┬────┬────┤ ├────┬────┬────┬────┬────┬────┬────┤
│ po │ út │ st │ čt │ pá │ so │ ne │ │ po │ út │ st │ čt │ pá │ so │ ne │

View file

@ -1,4 +1,4 @@
┌─────────────────────────────────────────┐ ┌─────────────────────────────────────────┐
│ august 2026 │ │ august 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤ ├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ man │ tir │ ons │ tor │ fre │ lør │ søn │ │ man │ tir │ ons │ tor │ fre │ lør │ søn │

View file

@ -1,4 +1,4 @@
┌─────────────────────────────────────────┐ ┌─────────────────────────────────────────┐
│ August 2026 │ │ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤ ├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │ │ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │

View file

@ -1,4 +1,4 @@
┌────────────────────────────────────────────────┐ ┌────────────────────────────────────────────────┐
│ agosto 2026 │ │ agosto 2026 │
├──────┬──────┬──────┬──────┬──────┬──────┬──────┤ ├──────┬──────┬──────┬──────┬──────┬──────┬──────┤
│ lun. │ mar. │ mié. │ jue. │ vie. │ sáb. │ dom. │ │ lun. │ mar. │ mié. │ jue. │ vie. │ sáb. │ dom. │

View file

@ -1,4 +1,4 @@
┌────────────────────────────────────────────────┐ ┌────────────────────────────────────────────────┐
│ août 2026 │ │ août 2026 │
├──────┬──────┬──────┬──────┬──────┬──────┬──────┤ ├──────┬──────┬──────┬──────┬──────┬──────┬──────┤
│ lun. │ mar. │ mer. │ jeu. │ ven. │ sam. │ dim. │ │ lun. │ mar. │ mer. │ jeu. │ ven. │ sam. │ dim. │

View file

@ -1,4 +1,4 @@
┌──────────────────────────────────┐ ┌──────────────────────────────────┐
│ augustus 2026 │ │ augustus 2026 │
├────┬────┬────┬────┬────┬────┬────┤ ├────┬────┬────┬────┬────┬────┬────┤
│ ma │ di │ wo │ do │ vr │ za │ zo │ │ ma │ di │ wo │ do │ vr │ za │ zo │

View file

@ -1,4 +1,4 @@
┌───────────────────────────────────────────────────────┐ ┌───────────────────────────────────────────────────────┐
│ ఆగస్టు 2026 │ │ ఆగస్టు 2026 │
├───────┬───────┬───────┬───────┬───────┬───────┬───────┤ ├───────┬───────┬───────┬───────┬───────┬───────┬───────┤
│ సోమ │ మంగళ │ బుధ │ గురు │ శుక్ర │ శని │ ఆది │ │ సోమ │ మంగళ │ బుధ │ గురు │ శుక్ర │ శని │ ఆది │

View file

@ -1,4 +1,4 @@
┌─────────────────────────────────────────┐ ┌─────────────────────────────────────────┐
│ August 2026 │ │ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤ ├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │ │ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │

View file

@ -1,4 +1,4 @@
Double-wide characters will display incorrectly but still tested until rendering can be improved Double-wide characters will display incorrectly but still tested until rendering can be improved
┌─────────────────────────────────────────┐ ┌─────────────────────────────────────────┐
│ August 2026 │ │ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤ ├─────┬─────┬─────┬─────┬─────┬─────┬─────┤

View file

@ -1,4 +1,4 @@
Emojis will display incorrectly in text but output correctly in terminals with UTF-8 encoding. Emojis will display incorrectly in text but output correctly in terminals with UTF-8 encoding.
┌─────────────────────────────────────────┐ ┌─────────────────────────────────────────┐
│ August 2026 │ │ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤ ├─────┬─────┬─────┬─────┬─────┬─────┬─────┤

View file

@ -1,4 +1,4 @@
┌──────────────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────────┐
│ August 2026 │ │ August 2026 │
├────────┬────────┬────────┬────────┬────────┬────────┬────────┤ ├────────┬────────┬────────┬────────┬────────┬────────┬────────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │ │ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │

View file

@ -1,4 +1,4 @@
┌─────────────────────────────────────────┐ ┌─────────────────────────────────────────┐
│ August 2026 │ │ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤ ├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │ │ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │

View file

@ -1,4 +1,4 @@
Start Day: Sunday Start Day: Sunday
┌─────────────────────────────────────────┐ ┌─────────────────────────────────────────┐
│ August 2026 │ │ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤ ├─────┬─────┬─────┬─────┬─────┬─────┬─────┤

View file

@ -1,4 +1,4 @@
using System.Text; using System.Text;
using ModuleCore.Calendar; using ModuleCore.Calendar;
namespace ModuleTests.Calendar; namespace ModuleTests.Calendar;

View file

@ -1,4 +1,4 @@
namespace ModuleTests.Calendar.TestData; namespace ModuleTests.Calendar.TestData;
public class CalendarTestDates : TestDataEnumerator<DateTime> public class CalendarTestDates : TestDataEnumerator<DateTime>
{ {

View file

@ -1,4 +1,4 @@
namespace ModuleTests.Calendar.TestData; namespace ModuleTests.Calendar.TestData;
public class CultureCodeTestData : TestDataEnumerator<string> public class CultureCodeTestData : TestDataEnumerator<string>
{ {

View file

@ -1,10 +1,10 @@
using System.Text; using System.Text;
using ModuleCore.Calendar;
using ModuleCore.Git; using ModuleCore.Git;
using ModuleTests.Git.TestData; using ModuleTests.Git.TestData;
namespace ModuleTests.Git; namespace ModuleTests.Git;
// TODO: [#12] GitRepoRegistration tests should reset database at start of each test
public class AddRegistrationTests public class AddRegistrationTests
{ {
private static readonly VerifySettings Settings; private static readonly VerifySettings Settings;
@ -23,9 +23,10 @@ public class AddRegistrationTests
public Task BasicRepoRegistration((int testId, string path) testData) public Task BasicRepoRegistration((int testId, string path) testData)
{ {
Settings.UseFileName($"{nameof(BasicRepoRegistration)}_{testData.testId}"); Settings.UseFileName($"{nameof(BasicRepoRegistration)}_{testData.testId}");
var gitManager = GitRepoRegistrationManager.InternalFreshInstance(nameof(BasicRepoRegistration));
var gitManager = GitManager.InternalFreshInstance; try
{
var repoRegistration = gitManager.RegisterRepo("Test:/some/test/repo", testData.path); var repoRegistration = gitManager.RegisterRepo("Test:/some/test/repo", testData.path);
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.AppendLine($"Attempted to register: {testData.path}") sb.AppendLine($"Attempted to register: {testData.path}")
@ -33,60 +34,82 @@ public class AddRegistrationTests
return Verify(sb, Settings); return Verify(sb, Settings);
} }
finally
{
gitManager.DeleteDatabase();
}
}
[Fact] [Fact]
public void RepoRegistrationWithEmptyName() public void RepoRegistrationWithEmptyName()
{ {
Settings.UseFileName(nameof(RepoRegistrationWithEmptyName)); Settings.UseFileName(nameof(RepoRegistrationWithEmptyName));
var gitManager = GitRepoRegistrationManager.InternalFreshInstance(nameof(RepoRegistrationWithEmptyName));
var gitManager = GitManager.InternalFreshInstance;
var testRepoAbsolutePath = "Test:/some/test/repo"; var testRepoAbsolutePath = "Test:/some/test/repo";
try
{
var emptyName = gitManager.RegisterRepo(testRepoAbsolutePath, ""); var emptyName = gitManager.RegisterRepo(testRepoAbsolutePath, "");
Assert.Equal("repo", emptyName); Assert.Equal("repo", emptyName);
} }
finally
{
gitManager.DeleteDatabase();
}
}
[Fact] [Fact]
public void RepoRegistrationWithNullName() public void RepoRegistrationWithNullName()
{ {
Settings.UseFileName(nameof(RepoRegistrationWithNullName)); Settings.UseFileName(nameof(RepoRegistrationWithNullName));
var gitManager = GitRepoRegistrationManager.InternalFreshInstance(nameof(RepoRegistrationWithNullName));
var gitManager = GitManager.InternalFreshInstance;
var testRepoAbsolutePath = "Test:/some/test/repo"; var testRepoAbsolutePath = "Test:/some/test/repo";
try
{
// Name is technically not-nullable, but string is a reference type so null can be passed in so we should test // Name is technically not-nullable, but string is a reference type so null can be passed in so we should test
// it regardless // it regardless
var nullName = gitManager.RegisterRepo(testRepoAbsolutePath, null!); var nullName = gitManager.RegisterRepo(testRepoAbsolutePath, null!);
Assert.Equal("repo", nullName); Assert.Equal("repo", nullName);
} }
finally
{
gitManager.DeleteDatabase();
}
}
[Fact] [Fact]
public void RepoRegistrationWithWhitespaceName() public void RepoRegistrationWithWhitespaceName()
{ {
Settings.UseFileName(nameof(RepoRegistrationWithWhitespaceName)); Settings.UseFileName(nameof(RepoRegistrationWithWhitespaceName));
var gitManager = GitManager.InternalFreshInstance; var gitManager = GitRepoRegistrationManager.InternalFreshInstance(nameof(RepoRegistrationWithWhitespaceName));
var testRepoAbsolutePath = "Test:/some/test/repo"; var testRepoAbsolutePath = "Test:/some/test/repo";
try
{
var whitespaceName = gitManager.RegisterRepo(testRepoAbsolutePath, " "); var whitespaceName = gitManager.RegisterRepo(testRepoAbsolutePath, " ");
Assert.Equal("repo", whitespaceName); Assert.Equal("repo", whitespaceName);
} }
finally
{
gitManager.DeleteDatabase();
}
}
[Fact] [Fact]
public void DuplicateRepoRegistrationShouldFail() public void DuplicateRepoRegistrationShouldFail()
{ {
Settings.UseFileName(nameof(RepoRegistrationWithWhitespaceName)); Settings.UseFileName(nameof(RepoRegistrationWithWhitespaceName));
var gitManager = GitRepoRegistrationManager.InternalFreshInstance(nameof(RepoRegistrationWithWhitespaceName));
var gitManager = GitManager.InternalFreshInstance;
var testRepoAbsolutePath = "Test:/some/test/repo"; var testRepoAbsolutePath = "Test:/some/test/repo";
string[] paths = ["test", "nested", "path"]; string[] paths = ["test", "nested", "path"];
var names = (NormalSeparator: string.Join(Path.DirectorySeparatorChar, paths), AltSeparator: string.Join(Path.AltDirectorySeparatorChar, paths)); var names = (NormalSeparator: string.Join(Path.DirectorySeparatorChar, paths), AltSeparator: string.Join(Path.AltDirectorySeparatorChar, paths));
try
{
var firstRegistration = gitManager.RegisterRepo(testRepoAbsolutePath, names.NormalSeparator); var firstRegistration = gitManager.RegisterRepo(testRepoAbsolutePath, names.NormalSeparator);
// TODO: make nested registrations fail in both directions and test
var secondRegistration = gitManager.RegisterRepo(testRepoAbsolutePath, Path.Combine(paths[..1])); var secondRegistration = gitManager.RegisterRepo(testRepoAbsolutePath, Path.Combine(paths[..1]));
Assert.Equal(Path.Combine(paths), firstRegistration); Assert.Equal(Path.Combine(paths), firstRegistration);
@ -94,19 +117,24 @@ public class AddRegistrationTests
Assert.Throws<Exception>(() => gitManager.RegisterRepo(testRepoAbsolutePath, Path.Combine(paths[..1]))); Assert.Throws<Exception>(() => gitManager.RegisterRepo(testRepoAbsolutePath, Path.Combine(paths[..1])));
} }
finally
{
gitManager.DeleteDatabase();
}
}
[Fact] [Fact]
public void DuplicateRepoRegistrationDifferentSlashShouldNotFail() public void DuplicateRepoRegistrationDifferentSlashShouldNotFail()
{ {
Settings.UseFileName(nameof(RepoRegistrationWithWhitespaceName)); Settings.UseFileName(nameof(RepoRegistrationWithWhitespaceName));
var gitManager = GitRepoRegistrationManager.InternalFreshInstance(nameof(RepoRegistrationWithWhitespaceName));
var gitManager = GitManager.InternalFreshInstance;
var testRepoAbsolutePath = "Test:/some/test/repo"; var testRepoAbsolutePath = "Test:/some/test/repo";
string[] paths = ["test", "nested", "path"]; string[] paths = ["test", "nested", "path"];
var names = (NormalSeparator: string.Join(Path.DirectorySeparatorChar, paths), AltSeparator: string.Join(Path.AltDirectorySeparatorChar, paths)); var names = (NormalSeparator: string.Join(Path.DirectorySeparatorChar, paths), AltSeparator: string.Join(Path.AltDirectorySeparatorChar, paths));
try
{
var firstRegistration = gitManager.RegisterRepo(testRepoAbsolutePath, names.NormalSeparator); var firstRegistration = gitManager.RegisterRepo(testRepoAbsolutePath, names.NormalSeparator);
// TODO: make nested registrations fail in both directions and test
var secondRegistration = gitManager.RegisterRepo(testRepoAbsolutePath, Path.Combine(paths[..1])); var secondRegistration = gitManager.RegisterRepo(testRepoAbsolutePath, Path.Combine(paths[..1]));
var differentPathSeparatorRegistration = gitManager.RegisterRepo(testRepoAbsolutePath, names.AltSeparator); var differentPathSeparatorRegistration = gitManager.RegisterRepo(testRepoAbsolutePath, names.AltSeparator);
@ -114,4 +142,9 @@ public class AddRegistrationTests
Assert.Equal(Path.Combine(paths[..1]), secondRegistration); Assert.Equal(Path.Combine(paths[..1]), secondRegistration);
Assert.Equal(names.AltSeparator, differentPathSeparatorRegistration); Assert.Equal(names.AltSeparator, differentPathSeparatorRegistration);
} }
finally
{
gitManager.DeleteDatabase();
}
}
} }

View file

@ -1,4 +1,4 @@
using System.Linq; using System.Linq;
namespace ModuleTests.Git.TestData; namespace ModuleTests.Git.TestData;

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>

View file

@ -1,4 +1,4 @@
namespace ModuleTests; namespace ModuleTests;
public class TestConstants public class TestConstants
{ {

View file

@ -1,4 +1,4 @@
using System.Collections; using System.Collections;
namespace ModuleTests; namespace ModuleTests;