diff --git a/Directory.Packages.props b/Directory.Packages.props
index 4f0b23586d00..495d82dff260 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -21,8 +21,7 @@
-
-
+
diff --git a/src/Files.App/Actions/Git/GitFetchAction.cs b/src/Files.App/Actions/Git/GitFetchAction.cs
index 26e84936f9d8..bd5260948c94 100644
--- a/src/Files.App/Actions/Git/GitFetchAction.cs
+++ b/src/Files.App/Actions/Git/GitFetchAction.cs
@@ -24,11 +24,9 @@ public GitFetchAction()
_context.PropertyChanged += Context_PropertyChanged;
}
- public Task ExecuteAsync(object? parameter = null)
+ public async Task ExecuteAsync(object? parameter = null)
{
- GitHelpers.FetchOrigin(_context.ShellPage!.InstanceViewModel.GitRepositoryPath);
-
- return Task.CompletedTask;
+ await GitHelpers.FetchOrigin(_context.ShellPage!.InstanceViewModel.GitRepositoryPath);
}
private void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e)
diff --git a/src/Files.App/Data/Enums/FileOperationType.cs b/src/Files.App/Data/Enums/FileOperationType.cs
index dfa51b51c514..21d632f383ed 100644
--- a/src/Files.App/Data/Enums/FileOperationType.cs
+++ b/src/Files.App/Data/Enums/FileOperationType.cs
@@ -72,5 +72,20 @@ public enum FileOperationType : byte
/// A font has been installed
///
InstallFont = 13,
+
+ ///
+ /// A git repo has been pushed
+ ///
+ GitPush = 14,
+
+ ///
+ /// A git repo has been fetched
+ ///
+ GitFetch = 15,
+
+ ///
+ /// A git repo has been pulled
+ ///
+ GitPull = 16,
}
}
diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw
index 1de736ba80bb..c27ca00e9012 100644
--- a/src/Files.App/Strings/en-US/Resources.resw
+++ b/src/Files.App/Strings/en-US/Resources.resw
@@ -3738,6 +3738,105 @@
Failed to empty Recycle Bin
Shown in a StatusCenter card.
+
+ Canceled pushing to "{0}"
+ Shown in a StatusCenter card.
+
+
+ Canceled pushing branch "{0}" to "{1}"
+ Shown in a StatusCenter card.
+
+
+ Pushed to "{0}"
+ Shown in a StatusCenter card.
+
+
+ Pushed branch "{0}" to "{1}"
+ Shown in a StatusCenter card.
+
+
+ Error pushing to "{0}"
+ Shown in a StatusCenter card.
+
+
+ Failed to push branch "{0}" to "{1}"
+ Shown in a StatusCenter card.
+
+
+ Pushing to "{0}"
+ Shown in a StatusCenter card.
+
+
+ Pushing branch "{0}" to "{1}"
+ Shown in a StatusCenter card.
+
+
+ Canceled fetching from "{0}"
+ Shown in a StatusCenter card.
+
+
+ Canceled fetching from "{0}"
+ Shown in a StatusCenter card.
+
+
+ Fetched from "{0}"
+ Shown in a StatusCenter card.
+
+
+ Fetched from "{0}"
+ Shown in a StatusCenter card.
+
+
+ Error fetching from "{0}"
+ Shown in a StatusCenter card.
+
+
+ Failed to fetch from "{0}"
+ Shown in a StatusCenter card.
+
+
+ Fetching from "{0}"
+ Shown in a StatusCenter card.
+
+
+ Fetching from "{0}"
+ Shown in a StatusCenter card.
+
+
+ Canceled pulling from "{0}"
+ Shown in a StatusCenter card.
+
+
+ Canceled pulling branch "{0}" from "{1}"
+ Shown in a StatusCenter card.
+
+
+ Pulled from "{0}"
+ Shown in a StatusCenter card.
+
+
+ Pulled branch "{0}" from "{1}"
+ Shown in a StatusCenter card.
+
+
+ Error pulling from "{0}"
+ Shown in a StatusCenter card.
+
+
+ We couldn't pull the latest changes from the remote right now.
+
+
+ Please commit or stash your changes before pulling.
+ Shown in a StatusCenter card.
+
+
+ Pulling from "{0}"
+ Shown in a StatusCenter card.
+
+
+ Pulling branch "{0}" from "{1}"
+ Shown in a StatusCenter card.
+
Preparing the operation...
Shown in a StatusCenter card.
diff --git a/src/Files.App/Utils/Git/GitHelpers.cs b/src/Files.App/Utils/Git/GitHelpers.cs
index e49032fa77fc..1ab18f33ed1d 100644
--- a/src/Files.App/Utils/Git/GitHelpers.cs
+++ b/src/Files.App/Utils/Git/GitHelpers.cs
@@ -29,11 +29,6 @@ internal static partial class GitHelpers
private static readonly IDialogService _dialogService = Ioc.Default.GetRequiredService();
- private static readonly FetchOptions _fetchOptions = new()
- {
- Prune = true
- };
-
private static readonly PullOptions _pullOptions = new();
private static readonly string _clientId = AppLifecycleHelper.AppEnvironment is AppEnvironment.Dev
@@ -355,7 +350,7 @@ public static bool ValidateBranchNameForRepository(string branchName, string rep
branch.FriendlyName.Equals(branchName, StringComparison.OrdinalIgnoreCase));
}
- public static async void FetchOrigin(string? repositoryPath, CancellationToken cancellationToken = default)
+ public static async Task FetchOrigin(string? repositoryPath, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(repositoryPath))
return;
@@ -363,57 +358,101 @@ public static async void FetchOrigin(string? repositoryPath, CancellationToken c
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))
+ var remoteName = repository.Network.Remotes.FirstOrDefault()?.Name ?? "origin";
+
+ // Add Status Center Card
+ var banner = StatusCenterHelper.AddCard_GitFetch(remoteName, ReturnResult.InProgress);
+ var fsProgress = new StatusCenterItemProgressModel(banner.ProgressEventSource, enumerationCompleted: true, FileSystemStatusCode.InProgress);
+
+ // Link CancellationTokens
+ using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, banner.CancellationToken);
+ var token = linkedCts.Token;
+
+ var fetchOptions = new FetchOptions
{
- _fetchOptions.CredentialsProvider = (url, user, cred)
- => new UsernamePasswordCredentials
+ CredentialsProvider = CredentialsHandler,
+ OnTransferProgress = (progress) =>
+ {
+ if (token.IsCancellationRequested)
+ return false;
+
+ // Ensure StatusCenter updates are dispatched to the UI thread
+ MainWindow.Instance.DispatcherQueue.TryEnqueue(() =>
{
- Username = signature.Name,
- Password = token
- };
- }
+ if (progress.TotalObjects > 0)
+ {
+ fsProgress.ItemsCount = progress.TotalObjects;
+ fsProgress.SetProcessedSize(progress.ReceivedBytes);
+ fsProgress.AddProcessedItemsCount(progress.ReceivedObjects);
+ fsProgress.Report((int)((progress.ReceivedObjects / (double)progress.TotalObjects) * 100));
+ }
+ });
+ return true;
+ },
+ Prune = true // Restore Prune behavior as requested in PR review
+ };
MainWindow.Instance.DispatcherQueue.TryEnqueue(() =>
{
IsExecutingGitAction = true;
});
- await DoGitOperationAsync(() =>
+ var result = await DoGitOperationAsync(() =>
{
- cancellationToken.ThrowIfCancellationRequested();
-
- var result = GitOperationResult.Success;
try
{
+ token.ThrowIfCancellationRequested();
+
+ // Iterate remotes (though usually only one matters for progress, we'll fetch all)
foreach (var remote in repository.Network.Remotes)
{
- cancellationToken.ThrowIfCancellationRequested();
+ token.ThrowIfCancellationRequested();
LibGit2Sharp.Commands.Fetch(
repository,
remote.Name,
remote.FetchRefSpecs.Select(rs => rs.Specification),
- _fetchOptions,
+ fetchOptions,
"git fetch updated a ref");
}
- cancellationToken.ThrowIfCancellationRequested();
+ token.ThrowIfCancellationRequested();
+ return GitOperationResult.Success;
+ }
+ catch (LibGit2SharpException e)
+ {
+ MainWindow.Instance.DispatcherQueue.TryEnqueue(async () =>
+ {
+ var dialog = new DynamicDialog(new DynamicDialogViewModel
+ {
+ TitleText = Strings.GitError.GetLocalizedResource(),
+ SubtitleText = e.Message,
+ CloseButtonText = Strings.Close.GetLocalizedResource(),
+ DynamicButtons = DynamicDialogButtons.Cancel
+ });
+ await dialog.TryShowAsync();
+ });
+ return GitOperationResult.GenericError;
}
catch (Exception ex)
{
- result = IsAuthorizationException(ex)
+ return IsAuthorizationException(ex)
? GitOperationResult.AuthorizationError
: GitOperationResult.GenericError;
}
+ });
- return result;
+ MainWindow.Instance.DispatcherQueue.TryEnqueue(() =>
+ {
+ StatusCenterViewModel.RemoveItem(banner);
+ StatusCenterHelper.AddCard_GitFetch(
+ remoteName,
+ result == GitOperationResult.Success ? ReturnResult.Success : ReturnResult.Failed);
});
MainWindow.Instance.DispatcherQueue.TryEnqueue(() =>
{
- if (cancellationToken.IsCancellationRequested)
- // Do nothing because the operation was cancelled and another fetch may be in progress
+ if (token.IsCancellationRequested)
return;
IsExecutingGitAction = false;
@@ -431,17 +470,40 @@ public static async Task PullOriginAsync(string? repositoryPath)
if (signature is null)
return;
- var token = CredentialsHelpers.GetPassword(GIT_RESOURCE_NAME, GIT_RESOURCE_USERNAME);
- if (!string.IsNullOrWhiteSpace(token))
+ var branchName = repository.Head.FriendlyName;
+ // We use 'origin' as generic remote name for display, though pull might use upstream
+ var remoteName = "origin";
+
+ // Add Status Center Card
+ var banner = StatusCenterHelper.AddCard_GitPull(remoteName, branchName, ReturnResult.InProgress);
+ var fsProgress = new StatusCenterItemProgressModel(banner.ProgressEventSource, enumerationCompleted: true, FileSystemStatusCode.InProgress);
+ var token = banner.CancellationToken;
+
+ var pullOptions = new PullOptions
{
- _pullOptions.FetchOptions ??= _fetchOptions;
- _pullOptions.FetchOptions.CredentialsProvider = (url, user, cred)
- => new UsernamePasswordCredentials
+ FetchOptions = new FetchOptions
+ {
+ CredentialsProvider = CredentialsHandler,
+ OnTransferProgress = (progress) =>
{
- Username = signature.Name,
- Password = token
- };
- }
+ if (token.IsCancellationRequested)
+ return false;
+
+ // Ensure StatusCenter updates are dispatched to the UI thread
+ MainWindow.Instance.DispatcherQueue.TryEnqueue(() =>
+ {
+ if (progress.TotalObjects > 0)
+ {
+ fsProgress.ItemsCount = progress.TotalObjects;
+ fsProgress.SetProcessedSize(progress.ReceivedBytes);
+ fsProgress.AddProcessedItemsCount(progress.ReceivedObjects);
+ fsProgress.Report((int)((progress.ReceivedObjects / (double)progress.TotalObjects) * 100));
+ }
+ });
+ return true;
+ }
+ }
+ };
MainWindow.Instance.DispatcherQueue.TryEnqueue(() =>
{
@@ -452,10 +514,41 @@ public static async Task PullOriginAsync(string? repositoryPath)
{
try
{
+ // LibGit2Sharp Pull doesn't take CancellationToken explicitly in all overloads, rely on callbacks
LibGit2Sharp.Commands.Pull(
repository,
signature,
- _pullOptions);
+ pullOptions);
+ }
+ catch (CheckoutConflictException)
+ {
+ MainWindow.Instance.DispatcherQueue.TryEnqueue(async () =>
+ {
+ var dialog = new DynamicDialog(new DynamicDialogViewModel
+ {
+ TitleText = Strings.GitError.GetLocalizedResource(),
+ SubtitleText = Strings.GitError_UncommittedChanges.GetLocalizedResource(),
+ CloseButtonText = Strings.Close.GetLocalizedResource(),
+ DynamicButtons = DynamicDialogButtons.Cancel
+ });
+ await dialog.TryShowAsync();
+ });
+ return GitOperationResult.GenericError;
+ }
+ catch (LibGit2SharpException e)
+ {
+ MainWindow.Instance.DispatcherQueue.TryEnqueue(async () =>
+ {
+ var dialog = new DynamicDialog(new DynamicDialogViewModel
+ {
+ TitleText = Strings.GitError.GetLocalizedResource(),
+ SubtitleText = e.Message,
+ CloseButtonText = Strings.Close.GetLocalizedResource(),
+ DynamicButtons = DynamicDialogButtons.Cancel
+ });
+ await dialog.TryShowAsync();
+ });
+ return GitOperationResult.GenericError;
}
catch (Exception ex)
{
@@ -467,6 +560,12 @@ public static async Task PullOriginAsync(string? repositoryPath)
return GitOperationResult.Success;
});
+ MainWindow.Instance.DispatcherQueue.TryEnqueue(() =>
+ {
+ StatusCenterViewModel.RemoveItem(banner);
+ StatusCenterHelper.AddCard_GitPull(remoteName, branchName, result == GitOperationResult.Success ? ReturnResult.Success : ReturnResult.Failed);
+ });
+
if (result is GitOperationResult.AuthorizationError)
{
await RequireGitAuthenticationAsync();
@@ -487,6 +586,10 @@ public static async Task PullOriginAsync(string? repositoryPath)
MainWindow.Instance.DispatcherQueue.TryEnqueue(() =>
{
IsExecutingGitAction = false;
+ if (result == GitOperationResult.Success)
+ {
+ GitFetchCompleted?.Invoke(null, EventArgs.Empty);
+ }
});
}
@@ -500,21 +603,31 @@ public static async Task PushToOriginAsync(string? repositoryPath, string? branc
if (signature is null)
return;
- var token = CredentialsHelpers.GetPassword(GIT_RESOURCE_NAME, GIT_RESOURCE_USERNAME);
- if (string.IsNullOrWhiteSpace(token))
+ // Add Status Center Card
+ var banner = StatusCenterHelper.AddCard_GitPush("origin", branchName, ReturnResult.InProgress);
+ var fsProgress = new StatusCenterItemProgressModel(banner.ProgressEventSource, enumerationCompleted: true, FileSystemStatusCode.InProgress);
+ banner.CancellationToken.Register(() =>
{
- await RequireGitAuthenticationAsync();
- token = CredentialsHelpers.GetPassword(GIT_RESOURCE_NAME, GIT_RESOURCE_USERNAME);
- }
+ // Handle cancellation if needed, though LibGit2Sharp might not support seamless cancellation mid-push easily without callback
+ });
var options = new PushOptions()
{
- CredentialsProvider = (url, user, cred)
- => new UsernamePasswordCredentials
+ CredentialsProvider = CredentialsHandler,
+ OnPushTransferProgress = (current, total, bytes) =>
+ {
+ if (banner.CancellationToken.IsCancellationRequested)
+ return false; // Cancel
+
+ if (total > 0)
{
- Username = signature.Name,
- Password = token
+ fsProgress.ItemsCount = total;
+ fsProgress.SetProcessedSize(bytes);
+ fsProgress.AddProcessedItemsCount(1); // Not accurate but shows activity
+ fsProgress.Report((int)((current / (double)total) * 100));
}
+ return true;
+ }
};
MainWindow.Instance.DispatcherQueue.TryEnqueue(() =>
@@ -522,6 +635,7 @@ public static async Task PushToOriginAsync(string? repositoryPath, string? branc
IsExecutingGitAction = true;
});
+ GitOperationResult result = GitOperationResult.GenericError;
try
{
var branch = repository.Branches[branchName];
@@ -534,10 +648,13 @@ public static async Task PushToOriginAsync(string? repositoryPath, string? branc
b => b.UpstreamBranch = branch.CanonicalName);
}
- var result = await DoGitOperationAsync(() =>
+ result = await DoGitOperationAsync(() =>
{
try
{
+ if (banner.CancellationToken.IsCancellationRequested)
+ return GitOperationResult.GenericError; // Or Cancelled
+
repository.Network.Push(branch, options);
}
catch (Exception ex)
@@ -558,9 +675,22 @@ public static async Task PushToOriginAsync(string? repositoryPath, string? branc
_logger.LogWarning(ex.Message);
}
+ // Remove InProgress Card
+ StatusCenterViewModel.RemoveItem(banner);
+
+ // Add Result Card
+ StatusCenterHelper.AddCard_GitPush(
+ "origin",
+ branchName,
+ result == GitOperationResult.Success ? ReturnResult.Success : ReturnResult.Failed);
+
MainWindow.Instance.DispatcherQueue.TryEnqueue(() =>
{
IsExecutingGitAction = false;
+ if (result == GitOperationResult.Success)
+ {
+ GitFetchCompleted?.Invoke(null, EventArgs.Empty);
+ }
});
}
@@ -858,25 +988,29 @@ private static bool IsAuthorizationException(Exception ex)
{
return
ex.Message.Contains("status code: 401", StringComparison.OrdinalIgnoreCase) ||
- ex.Message.Contains("authentication replays", StringComparison.OrdinalIgnoreCase);
+ ex.Message.Contains("authentication replays", StringComparison.OrdinalIgnoreCase) ||
+ ex.Message.Contains("authentication failed", StringComparison.OrdinalIgnoreCase) ||
+ ex.Message.Contains("user cancelled", StringComparison.OrdinalIgnoreCase);
}
private static async Task DoGitOperationAsync(Func