diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitDataTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitDataTests.cs
index 6eac1920d..921117c1c 100644
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/GitDataTests.cs
+++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitDataTests.cs
@@ -54,7 +54,7 @@ public void MinimalGitData()
Assert.Equal("1111111111111111111111111111111111111111", repository.GetHeadCommitSha());
var warnings = new List<(string, object?[])>();
- var sourceRoots = GitOperations.GetSourceRoots(repository, remoteName: null, warnOnMissingCommit: true, (message, args) => warnings.Add((message, args)));
+ var sourceRoots = GitOperations.GetSourceRoots(repository, remoteName: null, warnOnMissingCommitOrUnsupportedUri: true, (message, args) => warnings.Add((message, args)));
AssertEx.Equal(new[]
{
$@"'{repoDir.Path}{s}' SourceControl='git' RevisionId='1111111111111111111111111111111111111111' ScmRepositoryUrl='http://github.com/test-org/test-repo'",
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs
index 115f5809c..7a9122d2c 100644
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs
+++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs
@@ -192,147 +192,33 @@ public void GetRepositoryUrl_InsteadOf()
Assert.Empty(warnings);
}
- ///
- /// Test scenario where a local repository is cloned from another local repository that was cloned from a remote repository.
- ///
[Theory]
- [InlineData(true)]
- [InlineData(false)]
- public void GetRepositoryUrl_Local(bool useFileUrl)
+ [InlineData("local")]
+ [InlineData("file")]
+ [InlineData("xyz://a/b")]
+ public void GetRepositoryUrl_UnsupportedUrl(string kind)
{
using var temp = new TempRoot();
var dir = temp.CreateDirectory();
- var mainWorkingDir = dir.CreateDirectory("x \u1234");
- var mainGitDir = mainWorkingDir.CreateDirectory(".git");
- mainGitDir.CreateFile("HEAD");
- mainGitDir.CreateFile("config").WriteAllText(@"[remote ""origin""] url = http://github.com/repo");
+ var originRepoPath = dir.CreateDirectory("x \u1234").Path;
- var repo = CreateRepository(config: new GitConfig(ImmutableDictionary.CreateRange(new[]
- {
- KVP(new GitVariableName("remote", "origin", "url"),
- ImmutableArray.Create(useFileUrl ? new Uri(mainWorkingDir.Path).AbsolutePath : mainWorkingDir.Path)),
- })));
-
- var warnings = new List<(string, object?[])>();
- Assert.Equal("http://github.com/repo", GitOperations.GetRepositoryUrl(repo, remoteName: null, logWarning: (message, args) => warnings.Add((message, args))));
- Assert.Empty(warnings);
- }
-
- ///
- /// Test scenario where a local repository is cloned from another local repository that was cloned from a remote repository.
- /// With custom remote name.
- ///
- [Fact]
- public void GetRepositoryUrl_Local_CustomRemoteName()
- {
- using var temp = new TempRoot();
-
- var mainWorkingDir = temp.CreateDirectory();
- var mainGitDir = mainWorkingDir.CreateDirectory(".git");
- mainGitDir.CreateFile("HEAD");
- mainGitDir.CreateFile("config").WriteAllText(@"[remote ""origin""] url = http://github.com/repo");
-
- var repo = CreateRepository(config: new GitConfig(ImmutableDictionary.CreateRange(new[]
- {
- KVP(new GitVariableName("remote", "origin", "url"), ImmutableArray.Create(mainWorkingDir.Path)),
- })));
-
- var warnings = new List<(string, object?[])>();
- Assert.Equal("http://github.com/repo", GitOperations.GetRepositoryUrl(repo, remoteName: "myremote", logWarning: (message, args) => warnings.Add((message, args))));
- AssertEx.Equal(new[] { string.Format(Resources.RepositoryDoesNotHaveSpecifiedRemote, repo.WorkingDirectory, "myremote", "origin") }, warnings.Select(TestUtilities.InspectDiagnostic));
- }
-
- ///
- /// Test scenario where a local repository is cloned from another local repository that does not have remote URL specified.
- ///
- [Fact]
- public void GetRepositoryUrl_Local_NoRemoteOriginUrl()
- {
- using var temp = new TempRoot();
-
- var mainWorkingDir = temp.CreateDirectory();
- var mainGitDir = mainWorkingDir.CreateDirectory(".git");
- mainGitDir.CreateFile("HEAD");
-
- var repo = CreateRepository(config: new GitConfig(ImmutableDictionary.CreateRange(new[]
+ var url = kind switch
{
- KVP(new GitVariableName("remote", "origin", "url"), ImmutableArray.Create(mainWorkingDir.Path)),
- })));
-
- var warnings = new List<(string, object?[])>();
- Assert.Equal(new Uri(mainWorkingDir.Path).AbsoluteUri, GitOperations.GetRepositoryUrl(repo, remoteName: null, logWarning: (message, args) => warnings.Add((message, args))));
- AssertEx.Equal(new[] { string.Format(Resources.RepositoryHasNoRemote, mainWorkingDir.Path) }, warnings.Select(TestUtilities.InspectDiagnostic));
- }
-
- ///
- /// Test scenario where a local repository is cloned from another local repository that does not have a working directory.
- ///
- [Fact]
- public void GetRepositoryUrl_Local_NoWorkingDirectory()
- {
- using var temp = new TempRoot();
-
- var dir = temp.CreateDirectory();
-
- var gitDir1 = dir.CreateDirectory("1");
- gitDir1.CreateFile("HEAD");
- gitDir1.CreateFile("config").WriteAllText(@"[remote ""origin""] url = http://github.com/repo");
-
- var gitDir2 = dir.CreateDirectory("2");
- gitDir2.CreateFile("HEAD");
- gitDir2.CreateFile("config").WriteAllText(@"[remote ""origin""] url = ../1");
-
- var repo = CreateRepository(config: new GitConfig(ImmutableDictionary.CreateRange(new[]
- {
- KVP(new GitVariableName("remote", "origin", "url"), ImmutableArray.Create(gitDir2.Path)),
- })));
-
- var warnings = new List<(string, object?[])>();
- Assert.Null(GitOperations.GetRepositoryUrl(repo, remoteName: null, logWarning: (message, args) => warnings.Add((message, args))));
- AssertEx.Equal(new[] { string.Format(Resources.UnableToLocateRepository, gitDir2.Path) }, warnings.Select(TestUtilities.InspectDiagnostic));
- }
-
- ///
- /// Test scenario where a local repository is cloned from another local repository that was cloned from a remote repository.
- ///
- [Fact]
- public void GetRepositoryUrl_Local_BadRepo()
- {
- using var temp = new TempRoot();
-
- var mainWorkingDir = temp.CreateDirectory();
- var mainGitDir = mainWorkingDir.CreateDirectory(".git");
-
- var repo = CreateRepository(config: new GitConfig(ImmutableDictionary.CreateRange(new[]
- {
- KVP(new GitVariableName("remote", "origin", "url"), ImmutableArray.Create(mainWorkingDir.Path)),
- })));
-
- var warnings = new List<(string, object?[])>();
- Assert.Equal(new Uri(mainWorkingDir.Path).AbsoluteUri, GitOperations.GetRepositoryUrl(repo, remoteName: null, logWarning: (message, args) => warnings.Add((message, args))));
- AssertEx.Equal(new[] { string.Format(Resources.RepositoryHasNoRemote, mainWorkingDir.Path) }, warnings.Select(TestUtilities.InspectDiagnostic));
- }
-
- [Fact]
- public void GetRepositoryUrl_LocalRecursion()
- {
- using var temp = new TempRoot();
-
- var mainWorkingDir = temp.CreateDirectory();
- var mainGitDir = mainWorkingDir.CreateDirectory(".git");
- mainGitDir.CreateFile("HEAD");
- mainGitDir.CreateFile("config").WriteAllText($@"[remote ""origin""] url = {mainWorkingDir.Path.Replace('\\', '/')}");
+ "local" => originRepoPath,
+ "file" => new Uri(originRepoPath).AbsolutePath,
+ _ => kind
+ };
var repo = CreateRepository(config: new GitConfig(ImmutableDictionary.CreateRange(new[]
{
- KVP(new GitVariableName("remote", "origin", "url"),
- ImmutableArray.Create(mainWorkingDir.Path)),
+ KVP(new GitVariableName("remote", "origin", "url"), ImmutableArray.Create(url)),
})));
var warnings = new List<(string, object?[])>();
- Assert.Equal(new Uri(mainWorkingDir.Path).AbsoluteUri, GitOperations.GetRepositoryUrl(repo, remoteName: null, logWarning: (message, args) => warnings.Add((message, args))));
- AssertEx.Equal(new[] { string.Format(Resources.RepositoryUrlEvaluationExceededMaximumAllowedDepth, "10") }, warnings.Select(TestUtilities.InspectDiagnostic));
+ var uri = GitOperations.GetRepositoryUrl(repo, remoteName: null, warnOnMissingOrUnsupportedRemote: true, logWarning: (message, args) => warnings.Add((message, args)));
+ Assert.Null(uri);
+ AssertEx.Equal(new[] { string.Format(Resources.InvalidRepositoryRemoteUrl, "origin", url) }, warnings.Select(TestUtilities.InspectDiagnostic));
}
[Theory]
@@ -468,7 +354,7 @@ public void GetSourceRoots_RepoWithCommits_WithoutUrl()
commitSha: "0000000000000000000000000000000000000000");
var warnings = new List<(string, object?[])>();
- var items = GitOperations.GetSourceRoots(repo, remoteName: null, warnOnMissingCommit: true, (message, args) => warnings.Add((message, args)));
+ var items = GitOperations.GetSourceRoots(repo, remoteName: null, warnOnMissingCommitOrUnsupportedUri: true, (message, args) => warnings.Add((message, args)));
AssertEx.Equal(new[]
{
@@ -487,7 +373,7 @@ public void GetSourceRoots_RepoWithCommits_WithUrl()
("remote.origin.url", "http://github.com/abc")));
var warnings = new List<(string, object?[])>();
- var items = GitOperations.GetSourceRoots(repo, remoteName: null, warnOnMissingCommit: true, (message, args) => warnings.Add((message, args)));
+ var items = GitOperations.GetSourceRoots(repo, remoteName: null, warnOnMissingCommitOrUnsupportedUri: true, (message, args) => warnings.Add((message, args)));
AssertEx.Equal(new[]
{
@@ -517,7 +403,7 @@ public void GetSourceRoots_RepoWithoutCommitsWithSubmodules()
CreateSubmodule("sub6", "sub/6", "", "6666666666666666666666666666666666666666")));
var warnings = new List<(string, object?[])>();
- var items = GitOperations.GetSourceRoots(repo, remoteName: null, warnOnMissingCommit: false, (message, args) => warnings.Add((message, args)));
+ var items = GitOperations.GetSourceRoots(repo, remoteName: null, warnOnMissingCommitOrUnsupportedUri: false, (message, args) => warnings.Add((message, args)));
// Module without a configuration entry is not initialized.
// URLs listed in .submodules are ignored (they are used by git submodule initialize to generate URLs stored in config).
@@ -567,8 +453,9 @@ public void GetSourceRoots_RepoWithCommitsWithSubmodules(bool warnOnMissingCommi
}
}
- [Fact]
- public void GetSourceRoots_RelativeSubmodulePath()
+ [Theory]
+ [CombinatorialData]
+ public void GetSourceRoots_RelativeSubmodulePath(bool warnOnMissingCommitOrUnsupportedUri)
{
using var temp = new TempRoot();
@@ -586,20 +473,32 @@ public void GetSourceRoots_RelativeSubmodulePath()
workingDir: repoDir.Path,
commitSha: "0000000000000000000000000000000000000000",
config: CreateConfig(
- ("submodule.1.url", "../1")),
+ ("submodule.1.url", "../1"),
+ ("submodule.2.url", "xyz://a/b")),
submodules: ImmutableArray.Create(
- CreateSubmodule("1", "sub/1", "---", "1111111111111111111111111111111111111111", containingRepositoryWorkingDir: repoDir.Path)));
+ CreateSubmodule("1", "sub/1", "---", "1111111111111111111111111111111111111111", containingRepositoryWorkingDir: repoDir.Path),
+ CreateSubmodule("2", "sub/2", "---", "2222222222222222222222222222222222222222", containingRepositoryWorkingDir: repoDir.Path)));
var warnings = new List<(string, object?[])>();
- var items = GitOperations.GetSourceRoots(repo, remoteName: null, warnOnMissingCommit: false, (message, args) => warnings.Add((message, args)));
+ var items = GitOperations.GetSourceRoots(repo, remoteName: null, warnOnMissingCommitOrUnsupportedUri, (message, args) => warnings.Add((message, args)));
AssertEx.Equal(new[]
{
- $@"'{repoDir.Path}{s}' SourceControl='git' RevisionId='0000000000000000000000000000000000000000'",
- $@"'{repoDir.Path}{s}sub{s}1{s}' SourceControl='git' RevisionId='1111111111111111111111111111111111111111' NestedRoot='sub/1/' ContainingRoot='{repoDir.Path}{s}' ScmRepositoryUrl='http://github.com/repo1'",
+ $@"'{repoDir.Path}{s}' SourceControl='git' RevisionId='0000000000000000000000000000000000000000'"
}, items.Select(TestUtilities.InspectSourceRoot));
- Assert.Empty(warnings);
+ if (warnOnMissingCommitOrUnsupportedUri)
+ {
+ AssertEx.Equal(new[]
+ {
+ string.Format(Resources.SourceCodeWontBeAvailableViaSourceLink, string.Format(Resources.InvalidSubmoduleUrl, "1", "../1")),
+ string.Format(Resources.SourceCodeWontBeAvailableViaSourceLink, string.Format(Resources.InvalidSubmoduleUrl, "2", "xyz://a/b"))
+ }, warnings.Select(TestUtilities.InspectDiagnostic));
+ }
+ else
+ {
+ Assert.Empty(warnings);
+ }
}
private static GitOperations.DirectoryNode CreateNode(string name, string? submoduleWorkingDirectory, List? children = null)
diff --git a/src/Microsoft.Build.Tasks.Git/GitOperations.cs b/src/Microsoft.Build.Tasks.Git/GitOperations.cs
index 0fd5187c8..36fe13336 100644
--- a/src/Microsoft.Build.Tasks.Git/GitOperations.cs
+++ b/src/Microsoft.Build.Tasks.Git/GitOperations.cs
@@ -16,8 +16,6 @@ namespace Microsoft.Build.Tasks.Git
{
internal static class GitOperations
{
- private const int RemoteRepositoryRecursionLimit = 10;
-
private const string SourceControlName = "git";
private const string RemoteSectionName = "remote";
private const string SubmoduleSectionName = "submodule";
@@ -25,14 +23,11 @@ internal static class GitOperations
private const string UrlSectionName = "url";
private const string UrlVariableName = "url";
- public static string? GetRepositoryUrl(GitRepository repository, string? remoteName, bool warnOnMissingRemote = true, Action? logWarning = null)
- => GetRepositoryUrl(repository, remoteName, recursionDepth: 0, warnOnMissingRemote, logWarning);
-
- private static string? GetRepositoryUrl(GitRepository repository, string? remoteName, int recursionDepth, bool warnOnMissingRemote, Action? logWarning)
+ public static string? GetRepositoryUrl(GitRepository repository, string? remoteName, bool warnOnMissingOrUnsupportedRemote = true, Action? logWarning = null)
{
NullableDebug.Assert(repository.WorkingDirectory != null);
- var remoteUrl = GetRemoteUrl(repository, ref remoteName, warnOnMissingRemote, logWarning);
+ var remoteUrl = GetRemoteUrl(repository, ref remoteName, warnOnMissingOrUnsupportedRemote, logWarning);
if (remoteUrl == null)
{
return null;
@@ -45,7 +40,18 @@ internal static class GitOperations
return null;
}
- return ResolveUrl(uri, repository.Environment, remoteName, recursionDepth, warnOnMissingRemote, logWarning);
+ if (!IsSupportedScheme(uri.Scheme))
+ {
+ if (warnOnMissingOrUnsupportedRemote)
+ {
+ // TODO: Better message https://github.com/dotnet/sourcelink/issues/1149
+ logWarning?.Invoke(Resources.InvalidRepositoryRemoteUrl, new[] { remoteName, remoteUrl });
+ }
+
+ return null;
+ }
+
+ return uri.AbsoluteUri;
}
private static string? GetRemoteUrl(GitRepository repository, ref string? remoteName, bool warnOnMissingRemote, Action? logWarning)
@@ -79,40 +85,6 @@ internal static class GitOperations
return remoteUrl;
}
- private static string? ResolveUrl(Uri uri, GitEnvironment environment, string? remoteName, int recursionDepth, bool warnOnMissingRemote, Action? logWarning)
- {
- if (!uri.IsFile)
- {
- return uri.AbsoluteUri;
- }
-
- var repositoryPath = uri.LocalPath;
- if (!GitRepository.TryGetRepositoryLocation(repositoryPath, out var remoteRepositoryLocation))
- {
- if (warnOnMissingRemote)
- {
- logWarning?.Invoke(Resources.RepositoryHasNoRemote, new[] { repositoryPath });
- }
-
- return uri.AbsoluteUri;
- }
-
- if (recursionDepth > RemoteRepositoryRecursionLimit)
- {
- logWarning?.Invoke(Resources.RepositoryUrlEvaluationExceededMaximumAllowedDepth, new[] { RemoteRepositoryRecursionLimit.ToString() });
- return null;
- }
-
- var remoteRepository = GitRepository.OpenRepository(remoteRepositoryLocation, environment);
- if (remoteRepository.WorkingDirectory == null)
- {
- logWarning?.Invoke(Resources.UnableToLocateRepository, new[] { repositoryPath });
- return null;
- }
-
- return GetRepositoryUrl(remoteRepository, remoteName, recursionDepth + 1, warnOnMissingRemote, logWarning) ?? uri.AbsoluteUri;
- }
-
private static bool TryGetRemote(GitConfig config, [NotNullWhen(true)]out string? remoteName, [NotNullWhen(true)]out string? remoteUrl)
{
remoteName = RemoteOriginName;
@@ -177,6 +149,21 @@ internal static string ApplyInsteadOfUrlMapping(GitConfig config, string url)
return NormalizeUrl(ApplyInsteadOfUrlMapping(repository.Config, url), root: repository.WorkingDirectory);
}
+ ///
+ /// Git supports "http(s)", "ssh", "git" and "file" schemes. "ftp" support is obsolete. The scheme is case-sensitive.
+ /// See https://git-scm.com/docs/git-clone#_git_urls.
+ ///
+ /// Source Link does not support "file" scheme since repositories hosted locally or on a network share
+ /// are usually bare and the actual source files are not available (a work tree is not available).
+ /// The debugger also does not support "file" URLs in Source Link.
+ ///
+ /// The scenario of a repository on a network share that is not bare is rare since pushing into such repository
+ /// is not allowed by default (see configuration "receive.denyCurrentBranch") and the work tree needs
+ /// to be kept up to date by a service.
+ ///
+ private static bool IsSupportedScheme(string scheme)
+ => scheme is "http" or "https" or "ssh" or "git";
+
internal static Uri? NormalizeUrl(string url, string root)
{
// Since git supports scp-like syntax for SSH URLs we convert it here,
@@ -247,7 +234,7 @@ private static bool TryParseScp(string value, [NotNullWhen(true)]out Uri? uri)
return Uri.TryCreate(url, UriKind.Absolute, out uri);
}
- public static ITaskItem[] GetSourceRoots(GitRepository repository, string? remoteName, bool warnOnMissingCommit, Action logWarning)
+ public static ITaskItem[] GetSourceRoots(GitRepository repository, string? remoteName, bool warnOnMissingCommitOrUnsupportedUri, Action logWarning)
{
// Not supported for repositories without a working directory.
NullableDebug.Assert(repository.WorkingDirectory != null);
@@ -270,7 +257,7 @@ public static ITaskItem[] GetSourceRoots(GitRepository repository, string? remot
item.SetMetadata(Names.SourceRoot.RevisionId, revisionId);
result.Add(item);
}
- else if (warnOnMissingCommit)
+ else if (warnOnMissingCommitOrUnsupportedUri)
{
logWarning(Resources.RepositoryHasNoCommit, Array.Empty