feat: Add GitRepoRegistration cmdlets #5

Merged
pascal_nulah merged 69 commits from feature/git-provider into dev 2026-09-06 09:33:36 +10:00
7 changed files with 135 additions and 38 deletions
Showing only changes of commit 09236e826a - Show all commits

tests(git-provider): Add basic registration tests

- make ModuleCore internals visible to ModuleTests
Scott 2026-08-10 15:50:24 +10:00

View file

@ -6,12 +6,18 @@ public class GitManager
private static readonly Lazy<GitManager> GitManagerInstance = new(() => new GitManager());
public static GitManager Instance => GitManagerInstance.Value;
/// <summary>
/// Always returns a new clean instance of GitManager
/// </summary>
internal static GitManager InternalFreshInstance => new();
/// <summary>
/// Simply <see cref="Path.DirectorySeparatorChar"/>.ToString()
/// </summary>
private static readonly string DirectorySeparator = Path.DirectorySeparatorChar.ToString();
private readonly InternalDirectory _repositories;
private readonly Lock _readWriteLock = new();
private GitManager()
{
@ -20,6 +26,7 @@ public class GitManager
_repositories = new InternalDirectory()
{
Name = DirectorySeparator,
InternalPath = DirectorySeparator
};
}
@ -29,18 +36,35 @@ public class GitManager
/// </summary>
/// <param name="absoluteRepositoryLocation"></param>
/// <param name="registrationName"></param>
/// <returns></returns>
public void RegisterRepo(string absoluteRepositoryLocation, string registrationName)
/// <returns>The normalised string the repository was registered against</returns>
public string RegisterRepo(string absoluteRepositoryLocation, string registrationName)
{
// Regardless of if we get a name or not, the fully qualified version for us
// starts with a
var directorySegmentsFromName = NameToSegments(
string.IsNullOrWhiteSpace(registrationName)
// Depending on the caller, it might be possible that they've scripted automatic repo registration. Because I
// don't really want to account to all the subtle ways that can be parallised, I just naively lock on every
// registration attempt. This method should be quick regardless, and I could use ConcurrentDictionary except
// that means every instance of InternalDirectory would need it and yeah nah fuck that I can just lock at the
// top level
lock (_readWriteLock)
{
var normalisedName = NormaliseNamePath(string.IsNullOrWhiteSpace(registrationName)
? new DirectoryInfo(absoluteRepositoryLocation).Name
: registrationName
);
: registrationName);
_repositories.Add(absoluteRepositoryLocation, directorySegmentsFromName);
// Regardless of if we get a name or not, the fully qualified version for us
// starts with a /
var directorySegmentsFromName = NameToSegments(normalisedName);
var added = _repositories.Add(absoluteRepositoryLocation, directorySegmentsFromName);
// Not sure about this, the Add should throw any exceptions on duplicate/failures but for now I'll leave this
// here
if (added == null)
{
throw new Exception("Failed to register location");
}
return normalisedName;
}
}
/// <summary>
@ -50,7 +74,7 @@ public class GitManager
/// <returns></returns>
private Queue<string> NameToSegments(string name)
{
var segments = NormaliseNamePath(name).Split(DirectorySeparator);
var segments = name.Split(DirectorySeparator);
return segments.Length == 1
? new Queue<string>([DirectorySeparator, name])
@ -84,48 +108,55 @@ public class GitManager
public Dictionary<string, InternalDirectory> Children { get; set; } = [];
internal string InternalPath { get; set; }
/// <summary>
/// If not null, this is the absolute location of a registered git repository
/// </summary>
public string? FullRepositoryPath { get; set; }
internal void Add(string absoluteRepositoryLocation, Queue<string> directorySegmentsFromName)
/// <summary>
///
/// </summary>
/// <param name="absoluteRepositoryLocation"></param>
/// <param name="directorySegmentsFromName"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
internal InternalDirectory? Add(string absoluteRepositoryLocation, Queue<string> directorySegmentsFromName)
{
var topStack = directorySegmentsFromName.Dequeue();
if (topStack == Name)
{
// We're at the end of the directory segments so we can safely say we're at the end of the tree so
// we add it to the relevant dictionary
if (directorySegmentsFromName.Count == 0)
{
FullRepositoryPath = absoluteRepositoryLocation;
//Children.Add(topStack, directory);
}
else
{
var nextSegment = directorySegmentsFromName.Peek();
// Attempt to get the next level of the directory. If we don't have a key entry, create one
if (!Children.TryGetValue(nextSegment, out var nextChild))
{
nextChild = new InternalDirectory()
{
Name = nextSegment,
};
Children.Add(nextSegment, nextChild);
}
// add the next
nextChild.Add(absoluteRepositoryLocation, directorySegmentsFromName);
}
}
else
if (topStack != Name)
{
// logically it shouldn't be possible to have a value on top of the stack that _doesn't_ exist, but
// just incase we throw as this should only happen if an Add is attempted on the root and the queue was
// not correctly rooted to /
throw new Exception($"Directory segment does not seem to exist: {topStack}");
}
// We're at the end of the directory segments so we can safely say we're at the end of the tree so
// we add it to the relevant dictionary
if (directorySegmentsFromName.Count == 0)
{
FullRepositoryPath = absoluteRepositoryLocation;
return this;
}
var nextSegment = directorySegmentsFromName.Peek();
// Attempt to get the next level of the directory. If we don't have a key entry, create one
if (!Children.TryGetValue(nextSegment, out var nextChild))
{
nextChild = new InternalDirectory()
{
Name = nextSegment,
InternalPath = Path.Combine(InternalPath, nextSegment)
};
Children.Add(nextSegment, nextChild);
}
// add the next
return nextChild.Add(absoluteRepositoryLocation, directorySegmentsFromName);
}
}
}

