Git Clone Progress

This commit is contained in:
Canon
2026-02-04 11:56:23 +08:00
parent 0123b2104d
commit cb19220937
4 changed files with 108 additions and 89 deletions
+52 -3
View File
@@ -1,11 +1,15 @@
using System.Text.RegularExpressions;
using CliWrap;
using CliWrap.Buffered;
using FCPModUpdater.Models;
namespace FCPModUpdater.Services;
public class GitService : IGitService
public partial class GitService : IGitService
{
// Matches git progress output like: "Receiving objects: 45% (555/1234), 12.34 MiB | 1.23 MiB/s"
[GeneratedRegex(@"(\d+)%\s*\((\d+)/(\d+)\)", RegexOptions.Compiled)]
private static partial Regex GitProgressRegex();
public async Task<bool> IsGitRepositoryAsync(string path, CancellationToken ct = default)
{
var (exitCode, _, _) = await RunGitCommandAsync(path, "rev-parse --is-inside-work-tree", ct);
@@ -126,14 +130,18 @@ public class GitService : IGitService
}
public async Task<bool> CloneAsync(string url, string targetPath, IProgress<string>? progress = null,
CancellationToken ct = default)
IProgress<double>? percentProgress = null, CancellationToken ct = default)
{
progress?.Report($"Cloning {url}...");
var parentDir = Path.GetDirectoryName(targetPath) ?? ".";
var folderName = Path.GetFileName(targetPath);
var (exitCode, _, error) = await RunGitCommandAsync(parentDir, $"clone \"{url}\" \"{folderName}\"", ct,
var (exitCode, error) = await RunGitCommandWithProgressAsync(
parentDir,
$"clone --progress \"{url}\" \"{folderName}\"",
percentProgress,
ct,
timeoutMs: 300000); // 5 minute timeout for clone
if (exitCode != 0)
@@ -168,6 +176,47 @@ public class GitService : IGitService
return exitCode == 0 && !string.IsNullOrWhiteSpace(output);
}
private static async Task<(int ExitCode, string Error)> RunGitCommandWithProgressAsync(
string workingDirectory, string arguments, IProgress<double>? percentProgress,
CancellationToken ct, int timeoutMs = 30000)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(timeoutMs);
var errorLines = new List<string>();
try
{
var result = await Cli.Wrap("git")
.WithArguments(arguments)
.WithWorkingDirectory(workingDirectory)
.WithEnvironmentVariables(env => env
.Set("GIT_TERMINAL_PROMPT", "0"))
.WithValidation(CommandResultValidation.None)
.WithStandardOutputPipe(PipeTarget.Null)
.WithStandardErrorPipe(PipeTarget.ToDelegate(line =>
{
errorLines.Add(line);
if (percentProgress == null)
return;
var match = GitProgressRegex().Match(line);
if (match.Success && int.TryParse(match.Groups[1].Value, out var percent))
{
percentProgress.Report(percent);
}
}))
.ExecuteAsync(cts.Token);
return (result.ExitCode, string.Join(Environment.NewLine, errorLines));
}
catch (OperationCanceledException)
{
return (ExitCode: -1, Error: "Operation timed out or was cancelled");
}
}
private static IReadOnlyList<GitCommitInfo> ParseCommitLog(string output)
{
var commits = new List<GitCommitInfo>();
+1 -1
View File
@@ -12,7 +12,7 @@ public interface IGitService
Task<IReadOnlyList<GitCommitInfo>> GetIncomingCommitsAsync(string path, int limit = 10, CancellationToken ct = default);
Task<bool> FetchAsync(string path, IProgress<string>? progress = null, CancellationToken ct = default);
Task<bool> PullAsync(string path, IProgress<string>? progress = null, CancellationToken ct = default);
Task<bool> CloneAsync(string url, string targetPath, IProgress<string>? progress = null, CancellationToken ct = default);
Task<bool> CloneAsync(string url, string targetPath, IProgress<string>? progress = null, IProgress<double>? percentProgress = null, CancellationToken ct = default);
Task<IReadOnlyList<string>> GetRemoteBranchesAsync(string path, CancellationToken ct = default);
Task<bool> CheckoutAsync(string path, string branchOrCommit, CancellationToken ct = default);
Task<bool> HasLocalChangesAsync(string path, CancellationToken ct = default);