diff --git a/src/ModuleCore/Database/DatabaseManager.cs b/src/ModuleCore/Database/DatabaseManager.cs
deleted file mode 100644
index 6f6810d..0000000
--- a/src/ModuleCore/Database/DatabaseManager.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-using SQLite;
-
-namespace ModuleCore.Database;
-
-public class DatabaseManager
-{
- private readonly FileInfo _databaseLocation;
-
- ///
- /// Creates a new manager for the given database file by name
- ///
- ///
- /// Filename for the database with no extension. Slashes are accepted and will create directories as needed.
- ///
- public DatabaseManager(string databaseName)
- {
- // Even though the xmldoc says "with no extension", we strip off any extension regardless
- _databaseLocation = new FileInfo(Path.Combine(".", "data", $"{SanitiseFilename(databaseName)}.db"));
-
- Directory.CreateDirectory(_databaseLocation.DirectoryName!);
-
- if (!File.Exists(_databaseLocation.FullName))
- {
- var file = File.Create(_databaseLocation.FullName);
- file.Close();
- }
- }
-
- public void InConnection(Action dbAction)
- {
- using var conn = new SQLiteConnection(_databaseLocation.FullName);
- dbAction(conn);
- }
-
- public T InConnection(Func dbAction)
- {
- using var conn = new SQLiteConnection(_databaseLocation.FullName);
- return dbAction(conn);
- }
-
- ///
- /// Returns a bool for the given query. Convenience method for .
- ///
- /// A query starting with SELECT 1, optionally paramaterised with ?
- /// Parameter values
- ///
- public bool Exists(string query, params object[] args)
- {
- using var conn = new SQLiteConnection(_databaseLocation.FullName);
- var exists = conn.ExecuteScalar(query, args);
- return exists ?? false;
- }
-
- ///
- /// Removes double dots from the filename and removes the file extension
- ///
- ///
- ///
- private string SanitiseFilename(string filename)
- {
- // Honestly not really needed seeing as its just me and this isn't coming from user supplied code, but eh.
- return Path.GetFileNameWithoutExtension(filename.Replace("..", string.Empty));
- }
-}
\ No newline at end of file
diff --git a/src/ModuleCore/Git/GitManager.cs b/src/ModuleCore/Git/GitManager.cs
index a530c4e..0d5af37 100644
--- a/src/ModuleCore/Git/GitManager.cs
+++ b/src/ModuleCore/Git/GitManager.cs
@@ -1,31 +1,9 @@
-using System.Collections.Concurrent;
-using System.Diagnostics;
-using ModuleCore.Database;
-using ModuleCore.Git.Models;
-using SQLite;
-
-namespace ModuleCore.Git;
+namespace ModuleCore.Git;
// TODO: better name for this
public class GitManager
{
private static readonly Lazy GitManagerInstance = new(() => new GitManager());
- private readonly ConcurrentDictionary _registrations;
- private readonly DatabaseManager _db;
-
- private GitManager()
- {
- Debug.WriteLine($"{nameof(GitManager)} init");
-
- _registrations = new ConcurrentDictionary();
- _db = new DatabaseManager("git.db");
-
- _db.InConnection(conn =>
- {
- conn.CreateTable();
- });
- }
-
public static GitManager Instance => GitManagerInstance.Value;
///
@@ -34,7 +12,26 @@ public class GitManager
internal static GitManager InternalFreshInstance => new();
///
- /// Registers a git repository based on an absolute location. If is null or empty,
+ /// Simply .ToString()
+ ///
+ private static readonly string DirectorySeparator = Path.DirectorySeparatorChar.ToString();
+
+ private readonly InternalDirectory _repositories;
+ private readonly Lock _readWriteLock = new();
+
+ private GitManager()
+ {
+ Console.WriteLine($"{nameof(GitManager)} init");
+ // Initialise the root container
+ _repositories = new InternalDirectory()
+ {
+ Name = DirectorySeparator,
+ InternalPath = DirectorySeparator
+ };
+ }
+
+ ///
+ /// Registers a git repository based on an absolute location. If is null or empty,
/// the registration will use the folder name for the git repo at the top level.
///
///
@@ -42,149 +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;
-
- var gitRegistration = new InternalGitRegistration
+ // 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)
{
- Name = registrationName,
- Location = absoluteRepositoryLocation,
- Id = Guid.CreateVersion7(),
- };
+ var normalisedName = NormaliseNamePath(string.IsNullOrWhiteSpace(registrationName)
+ ? new DirectoryInfo(absoluteRepositoryLocation).Name
+ : registrationName);
- return _db.InConnection(conn =>
- {
- // Query if we already have a registration either by name or location.
- // tbh this is a bit of a janky way to do exists when I have to pass the query in anyway, but I just didn't
- // want to do null checks and a truthy check so I wrap it in a barely-valuable method.
- var registrationExists = _db.Exists(
- $"""
- SELECT 1
- FROM {InternalGitRegistration.TableName}
- WHERE Name = ? OR
- Location = ?
- """,
- gitRegistration.Name,
- gitRegistration.Location
- );
+ // Regardless of if we get a name or not, the fully qualified version for us
+ // starts with a /
+ var directorySegmentsFromName = NameToSegments(normalisedName);
- if (registrationExists)
+ 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($"A Git repo is already registered with the name {registrationName} or location {absoluteRepositoryLocation}");
+ throw new Exception("Failed to register location");
}
- // Insert the new record
- conn.Insert(gitRegistration);
-
- if (_registrations.TryAdd(registrationName, gitRegistration))
- {
- return registrationName;
- }
-
- // This error case should be unlikely, but if a registration was removed but the registrations wasn't updated
- // correctly then we'd unable to re-add a repo with the same name
- throw new Exception("An error occured during registration.");
- });
- }
-
- public List ListRepos()
- {
- return _registrations.Select(x =>
- new GitRegistration
- {
- Name = x.Value.Name,
- Location = x.Value.Location,
- CurrentBranch = x.Value.CurrentBranch,
- }
- )
- .ToList();
- }
-
- public string GetRepo(string? registeredName)
- {
- if (string.IsNullOrEmpty(registeredName))
- {
- throw new Exception("Name cannot be null");
+ return normalisedName;
}
-
- if (_registrations.TryGetValue(registeredName, out var registration))
- {
- return registration.Location;
- }
-
- throw new Exception($"No git repo has been registered with the name {registeredName}");
}
///
- /// Used for internal git registration and handles getting the current branch
+ /// Takes a name and returns it as a queue of its parts, starting with a root of
///
- [Table(TableName)]
- private class InternalGitRegistration
+ ///
+ ///
+ private Queue NameToSegments(string name)
{
- private string _currentBranch = string.Empty;
- private long _nextCheckTime;
- internal const string TableName = "GitRegistration";
+ var segments = name.Split(DirectorySeparator);
- [PrimaryKey]
- public Guid Id { get; set; }
+ return segments.Length == 1
+ ? new Queue([DirectorySeparator, name])
+ : new Queue([DirectorySeparator, ..segments]);
+ }
- [Indexed(Unique = true)]
+ ///
+ /// 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!;
- [Indexed(Unique = true)]
- public string Location { get; set; } = null!;
+ public Dictionary Children { get; set; } = [];
- public string CurrentBranch => GetCurrentBranch();
+ internal string InternalPath { get; set; }
- // TODO: not fully decided on if I want this feature or not, but keeping it in for now
- private string GetCurrentBranch()
+ ///
+ /// 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 now = DateTime.Now;
- // git branch should be quick enough that even with a large number of registrations this shouldn't be that slow
- // when doing Get-GitRepo, but regardless we still only get the current branch via git if it's been some amount
- // of time since the last time we did.
- if (now.Ticks < _nextCheckTime)
+ var topStack = directorySegmentsFromName.Dequeue();
+
+ if (topStack != Name)
{
- return _currentBranch;
+ // 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}");
}
- // use -C for the git command so we don't need to set the working directory and the git command can be run
- // from anywhere against the appropriate location
- var ps = new ProcessStartInfo("git",
- ["-C", Location, "branch", "--show-current"])
+ // 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)
{
- 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? Not really sure of the conditions where the process could be started
- // but return null, but I'm going to consider that unrecoverable error territory
- if (gitProcess is null)
- {
- throw new Exception("git failed to start");
+ FullRepositoryPath = absoluteRepositoryLocation;
+ return this;
}
- gitProcess.WaitForExit();
+ var nextSegment = directorySegmentsFromName.Peek();
- if (!gitProcess.StandardOutput.EndOfStream)
+ // 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))
{
- _currentBranch = gitProcess.StandardOutput.ReadToEnd();
+ nextChild = new InternalDirectory()
+ {
+ Name = nextSegment,
+ InternalPath = Path.Combine(InternalPath, nextSegment)
+ };
+ Children.Add(nextSegment, nextChild);
}
- if (!gitProcess.StandardError.EndOfStream)
- {
- _currentBranch = gitProcess.StandardError.ReadToEnd();
- }
-
- // Set the next check to be in the future so we don't hold up any list commands every time.
- _nextCheckTime = now.AddMinutes(15).Ticks;
-
- return _currentBranch;
+ // add the next
+ return nextChild.Add(absoluteRepositoryLocation, directorySegmentsFromName);
}
}
}
\ No newline at end of file
diff --git a/src/ModuleCore/Git/Models/GitRegistration.cs b/src/ModuleCore/Git/Models/GitRegistration.cs
deleted file mode 100644
index 11eea11..0000000
--- a/src/ModuleCore/Git/Models/GitRegistration.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace ModuleCore.Git.Models;
-
-public class GitRegistration
-{
- public required string Name { get; set; }
- public required string Location { get; set; }
- public required string CurrentBranch { get; set; }
-}
\ No newline at end of file
diff --git a/src/ModuleCore/ModuleCore.csproj b/src/ModuleCore/ModuleCore.csproj
index 81bd88b..2f41110 100644
--- a/src/ModuleCore/ModuleCore.csproj
+++ b/src/ModuleCore/ModuleCore.csproj
@@ -13,8 +13,4 @@
-
-
-
-
diff --git a/src/PowershellModule/Git/Commands/GetGitRepoCommand.cs b/src/PowershellModule/Git/Commands/GetGitRepoCommand.cs
deleted file mode 100644
index 1859375..0000000
--- a/src/PowershellModule/Git/Commands/GetGitRepoCommand.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using System.Management.Automation;
-using ModuleCore.Git;
-
-namespace PowershellModule.Git.Commands;
-
-[Cmdlet(VerbsCommon.Get, GitCommands.GitRepoNoun)]
-public class ListGitRepoCommand : PSCmdlet
-{
- protected override void BeginProcessing()
- {
- var repos = GitManager.Instance.ListRepos();
-
- WriteObject(repos);
-
- base.BeginProcessing();
- }
-}
\ No newline at end of file
diff --git a/src/PowershellModule/Git/Commands/GitCommands.cs b/src/PowershellModule/Git/Commands/GitCommands.cs
deleted file mode 100644
index 2d5c888..0000000
--- a/src/PowershellModule/Git/Commands/GitCommands.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace PowershellModule.Git.Commands;
-
-public class GitCommands
-{
- public const string GitRepoNoun = "GitRepo";
-}
\ No newline at end of file
diff --git a/src/PowershellModule/Git/Commands/ShowGitRepoCommand.cs b/src/PowershellModule/Git/Commands/ShowGitRepoCommand.cs
deleted file mode 100644
index 6ff7909..0000000
--- a/src/PowershellModule/Git/Commands/ShowGitRepoCommand.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using System;
-using System.Management.Automation;
-using ModuleCore.Git;
-
-namespace PowershellModule.Git.Commands;
-
-// TODO: decide on if I like Show to be used as the verb name. Other options I have are push/pop and open.
-// Seeing as this is likely just for me currently, Show-GitRepo suits my workflow more where I'll want to quickly just
-// pushd into a git repo, do whatever I want to do with it/change directories in it whatever, and then popd at the end.
-// I'll also be integrating the current stack into the custom prompt whenever I get around to doing that
-[Cmdlet(VerbsCommon.Show, GitCommands.GitRepoNoun)]
-public class ShowGitRepoCommand : PSCmdlet
-{
- [Parameter(
- Position = 0,
- ValueFromPipeline = true,
- Mandatory = true,
- HelpMessage = "Reference name for the repo")]
- public string? Name { get; set; }
-
- [Parameter(
- Mandatory = false,
- HelpMessage = "Changes directory directly instead of using Set-Location")]
- [Alias("NoSetLocation")]
- public SwitchParameter NoStack { get; set; }
-
- protected override void BeginProcessing()
- {
- var location = GitManager.Instance.GetRepo(Name);
-
- // 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.
- // TODO: incorporate this into the custom prompt when I develop that
- if (!NoStack)
- {
- // Push the current location to the stack
- // TODO: support named stacks. PowerShell *-Location commands support named stacks, but I don't personally
- // use them myself so I haven't implemented them initially. I would like to in the future though, but right
- // now it's low value to me.
- // https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/set-location?view=powershell-7.6#example-4-set-the-current-location-to-a-named-stack
- SessionState.Path.PushCurrentLocation(null);
- }
-
- SessionState.Path.SetLocation(location);
- base.BeginProcessing();
- }
-}
\ No newline at end of file
diff --git a/src/PowershellModule/Git/Commands/NewGitRepoCommand.cs b/src/PowershellModule/Git/NewGitRepoCommand.cs
similarity index 88%
rename from src/PowershellModule/Git/Commands/NewGitRepoCommand.cs
rename to src/PowershellModule/Git/NewGitRepoCommand.cs
index 103cffa..2b4bf6a 100644
--- a/src/PowershellModule/Git/Commands/NewGitRepoCommand.cs
+++ b/src/PowershellModule/Git/NewGitRepoCommand.cs
@@ -4,11 +4,13 @@ using System.IO;
using System.Management.Automation;
using ModuleCore.Git;
-namespace PowershellModule.Git.Commands;
+namespace PowershellModule.Git;
-[Cmdlet(VerbsCommon.New, GitCommands.GitRepoNoun)]
-public sealed class NewGitRepoCommand : PSCmdlet
+[Cmdlet(VerbsCommon.New, Noun)]
+public class NewGitRepoCommand : PSCmdlet
{
+ private const string Noun = "GitRepo";
+
[Parameter(
Position = 0,
ValueFromPipeline = true,
@@ -17,12 +19,13 @@ public sealed class NewGitRepoCommand : PSCmdlet
public NewGitRepoCommand()
{
+ Console.WriteLine($"{nameof(NewGitRepoCommand)} init");
}
protected override void BeginProcessing()
{
var pwd = this.SessionState.Path.CurrentLocation.Path;
- WriteDebug("Checking if current directory is a git repository...");
+ WriteObject("Checking if current directory is a git repository...");
var repoFolfder = IsGitRepo(pwd);
@@ -101,11 +104,11 @@ public sealed class NewGitRepoCommand : PSCmdlet
///
/// The full path to the top level folder containing a git repository
///
- public string Directory { get; init; } = null!;
+ public string Directory { get; set; } = null!;
///
/// The last folder name of the directory
///
- public string Folder { get; init; } = null!;
+ public string Folder { get; set; } = null!;
}
}
\ No newline at end of file
diff --git a/src/PowershellModule/Git/Commands/SetGitRepoCommand.cs b/src/PowershellModule/Git/SetGitRepoCommand.cs
similarity index 51%
rename from src/PowershellModule/Git/Commands/SetGitRepoCommand.cs
rename to src/PowershellModule/Git/SetGitRepoCommand.cs
index 383b436..9d6a7f1 100644
--- a/src/PowershellModule/Git/Commands/SetGitRepoCommand.cs
+++ b/src/PowershellModule/Git/SetGitRepoCommand.cs
@@ -1,14 +1,18 @@
-using System.Management.Automation;
+using System;
+using System.Management.Automation;
using ModuleCore.Git;
-namespace PowershellModule.Git.Commands;
+namespace PowershellModule.Git;
-// TODO: decide if I want to use this verb instead of show. Currently this implementation is under Show-GitRepo
-[Cmdlet(VerbsCommon.Set, GitCommands.GitRepoNoun)]
+[Cmdlet(VerbsCommon.Set, Noun)]
public class SetGitRepoCommand : PSCmdlet
{
+ private const string Noun = "GitRepo";
+
public SetGitRepoCommand()
{
+ Console.WriteLine($"{nameof(NewGitRepoCommand)} init");
+ var a = GitManager.Instance;
}
protected override void BeginProcessing()
diff --git a/src/PowershellModule/PostBuild.ps1 b/src/PowershellModule/PostBuild.ps1
index cb7b8de..d4c0725 100644
--- a/src/PowershellModule/PostBuild.ps1
+++ b/src/PowershellModule/PostBuild.ps1
@@ -22,7 +22,6 @@ $allowList = @(
"ModuleCore*"
"PowershellModule*"
"*SQLite*"
- "data"
)
Write-Host "Removing all non-module required files from '$targetDir'"
Get-ChildItem -Path $targetDir -exclude $allowList | Remove-Item -Recurse
\ No newline at end of file
diff --git a/src/PowershellModule/PowershellModule.csproj b/src/PowershellModule/PowershellModule.csproj
index 56f10c3..95ccbdc 100644
--- a/src/PowershellModule/PowershellModule.csproj
+++ b/src/PowershellModule/PowershellModule.csproj
@@ -12,6 +12,7 @@
All
+
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