diff --git a/src/Files.App/Utils/Git/IVersionControl.cs b/src/Files.App/Data/Contracts/IVersionControlService.cs similarity index 99% rename from src/Files.App/Utils/Git/IVersionControl.cs rename to src/Files.App/Data/Contracts/IVersionControlService.cs index b50be4dd4bb1..3236b56b5c46 100644 --- a/src/Files.App/Utils/Git/IVersionControl.cs +++ b/src/Files.App/Data/Contracts/IVersionControlService.cs @@ -4,7 +4,7 @@ using LibGit2Sharp; using System.ComponentModel; -namespace Files.App.Utils.Git +namespace Files.App.Data.Contracts { /// /// Defines a version control abstraction. @@ -12,7 +12,7 @@ namespace Files.App.Utils.Git /// /// This interface is intended to decouple the app from a specific backend implementation (e.g. a library such as LibGit2Sharp, or a command-line implementation backed by the git.exe executable). /// - internal interface IVersionControl + public interface IVersionControlService { /// /// Attempts to locate the root of a version control repository (by walking up the directory hierarchy). diff --git a/src/Files.App/Data/Models/GitItemModel.cs b/src/Files.App/Data/Models/GitItemModel.cs index a82cd9d79e38..41b4dfbec06e 100644 --- a/src/Files.App/Data/Models/GitItemModel.cs +++ b/src/Files.App/Data/Models/GitItemModel.cs @@ -6,7 +6,7 @@ namespace Files.App.Data.Models /// /// Represents a model for Git items /// - internal sealed class GitItemModel + public sealed class GitItemModel { /// /// Gets or initializes file change kind diff --git a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs index 728a9824da83..54b4a60aa302 100644 --- a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs +++ b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Files.App.Helpers.Application; +using Files.App.Services.Git; using Files.App.Services.SizeProvider; using Files.App.Utils.Logger; using Files.App.ViewModels.Settings; @@ -265,6 +266,7 @@ public static IHost ConfigureHost() .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() // ViewModels .AddSingleton() .AddSingleton() diff --git a/src/Files.App/Utils/Git/LibGit2.cs b/src/Files.App/Services/Git/LibGit2Service.cs similarity index 79% rename from src/Files.App/Utils/Git/LibGit2.cs rename to src/Files.App/Services/Git/LibGit2Service.cs index 2f272fe4dff7..ed3ff3303a80 100644 --- a/src/Files.App/Utils/Git/LibGit2.cs +++ b/src/Files.App/Services/Git/LibGit2Service.cs @@ -3,9 +3,9 @@ using System.Text; using System.Text.RegularExpressions; -namespace Files.App.Utils.Git; +namespace Files.App.Services.Git; -internal sealed partial class LibGit2 // : IVersionControl +internal sealed partial class LibGit2Service // : IVersionControl { private const string GIT_RESOURCE_NAME = "Files:https://github.com"; private const string GIT_RESOURCE_USERNAME = "Personal Access Token"; @@ -30,7 +30,7 @@ internal sealed partial class LibGit2 // : IVersionControl public bool IsExecutingGitAction { get => _isExecutingGitAction; - private set + internal set // TODO: Make set method private again when move finished { if (_isExecutingGitAction != value) { @@ -159,6 +159,72 @@ public async Task GetBranchNames(string? path) return returnValue; } + public async void FetchOrigin(string? repositoryPath, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(repositoryPath)) + return; + + using var repository = new Repository(repositoryPath); + var signature = repository.Config.BuildSignature(DateTimeOffset.Now); + + var token = CredentialsHelpers.GetPassword(GIT_RESOURCE_NAME, GIT_RESOURCE_USERNAME); + if (signature is not null && !string.IsNullOrWhiteSpace(token)) + { + _fetchOptions.CredentialsProvider = (url, user, cred) + => new UsernamePasswordCredentials + { + Username = signature.Name, + Password = token + }; + } + + MainWindow.Instance.DispatcherQueue.TryEnqueue(() => + { + IsExecutingGitAction = true; + }); + + await DoGitOperationAsync(() => + { + cancellationToken.ThrowIfCancellationRequested(); + + var result = GitOperationResult.Success; + try + { + foreach (var remote in repository.Network.Remotes) + { + cancellationToken.ThrowIfCancellationRequested(); + + LibGit2Sharp.Commands.Fetch( + repository, + remote.Name, + remote.FetchRefSpecs.Select(rs => rs.Specification), + _fetchOptions, + "git fetch updated a ref"); + } + + cancellationToken.ThrowIfCancellationRequested(); + } + catch (Exception ex) + { + result = IsAuthorizationException(ex) + ? GitOperationResult.AuthorizationError + : GitOperationResult.GenericError; + } + + return result; + }); + + MainWindow.Instance.DispatcherQueue.TryEnqueue(() => + { + if (cancellationToken.IsCancellationRequested) + // Do nothing because the operation was cancelled and another fetch may be in progress + return; + + IsExecutingGitAction = false; + GitFetchCompleted?.Invoke(null, EventArgs.Empty); + }); + } + private static bool IsRepoValid(string path) { return SafetyExtensions.IgnoreExceptions(() => Repository.IsValid(path)); diff --git a/src/Files.App/Utils/Git/GitHelpers.cs b/src/Files.App/Utils/Git/GitHelpers.cs index ba767099e424..6617a6166b6f 100644 --- a/src/Files.App/Utils/Git/GitHelpers.cs +++ b/src/Files.App/Utils/Git/GitHelpers.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Files.App.Dialogs; +using Files.App.Services.Git; using LibGit2Sharp; using Microsoft.Extensions.Logging; using Sentry; @@ -15,20 +16,40 @@ namespace Files.App.Utils.Git internal static partial class GitHelpers { // The implementation of the version control interface; it's hardcoded right now but will be made configurable in the future (#16738) - private static readonly LibGit2 _implementation = new LibGit2(); // TODO: Replace with IVersionControl abstraction when it is complete + private static LibGit2Service _implementation = Ioc.Default.GetRequiredService(); // TODO: Replace with IVersionControl abstraction when it is complete - /// + /// public static string? GetGitRepositoryPath(string? path, string root) => _implementation.GetGitRepositoryPath(path, root); - /// + /// public static string GetOriginRepositoryName(string? path) => _implementation.GetOriginRepositoryName(path); - /// + /// public static Task GetBranchNames(string? path) => _implementation.GetBranchNames(path); - /// + /// public static Task GetRepositoryHead(string? path) => _implementation.GetRepositoryHead(path); + /// + public static async void FetchOrigin(string? repositoryPath, CancellationToken cancellationToken = default) => _implementation.FetchOrigin(repositoryPath, cancellationToken); + + /// + public static bool IsExecutingGitAction => _implementation.IsExecutingGitAction; + + /// + public static event PropertyChangedEventHandler? IsExecutingGitActionChanged + { + add => _implementation.IsExecutingGitActionChanged += value; + remove => _implementation.IsExecutingGitActionChanged -= value; + } + + /// + public static event EventHandler? GitFetchCompleted + { + add => _implementation.GitFetchCompleted += value; + remove => _implementation.GitFetchCompleted -= value; + } + #region Legacy implementation // Property already moved into abstraction @@ -72,29 +93,6 @@ internal static partial class GitHelpers // Property already moved into abstraction private static readonly SemaphoreSlim GitOperationSemaphore = new SemaphoreSlim(1, 1); - // Field already moved into abstraction - private static bool _IsExecutingGitAction; - - // Field already moved into abstraction - public static bool IsExecutingGitAction - { - get => _IsExecutingGitAction; - private set - { - if (_IsExecutingGitAction != value) - { - _IsExecutingGitAction = value; - IsExecutingGitActionChanged?.Invoke(null, new PropertyChangedEventArgs(nameof(IsExecutingGitAction))); - } - } - } - - // Event handler already moved into abstraction - public static event PropertyChangedEventHandler? IsExecutingGitActionChanged; - - // Event handler already moved into abstraction - public static event EventHandler? GitFetchCompleted; - public static async Task Checkout(string? repositoryPath, string? branch) { SentrySdk.Experimental.Metrics.EmitCounter("Triggered git checkout", 1); @@ -110,7 +108,7 @@ public static async Task Checkout(string? repositoryPath, string? branch) var options = new CheckoutOptions(); var isBringingChanges = false; - IsExecutingGitAction = true; + _implementation.IsExecutingGitAction = true; if (repository.Index.Conflicts.Any()) { @@ -122,7 +120,7 @@ public static async Task Checkout(string? repositoryPath, string? branch) switch (resolveConflictOption) { case GitCheckoutOptions.None: - IsExecutingGitAction = false; + _implementation.IsExecutingGitAction = false; return false; case GitCheckoutOptions.AbortMerge: repository.Reset(ResetMode.Hard); @@ -139,7 +137,7 @@ public static async Task Checkout(string? repositoryPath, string? branch) switch (resolveConflictOption) { case GitCheckoutOptions.None: - IsExecutingGitAction = false; + _implementation.IsExecutingGitAction = false; return false; case GitCheckoutOptions.DiscardChanges: options.CheckoutModifiers = CheckoutModifiers.Force; @@ -149,7 +147,7 @@ public static async Task Checkout(string? repositoryPath, string? branch) var signature = repository.Config.BuildSignature(DateTimeOffset.Now); if (signature is null) { - IsExecutingGitAction = false; + _implementation.IsExecutingGitAction = false; return false; } @@ -183,7 +181,7 @@ public static async Task Checkout(string? repositoryPath, string? branch) return GitOperationResult.Success; }); - IsExecutingGitAction = false; + _implementation.IsExecutingGitAction = false; return result is GitOperationResult.Success; } @@ -204,7 +202,7 @@ public static async Task CreateNewBranchAsync(string repositoryPath, string acti using var repository = new Repository(repositoryPath); - IsExecutingGitAction = true; + _implementation.IsExecutingGitAction = true; if (repository.Head.FriendlyName.Equals(viewModel.NewBranchName) || await Checkout(repositoryPath, viewModel.BasedOn)) @@ -215,7 +213,7 @@ await Checkout(repositoryPath, viewModel.BasedOn)) await Checkout(repositoryPath, viewModel.NewBranchName); } - IsExecutingGitAction = false; + _implementation.IsExecutingGitAction = false; } public static async Task DeleteBranchAsync(string? repositoryPath, string? activeBranch, string? branchToDelete) @@ -236,7 +234,7 @@ public static async Task DeleteBranchAsync(string? repositoryPath, string? activ if (!(dialog.ViewModel.AdditionalData as bool? ?? false)) return; - IsExecutingGitAction = true; + _implementation.IsExecutingGitAction = true; await DoGitOperationAsync(() => { @@ -253,10 +251,9 @@ await DoGitOperationAsync(() => return GitOperationResult.Success; }); - IsExecutingGitAction = false; + _implementation.IsExecutingGitAction = false; } - public static bool ValidateBranchNameForRepository(string branchName, string repositoryPath) { if (string.IsNullOrEmpty(branchName) || !IsRepoValid(repositoryPath)) @@ -271,72 +268,6 @@ public static bool ValidateBranchNameForRepository(string branchName, string rep branch.FriendlyName.Equals(branchName, StringComparison.OrdinalIgnoreCase)); } - public static async void FetchOrigin(string? repositoryPath, CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(repositoryPath)) - return; - - using var repository = new Repository(repositoryPath); - var signature = repository.Config.BuildSignature(DateTimeOffset.Now); - - var token = CredentialsHelpers.GetPassword(GIT_RESOURCE_NAME, GIT_RESOURCE_USERNAME); - if (signature is not null && !string.IsNullOrWhiteSpace(token)) - { - _fetchOptions.CredentialsProvider = (url, user, cred) - => new UsernamePasswordCredentials - { - Username = signature.Name, - Password = token - }; - } - - MainWindow.Instance.DispatcherQueue.TryEnqueue(() => - { - IsExecutingGitAction = true; - }); - - await DoGitOperationAsync(() => - { - cancellationToken.ThrowIfCancellationRequested(); - - var result = GitOperationResult.Success; - try - { - foreach (var remote in repository.Network.Remotes) - { - cancellationToken.ThrowIfCancellationRequested(); - - LibGit2Sharp.Commands.Fetch( - repository, - remote.Name, - remote.FetchRefSpecs.Select(rs => rs.Specification), - _fetchOptions, - "git fetch updated a ref"); - } - - cancellationToken.ThrowIfCancellationRequested(); - } - catch (Exception ex) - { - result = IsAuthorizationException(ex) - ? GitOperationResult.AuthorizationError - : GitOperationResult.GenericError; - } - - return result; - }); - - MainWindow.Instance.DispatcherQueue.TryEnqueue(() => - { - if (cancellationToken.IsCancellationRequested) - // Do nothing because the operation was cancelled and another fetch may be in progress - return; - - IsExecutingGitAction = false; - GitFetchCompleted?.Invoke(null, EventArgs.Empty); - }); - } - public static async Task PullOriginAsync(string? repositoryPath) { if (string.IsNullOrWhiteSpace(repositoryPath)) @@ -361,7 +292,7 @@ public static async Task PullOriginAsync(string? repositoryPath) MainWindow.Instance.DispatcherQueue.TryEnqueue(() => { - IsExecutingGitAction = true; + _implementation.IsExecutingGitAction = true; }); var result = await DoGitOperationAsync(() => @@ -402,7 +333,7 @@ public static async Task PullOriginAsync(string? repositoryPath) MainWindow.Instance.DispatcherQueue.TryEnqueue(() => { - IsExecutingGitAction = false; + _implementation.IsExecutingGitAction = false; }); } @@ -435,7 +366,7 @@ public static async Task PushToOriginAsync(string? repositoryPath, string? branc MainWindow.Instance.DispatcherQueue.TryEnqueue(() => { - IsExecutingGitAction = true; + _implementation.IsExecutingGitAction = true; }); try @@ -476,7 +407,7 @@ public static async Task PushToOriginAsync(string? repositoryPath, string? branc MainWindow.Instance.DispatcherQueue.TryEnqueue(() => { - IsExecutingGitAction = false; + _implementation.IsExecutingGitAction = false; }); }