View file

@ -7,4 +7,10 @@
<LangVersion>latestmajor</LangVersion>
</PropertyGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>ModuleTests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>

View file

@ -0,0 +1,36 @@
using System.Text;
using ModuleCore.Calendar;
using ModuleCore.Git;
using ModuleTests.Git.TestData;
namespace ModuleTests.Git;
public class AddRegistrationTests
pascal_nulah marked this conversation as resolved

todo: All of the tests in this suite should be wrapped in a try .. finally with the finally block calling DeleteDatabase so test databases don't linger around.

Alternatively (or in addition to), each test should attempt to rest any previously existing database with a ResetDatabase() method for instances where a test is run in debug with breakpoints and a dev hits stop instead of letting the test fail (which will result in the finally block never being called)

todo: All of the tests in this suite should be wrapped in a `try .. finally` with the finally block calling `DeleteDatabase` so test databases don't linger around. Alternatively (or in addition to), each test should attempt to rest any previously existing database with a `ResetDatabase()` method for instances where a test is run in debug with breakpoints and a dev hits stop instead of letting the test fail (which will result in the `finally` block never being called)
[GitRepoRegistration tests should reset database at start of each test #12](https://moar.ws/git/pascal_nulah/Nulah.PowerShell/issues/12)
{
private static readonly VerifySettings Settings;
static AddRegistrationTests()
{
Settings = new VerifySettings();
var testBaseDirectory = Path.Join(TestConstants.SnapshotFolderName, nameof(AddRegistrationTests));
Settings.UseDirectory(testBaseDirectory);
Settings.DisableDiff();
}
[Theory]
[ClassData(typeof(AddRegistrationTestData))]
public Task BasicRepoRegistration((int testId, string path) testData)
{
Settings.UseFileName($"{nameof(BasicRepoRegistration)}_{testData.testId}");
var gitManager = GitManager.InternalFreshInstance;
var repoRegistration = gitManager.RegisterRepo("Test:/some/test/repo", testData.path);
var sb = new StringBuilder();
sb.AppendLine($"Attempted to register: {testData.path}")
.AppendLine($"Registration result: {repoRegistration}");
return Verify(sb, Settings);
}
}

View file

@ -0,0 +1,2 @@
Attempted to register: test
Registration result: test

View file

@ -0,0 +1,2 @@
Attempted to register: test\path
Registration result: test\path

View file

@ -0,0 +1,2 @@
Attempted to register: other/path
Registration result: other\path

View file

@ -0,0 +1,18 @@
using System.Linq;
namespace ModuleTests.Git.TestData;
public class AddRegistrationTestData : TestDataEnumerator<(int testId, string path)>
{
public AddRegistrationTestData()
{
Data = new List<string>()
{
"test",
$"test{Path.DirectorySeparatorChar}path",
$"other{Path.AltDirectorySeparatorChar}path"
}
.Select((x, i) => (i, x))
.ToList();
}
}