Tim Jones 4 месяцев назад
Родитель
Сommit
63f47a498d

+ 1 - 2
RackPeek.Domain/Git/GitStatus.cs

@@ -1,7 +1,6 @@
 namespace RackPeek.Domain.Git;
 
-public enum GitRepoStatus
-{
+public enum GitRepoStatus {
     NotAvailable,
     Clean,
     Dirty

+ 1 - 2
RackPeek.Domain/Git/IGitRepository.cs

@@ -1,7 +1,6 @@
 namespace RackPeek.Domain.Git;
 
-public interface IGitRepository
-{
+public interface IGitRepository {
     bool IsAvailable { get; }
     void Init();
     GitRepoStatus GetStatus();

+ 123 - 146
RackPeek.Domain/Git/LibGit2GitRepository.cs

@@ -1,96 +1,118 @@
 using LibGit2Sharp;
+using LibGit2Sharp.Handlers;
 
 namespace RackPeek.Domain.Git;
 
-public sealed class LibGit2GitRepository : IGitRepository
-{
-    private readonly string _repoPath;
-    private bool _isAvailable;
-
-    public LibGit2GitRepository(string configDirectory)
-    {
-        _repoPath = configDirectory;
-        _isAvailable = Repository.IsValid(configDirectory);
+public interface IGitCredentialsProvider {
+    CredentialsHandler GetHandler();
+}
+public sealed class GitHubTokenCredentialsProvider(string username, string token) : IGitCredentialsProvider {
+    private readonly string _username = username ?? throw new ArgumentNullException(nameof(username));
+    private readonly string _token = token ?? throw new ArgumentNullException(nameof(token));
+
+    public CredentialsHandler GetHandler() {
+        return (_, _, _) => new UsernamePasswordCredentials {
+            Username = _username,
+            Password = _token
+        };
     }
+}
+
+public sealed class LibGit2GitRepository(
+    string configDirectory,
+    IGitCredentialsProvider credentialsProvider) : IGitRepository {
+    private readonly CredentialsHandler _credentials = credentialsProvider.GetHandler();
+
+    private bool _isAvailable = Repository.IsValid(configDirectory);
 
     public bool IsAvailable => _isAvailable;
 
-    public void Init()
-    {
-        Repository.Init(_repoPath);
+    public void Init() {
+        Repository.Init(configDirectory);
         _isAvailable = true;
     }
 
-    public GitRepoStatus GetStatus()
-    {
+    private Repository OpenRepo() => new(configDirectory);
+
+    private static Signature GetSignature(Repository repo) {
+        var name = repo.Config.Get<string>("user.name")?.Value ?? "RackPeek";
+        var email = repo.Config.Get<string>("user.email")?.Value ?? "rackpeek@local";
+
+        return new Signature(name, email, DateTimeOffset.Now);
+    }
+
+    private static Remote GetRemote(Repository repo) => repo.Network.Remotes["origin"] ?? repo.Network.Remotes.First();
+
+    public GitRepoStatus GetStatus() {
         if (!_isAvailable)
             return GitRepoStatus.NotAvailable;
 
-        using var repo = new Repository(_repoPath);
-        return repo.RetrieveStatus().IsDirty ? GitRepoStatus.Dirty : GitRepoStatus.Clean;
+        using Repository repo = OpenRepo();
+
+        return repo.RetrieveStatus().IsDirty
+            ? GitRepoStatus.Dirty
+            : GitRepoStatus.Clean;
     }
 
-    public void StageAll()
-    {
-        using var repo = new Repository(_repoPath);
+    public void StageAll() {
+        using Repository repo = OpenRepo();
         Commands.Stage(repo, "*");
     }
 
-    public void Commit(string message)
-    {
-        using var repo = new Repository(_repoPath);
+    public void Commit(string message) {
+        using Repository repo = OpenRepo();
+
         Signature signature = GetSignature(repo);
         repo.Commit(message, signature, signature);
     }
 
-    public string GetDiff()
-    {
-        using var repo = new Repository(_repoPath);
-        Patch changes = repo.Diff.Compare<Patch>(
+    public string GetDiff() {
+        using Repository repo = OpenRepo();
+
+        Patch patch = repo.Diff.Compare<Patch>(
             repo.Head.Tip?.Tree,
             DiffTargets.Index | DiffTargets.WorkingDirectory);
-        return changes?.Content ?? string.Empty;
+
+        return patch?.Content ?? string.Empty;
     }
 
-    public string[] GetChangedFiles()
-    {
-        using var repo = new Repository(_repoPath);
-        RepositoryStatus status = repo.RetrieveStatus();
-        return status
+    public string[] GetChangedFiles() {
+        using Repository repo = OpenRepo();
+
+        return repo.RetrieveStatus()
             .Where(e => e.State != FileStatus.Ignored)
-            .Select(e =>
-            {
-                var prefix = e.State switch
-                {
-                    FileStatus.NewInWorkdir or FileStatus.NewInIndex => "A",
-                    FileStatus.DeletedFromWorkdir or FileStatus.DeletedFromIndex => "D",
-                    FileStatus.RenamedInWorkdir or FileStatus.RenamedInIndex => "R",
-                    _ when e.State.HasFlag(FileStatus.ModifiedInWorkdir)
-                        || e.State.HasFlag(FileStatus.ModifiedInIndex) => "M",
-                    _ => "?"
-                };
-                return $"{prefix}  {e.FilePath}";
-            })
+            .Select(e => $"{GetPrefix(e.State)}  {e.FilePath}")
             .ToArray();
     }
 
-    public void RestoreAll()
-    {
-        using var repo = new Repository(_repoPath);
-        var options = new CheckoutOptions { CheckoutModifiers = CheckoutModifiers.Force };
-        repo.CheckoutPaths(repo.Head.FriendlyName, new[] { "*" }, options);
+    private static string GetPrefix(FileStatus state) => state switch {
+        FileStatus.NewInWorkdir or FileStatus.NewInIndex => "A",
+        FileStatus.DeletedFromWorkdir or FileStatus.DeletedFromIndex => "D",
+        FileStatus.RenamedInWorkdir or FileStatus.RenamedInIndex => "R",
+        _ when state.HasFlag(FileStatus.ModifiedInWorkdir)
+          || state.HasFlag(FileStatus.ModifiedInIndex) => "M",
+        _ => "?"
+    };
+
+    public void RestoreAll() {
+        using Repository repo = OpenRepo();
+
+        repo.CheckoutPaths(
+            repo.Head.FriendlyName,
+            ["*"],
+            new CheckoutOptions { CheckoutModifiers = CheckoutModifiers.Force });
+
         repo.RemoveUntrackedFiles();
     }
 
-    public string GetCurrentBranch()
-    {
-        using var repo = new Repository(_repoPath);
+    public string GetCurrentBranch() {
+        using Repository repo = OpenRepo();
         return repo.Head.FriendlyName;
     }
 
-    public GitLogEntry[] GetLog(int count)
-    {
-        using var repo = new Repository(_repoPath);
+    public GitLogEntry[] GetLog(int count) {
+        using Repository repo = OpenRepo();
+
         if (repo.Head.Tip is null)
             return [];
 
@@ -104,133 +126,88 @@ public sealed class LibGit2GitRepository : IGitRepository
             .ToArray();
     }
 
-    public bool HasRemote()
-    {
-        using var repo = new Repository(_repoPath);
+    public bool HasRemote() {
+        using Repository repo = OpenRepo();
         return repo.Network.Remotes.Any();
     }
 
-    public GitSyncStatus FetchAndGetSyncStatus()
-    {
-        using var repo = new Repository(_repoPath);
+    public GitSyncStatus FetchAndGetSyncStatus() {
+        using Repository repo = OpenRepo();
 
         if (!repo.Network.Remotes.Any())
-            return new GitSyncStatus(0, 0, false);
+            return new(0, 0, false);
 
-        Remote remote = repo.Network.Remotes["origin"]
-            ?? repo.Network.Remotes.First();
+        Remote remote = GetRemote(repo);
 
-        var fetchOptions = new FetchOptions();
-        ConfigureCredentials(fetchOptions);
-        IEnumerable<string> refSpecs = remote.FetchRefSpecs.Select(r => r.Specification);
-        Commands.Fetch(repo, remote.Name, refSpecs, fetchOptions, null);
+        Commands.Fetch(
+            repo,
+            remote.Name,
+            remote.FetchRefSpecs.Select(r => r.Specification),
+            new FetchOptions { CredentialsProvider = _credentials },
+            null);
 
         Branch? tracking = repo.Head.TrackedBranch;
+
         if (tracking is null)
-        {
-            var localCount = repo.Head.Tip is not null
-                ? repo.Commits.Count()
-                : 0;
-            return new GitSyncStatus(localCount, 0, true);
-        }
+            return new(repo.Commits.Count(), 0, true);
 
         HistoryDivergence divergence = repo.ObjectDatabase.CalculateHistoryDivergence(
-            repo.Head.Tip, tracking.Tip);
-
-        var ahead = divergence.AheadBy ?? 0;
-        var behind = divergence.BehindBy ?? 0;
+            repo.Head.Tip,
+            tracking.Tip);
 
-        return new GitSyncStatus(ahead, behind, true);
+        return new(
+            divergence.AheadBy ?? 0,
+            divergence.BehindBy ?? 0,
+            true);
     }
 
-    public void Push()
-    {
-        using var repo = new Repository(_repoPath);
+    public void Push() {
+        using Repository repo = OpenRepo();
 
-        Remote remote = repo.Network.Remotes["origin"]
-            ?? repo.Network.Remotes.First();
+        Remote remote = GetRemote(repo);
 
-        var pushOptions = new PushOptions();
-        ConfigureCredentials(pushOptions);
+        repo.Network.Push(
+            remote,
+            $"refs/heads/{repo.Head.FriendlyName}",
+            new PushOptions { CredentialsProvider = _credentials });
 
-        var pushRefSpec = $"refs/heads/{repo.Head.FriendlyName}";
-        repo.Network.Push(remote, pushRefSpec, pushOptions);
+        if (repo.Head.TrackedBranch is null) {
+            Branch remoteBranch = repo.Branches[$"{remote.Name}/{repo.Head.FriendlyName}"];
 
-        if (repo.Head.TrackedBranch is null)
-        {
-            Branch? remoteBranch = repo.Branches[$"{remote.Name}/{repo.Head.FriendlyName}"];
-            if (remoteBranch is not null)
-            {
+            if (remoteBranch != null) {
                 repo.Branches.Update(repo.Head,
                     b => b.TrackedBranch = remoteBranch.CanonicalName);
             }
         }
     }
 
-    public void Pull()
-    {
-        using var repo = new Repository(_repoPath);
+    public void Pull() {
+        using Repository repo = OpenRepo();
 
-        var pullOptions = new PullOptions
-        {
-            FetchOptions = new FetchOptions()
-        };
-        ConfigureCredentials(pullOptions.FetchOptions);
-
-        Signature signature = GetSignature(repo);
-        Commands.Pull(repo, signature, pullOptions);
+        Commands.Pull(
+            repo,
+            GetSignature(repo),
+            new PullOptions {
+                FetchOptions = new FetchOptions {
+                    CredentialsProvider = _credentials
+                }
+            });
     }
 
-    public void AddRemote(string name, string url)
-    {
-        using var repo = new Repository(_repoPath);
+    public void AddRemote(string name, string url) {
+        using Repository repo = OpenRepo();
         repo.Network.Remotes.Add(name, url);
     }
 
-    private static Signature GetSignature(Repository repo)
-    {
-        Configuration config = repo.Config;
-        var name = config.Get<string>("user.name")?.Value ?? "RackPeek";
-        var email = config.Get<string>("user.email")?.Value ?? "rackpeek@local";
-        return new Signature(name, email, DateTimeOffset.Now);
-    }
-
-    private static void ConfigureCredentials(FetchOptions options)
-    {
-        var token = Environment.GetEnvironmentVariable("GIT_TOKEN");
-        if (!string.IsNullOrEmpty(token))
-        {
-            options.CredentialsProvider = (_, _, _) =>
-                new UsernamePasswordCredentials
-                {
-                    Username = "git",
-                    Password = token
-                };
-        }
-    }
-
-    private static void ConfigureCredentials(PushOptions options)
-    {
-        var token = Environment.GetEnvironmentVariable("GIT_TOKEN");
-        if (!string.IsNullOrEmpty(token))
-        {
-            options.CredentialsProvider = (_, _, _) =>
-                new UsernamePasswordCredentials
-                {
-                    Username = "git",
-                    Password = token
-                };
-        }
-    }
-
-    private static string FormatRelativeDate(DateTimeOffset date)
-    {
+    private static string FormatRelativeDate(DateTimeOffset date) {
         TimeSpan diff = DateTimeOffset.Now - date;
+
         if (diff.TotalMinutes < 1) return "just now";
         if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes} minutes ago";
         if (diff.TotalHours < 24) return $"{(int)diff.TotalHours} hours ago";
         if (diff.TotalDays < 30) return $"{(int)diff.TotalDays} days ago";
         if (diff.TotalDays < 365) return $"{(int)(diff.TotalDays / 30)} months ago";
+
         return $"{(int)(diff.TotalDays / 365)} years ago";
     }
 }

+ 1 - 2
RackPeek.Domain/Git/NullGitRepository.cs

@@ -1,7 +1,6 @@
 namespace RackPeek.Domain.Git;
 
-public sealed class NullGitRepository : IGitRepository
-{
+public sealed class NullGitRepository : IGitRepository {
     public bool IsAvailable => false;
     public void Init() { }
     public GitRepoStatus GetStatus() => GitRepoStatus.NotAvailable;

+ 0 - 11
RackPeek.Domain/Git/Queries/GetBranchQuery.cs

@@ -1,11 +0,0 @@
-namespace RackPeek.Domain.Git.Queries;
-
-public interface IGetBranchQuery
-{
-    string Execute();
-}
-
-public class GetBranchQuery(IGitRepository repo) : IGetBranchQuery
-{
-    public string Execute() => repo.IsAvailable ? repo.GetCurrentBranch() : string.Empty;
-}

+ 0 - 11
RackPeek.Domain/Git/Queries/GetChangedFilesQuery.cs

@@ -1,11 +0,0 @@
-namespace RackPeek.Domain.Git.Queries;
-
-public interface IGetChangedFilesQuery
-{
-    string[] Execute();
-}
-
-public class GetChangedFilesQuery(IGitRepository repo) : IGetChangedFilesQuery
-{
-    public string[] Execute() => repo.IsAvailable ? repo.GetChangedFiles() : [];
-}

+ 0 - 11
RackPeek.Domain/Git/Queries/GetDiffQuery.cs

@@ -1,11 +0,0 @@
-namespace RackPeek.Domain.Git.Queries;
-
-public interface IGetDiffQuery
-{
-    string Execute();
-}
-
-public class GetDiffQuery(IGitRepository repo) : IGetDiffQuery
-{
-    public string Execute() => repo.IsAvailable ? repo.GetDiff() : string.Empty;
-}

+ 0 - 12
RackPeek.Domain/Git/Queries/GetLogQuery.cs

@@ -1,12 +0,0 @@
-namespace RackPeek.Domain.Git.Queries;
-
-public interface IGetLogQuery
-{
-    GitLogEntry[] Execute(int count = 20);
-}
-
-public class GetLogQuery(IGitRepository repo) : IGetLogQuery
-{
-    public GitLogEntry[] Execute(int count = 20) =>
-        repo.IsAvailable ? repo.GetLog(count) : [];
-}

+ 0 - 11
RackPeek.Domain/Git/Queries/GetStatusQuery.cs

@@ -1,11 +0,0 @@
-namespace RackPeek.Domain.Git.Queries;
-
-public interface IGetStatusQuery
-{
-    GitRepoStatus Execute();
-}
-
-public class GetStatusQuery(IGitRepository repo) : IGetStatusQuery
-{
-    public GitRepoStatus Execute() => repo.GetStatus();
-}

+ 0 - 24
RackPeek.Domain/Git/Queries/GetSyncStatusQuery.cs

@@ -1,24 +0,0 @@
-namespace RackPeek.Domain.Git.Queries;
-
-public interface IGetSyncStatusQuery
-{
-    GitSyncStatus Execute();
-}
-
-public class GetSyncStatusQuery(IGitRepository repo) : IGetSyncStatusQuery
-{
-    public GitSyncStatus Execute()
-    {
-        if (!repo.IsAvailable || !repo.HasRemote())
-            return new GitSyncStatus(0, 0, false);
-
-        try
-        {
-            return repo.FetchAndGetSyncStatus();
-        }
-        catch (Exception ex)
-        {
-            return new GitSyncStatus(0, 0, true, $"Fetch failed: {ex.Message}");
-        }
-    }
-}

+ 4 - 13
RackPeek.Domain/Git/UseCases/AddRemoteUseCase.cs

@@ -1,14 +1,7 @@
 namespace RackPeek.Domain.Git.UseCases;
 
-public interface IAddRemoteUseCase
-{
-    Task<string?> ExecuteAsync(string url);
-}
-
-public class AddRemoteUseCase(IGitRepository repo) : IAddRemoteUseCase
-{
-    public Task<string?> ExecuteAsync(string url)
-    {
+public class AddRemoteUseCase(IGitRepository repo) : IUseCase {
+    public Task<string?> ExecuteAsync(string url) {
         if (!repo.IsAvailable)
             return Task.FromResult<string?>("Git is not available.");
 
@@ -21,13 +14,11 @@ public class AddRemoteUseCase(IGitRepository repo) : IAddRemoteUseCase
         if (repo.HasRemote())
             return Task.FromResult<string?>("Remote already configured.");
 
-        try
-        {
+        try {
             repo.AddRemote("origin", url.Trim());
             return Task.FromResult<string?>(null);
         }
-        catch (Exception ex)
-        {
+        catch (Exception ex) {
             return Task.FromResult<string?>($"Add remote failed: {ex.Message}");
         }
     }

+ 4 - 13
RackPeek.Domain/Git/UseCases/CommitAllUseCase.cs

@@ -1,19 +1,11 @@
 namespace RackPeek.Domain.Git.UseCases;
 
-public interface ICommitAllUseCase
-{
-    Task<string?> ExecuteAsync(string message);
-}
-
-public class CommitAllUseCase(IGitRepository repo) : ICommitAllUseCase
-{
-    public Task<string?> ExecuteAsync(string message)
-    {
+public class CommitAllUseCase(IGitRepository repo) : IUseCase {
+    public Task<string?> ExecuteAsync(string message) {
         if (!repo.IsAvailable)
             return Task.FromResult<string?>("Git is not available.");
 
-        try
-        {
+        try {
             repo.StageAll();
 
             if (repo.GetStatus() != GitRepoStatus.Dirty)
@@ -22,8 +14,7 @@ public class CommitAllUseCase(IGitRepository repo) : ICommitAllUseCase
             repo.Commit(message);
             return Task.FromResult<string?>(null);
         }
-        catch (Exception ex)
-        {
+        catch (Exception ex) {
             return Task.FromResult<string?>($"Commit failed: {ex.Message}");
         }
     }

+ 4 - 13
RackPeek.Domain/Git/UseCases/InitRepoUseCase.cs

@@ -1,24 +1,15 @@
 namespace RackPeek.Domain.Git.UseCases;
 
-public interface IInitRepoUseCase
-{
-    Task<string?> ExecuteAsync();
-}
-
-public class InitRepoUseCase(IGitRepository repo) : IInitRepoUseCase
-{
-    public Task<string?> ExecuteAsync()
-    {
+public class InitRepoUseCase(IGitRepository repo) : IUseCase {
+    public Task<string?> ExecuteAsync() {
         if (repo.IsAvailable)
             return Task.FromResult<string?>(null);
 
-        try
-        {
+        try {
             repo.Init();
             return Task.FromResult<string?>(null);
         }
-        catch (Exception ex)
-        {
+        catch (Exception ex) {
             return Task.FromResult<string?>($"Init failed: {ex.Message}");
         }
     }

+ 4 - 13
RackPeek.Domain/Git/UseCases/PullUseCase.cs

@@ -1,27 +1,18 @@
 namespace RackPeek.Domain.Git.UseCases;
 
-public interface IPullUseCase
-{
-    Task<string?> ExecuteAsync();
-}
-
-public class PullUseCase(IGitRepository repo) : IPullUseCase
-{
-    public Task<string?> ExecuteAsync()
-    {
+public class PullUseCase(IGitRepository repo) : IUseCase {
+    public Task<string?> ExecuteAsync() {
         if (!repo.IsAvailable)
             return Task.FromResult<string?>("Git is not available.");
 
         if (!repo.HasRemote())
             return Task.FromResult<string?>("No remote configured.");
 
-        try
-        {
+        try {
             repo.Pull();
             return Task.FromResult<string?>(null);
         }
-        catch (Exception ex)
-        {
+        catch (Exception ex) {
             return Task.FromResult<string?>($"Pull failed: {ex.Message}");
         }
     }

+ 4 - 12
RackPeek.Domain/Git/UseCases/PushUseCase.cs

@@ -1,27 +1,19 @@
 namespace RackPeek.Domain.Git.UseCases;
 
-public interface IPushUseCase
-{
-    Task<string?> ExecuteAsync();
-}
 
-public class PushUseCase(IGitRepository repo) : IPushUseCase
-{
-    public Task<string?> ExecuteAsync()
-    {
+public class PushUseCase(IGitRepository repo) : IUseCase {
+    public Task<string?> ExecuteAsync() {
         if (!repo.IsAvailable)
             return Task.FromResult<string?>("Git is not available.");
 
         if (!repo.HasRemote())
             return Task.FromResult<string?>("No remote configured.");
 
-        try
-        {
+        try {
             repo.Push();
             return Task.FromResult<string?>(null);
         }
-        catch (Exception ex)
-        {
+        catch (Exception ex) {
             return Task.FromResult<string?>($"Push failed: {ex.Message}");
         }
     }

+ 4 - 13
RackPeek.Domain/Git/UseCases/RestoreAllUseCase.cs

@@ -1,24 +1,15 @@
 namespace RackPeek.Domain.Git.UseCases;
 
-public interface IRestoreAllUseCase
-{
-    Task<string?> ExecuteAsync();
-}
-
-public class RestoreAllUseCase(IGitRepository repo) : IRestoreAllUseCase
-{
-    public Task<string?> ExecuteAsync()
-    {
+public class RestoreAllUseCase(IGitRepository repo) : IUseCase {
+    public Task<string?> ExecuteAsync() {
         if (!repo.IsAvailable)
             return Task.FromResult<string?>("Git is not available.");
 
-        try
-        {
+        try {
             repo.RestoreAll();
             return Task.FromResult<string?>(null);
         }
-        catch (Exception ex)
-        {
+        catch (Exception ex) {
             return Task.FromResult<string?>($"Restore failed: {ex.Message}");
         }
     }

+ 1 - 0
RackPeek.Domain/RackPeek.Domain.csproj

@@ -9,6 +9,7 @@
     <ItemGroup>
         <PackageReference Include="DocMigrator.Yaml" Version="10.0.3" />
         <PackageReference Include="LibGit2Sharp" Version="0.31.0" />
+        <PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.3" />
         <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
         <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
         <PackageReference Include="YamlDotNet" Version="16.3.0" />

+ 2 - 0
RackPeek.Domain/RpkConstants.cs

@@ -2,4 +2,6 @@ namespace RackPeek.Domain;
 
 public static class RpkConstants {
     public const string Version = "v1.3.0";
+
+    public static bool HasGitServices = false;
 }

+ 27 - 0
RackPeek.Domain/ServiceCollectionExtensions.cs

@@ -1,5 +1,7 @@
 using System.Reflection;
+using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Git;
 using RackPeek.Domain.Persistence;
 using RackPeek.Domain.Resources;
 using RackPeek.Domain.Resources.Connections;
@@ -20,6 +22,31 @@ public interface IResourceUseCase<T> where T : Resource {
 }
 
 public static class ServiceCollectionExtensions {
+
+    public static IServiceCollection AddGitServices(
+        this IServiceCollection services,
+        IConfiguration config,
+        string? yamlPath = null) {
+        var gitToken = config["GIT_TOKEN"];
+        if (!string.IsNullOrEmpty(gitToken) && !string.IsNullOrWhiteSpace(yamlPath)) {
+            var gitUsername = config["GIT_USERNAME"] ?? "git";
+
+            services.AddSingleton<IGitCredentialsProvider>(
+                _ => new GitHubTokenCredentialsProvider(gitUsername, gitToken));
+
+            services.AddSingleton<IGitRepository>(sp => {
+                IGitCredentialsProvider creds = sp.GetRequiredService<IGitCredentialsProvider>();
+                return new LibGit2GitRepository(yamlPath, creds);
+            });
+            RpkConstants.HasGitServices = true;
+        }
+        else {
+            RpkConstants.HasGitServices = false;
+            services.AddSingleton<IGitRepository, NullGitRepository>();
+        }
+
+        return services;
+    }
     public static IServiceCollection AddResourceUseCases(
         this IServiceCollection services,
         Assembly assembly) {

+ 1 - 15
RackPeek.Web.Viewer/Program.cs

@@ -3,8 +3,6 @@ using Microsoft.AspNetCore.Components.Web;
 using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
 using RackPeek.Domain;
 using RackPeek.Domain.Git;
-using RackPeek.Domain.Git.UseCases;
-using RackPeek.Domain.Git.Queries;
 using RackPeek.Domain.Persistence;
 using RackPeek.Domain.Persistence.Yaml;
 using Shared.Rcl;
@@ -27,20 +25,8 @@ public class Program {
             };
         });
 
+        builder.Services.AddGitServices(builder.Configuration);
 
-        builder.Services.AddSingleton<IGitRepository>(new NullGitRepository());
-        builder.Services.AddSingleton<IInitRepoUseCase, InitRepoUseCase>();
-        builder.Services.AddSingleton<ICommitAllUseCase, CommitAllUseCase>();
-        builder.Services.AddSingleton<IRestoreAllUseCase, RestoreAllUseCase>();
-        builder.Services.AddSingleton<IPushUseCase, PushUseCase>();
-        builder.Services.AddSingleton<IPullUseCase, PullUseCase>();
-        builder.Services.AddSingleton<IAddRemoteUseCase, AddRemoteUseCase>();
-        builder.Services.AddSingleton<IGetStatusQuery, GetStatusQuery>();
-        builder.Services.AddSingleton<IGetBranchQuery, GetBranchQuery>();
-        builder.Services.AddSingleton<IGetDiffQuery, GetDiffQuery>();
-        builder.Services.AddSingleton<IGetChangedFilesQuery, GetChangedFilesQuery>();
-        builder.Services.AddSingleton<IGetLogQuery, GetLogQuery>();
-        builder.Services.AddSingleton<IGetSyncStatusQuery, GetSyncStatusQuery>();
         builder.Services.AddScoped<ITextFileStore, WasmTextFileStore>();
 
         var resources = new ResourceCollection();

+ 2 - 17
RackPeek.Web/Program.cs

@@ -3,8 +3,6 @@ using Microsoft.AspNetCore.Components;
 using Microsoft.AspNetCore.Hosting.StaticWebAssets;
 using RackPeek.Domain;
 using RackPeek.Domain.Git;
-using RackPeek.Domain.Git.UseCases;
-using RackPeek.Domain.Git.Queries;
 using RackPeek.Domain.Persistence;
 using RackPeek.Domain.Persistence.Yaml;
 using RackPeek.Web.Api;
@@ -30,20 +28,6 @@ public class Program {
 
         Directory.CreateDirectory(yamlPath);
 
-        builder.Services.AddSingleton<IGitRepository>(new LibGit2GitRepository(yamlPath));
-        builder.Services.AddSingleton<IInitRepoUseCase, InitRepoUseCase>();
-        builder.Services.AddSingleton<ICommitAllUseCase, CommitAllUseCase>();
-        builder.Services.AddSingleton<IRestoreAllUseCase, RestoreAllUseCase>();
-        builder.Services.AddSingleton<IPushUseCase, PushUseCase>();
-        builder.Services.AddSingleton<IPullUseCase, PullUseCase>();
-        builder.Services.AddSingleton<IAddRemoteUseCase, AddRemoteUseCase>();
-        builder.Services.AddSingleton<IGetStatusQuery, GetStatusQuery>();
-        builder.Services.AddSingleton<IGetBranchQuery, GetBranchQuery>();
-        builder.Services.AddSingleton<IGetDiffQuery, GetDiffQuery>();
-        builder.Services.AddSingleton<IGetChangedFilesQuery, GetChangedFilesQuery>();
-        builder.Services.AddSingleton<IGetLogQuery, GetLogQuery>();
-        builder.Services.AddSingleton<IGetSyncStatusQuery, GetSyncStatusQuery>();
-
         var yamlFilePath = Path.Combine(yamlPath, yamlFileName);
 
         if (!File.Exists(yamlFilePath)) {
@@ -70,9 +54,10 @@ public class Program {
             };
         });
 
+        builder.Services.AddGitServices(builder.Configuration, yamlPath);
+
         var resources = new ResourceCollection();
         builder.Services.AddSingleton(resources);
-
         builder.Services.AddScoped<RackPeekConfigMigrationDeserializer>();
         builder.Services.AddScoped<IResourceYamlMigrationService, ResourceYamlMigrationService>();
 

+ 36 - 30
Shared.Rcl/Layout/GitStatusIndicator.razor

@@ -1,18 +1,11 @@
 @using RackPeek.Domain.Git
 @using RackPeek.Domain.Git.UseCases
-@using RackPeek.Domain.Git.Queries
-@inject IGetStatusQuery GetStatus
-@inject IGetBranchQuery GetBranch
-@inject IGetDiffQuery GetDiff
-@inject IGetChangedFilesQuery GetChangedFiles
-@inject IGetLogQuery GetLog
-@inject IGetSyncStatusQuery GetSyncStatus
-@inject IInitRepoUseCase InitRepo
-@inject ICommitAllUseCase CommitAll
-@inject IRestoreAllUseCase RestoreAll
-@inject IPushUseCase PushUseCase
-@inject IPullUseCase PullUseCase
-@inject IAddRemoteUseCase AddRemoteUseCase
+@inject InitRepoUseCase InitRepo
+@inject CommitAllUseCase CommitAll
+@inject RestoreAllUseCase RestoreAll
+@inject PushUseCase PushUseCase
+@inject PullUseCase PullUseCase
+@inject AddRemoteUseCase AddRemoteUseCase
 @inject IGitRepository GitRepo
 @implements IDisposable
 
@@ -381,14 +374,17 @@ else
 
     protected override async Task OnInitializedAsync()
     {
-        _status = GetStatus.Execute();
+        _status = GitRepo.GetStatus();
 
         if (_status == GitRepoStatus.NotAvailable)
             return;
 
-        _branch = GetBranch.Execute();
+        _branch = GitRepo.GetCurrentBranch();
         _hasRemote = GitRepo.HasRemote();
 
+        _cts?.Cancel();
+        _timer?.Dispose();
+        
         _cts = new CancellationTokenSource();
         _timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
         _ = PollStatusAsync(_cts.Token);
@@ -400,21 +396,26 @@ else
     {
         try
         {
-            while (await _timer!.WaitForNextTickAsync(ct))
+            while (_timer != null && await _timer.WaitForNextTickAsync(ct))
             {
-                if (_isBusy) continue;
+                if (_isBusy)
+                    continue;
+
+                var newStatus = GitRepo.GetStatus();
 
-                GitRepoStatus newStatus = GetStatus.Execute();
                 if (newStatus != _status)
                 {
                     _status = newStatus;
                     _confirmDiscard = false;
                     _showDropdown = false;
+
                     await InvokeAsync(StateHasChanged);
                 }
             }
         }
-        catch (OperationCanceledException) { }
+        catch (OperationCanceledException)
+        {
+        }
     }
 
     private async Task InitRepoAsync()
@@ -425,18 +426,23 @@ else
         try
         {
             var error = await InitRepo.ExecuteAsync();
+
             if (error is not null)
             {
                 _errorMessage = error;
                 return;
             }
 
-            _status = GetStatus.Execute();
-            _branch = GetBranch.Execute();
+            _status = GitRepo.GetStatus();
+            _branch = GitRepo.GetCurrentBranch();
             _hasRemote = GitRepo.HasRemote();
 
+            _cts?.Cancel();
+            _timer?.Dispose();
+
             _cts = new CancellationTokenSource();
             _timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
+
             _ = PollStatusAsync(_cts.Token);
         }
         catch (Exception ex)
@@ -493,7 +499,7 @@ else
             if (error is not null)
                 _errorMessage = error;
 
-            _status = GetStatus.Execute();
+            _status = GitRepo.GetStatus();
         }
         catch (Exception ex)
         {
@@ -527,7 +533,7 @@ else
 
         try
         {
-            _syncStatus = GetSyncStatus.Execute();
+            _syncStatus = GitRepo.FetchAndGetSyncStatus();
             if (_syncStatus.Error is not null)
                 _errorMessage = _syncStatus.Error;
         }
@@ -551,8 +557,8 @@ else
     private void OpenDiffAsync()
     {
         _showDropdown = false;
-        _changedFiles = GetChangedFiles.Execute();
-        _diffContent = GetDiff.Execute();
+        _changedFiles = GitRepo.GetChangedFiles();
+        _diffContent = GitRepo.GetDiff();
         _showDiff = true;
     }
 
@@ -573,7 +579,7 @@ else
 
         _showDropdown = false;
         _showSyncDropdown = false;
-        _logEntries = GetLog.Execute();
+        _logEntries = GitRepo.GetLog(20);
         _showHistory = true;
     }
 
@@ -596,7 +602,7 @@ else
             if (error is not null)
                 _errorMessage = error;
 
-            _status = GetStatus.Execute();
+            _status = GitRepo.GetStatus();
         }
         catch (Exception ex)
         {
@@ -619,7 +625,7 @@ else
             if (error is not null)
                 _errorMessage = error;
 
-            _syncStatus = GetSyncStatus.Execute();
+            _syncStatus = GitRepo.FetchAndGetSyncStatus();
         }
         catch (Exception ex)
         {
@@ -642,8 +648,8 @@ else
             if (error is not null)
                 _errorMessage = error;
 
-            _syncStatus = GetSyncStatus.Execute();
-            _status = GetStatus.Execute();
+            _syncStatus = GitRepo.FetchAndGetSyncStatus();
+            _status = GitRepo.GetStatus();
         }
         catch (Exception ex)
         {

+ 53 - 50
Shared.Rcl/Layout/MainLayout.razor

@@ -27,59 +27,62 @@
         </NavLink>
 
         <div class="flex items-center gap-6">
-            <GitStatusIndicator />
+            @if (RpkConstants.HasGitServices)
+            {
+                <GitStatusIndicator/>
+            }
 
             <nav class="space-x-6 text-sm" data-testid="main-nav">
 
-            <NavLink href=""
-                     Match="NavLinkMatch.All"
-                     data-testid="nav-home"
-                     class="hover:text-emerald-400"
-                     activeClass="text-emerald-400 font-semibold">
-                Home
-            </NavLink>
-
-            <NavLink href="cli"
-                     class="hover:text-emerald-400"
-                     activeClass="text-emerald-400 font-semibold"
-                     data-testid="nav-cli">
-                CLI
-            </NavLink>
-
-            <NavLink href="yaml"
-                     class="hover:text-emerald-400"
-                     activeClass="text-emerald-400 font-semibold"
-                     data-testid="nav-yaml">
-                Yaml
-            </NavLink>
-
-            <NavLink href="hardware/tree"
-                     class="hover:text-emerald-400"
-                     activeClass="text-emerald-400 font-semibold"
-                     data-testid="nav-hardware">
-                Hardware
-            </NavLink>
-
-            <NavLink href="systems/list"
-                     class="hover:text-emerald-400"
-                     activeClass="text-emerald-400 font-semibold"
-                     data-testid="nav-systems">
-                Systems
-            </NavLink>
-
-            <NavLink href="services/list"
-                     class="hover:text-emerald-400"
-                     activeClass="text-emerald-400 font-semibold"
-                     data-testid="nav-services">
-                Services
-            </NavLink>
-
-            <NavLink href="docs"
-                     class="hover:text-emerald-400"
-                     activeClass="text-emerald-400 font-semibold"
-                     data-testid="nav-docs">
-                Docs
-            </NavLink>
+                <NavLink href=""
+                         Match="NavLinkMatch.All"
+                         data-testid="nav-home"
+                         class="hover:text-emerald-400"
+                         activeClass="text-emerald-400 font-semibold">
+                    Home
+                </NavLink>
+
+                <NavLink href="cli"
+                         class="hover:text-emerald-400"
+                         activeClass="text-emerald-400 font-semibold"
+                         data-testid="nav-cli">
+                    CLI
+                </NavLink>
+
+                <NavLink href="yaml"
+                         class="hover:text-emerald-400"
+                         activeClass="text-emerald-400 font-semibold"
+                         data-testid="nav-yaml">
+                    Yaml
+                </NavLink>
+
+                <NavLink href="hardware/tree"
+                         class="hover:text-emerald-400"
+                         activeClass="text-emerald-400 font-semibold"
+                         data-testid="nav-hardware">
+                    Hardware
+                </NavLink>
+
+                <NavLink href="systems/list"
+                         class="hover:text-emerald-400"
+                         activeClass="text-emerald-400 font-semibold"
+                         data-testid="nav-systems">
+                    Systems
+                </NavLink>
+
+                <NavLink href="services/list"
+                         class="hover:text-emerald-400"
+                         activeClass="text-emerald-400 font-semibold"
+                         data-testid="nav-services">
+                    Services
+                </NavLink>
+
+                <NavLink href="docs"
+                         class="hover:text-emerald-400"
+                         activeClass="text-emerald-400 font-semibold"
+                         data-testid="nav-docs">
+                    Docs
+                </NavLink>