diff --git a/src/Common/AzureDevOps/AzureDevOpsUrlParser.cs b/src/Common/AzureDevOps/AzureDevOpsUrlParser.cs
index e90eb3cd35..0b22fe9038 100644
--- a/src/Common/AzureDevOps/AzureDevOpsUrlParser.cs
+++ b/src/Common/AzureDevOps/AzureDevOpsUrlParser.cs
@@ -110,11 +110,17 @@ public static bool TryParseOnPremHttp(string relativeUrl, string virtualDirector
return true;
}
- public static bool TryParseHostedSsh(Uri uri, [NotNullWhen(true)]out string? account, [NotNullWhen(true)]out string? repositoryPath, [NotNullWhen(true)]out string? repositoryName)
+ public static bool TryParseHostedSsh(
+ Uri uri,
+ [NotNullWhen(true)] out string? account,
+ [NotNullWhen(true)] out string? repositoryPath,
+ [NotNullWhen(true)] out string? repositoryName,
+ out bool isUnsupportedFormat)
{
NullableDebug.Assert(uri != null);
account = repositoryPath = repositoryName = null;
+ isUnsupportedFormat = false;
// {"DefaultCollection"|""}/{repositoryPath}/"_ssh"/{"_full"|"_optimized"}/{repositoryName}
if (!UriUtilities.TrySplitRelativeUrl(uri.GetPath(), out var parts) || parts.Length == 0)
@@ -125,7 +131,7 @@ public static bool TryParseHostedSsh(Uri uri, [NotNullWhen(true)]out string? acc
// Check for v3 url format
if (parts[0] == "v3" &&
parts.Length >= 3 &&
- TryParsePath(parts, 2, type: null, out repositoryPath, out repositoryName) &&
+ TryParsePath(parts, startIndex: 2, type: null, out repositoryPath, out repositoryName) &&
repositoryPath != "")
{
// ssh://{user}@{domain}:{port}/v3/{account}/{repositoryPath}/{'_full'|'_optimized'|''}/{repositoryName}
@@ -136,8 +142,6 @@ public static bool TryParseHostedSsh(Uri uri, [NotNullWhen(true)]out string? acc
// ssh v1/v2 url formats
// ssh://{account}@vs-ssh.visualstudio.com/
- account = uri.UserInfo;
-
int index = 0;
if (StringComparer.OrdinalIgnoreCase.Equals(parts[0], "DefaultCollection"))
{
@@ -149,14 +153,15 @@ public static bool TryParseHostedSsh(Uri uri, [NotNullWhen(true)]out string? acc
// Failed to parse path
return false;
}
- }
- if (account.Length == 0)
- {
+ // The format uses SSH connection user name as an account name.
+ // It is no longer supported since GitOperations.GetRepositoryUrl strips the user info
+ // to prevent leaking credentials.
+ isUnsupportedFormat = true;
return false;
}
- return true;
+ return account.Length > 0;
}
public static bool TryParseOnPremSsh(Uri uri, [NotNullWhen(true)]out string? repositoryPath, [NotNullWhen(true)]out string? repositoryName)
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs
index 0832293988..ccb95432cc 100644
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs
+++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs
@@ -224,23 +224,26 @@ public void GetRepositoryUrl_UnsupportedUrl(string kind)
}
[Theory]
- [InlineData("https://github.com/org/repo")]
- [InlineData("http://github.com/org/repo")]
- [InlineData("http://github.com:102/org/repo")]
- [InlineData("ssh://user@github.com/org/repo")]
- [InlineData("abc://user@github.com/org/repo")]
- public void NormalizeUrl_PlatformAgnostic1(string url)
+ [InlineData("https://github.com/org/repo", "https://github.com/org/repo")]
+ [InlineData("http://github.com/org/repo", "http://github.com/org/repo")]
+ [InlineData("http://github.com:102/org/repo", "http://github.com:102/org/repo")]
+ [InlineData("ssh://user@github.com/org/repo", "ssh://git@github.com/org/repo")] // "user" replaced with "git" in SSH URL
+ [InlineData("abc://user@github.com/org/repo", "abc://github.com/org/repo")]
+ public void NormalizeUrl_PlatformAgnostic1(string url, string expected)
{
- AssertEx.AreEqual(url, GitOperations.NormalizeUrl(url, s_root)?.AbsoluteUri);
+ AssertEx.AreEqual(expected, GitOperations.NormalizeUrl(url, s_root)?.AbsoluteUri);
}
[Theory]
[InlineData("http://?", null)]
[InlineData("https://github.com/org/repo/./.", "https://github.com/org/repo/")]
[InlineData("http://github.com/org/" + TestStrings.RepoName, "http://github.com/org/" + TestStrings.RepoNameFullyEscaped)]
- [InlineData("ssh://github.com/org/../repo", "ssh://github.com/repo")]
- [InlineData("ssh://github.com/%32/repo", "ssh://github.com/2/repo")]
- [InlineData("ssh://github.com/%3F/repo", "ssh://github.com/%3F/repo")]
+ [InlineData("ssh://github.com/org/../repo", "ssh://git@github.com/repo")]
+ [InlineData("ssh://github.com/%32/repo", "ssh://git@github.com/2/repo")]
+ [InlineData("ssh://github.com/%3F/repo", "ssh://git@github.com/%3F/repo")]
+ [InlineData(@"../.:./../../relative/path", null)]
+ [InlineData(@".:/../../relative/path", null)]
+ [InlineData(@"..:/../../relative/path", null)]
public void NormalizeUrl_PlatformAgnostic2(string url, string? expectedUrl)
{
AssertEx.AreEqual(expectedUrl, GitOperations.NormalizeUrl(url, s_root)?.AbsoluteUri);
@@ -269,14 +272,14 @@ public void NormalizeUrl_Unix(string url, string expectedUrl)
}
[Theory]
- [InlineData("abc:org/repo", "ssh://abc/org/repo")]
- [InlineData("abc:org/x%20y", "ssh://abc/org/x%20y")]
- [InlineData("ABC:ORG/REPO/X/Y", "ssh://abc/ORG/REPO/X/Y")]
- [InlineData("github.com:org/repo", "ssh://github.com/org/repo")]
- [InlineData("git@github.com:org/repo", "ssh://git@github.com/org/repo")]
- [InlineData("@github.com:org/repo", "ssh://@github.com/org/repo")]
- [InlineData("http:x//y", "ssh://http/x//y")]
- public void GetRepositoryUrl_ScpSyntax(string url, string expectedUrl)
+ [InlineData("abc:org/repo", "ssh://git@abc/org/repo")]
+ [InlineData("abc:org/x%20y", "ssh://git@abc/org/x%20y")]
+ [InlineData("ABC:ORG/REPO/X/Y", "ssh://git@abc/ORG/REPO/X/Y")]
+ [InlineData("github.com:org/repo", "ssh://git@github.com/org/repo")]
+ [InlineData("user@github.com:org/repo", "ssh://git@github.com/org/repo")] // "user" replaced with "git" in SSH URL
+ [InlineData("@github.com:org/repo", "ssh://git@github.com/org/repo")]
+ [InlineData("http:x//y", "ssh://git@http/x//y")]
+ public void NormalizeUrl_ScpSyntax(string url, string expectedUrl)
{
Assert.Equal(expectedUrl, GitOperations.NormalizeUrl(url, s_root)?.AbsoluteUri);
}
@@ -398,7 +401,7 @@ public void GetSourceRoots_RepoWithoutCommitsWithSubmodules()
// URLs listed in .submodules are ignored (they are used by git submodule initialize to generate URLs stored in config).
AssertEx.Equal(new[]
{
- $@"'{_workingDir}{s}sub{s}1{s}' SourceControl='git' RevisionId='1111111111111111111111111111111111111111' NestedRoot='sub/1/' ContainingRoot='{_workingDir}{s}' ScmRepositoryUrl='ssh://github.com/sub-1'",
+ $@"'{_workingDir}{s}sub{s}1{s}' SourceControl='git' RevisionId='1111111111111111111111111111111111111111' NestedRoot='sub/1/' ContainingRoot='{_workingDir}{s}' ScmRepositoryUrl='ssh://git@github.com/sub-1'",
$@"'{_workingDir}{s}sub{s}3{s}' SourceControl='git' RevisionId='3333333333333333333333333333333333333333' NestedRoot='sub/3/' ContainingRoot='{_workingDir}{s}' ScmRepositoryUrl='https://github.com/sub-3'",
$@"'{_workingDir}{s}sub{s}6{s}' SourceControl='git' RevisionId='6666666666666666666666666666666666666666' NestedRoot='sub/6/' ContainingRoot='{_workingDir}{s}' ScmRepositoryUrl='https://github.com/sub-6'",
}, items.Select(TestUtilities.InspectSourceRoot));
diff --git a/src/Microsoft.Build.Tasks.Git/GitOperations.cs b/src/Microsoft.Build.Tasks.Git/GitOperations.cs
index cc0301ae11..b135b60d5e 100644
--- a/src/Microsoft.Build.Tasks.Git/GitOperations.cs
+++ b/src/Microsoft.Build.Tasks.Git/GitOperations.cs
@@ -164,7 +164,35 @@ internal static string ApplyInsteadOfUrlMapping(GitConfig config, string url)
private static bool IsSupportedScheme(string scheme)
=> scheme is "http" or "https" or "ssh" or "git";
+ // internal for testing
internal static Uri? NormalizeUrl(string url, string root)
+ {
+ var normalizedUrl = NormalizeUrlImpl(url, root);
+ if (normalizedUrl == null)
+ {
+ return null;
+ }
+
+ // remove user info to avoid embedding access tokens to build artifacts:
+ var builder = new UriBuilder(normalizedUrl)
+ {
+ // If user name is not specified in SSH URL, the local user name is used for connecting to the repo.
+ // Use "git" placeholder instead of a specific user name.
+ UserName = normalizedUrl.Scheme is "ssh" ? "git" : null,
+ Password = null,
+ };
+
+ try
+ {
+ return builder.Uri;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private static Uri? NormalizeUrlImpl(string url, string root)
{
// Since git supports scp-like syntax for SSH URLs we convert it here,
// so that RepositoryUrl is actually a valid URL in that case.
diff --git a/src/SourceLink.AzureRepos.Git.UnitTests/AzureDevOpsUrlParserHostedTests.cs b/src/SourceLink.AzureRepos.Git.UnitTests/AzureDevOpsUrlParserHostedTests.cs
index 2284ff1ffd..a1fa02d7dd 100644
--- a/src/SourceLink.AzureRepos.Git.UnitTests/AzureDevOpsUrlParserHostedTests.cs
+++ b/src/SourceLink.AzureRepos.Git.UnitTests/AzureDevOpsUrlParserHostedTests.cs
@@ -95,37 +95,42 @@ public void TryParseHostedSsh_Error(string url)
}
[Theory]
- [InlineData("ssh://account@vs-ssh.visualstudio.com/project/_ssh/repo", "account", "project", "repo")]
- [InlineData("ssh://account@vs-ssh.visualstudio.com/project/team/_ssh/repo", "account", "project/team", "repo")]
- [InlineData("ssh://account@vs-ssh.visualstudio.com/DefaultCollection/project/_ssh/repo", "account", "project", "repo")]
- [InlineData("ssh://account@vs-ssh.visualstudio.com/DefaultCollection/project/team/_ssh/repo", "account", "project/team", "repo")]
- [InlineData("ssh://account@vs-ssh.visualstudio.com/DefaultCollection/project/team/_ssh/_full/repo", "account", "project/team", "repo")]
- [InlineData("ssh://account@vs-ssh.visualstudio.com/DefaultCollection/project/team/_ssh/_optimized/repo", "account", "project/team", "repo")]
- [InlineData("ssh://account@vs-ssh.visualstudio.com/DefaultCollection/_ssh/repo", "account", "", "repo")]
- [InlineData("ssh://account@vs-ssh.visualstudio.com/_ssh/repo", "account", "", "repo")]
-
- [InlineData("ssh://account@vs-ssh.vsts.me/project/_ssh/repo", "account", "project", "repo")]
- [InlineData("ssh://account@vs-ssh.vsts.me/project/team/_ssh/repo", "account", "project/team", "repo")]
- [InlineData("ssh://account@vs-ssh.vsts.me/DefaultCollection/project/_ssh/repo", "account", "project", "repo")]
- [InlineData("ssh://account@vs-ssh.vsts.me/DefaultCollection/project/team/_ssh/repo", "account", "project/team", "repo")]
- [InlineData("ssh://account@vs-ssh.vsts.me/DefaultCollection/project/team/_ssh/_full/repo", "account", "project/team", "repo")]
- [InlineData("ssh://account@vs-ssh.vsts.me/DefaultCollection/project/team/_ssh/_optimized/repo", "account", "project/team", "repo")]
- [InlineData("ssh://account@vs-ssh.vsts.me/DefaultCollection/_ssh/repo", "account", "", "repo")]
- [InlineData("ssh://account@vs-ssh.vsts.me/_ssh/repo", "account", "", "repo")]
-
- [InlineData("ssh://account@ssh.contoso.com/project/_ssh/repo", "account", "project", "repo")]
- [InlineData("ssh://account@ssh.contoso.com/project/team/_ssh/repo", "account", "project/team", "repo")]
- [InlineData("ssh://account@ssh.contoso.com/project/team/_ssh/_full/repo", "account", "project/team", "repo")]
- [InlineData("ssh://account@ssh.contoso.com/project/team/_ssh/_optimized/repo", "account", "project/team", "repo")]
+ [InlineData("ssh://account@vs-ssh.visualstudio.com/project/_ssh/repo")]
+ [InlineData("ssh://account@vs-ssh.visualstudio.com/project/team/_ssh/repo")]
+ [InlineData("ssh://account@vs-ssh.visualstudio.com/DefaultCollection/project/_ssh/repo")]
+ [InlineData("ssh://account@vs-ssh.visualstudio.com/DefaultCollection/project/team/_ssh/repo")]
+ [InlineData("ssh://account@vs-ssh.visualstudio.com/DefaultCollection/project/team/_ssh/_full/repo")]
+ [InlineData("ssh://account@vs-ssh.visualstudio.com/DefaultCollection/project/team/_ssh/_optimized/repo")]
+ [InlineData("ssh://account@vs-ssh.visualstudio.com/DefaultCollection/_ssh/repo")]
+ [InlineData("ssh://account@vs-ssh.visualstudio.com/_ssh/repo")]
+ [InlineData("ssh://account@vs-ssh.vsts.me/project/_ssh/repo")]
+ [InlineData("ssh://account@vs-ssh.vsts.me/project/team/_ssh/repo")]
+ [InlineData("ssh://account@vs-ssh.vsts.me/DefaultCollection/project/_ssh/repo")]
+ [InlineData("ssh://account@vs-ssh.vsts.me/DefaultCollection/project/team/_ssh/repo")]
+ [InlineData("ssh://account@vs-ssh.vsts.me/DefaultCollection/project/team/_ssh/_full/repo")]
+ [InlineData("ssh://account@vs-ssh.vsts.me/DefaultCollection/project/team/_ssh/_optimized/repo")]
+ [InlineData("ssh://account@vs-ssh.vsts.me/DefaultCollection/_ssh/repo")]
+ [InlineData("ssh://account@vs-ssh.vsts.me/_ssh/repo")]
+ [InlineData("ssh://account@ssh.contoso.com/project/_ssh/repo")]
+ [InlineData("ssh://account@ssh.contoso.com/project/team/_ssh/repo")]
+ [InlineData("ssh://account@ssh.contoso.com/project/team/_ssh/_full/repo")]
+ [InlineData("ssh://account@ssh.contoso.com/project/team/_ssh/_optimized/repo")]
+ [InlineData("ssh://account@vs-ssh.visualstudio.com/v3/_ssh/repo")]
+ [InlineData("ssh://account@vs-ssh.visualstudio.com/v3/team/_ssh/repo")]
+ public void TryParseHostedSshV1V2_Success(string url)
+ {
+ Assert.False(AzureDevOpsUrlParser.TryParseHostedSsh(
+ new Uri(url, UriKind.Absolute), out _, out _, out _, out var isUnsupportedFormat));
+ Assert.True(isUnsupportedFormat);
+ }
- [InlineData("ssh://account@vs-ssh.visualstudio.com/v3/_ssh/repo", "account", "v3", "repo")]
- [InlineData("ssh://account@vs-ssh.visualstudio.com/v3/team/_ssh/repo", "account", "v3/team", "repo")]
- public void TryParseHostedSshV1V2_Success(string url, string account, string repositoryPath, string repositoryName)
+ [Theory]
+ [InlineData("ssh://account@vs-ssh.visualstudio.com/v4/team/_ssh")]
+ public void TryParseHostedSshV1V2_Error(string url)
{
- Assert.True(AzureDevOpsUrlParser.TryParseHostedSsh(new Uri(url, UriKind.Absolute), out var actualAccount, out var actualRepositoryPath, out var actualRepositoryName));
- Assert.Equal(account, actualAccount);
- Assert.Equal(repositoryPath, actualRepositoryPath);
- Assert.Equal(repositoryName, actualRepositoryName);
+ Assert.False(AzureDevOpsUrlParser.TryParseHostedSsh(
+ new Uri(url, UriKind.Absolute), out _, out _, out _, out var isUnsupportedFormat));
+ Assert.False(isUnsupportedFormat);
}
[Theory]
@@ -145,7 +150,9 @@ public void TryParseHostedSshV1V2_Success(string url, string account, string rep
[InlineData("ssh://account1@ssh.contoso.com/v3/account2/project/team/_optimized/repo", "account2", "project/team", "repo")]
public void TryParseHostedSshV3_Success(string url, string account, string repositoryPath, string repositoryName)
{
- Assert.True(AzureDevOpsUrlParser.TryParseHostedSsh(new Uri(url, UriKind.Absolute), out var actualAccount, out var actualRepositoryPath, out var actualRepositoryName));
+ Assert.True(AzureDevOpsUrlParser.TryParseHostedSsh(
+ new Uri(url, UriKind.Absolute), out var actualAccount, out var actualRepositoryPath, out var actualRepositoryName, out var isUnsupportedFormat));
+ Assert.False(isUnsupportedFormat);
Assert.Equal(account, actualAccount);
Assert.Equal(repositoryPath, actualRepositoryPath);
Assert.Equal(repositoryName, actualRepositoryName);
diff --git a/src/SourceLink.AzureRepos.Git.UnitTests/TranslateRepositoryUrlsTests.cs b/src/SourceLink.AzureRepos.Git.UnitTests/TranslateRepositoryUrlsTests.cs
index 5ad83c5d2a..3f1a44a012 100644
--- a/src/SourceLink.AzureRepos.Git.UnitTests/TranslateRepositoryUrlsTests.cs
+++ b/src/SourceLink.AzureRepos.Git.UnitTests/TranslateRepositoryUrlsTests.cs
@@ -18,21 +18,21 @@ public void Translate()
var task = new TranslateRepositoryUrls()
{
BuildEngine = engine,
- RepositoryUrl = "ssh://account@vs-ssh.visualstudio.com/project/team/_ssh/repo",
+ RepositoryUrl = "ssh://vs-ssh.visualstudio.com/v3/account/project/team/repo",
IsSingleProvider = true,
SourceRoots = new[]
{
- new MockItem("/1/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://account@vs-ssh.visualstudio.com:22/project/team/_ssh/repo")), // ok
- new MockItem("/2/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://test@vs-ssh.visualstudio.com:22/project/_ssh/repo")), // ok
+ new MockItem("/1/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://vs-ssh.visualstudio.com:22/v3/account/project/team/repo")), // ok
+ new MockItem("/2/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://user@vs-ssh.visualstudio.com:22/v3/test/project/repo")), // ok
new MockItem("/3/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://user@vs-ssh.visualstudio.com:22/v3/account/project/team/repo")), // ok
- new MockItem("/4/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://account@vs-ssh.visualstudio.com/_ssh/repo")), // ok
- new MockItem("/5/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://account@ssh.contoso.com:22/project/team/_ssh/repo")), // ok
+ new MockItem("/4/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://vs-ssh.visualstudio.com/v3/account/project/repo")), // ok
+ new MockItem("/5/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://ssh.contoso.com:22/v3/account/project/team/repo")), // ok
new MockItem("/6/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://user@ssh.contoso.com/v3/account/project/team/repo")), // ok
- new MockItem("/7/", KVP("SourceControl", "tfvc"), KVP("ScmRepositoryUrl", "ssh://account@vs-ssh.visualstudio.com:22/project/team/_ssh/repo")), // different source control
- new MockItem("/8/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://account@contoso.com:22/project/team/_ssh/repo")), // no "vs-ssh." prefix
- new MockItem("/9/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://account@vs-ssh.contoso.com:22/project/team/_ssh/repo")), // known host, but not visualstudio.com
- new MockItem("/A/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://account@vs-ssh.contoso2.com:22/project/team/_ssh/repo")), // unknown host
- new MockItem("/B/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://account@vs-ssh.contoso.com:22/project/team/ZZZ/repo")), // bad format
+ new MockItem("/7/", KVP("SourceControl", "tfvc"), KVP("ScmRepositoryUrl", "ssh://vs-ssh.visualstudio.com:22/v3/account/project/team/repo")), // different source control
+ new MockItem("/8/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://contoso.com:22/v3/account/project/team/repo")), // no "vs-ssh." prefix
+ new MockItem("/9/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://vs-ssh.contoso.com:22/v3/account/project/team/repo")), // known host, but not visualstudio.com
+ new MockItem("/A/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://vs-ssh.contoso2.com:22/v3/account/project/team/repo")), // unknown host
+ new MockItem("/B/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://vs-ssh.contoso.com:22/v3/account/project/team/ZZZ/repo")), // bad format
},
Hosts = new[]
{
@@ -50,14 +50,14 @@ public void Translate()
"https://account.visualstudio.com/project/team/_git/repo",
"https://test.visualstudio.com/project/_git/repo",
"https://account.visualstudio.com/project/team/_git/repo",
- "https://account.visualstudio.com/_git/repo",
+ "https://account.visualstudio.com/project/_git/repo",
"https://contoso.com/account/project/team/_git/repo",
"https://contoso.com/account/project/team/_git/repo",
- "ssh://account@vs-ssh.visualstudio.com:22/project/team/_ssh/repo",
- "ssh://account@contoso.com:22/project/team/_ssh/repo",
- "ssh://account@vs-ssh.contoso.com:22/project/team/_ssh/repo",
- "ssh://account@vs-ssh.contoso2.com:22/project/team/_ssh/repo",
- "ssh://account@vs-ssh.contoso.com:22/project/team/ZZZ/repo"
+ "ssh://vs-ssh.visualstudio.com:22/v3/account/project/team/repo",
+ "ssh://contoso.com:22/v3/account/project/team/repo",
+ "ssh://vs-ssh.contoso.com:22/v3/account/project/team/repo",
+ "ssh://vs-ssh.contoso2.com:22/v3/account/project/team/repo",
+ "ssh://vs-ssh.contoso.com:22/v3/account/project/team/ZZZ/repo"
}, task.TranslatedSourceRoots?.Select(r => r.GetMetadata("ScmRepositoryUrl")));
Assert.True(result);
diff --git a/src/SourceLink.AzureRepos.Git/Microsoft.SourceLink.AzureRepos.Git.csproj b/src/SourceLink.AzureRepos.Git/Microsoft.SourceLink.AzureRepos.Git.csproj
index 7e68aacbd4..52dc58ee62 100644
--- a/src/SourceLink.AzureRepos.Git/Microsoft.SourceLink.AzureRepos.Git.csproj
+++ b/src/SourceLink.AzureRepos.Git/Microsoft.SourceLink.AzureRepos.Git.csproj
@@ -15,5 +15,6 @@
+
diff --git a/src/SourceLink.AzureRepos.Git/Resources.resx b/src/SourceLink.AzureRepos.Git/Resources.resx
index aae4915e43..1c42f26e5d 100644
--- a/src/SourceLink.AzureRepos.Git/Resources.resx
+++ b/src/SourceLink.AzureRepos.Git/Resources.resx
@@ -129,4 +129,7 @@
The value passed to task parameter {0} is not a valid domain name: '{1}'
+
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+
\ No newline at end of file
diff --git a/src/SourceLink.AzureRepos.Git/TranslateRepositoryUrls.cs b/src/SourceLink.AzureRepos.Git/TranslateRepositoryUrls.cs
index 4dbdf60bad..01f6fccb71 100644
--- a/src/SourceLink.AzureRepos.Git/TranslateRepositoryUrls.cs
+++ b/src/SourceLink.AzureRepos.Git/TranslateRepositoryUrls.cs
@@ -10,9 +10,9 @@ namespace Microsoft.SourceLink.AzureRepos.Git
public sealed class TranslateRepositoryUrls : TranslateRepositoryUrlsGitTask
{
// Translates
- // ssh://{account}@{ssh-subdomain}.{domain}:{port}/{repositoryPath}/_ssh/{"_full"|"_optimized"}/{repositoryName}
+ // ssh://{user}@{domain}:{port}/v3/{account}/{repositoryPath}/{'_full'|'_optimized'|''}/{repositoryName}
// to
- // https://{http-domain}/{account}/{repositoryPath}/_git/{repositoryName}
+ // https://.../{repositoryPath}/_git/{repositoryName}
//
// Dommain mapping:
// ssh://vs-ssh.*.com -> https://{account}.*.com
@@ -27,8 +27,13 @@ public sealed class TranslateRepositoryUrls : TranslateRepositoryUrlsGitTask
return null;
}
- if (!AzureDevOpsUrlParser.TryParseHostedSsh(uri, out var account, out var repositoryPath, out var repositoryName))
+ if (!AzureDevOpsUrlParser.TryParseHostedSsh(uri, out var account, out var repositoryPath, out var repositoryName, out var isUnsupportedFormat))
{
+ if (isUnsupportedFormat)
+ {
+ throw new NotSupportedException(string.Format(Resources.RemoteUrlFormatNoLongerSupported, uri.AbsoluteUri));
+ }
+
return null;
}
diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.cs.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.cs.xlf
index aef5fb2ead..aba88fb561 100644
--- a/src/SourceLink.AzureRepos.Git/xlf/Resources.cs.xlf
+++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.cs.xlf
@@ -7,6 +7,11 @@
Hodnota proměnné prostředí {0} není správně formátovaný seznam párů adres URL: {1}"
+
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+
+ The value of {0} with identity '{1}' is invalid: '{2}'"Hodnota {0} s identitou {1} je neplatná: {2}"
diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.de.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.de.xlf
index f9d6cd8ca1..2165061490 100644
--- a/src/SourceLink.AzureRepos.Git/xlf/Resources.de.xlf
+++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.de.xlf
@@ -7,6 +7,11 @@
Der Wert der Umgebungsvariablen "{0}" ist keine wohlgeformte Liste mit URL-Paaren: "{1}"
+
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+
+ The value of {0} with identity '{1}' is invalid: '{2}'"Der Wert von "{0}" mit der Identität "{1}" ist ungültig: "{2}"
diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.es.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.es.xlf
index cf3efa11f2..4d672c6b74 100644
--- a/src/SourceLink.AzureRepos.Git/xlf/Resources.es.xlf
+++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.es.xlf
@@ -7,6 +7,11 @@
El valor de la variable de entorno {0} no es una lista bien formada de pares de direcciones URL: '{1}'"
+
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+
+ The value of {0} with identity '{1}' is invalid: '{2}'"El valor de {0} con identidad '{1}' no es válido '{2}'"
diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.fr.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.fr.xlf
index 3cfb005474..681e6ee0ef 100644
--- a/src/SourceLink.AzureRepos.Git/xlf/Resources.fr.xlf
+++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.fr.xlf
@@ -7,6 +7,11 @@
La valeur de la variable d'environnement {0} n'est pas une liste correctement formée de paires d'URL : '{1}'"
+
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+
+ The value of {0} with identity '{1}' is invalid: '{2}'"La valeur de {0} avec l'identité '{1}' n'est pas valide : '{2}'"
diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.it.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.it.xlf
index d63957052b..899c383c81 100644
--- a/src/SourceLink.AzureRepos.Git/xlf/Resources.it.xlf
+++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.it.xlf
@@ -7,6 +7,11 @@
Il valore della variabile di ambiente {0} non è un elenco ben formato di coppie di URL: '{1}'"
+
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+
+ The value of {0} with identity '{1}' is invalid: '{2}'"Il valore di {0} con identità '{1}' non è valido: '{2}'"
diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.ja.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.ja.xlf
index a46569e801..ec7c89992d 100644
--- a/src/SourceLink.AzureRepos.Git/xlf/Resources.ja.xlf
+++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.ja.xlf
@@ -7,6 +7,11 @@
環境変数 {0} の値は正しい形式の URL のペアの一覧ではありません: '{1}'"
+
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+
+ The value of {0} with identity '{1}' is invalid: '{2}'"ID '{1}' の {0} の値は無効です: '{2}'"
diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.ko.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.ko.xlf
index 8e025896ad..0b3184aff1 100644
--- a/src/SourceLink.AzureRepos.Git/xlf/Resources.ko.xlf
+++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.ko.xlf
@@ -7,6 +7,11 @@
환경 변수 {0}의 값이 잘 구성된(Well-Formed) URL 쌍 목록이 아닙니다. '{1}'"
+
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+
+ The value of {0} with identity '{1}' is invalid: '{2}'"ID가 '{1}'인 {0}의 값이 잘못되었습니다. '{2}'"
diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.pl.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.pl.xlf
index 16889347ff..a8d98b0fde 100644
--- a/src/SourceLink.AzureRepos.Git/xlf/Resources.pl.xlf
+++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.pl.xlf
@@ -7,6 +7,11 @@
Wartość zmiennej środowiskowej {0} nie jest prawidłowo sformułowaną listą par adresów URL: „{1}”"
+
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+
+ The value of {0} with identity '{1}' is invalid: '{2}'"Wartość elementu {0} z tożsamością „{1}” jest nieprawidłowa: „{2}”"
diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.pt-BR.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.pt-BR.xlf
index 38d4241ec7..78a60304fd 100644
--- a/src/SourceLink.AzureRepos.Git/xlf/Resources.pt-BR.xlf
+++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.pt-BR.xlf
@@ -7,6 +7,11 @@
O valor da variável de ambiente {0} não é uma lista bem-formada de pares de URLs: '{1}'"
+
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+
+ The value of {0} with identity '{1}' is invalid: '{2}'"O valor de {0} com a identidade '{1}' é inválido: '{2}'"
diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.ru.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.ru.xlf
index 4010c44a3d..b85b11c707 100644
--- a/src/SourceLink.AzureRepos.Git/xlf/Resources.ru.xlf
+++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.ru.xlf
@@ -7,6 +7,11 @@
Значение переменной среды {0} не является списком пар URL в корректном формате: "{1}"
+
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+
+ The value of {0} with identity '{1}' is invalid: '{2}'"Значение {0} с идентификатором "{1}" недопустимо: "{2}"
diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.tr.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.tr.xlf
index bab3b68924..a4b6ff2d24 100644
--- a/src/SourceLink.AzureRepos.Git/xlf/Resources.tr.xlf
+++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.tr.xlf
@@ -7,6 +7,11 @@
{0} ortam değişkeninin değeri doğru biçimlendirilmiş bir URL çiftleri listesi değil: '{1}'"
+
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+
+ The value of {0} with identity '{1}' is invalid: '{2}'"'{1}' kimliğine sahip {0} değeri geçersiz: '{2}'"
diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.zh-Hans.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.zh-Hans.xlf
index bc963ae656..2180e87150 100644
--- a/src/SourceLink.AzureRepos.Git/xlf/Resources.zh-Hans.xlf
+++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.zh-Hans.xlf
@@ -7,6 +7,11 @@
环境变量 {0} 的值不是形式正确的 URL 对列表: '{1}'"
+
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+
+ The value of {0} with identity '{1}' is invalid: '{2}'"带有 '{1}' 标识的 {0} 的值无效: '{2}'"
diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.zh-Hant.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.zh-Hant.xlf
index 177fe5765a..fdcd3c45d2 100644
--- a/src/SourceLink.AzureRepos.Git/xlf/Resources.zh-Hant.xlf
+++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.zh-Hant.xlf
@@ -7,6 +7,11 @@
環境變數 {0} 的值不是正確格式的 URL 配對清單: '{1}'"
+
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+ Remote URL '{0}' appears to be in a format that is no longer supported.
+
+ The value of {0} with identity '{1}' is invalid: '{2}'"識別為 {1} 之 {0} 的值無效: '{2}'"
diff --git a/src/SourceLink.Git.IntegrationTests/AzureReposTests.cs b/src/SourceLink.Git.IntegrationTests/AzureReposTests.cs
index 4755f68001..dcc0341104 100644
--- a/src/SourceLink.Git.IntegrationTests/AzureReposTests.cs
+++ b/src/SourceLink.Git.IntegrationTests/AzureReposTests.cs
@@ -3,6 +3,7 @@
// See the License.txt file in the project root for more information.
using System.IO;
+using Microsoft.SourceLink.AzureRepos.Git;
using TestUtilities;
using Xunit;
@@ -10,7 +11,7 @@ namespace Microsoft.SourceLink.IntegrationTests
{
public class AzureReposTests : DotNetSdkTestBase
{
- public AzureReposTests()
+ public AzureReposTests()
: base("Microsoft.SourceLink.AzureRepos.Git")
{
}
@@ -64,12 +65,12 @@ public void FullValidation_Https(string host)
File.ReadAllText(Path.Combine(ProjectDir.Path, s_relativeSourceLinkJsonPath)));
TestUtilities.ValidateAssemblyInformationalVersion(
- Path.Combine(ProjectDir.Path, s_relativeOutputFilePath),
+ Path.Combine(ProjectDir.Path, s_relativeOutputFilePath),
"1.0.0+" + commitSha);
TestUtilities.ValidateNuSpecRepository(
Path.Combine(ProjectDir.Path, s_relativePackagePath),
- type: "git",
+ type: "git",
commit: commitSha,
url: $"https://test.{host}/test-org/_git/{repoName}");
}
@@ -79,58 +80,28 @@ public void FullValidation_Https(string host)
[InlineData("vsts.me")]
public void FullValidation_Ssh(string host)
{
- // Test non - ascii characters and escapes in the URL.
- // Escaped URI reserved characters should remain escaped, non-reserved characters unescaped in the results.
- var repoUrl = $"ssh://test@vs-ssh.{host}:22/test-org/_ssh/test-%72epo{TestStrings.RepoName}";
- var repoName = $"test-repo{TestStrings.RepoNameEscaped}";
+ var repoUrl = $"ssh://user@vs-ssh.{host}:22/test-org/_ssh/test-repo";
var repo = GitUtilities.CreateGitRepository(ProjectDir.Path, new[] { ProjectFileName }, repoUrl);
var commitSha = repo.Head.Tip.Sha;
VerifyValues(
customProps: @"
-
- true
-
",
customTargets: "",
targets: new[]
{
- "Build", "Pack"
+ "Build",
},
expressions: new[]
{
"@(SourceRoot)",
- "@(SourceRoot->'%(SourceLinkUrl)')",
- "@(SourceRoot->'%(BranchName)')",
- "$(SourceLink)",
- "$(PrivateRepositoryUrl)",
- "$(RepositoryUrl)"
},
- expectedResults: new[]
+ expectedErrors: new[]
{
- NuGetPackageFolders,
- ProjectSourceRoot,
- $"https://test.{host}/test-org/_apis/git/repositories/{repoName}/items?api-version=1.0&versionType=commit&version={commitSha}&path=/*",
- "refs/heads/main",
- s_relativeSourceLinkJsonPath,
- $"https://test.{host}/test-org/_git/{repoName}",
- $"https://test.{host}/test-org/_git/{repoName}",
+ string.Format(Resources.RemoteUrlFormatNoLongerSupported,
+ $"ssh://git@vs-ssh.{host}:22/test-org/_ssh/test-repo")
});
-
- AssertEx.AreEqual(
- $@"{{""documents"":{{""{ProjectSourceRoot.Replace(@"\", @"\\")}*"":""https://test.{host}/test-org/_apis/git/repositories/{repoName}/items?api-version=1.0&versionType=commit&version={commitSha}&path=/*""}}}}",
- File.ReadAllText(Path.Combine(ProjectDir.Path, s_relativeSourceLinkJsonPath)));
-
- TestUtilities.ValidateAssemblyInformationalVersion(
- Path.Combine(ProjectDir.Path, s_relativeOutputFilePath),
- "1.0.0+" + commitSha);
-
- TestUtilities.ValidateNuSpecRepository(
- Path.Combine(ProjectDir.Path, s_relativePackagePath),
- type: "git",
- commit: commitSha,
- url: $"https://test.{host}/test-org/_git/{repoName}");
}
}
}
diff --git a/src/SourceLink.Git.IntegrationTests/CloudHostedProvidersTests.cs b/src/SourceLink.Git.IntegrationTests/CloudHostedProvidersTests.cs
index c1dc9f0d8e..e2b2805321 100644
--- a/src/SourceLink.Git.IntegrationTests/CloudHostedProvidersTests.cs
+++ b/src/SourceLink.Git.IntegrationTests/CloudHostedProvidersTests.cs
@@ -242,7 +242,7 @@ public void CustomTranslation()
{
// Test non-ascii characters and escapes in the URL.
// Escaped URI reserved characters should remain escaped, non-reserved characters unescaped in the results.
- var repoUrl = $"ssh://test@vs-ssh.visualstudio.com:22/test-org/_ssh/test-%72epo{TestStrings.RepoName}";
+ var repoUrl = $"ssh://test@vs-ssh.visualstudio.com/v3/account/test-org/test-%72epo{TestStrings.RepoName}";
var repoName = $"test-repo{TestStrings.RepoNameEscaped}";
var repo = GitUtilities.CreateGitRepository(ProjectDir.Path, new[] { ProjectFileName }, repoUrl);
diff --git a/src/SourceLink.Git.IntegrationTests/GitWebTests.cs b/src/SourceLink.Git.IntegrationTests/GitWebTests.cs
index 2a5a8aa7ff..3e71cd76a7 100644
--- a/src/SourceLink.Git.IntegrationTests/GitWebTests.cs
+++ b/src/SourceLink.Git.IntegrationTests/GitWebTests.cs
@@ -21,7 +21,7 @@ public void FullValidation_Ssh()
{
// Test non-ascii characters and escapes in the URL. Escaped URI reserved characters
// should remain escaped, non-reserved characters unescaped in the results.
- var repoUrl = $"ssh://git@{TestStrings.DomainName}.com/test-%72epo{TestStrings.RepoName}.git";
+ var repoUrl = $"ssh://user@{TestStrings.DomainName}.com/test-%72epo{TestStrings.RepoName}.git";
var repoName = $"test-repo{TestStrings.RepoNameEscaped}.git";
var repoNameFullyEscaped = $"test-repo{TestStrings.RepoNameFullyEscaped}.git";
@@ -58,6 +58,7 @@ public void FullValidation_Ssh()
$"https://{TestStrings.DomainName}.com/gitweb/?p={repoName};a=blob_plain;hb={commitSha};f=*",
"refs/heads/main",
s_relativeSourceLinkJsonPath,
+ // note that "user" was replaced with "git" to avoid leaking user info
$"ssh://git@{TestStrings.DomainName}.com/{repoNameFullyEscaped}",
$"ssh://git@{TestStrings.DomainName}.com/{repoNameFullyEscaped}"
});
@@ -70,6 +71,7 @@ public void FullValidation_Ssh()
Path.Combine(ProjectDir.Path, s_relativeOutputFilePath),
"1.0.0+" + commitSha);
+ // note that "user" was replaced with "git" to avoid leaking user info
TestUtilities.ValidateNuSpecRepository(
Path.Combine(ProjectDir.Path, s_relativePackagePath),
type: "git",
diff --git a/src/SourceLink.Git.IntegrationTests/Microsoft.SourceLink.Git.IntegrationTests.csproj b/src/SourceLink.Git.IntegrationTests/Microsoft.SourceLink.Git.IntegrationTests.csproj
index f17a92da30..bb0a432536 100644
--- a/src/SourceLink.Git.IntegrationTests/Microsoft.SourceLink.Git.IntegrationTests.csproj
+++ b/src/SourceLink.Git.IntegrationTests/Microsoft.SourceLink.Git.IntegrationTests.csproj
@@ -5,6 +5,7 @@
+
diff --git a/src/TestUtilities/DotNetSdk/DotNetSdkTestBase.cs b/src/TestUtilities/DotNetSdk/DotNetSdkTestBase.cs
index 8ff5323731..081b81f2fd 100644
--- a/src/TestUtilities/DotNetSdk/DotNetSdkTestBase.cs
+++ b/src/TestUtilities/DotNetSdk/DotNetSdkTestBase.cs
@@ -52,7 +52,7 @@ private static string GetLocalNuGetConfigContent(string packagesDir) =>
-
+