diff --git a/src/ModuleCore/Git/GitManager.cs b/src/ModuleCore/Git/GitManager.cs index fe1f69e..0d5af37 100644 --- a/src/ModuleCore/Git/GitManager.cs +++ b/src/ModuleCore/Git/GitManager.cs @@ -1,6 +1,4 @@ -using System.Collections.Concurrent; - -namespace ModuleCore.Git; +namespace ModuleCore.Git; // TODO: better name for this public class GitManager @@ -13,19 +11,23 @@ public class GitManager /// internal static GitManager InternalFreshInstance => new(); - private class GitRegistration - { - public required string Name { get; set; } - public required string Location { get; set; } - } + /// + /// Simply .ToString() + /// + private static readonly string DirectorySeparator = Path.DirectorySeparatorChar.ToString(); - private readonly ConcurrentDictionary _registrations; + private readonly InternalDirectory _repositories; + private readonly Lock _readWriteLock = new(); private GitManager() { Console.WriteLine($"{nameof(GitManager)} init"); - - _registrations = new ConcurrentDictionary(); + // Initialise the root container + _repositories = new InternalDirectory() + { + Name = DirectorySeparator, + InternalPath = DirectorySeparator + }; } /// @@ -37,19 +39,124 @@ public class GitManager /// The normalised string the repository was registered against public string RegisterRepo(string absoluteRepositoryLocation, string registrationName) { - registrationName = string.IsNullOrWhiteSpace(registrationName) - ? new DirectoryInfo(absoluteRepositoryLocation).Name - : registrationName; - - if (_registrations.TryAdd(registrationName, new GitRegistration() - { - Name = registrationName, - Location = absoluteRepositoryLocation, - })) + // 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) { - return registrationName; - } + var normalisedName = NormaliseNamePath(string.IsNullOrWhiteSpace(registrationName) + ? new DirectoryInfo(absoluteRepositoryLocation).Name + : registrationName); - throw new Exception($"Git repo already registered with the name {registrationName}"); + // 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; + } + } + + /// + /// Takes a name and returns it as a queue of its parts, starting with a root of + /// + /// + /// + private Queue NameToSegments(string name) + { + var segments = name.Split(DirectorySeparator); + + return segments.Length == 1 + ? new Queue([DirectorySeparator, name]) + : new Queue([DirectorySeparator, ..segments]); + } + + /// + /// Normalises the path separators in the given string to use Path.DirectorySeparatorChar + /// + /// + /// + private string NormaliseNamePath(string name) + { + // Feels a bit hacky, but this will actually normalise a path to a valid form. So if the input is + // some/directory/paths, Path.GetRelativePath will normalise it to some\directory\paths, relative to ./ + // which is kind of handy but I also just wish there was a Path method that would do this for me. I know that + // the whole point of Path is that it's based on a file system, but file systems can also be arbitrary and not + // always be drive rooted. + // Either way, this works and saves me having to reimplement a worse method when it's more important that users + // are able to use file paths in whatever form they prefer, which means we leverage the internal implementation + // in a weird way. + return Path.GetRelativePath("./", name); + } + + private class InternalDirectory + { + /// + /// Name of the folder this + /// + public string Name { get; set; } = null!; + + public Dictionary Children { get; set; } = []; + + internal string InternalPath { get; set; } + + /// + /// If not null, this is the absolute location of a registered git repository + /// + public string? FullRepositoryPath { get; set; } + + /// + /// + /// + /// + /// + /// + /// + internal InternalDirectory? Add(string absoluteRepositoryLocation, Queue directorySegmentsFromName) + { + var topStack = directorySegmentsFromName.Dequeue(); + + 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); + } } } \ No newline at end of file diff --git a/tests/ModuleTests/Git/AddRegistrationTests.cs b/tests/ModuleTests/Git/AddRegistrationTests.cs index 511833a..6de31d6 100644 --- a/tests/ModuleTests/Git/AddRegistrationTests.cs +++ b/tests/ModuleTests/Git/AddRegistrationTests.cs @@ -74,44 +74,4 @@ public class AddRegistrationTests Assert.Equal("repo", whitespaceName); } - - [Fact] - public void DuplicateRepoRegistrationShouldFail() - { - Settings.UseFileName(nameof(RepoRegistrationWithWhitespaceName)); - - var gitManager = GitManager.InternalFreshInstance; - var testRepoAbsolutePath = "Test:/some/test/repo"; - string[] paths = ["test", "nested", "path"]; - var names = (NormalSeparator: string.Join(Path.DirectorySeparatorChar, paths), AltSeparator: string.Join(Path.AltDirectorySeparatorChar, paths)); - - 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])); - - Assert.Equal(Path.Combine(paths), firstRegistration); - Assert.Equal(Path.Combine(paths[..1]), secondRegistration); - - Assert.Throws(() => gitManager.RegisterRepo(testRepoAbsolutePath, Path.Combine(paths[..1]))); - } - - [Fact] - public void DuplicateRepoRegistrationDifferentSlashShouldNotFail() - { - Settings.UseFileName(nameof(RepoRegistrationWithWhitespaceName)); - - var gitManager = GitManager.InternalFreshInstance; - var testRepoAbsolutePath = "Test:/some/test/repo"; - string[] paths = ["test", "nested", "path"]; - var names = (NormalSeparator: string.Join(Path.DirectorySeparatorChar, paths), AltSeparator: string.Join(Path.AltDirectorySeparatorChar, paths)); - - 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 differentPathSeparatorRegistration = gitManager.RegisterRepo(testRepoAbsolutePath, names.AltSeparator); - - Assert.Equal(Path.Combine(paths), firstRegistration); - Assert.Equal(Path.Combine(paths[..1]), secondRegistration); - Assert.Equal(names.AltSeparator, differentPathSeparatorRegistration); - } } \ No newline at end of file diff --git a/tests/ModuleTests/Git/Snapshots/AddRegistrationTests/BasicRepoRegistration_2.verified.txt b/tests/ModuleTests/Git/Snapshots/AddRegistrationTests/BasicRepoRegistration_2.verified.txt index 7f2c4e6..3c0b092 100644 --- a/tests/ModuleTests/Git/Snapshots/AddRegistrationTests/BasicRepoRegistration_2.verified.txt +++ b/tests/ModuleTests/Git/Snapshots/AddRegistrationTests/BasicRepoRegistration_2.verified.txt @@ -1,2 +1,2 @@ Attempted to register: other/path -Registration result: other/path +Registration result: other\path