diff --git a/src/Verlite.Core/Command.cs b/src/Verlite.Core/Command.cs index cc0e9a1..6d1a2b0 100644 --- a/src/Verlite.Core/Command.cs +++ b/src/Verlite.Core/Command.cs @@ -7,7 +7,44 @@ namespace Verlite { /// - /// The command class + /// An interface to run commands. + /// + public interface ICommandRunner + { + /// + /// Asynchronously execute a command. + /// + /// The working directory in which to start the executable. + /// The command to execute. + /// Arguments to pass to the command. + /// The enviornment variables to start the process with. + /// Thrown if the process returns a non-zero exit code. + /// A task that completes upon the process exiting, containing the standard out and error streams. + Task<(string stdout, string stderr)> Run( + string directory, + string command, + string[] args, + IDictionary? envVars = null); + } + + /// + /// Run commands using + /// + public class SystemCommandRunner : ICommandRunner + { + /// + public async Task<(string stdout, string stderr)> Run( + string directory, + string command, + string[] args, + IDictionary? envVars = null) + { + return await Command.Run(directory, command, args, envVars); + } + } + + /// + /// A class for executing commands. /// public static class Command { diff --git a/src/Verlite.Core/GitRepoInspector.cs b/src/Verlite.Core/GitRepoInspector.cs index f5cbd41..4549349 100644 --- a/src/Verlite.Core/GitRepoInspector.cs +++ b/src/Verlite.Core/GitRepoInspector.cs @@ -59,14 +59,20 @@ public sealed class GitRepoInspector : IRepoInspector /// /// The path of the Git repository. /// A logger for diagnostics. + /// A command runner to use. Defaults to if null is given. /// Thrown if the path is not a Git repository. /// A task containing the Git repo inspector. - public static async Task FromPath(string path, ILogger? log = null) + public static async Task FromPath(string path, ILogger? log = null, ICommandRunner? commandRunner = null) { + commandRunner ??= new SystemCommandRunner(); + try { - var (root, _) = await Command.Run(path, "git", new string[] { "rev-parse", "--show-toplevel" }); - var ret = new GitRepoInspector(root, log); + var (root, _) = await commandRunner.Run(path, "git", new string[] { "rev-parse", "--show-toplevel" }); + var ret = new GitRepoInspector( + root, + log, + commandRunner); await ret.CacheParents(); return ret; } @@ -77,6 +83,7 @@ public static async Task FromPath(string path, ILogger? log = } private ILogger? Log { get; } + private ICommandRunner CommandRunner { get; } /// /// Can the Git repository be deepened to fetch commits not in the local repository. /// @@ -88,13 +95,14 @@ public static async Task FromPath(string path, ILogger? log = private Dictionary CachedParents { get; } = new(); private (int depth, bool shallow, Commit deepest)? FetchDepth { get; set; } - private GitRepoInspector(string root, ILogger? log) + private GitRepoInspector(string root, ILogger? log, ICommandRunner commandRunner) { Root = root; Log = log; + CommandRunner = commandRunner; } - private Task<(string stdout, string stderr)> Git(params string[] args) => Command.Run(Root, "git", args); + private Task<(string stdout, string stderr)> Git(params string[] args) => CommandRunner.Run(Root, "git", args); /// public async Task GetHead() @@ -161,14 +169,14 @@ private GitRepoInspector(string root, ILogger? log) if (commitObj is null) { Log?.Verbatim($"MeasureDepth() -> (depth: {depth}, shallow: true)"); - return (depth: depth, shallow: true, deepestCommit: deepest); + return (depth, shallow: true, deepestCommit: deepest); } Commit? parent = ParseCommitObjectParent(commitObj); if (parent is null) { Log?.Verbatim($"MeasureDepth() -> (depth: {depth}, shallow: false)"); - return (depth: depth, shallow: false, deepestCommit: current); + return (depth, shallow: false, deepestCommit: current); } depth++; diff --git a/src/Verlite.Core/PublicAPI.Shipped.txt b/src/Verlite.Core/PublicAPI.Shipped.txt index cd593d3..745e140 100644 --- a/src/Verlite.Core/PublicAPI.Shipped.txt +++ b/src/Verlite.Core/PublicAPI.Shipped.txt @@ -13,7 +13,7 @@ override Verlite.TaggedVersion.GetHashCode() -> int static Verlite.Command.Run(string! directory, string! command, string![]! args, System.Collections.Generic.IDictionary? envVars = null) -> System.Threading.Tasks.Task<(string! stdout, string! stderr)>! static Verlite.Commit.operator !=(Verlite.Commit left, Verlite.Commit right) -> bool static Verlite.Commit.operator ==(Verlite.Commit left, Verlite.Commit right) -> bool -static Verlite.GitRepoInspector.FromPath(string! path, Verlite.ILogger? log = null) -> System.Threading.Tasks.Task! +static Verlite.GitRepoInspector.FromPath(string! path, Verlite.ILogger? log = null, Verlite.ICommandRunner? commandRunner = null) -> System.Threading.Tasks.Task! static Verlite.HeightCalculator.FromRepository(Verlite.IRepoInspector! repo, string! tagPrefix, bool queryRemoteTags, Verlite.ILogger? log = null) -> System.Threading.Tasks.Task<(int height, Verlite.TaggedVersion?)>! static Verlite.SemVer.ComparePrerelease(string! left, string! right) -> int static Verlite.SemVer.IsValidIdentifierCharacter(char input) -> bool @@ -59,6 +59,8 @@ Verlite.GitRepoInspector.GetParent(Verlite.Commit commit) -> System.Threading.Ta Verlite.GitRepoInspector.GetTags(Verlite.QueryTarget queryTarget) -> System.Threading.Tasks.Task! Verlite.GitRepoInspector.Root.get -> string! Verlite.HeightCalculator +Verlite.ICommandRunner +Verlite.ICommandRunner.Run(string! directory, string! command, string![]! args, System.Collections.Generic.IDictionary? envVars = null) -> System.Threading.Tasks.Task<(string! stdout, string! stderr)>! Verlite.ILogger Verlite.ILogger.Normal(string! message) -> void Verlite.ILogger.Verbatim(string! message) -> void @@ -94,6 +96,9 @@ Verlite.SemVer.Prerelease.get -> string? Verlite.SemVer.Prerelease.set -> void Verlite.SemVer.SemVer() -> void Verlite.SemVer.SemVer(int major, int minor, int patch, string? prerelease = null, string? buildMetadata = null) -> void +Verlite.SystemCommandRunner +Verlite.SystemCommandRunner.Run(string! directory, string! command, string![]! args, System.Collections.Generic.IDictionary? envVars = null) -> System.Threading.Tasks.Task<(string! stdout, string! stderr)>! +Verlite.SystemCommandRunner.SystemCommandRunner() -> void Verlite.Tag Verlite.Tag.Equals(Verlite.Tag other) -> bool Verlite.Tag.Name.get -> string! diff --git a/tests/UnitTests/GitRepoInspectorTests.cs b/tests/UnitTests/GitRepoInspectorTests.cs index c38bf21..78b6da5 100644 --- a/tests/UnitTests/GitRepoInspectorTests.cs +++ b/tests/UnitTests/GitRepoInspectorTests.cs @@ -53,9 +53,12 @@ public GitTestDirectory() Directory.CreateDirectory(RootPath); } - public Task MakeInspector() + public Task MakeInspector(ICommandRunner? commandRunner = null) { - return GitRepoInspector.FromPath(RootPath); + return GitRepoInspector.FromPath( + path: RootPath, + log: null, + commandRunner); } public void Dispose() @@ -391,5 +394,30 @@ public async Task FetchingTagInDeepCloneDoesNotMakeShallow() var deeperParent = await repo.GetParent(deeperTag.PointsTo); deeperParent.Should().Be(new Commit("b2000fc1f1d2e5f816cfa51a4ad8764048f22f0a")); } + + [Fact] + public async Task ShallowGitFetchFromCommitCanFallBack() + { + await TestRepo.Git("init"); + await TestRepo.Git("commit", "--allow-empty", "-m", "first"); + await TestRepo.Git("tag", "tag-one"); + await TestRepo.Git("commit", "--allow-empty", "-m", "second"); + await TestRepo.Git("tag", "tag-two"); + await TestRepo.Git("commit", "--allow-empty", "-m", "third"); + + using var clone = new GitTestDirectory(); + await clone.Git("clone", TestRepo.RootUri, ".", "--branch", "master", "--depth", "1"); + + var repo = await clone.MakeInspector( + new MockCommandRunnerWithOldRemoteGitVersion( + new SystemCommandRunner())); + + repo.CanDeepen = true; + var head = await repo.GetHead(); + var parent = await repo.GetParent(head.Value); + var parentsParent = await repo.GetParent(parent.Value); + + parentsParent.Should().Be(new Commit("b2000fc1f1d2e5f816cfa51a4ad8764048f22f0a")); + } } } diff --git a/tests/UnitTests/Mocks/MockCommandRunnerWithOldRemoteGitVersion.cs b/tests/UnitTests/Mocks/MockCommandRunnerWithOldRemoteGitVersion.cs new file mode 100644 index 0000000..5309fdf --- /dev/null +++ b/tests/UnitTests/Mocks/MockCommandRunnerWithOldRemoteGitVersion.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Verlite; + +namespace UnitTests +{ + public sealed class MockCommandRunnerWithOldRemoteGitVersion : ICommandRunner + { + private ICommandRunner BaseRunner { get; } + public MockCommandRunnerWithOldRemoteGitVersion(ICommandRunner baseRunner) + { + BaseRunner = baseRunner; + } + + Task<(string stdout, string stderr)> ICommandRunner.Run( + string directory, + string command, + string[] args, + IDictionary? envVars) + { + string? firstArg = args.Length > 0 ? args[0] : null; + + return (command, firstArg, args) switch + { + ("git", "fetch", _) when args.Contains("origin") => + throw new CommandException(128, "", "error: Server does not allow request for unadvertised object a1b2c3"), + _ => BaseRunner.Run(directory, command, args, envVars), + }; + } + } +} diff --git a/tests/UnitTests/Helpers/MockRepoInspector.cs b/tests/UnitTests/Mocks/MockRepoInspector.cs similarity index 100% rename from tests/UnitTests/Helpers/MockRepoInspector.cs rename to tests/UnitTests/Mocks/MockRepoInspector.cs