diff --git a/Directory.Packages.props b/Directory.Packages.props
index 55b9e6c2c..dc6add049 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -13,6 +13,7 @@
+
diff --git a/src/Fallout.Build/Fallout.Build.csproj b/src/Fallout.Build/Fallout.Build.csproj
index ea2b968c3..63df976ba 100644
--- a/src/Fallout.Build/Fallout.Build.csproj
+++ b/src/Fallout.Build/Fallout.Build.csproj
@@ -18,6 +18,7 @@
+
diff --git a/src/Fallout.Build/VCS/GitRepository.cs b/src/Fallout.Build/VCS/GitRepository.cs
index b97e2c3d5..d0405cf21 100644
--- a/src/Fallout.Build/VCS/GitRepository.cs
+++ b/src/Fallout.Build/VCS/GitRepository.cs
@@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Text.RegularExpressions;
using Fallout.Common.CI;
using Fallout.Common.IO;
using Fallout.Common.Utilities;
+using LibGit2Sharp;
namespace Fallout.Common.Git;
@@ -40,14 +40,15 @@ public static GitRepository FromUrl(string url, string branch = null)
public static GitRepository FromLocalDirectory(AbsolutePath directory)
{
var rootDirectory = directory.FindParentOrSelf(x => x.ContainsDirectory(".git")).NotNull($"No parent Git directory for '{directory}'");
- var gitDirectory = rootDirectory / ".git";
- var head = GetHead(gitDirectory);
- var branch = (GetBranchFromCI() ?? GetHeadIfAttached(head))?.TrimStart("refs/heads/").TrimStart("origin/");
- var commit = GetCommitFromCI() ?? GetCommitFromHead(gitDirectory, head);
- var tags = GetTagsFromCommit(gitDirectory, commit);
- var (remoteName, remoteBranch) = GetRemoteNameAndBranch(gitDirectory, branch);
- var (protocol, endpoint, identifier) = GetRemoteConnectionFromConfig(gitDirectory, remoteName ?? FallbackRemoteName);
+ using var repository = new Repository(rootDirectory);
+
+ var head = GetHead(repository);
+ var branch = (GetBranchFromCI() ?? GetHeadBranch(repository))?.TrimStart("refs/heads/").TrimStart("origin/");
+ var commit = GetCommitFromCI() ?? repository.Head.Tip?.Sha;
+ var tags = GetTagsFromCommit(repository, commit);
+ var (remoteName, remoteBranch) = GetRemoteNameAndBranch(repository, branch);
+ var (protocol, endpoint, identifier) = GetRemoteConnectionFromConfig(repository, remoteName ?? FallbackRemoteName);
return new GitRepository(
protocol,
@@ -62,54 +63,32 @@ public static GitRepository FromLocalDirectory(AbsolutePath directory)
remoteBranch);
}
- private static (string Name, string Branch) GetRemoteNameAndBranch(AbsolutePath gitDirectory, string branch)
+ private static string GetHead(Repository repository)
{
- if (branch == null)
- return (null, null);
-
- var configFile = gitDirectory / "config";
- var configFileContent = configFile.ReadAllLines();
- var data = configFileContent
- .Select(x => x.Trim())
- .SkipWhile(x => x != $"[branch {branch.DoubleQuote()}]")
- .Skip(1)
- .TakeWhile(x => !x.StartsWith("["))
- .Select(x => x.Split('='))
- .ToDictionary(x => x.ElementAt(0).Trim(), x => x.ElementAt(1).Trim());
- return data.TryGetValue("remote", out var remote) && data.TryGetValue("merge", out var merge)
- ? (remote, merge.TrimStart("refs/heads/"))
- : (null, null);
+ // Mirrors the value previously read from .git/HEAD: the symbolic ref for an
+ // attached head (refs/heads/), or the commit sha for a detached head.
+ var head = repository.Head;
+ return head.Reference is SymbolicReference symbolic
+ ? symbolic.TargetIdentifier
+ : head.Tip?.Sha;
}
- internal static string GetHeadIfAttached(string head)
+ private static string GetHeadBranch(Repository repository)
{
- return head.StartsWith("refs/heads/") ? head : null;
+ var head = repository.Head;
+ return head.Reference is SymbolicReference symbolic ? symbolic.TargetIdentifier : null;
}
- internal static string GetCommitFromHead(AbsolutePath gitDirectory, string head)
+ private static (string Name, string Branch) GetRemoteNameAndBranch(Repository repository, string branch)
{
- if (!head.StartsWith("refs/heads/"))
- return head;
-
- var headRefFile = gitDirectory / head;
- if (headRefFile.Exists())
- return headRefFile.ReadAllLines().First();
-
- var commit = GetPackedRefs(gitDirectory)
- .Where(x => x.Reference == head)
- .Select(x => x.Commit)
- .FirstOrDefault();
-
- commit.NotNull("Could not find commit information");
+ if (branch == null)
+ return (null, null);
- return commit;
- }
+ var trackedBranch = repository.Branches[branch]?.TrackedBranch;
+ if (trackedBranch == null || !trackedBranch.IsRemote)
+ return (null, null);
- private static string GetHead(AbsolutePath gitDirectory)
- {
- var headFile = gitDirectory / "HEAD";
- Assert.FileExists(headFile);
- return headFile.ReadAllText().TrimStart("ref: ").Trim();
+ return (trackedBranch.RemoteName, trackedBranch.UpstreamBranchCanonicalName?.TrimStart("refs/heads/"));
}
internal static string GetBranchFromCI()
@@ -122,68 +101,63 @@ internal static string GetCommitFromCI()
return (Host.Instance as IBuildServer)?.Commit;
}
- private static IReadOnlyCollection GetTagsFromCommit(AbsolutePath gitDirectory, string commit)
+ private static IReadOnlyCollection GetTagsFromCommit(Repository repository, string commit)
{
if (commit == null)
return Array.Empty();
- var packedTags = GetPackedRefs(gitDirectory)
- .Where(x => x.Commit == commit && x.Reference.StartsWithOrdinalIgnoreCase("refs/tags"))
- .Select(x => x.Reference.TrimStart("refs/tags/"));
-
- var tagsDirectory = gitDirectory / "refs" / "tags";
- var localTags = tagsDirectory
- .GlobFiles("**/*")
- .Where(x => x.ReadAllText().Trim() == commit)
- .Select(x => tagsDirectory.GetUnixRelativePathTo(x).ToString());
-
- return localTags.Concat(packedTags).ToList();
+ return repository.Tags
+ .Where(x => x.Target.Sha == commit)
+ .Select(x => x.FriendlyName)
+ .ToList();
}
- private static IEnumerable<(string Commit, string Reference)> GetPackedRefs(AbsolutePath gitDirectory)
+ private static (GitProtocol? Protocol, string Endpoint, string Identifier) GetRemoteConnectionFromConfig(
+ Repository repository,
+ string remote)
{
- var packedRefsFile = gitDirectory / "packed-refs";
- if (!packedRefsFile.Exists())
- return Array.Empty<(string Commit, string Reference)>();
-
- return packedRefsFile.ReadAllLines()
- .Where(x => !x.StartsWith("#") && !x.StartsWith("^"))
- .Select(x => x.Split(' '))
- .Select(x => (Commit: x[0], Reference: x[1]));
+ var url = repository.Network.Remotes[remote]?.Url;
+ return url == null
+ ? (null, null, null)
+ : GetRemoteConnectionFromUrl(url);
}
private static (GitProtocol Protocol, string Endpoint, string Identifier) GetRemoteConnectionFromUrl(string url)
{
- var regex = new Regex(
- @"^(?'protocol'\w+)?(\:\/\/)?(?>(?'user'.*)@)?(?'endpoint'[^\/:]+)(?>\:(?'port'\d+))?[\/:](?'identifier'.*?)\/?(?>\.git)?$");
- var match = regex.Match(url.NotNull().Trim());
-
- Assert.True(match.Success, $"Url '{url}' could not be parsed.");
- var protocol = match.Groups["protocol"].Value.EqualsOrdinalIgnoreCase(GitProtocol.Https.ToString())
- ? GitProtocol.Https
- : GitProtocol.Ssh;
- return (protocol, match.Groups["endpoint"].Value, match.Groups["identifier"].Value);
+ url = url.NotNull().Trim();
+
+ // Standard schemes (https://, ssh://, git://, http://) parse cleanly via Uri.
+ // SCP-like syntax (git@host:path) is not a valid Uri, so it is handled separately.
+ if (Uri.TryCreate(url, UriKind.Absolute, out var uri) && uri.HostNameType != UriHostNameType.Unknown)
+ {
+ var protocol = uri.Scheme.EqualsOrdinalIgnoreCase(Uri.UriSchemeHttps)
+ ? GitProtocol.Https
+ : GitProtocol.Ssh;
+ return (protocol, uri.Host, NormalizeIdentifier(uri.AbsolutePath));
+ }
+
+ return ParseScpLikeUrl(url);
}
- private static (GitProtocol? Protocol, string Endpoint, string Identifier) GetRemoteConnectionFromConfig(
- AbsolutePath gitDirectory,
- string remote)
+ private static (GitProtocol Protocol, string Endpoint, string Identifier) ParseScpLikeUrl(string url)
+ {
+ // Forms: [user@]host:path or [user@]host/path (no scheme prefix).
+ var withoutUser = url.Contains('@') ? url.Substring(url.IndexOf('@') + 1) : url;
+
+ var separatorIndex = withoutUser.IndexOfAny(new[] { ':', '/' });
+ Assert.True(separatorIndex > 0, $"Url '{url}' could not be parsed.");
+
+ var endpoint = withoutUser.Substring(0, separatorIndex);
+ var path = withoutUser.Substring(separatorIndex + 1);
+
+ return (GitProtocol.Ssh, endpoint, NormalizeIdentifier(path));
+ }
+
+ private static string NormalizeIdentifier(string path)
{
- var configFile = gitDirectory / "config";
- var configFileContent = configFile.ReadAllLines();
- var url = configFileContent
- .Select(x => x.Trim())
- .SkipWhile(x => x != $"[remote {remote.DoubleQuote()}]")
- .Skip(1)
- .TakeWhile(x => !x.StartsWith("["))
- .SingleOrDefault(x => x.StartsWithOrdinalIgnoreCase("url = "))
- ?.Split('=').ElementAt(1)
- .Trim();
-
- if (url == null)
- return (null, null, null);
-
- return GetRemoteConnectionFromUrl(url);
+ return path
+ .Trim('/')
+ .TrimEnd(".git");
}
public GitRepository(
diff --git a/tests/Fallout.Build.Tests/GitRepositoryTest.cs b/tests/Fallout.Build.Tests/GitRepositoryTest.cs
index 6fa952f64..119c4e7db 100644
--- a/tests/Fallout.Build.Tests/GitRepositoryTest.cs
+++ b/tests/Fallout.Build.Tests/GitRepositoryTest.cs
@@ -27,6 +27,10 @@ public class GitRepositoryTest
[InlineData("https://git.test.org:1234/test/test", "git.test.org", "test/test")]
[InlineData("git://git.test.org:1234/test/test", "git.test.org", "test/test")]
[InlineData("git://git.test.org/test/test", "git.test.org", "test/test")]
+ [InlineData("ssh://git@github.com:22/nuke-build/nuke.git", "github.com", "nuke-build/nuke")]
+ [InlineData("http://git.test.org/test/test.git", "git.test.org", "test/test")]
+ [InlineData("file://server/share/repo.git", "server", "share/repo")]
+ [InlineData(@"\\server\share\repo.git", "server", "share/repo")]
public void FromUrlTest(string url, string endpoint, string identifier)
{
var repository = GitRepository.FromUrl(url);