From e18ef2e2f01ea364140b04aea9091ead76842f43 Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Fri, 15 May 2026 14:56:52 -0700 Subject: [PATCH 1/5] Strip user name and password from repository URLs. Removes support for V1 and V2 formats of AzureRepos SSH URLs. These formats rely on the user info being present in the URL. --- .../AzureDevOps/AzureDevOpsUrlParser.cs | 20 ++++--- .../GitOperationsTests.cs | 38 ++++++------ .../GitOperations.cs | 21 +++++++ .../AzureDevOpsUrlParserHostedTests.cs | 60 +++++++++---------- ...Microsoft.SourceLink.AzureRepos.Git.csproj | 1 + src/SourceLink.AzureRepos.Git/Resources.resx | 3 + .../TranslateRepositoryUrls.cs | 11 +++- .../xlf/Resources.cs.xlf | 5 ++ .../xlf/Resources.de.xlf | 5 ++ .../xlf/Resources.es.xlf | 5 ++ .../xlf/Resources.fr.xlf | 5 ++ .../xlf/Resources.it.xlf | 5 ++ .../xlf/Resources.ja.xlf | 5 ++ .../xlf/Resources.ko.xlf | 5 ++ .../xlf/Resources.pl.xlf | 5 ++ .../xlf/Resources.pt-BR.xlf | 5 ++ .../xlf/Resources.ru.xlf | 5 ++ .../xlf/Resources.tr.xlf | 5 ++ .../xlf/Resources.zh-Hans.xlf | 5 ++ .../xlf/Resources.zh-Hant.xlf | 5 ++ .../AzureReposTests.cs | 47 +++------------ .../CloudHostedProvidersTests.cs | 2 +- .../GitWebTests.cs | 4 +- ...oft.SourceLink.Git.IntegrationTests.csproj | 1 + 24 files changed, 172 insertions(+), 101 deletions(-) diff --git a/src/Common/AzureDevOps/AzureDevOpsUrlParser.cs b/src/Common/AzureDevOps/AzureDevOpsUrlParser.cs index e90eb3cd35..277da79ecc 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,14 @@ public static bool TryParseHostedSsh(Uri uri, [NotNullWhen(true)]out string? acc // Failed to parse path return false; } - } - if (account.Length == 0) - { + // No longer supported since the format uses SSH connection user name as an account name, + // which means that user info could leak into Source Link. + 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..bb399a3cce 100644 --- a/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs +++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs @@ -224,23 +224,23 @@ 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")] public void NormalizeUrl_PlatformAgnostic2(string url, string? expectedUrl) { AssertEx.AreEqual(expectedUrl, GitOperations.NormalizeUrl(url, s_root)?.AbsoluteUri); @@ -269,14 +269,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 +398,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..52df73119b 100644 --- a/src/Microsoft.Build.Tasks.Git/GitOperations.cs +++ b/src/Microsoft.Build.Tasks.Git/GitOperations.cs @@ -164,7 +164,28 @@ 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, + }; + + return builder.Uri; + } + + 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..f57c8a1bfb 100644 --- a/src/SourceLink.AzureRepos.Git.UnitTests/AzureDevOpsUrlParserHostedTests.cs +++ b/src/SourceLink.AzureRepos.Git.UnitTests/AzureDevOpsUrlParserHostedTests.cs @@ -95,37 +95,33 @@ 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/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) + [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.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.True(isUnsupportedFormat); } [Theory] @@ -145,7 +141,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/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 @@ + From b90ed04c5777e75d5266cbbdb4715ccaa25f8ddd Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Tue, 19 May 2026 09:25:33 -0700 Subject: [PATCH 2/5] Feedback --- src/Common/AzureDevOps/AzureDevOpsUrlParser.cs | 5 +++-- .../AzureDevOpsUrlParserHostedTests.cs | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Common/AzureDevOps/AzureDevOpsUrlParser.cs b/src/Common/AzureDevOps/AzureDevOpsUrlParser.cs index 277da79ecc..0b22fe9038 100644 --- a/src/Common/AzureDevOps/AzureDevOpsUrlParser.cs +++ b/src/Common/AzureDevOps/AzureDevOpsUrlParser.cs @@ -154,8 +154,9 @@ public static bool TryParseHostedSsh( return false; } - // No longer supported since the format uses SSH connection user name as an account name, - // which means that user info could leak into Source Link. + // 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; } diff --git a/src/SourceLink.AzureRepos.Git.UnitTests/AzureDevOpsUrlParserHostedTests.cs b/src/SourceLink.AzureRepos.Git.UnitTests/AzureDevOpsUrlParserHostedTests.cs index f57c8a1bfb..a1fa02d7dd 100644 --- a/src/SourceLink.AzureRepos.Git.UnitTests/AzureDevOpsUrlParserHostedTests.cs +++ b/src/SourceLink.AzureRepos.Git.UnitTests/AzureDevOpsUrlParserHostedTests.cs @@ -124,6 +124,15 @@ public void TryParseHostedSshV1V2_Success(string url) Assert.True(isUnsupportedFormat); } + [Theory] + [InlineData("ssh://account@vs-ssh.visualstudio.com/v4/team/_ssh")] + public void TryParseHostedSshV1V2_Error(string url) + { + Assert.False(AzureDevOpsUrlParser.TryParseHostedSsh( + new Uri(url, UriKind.Absolute), out _, out _, out _, out var isUnsupportedFormat)); + Assert.False(isUnsupportedFormat); + } + [Theory] [InlineData("ssh://account1@vs-ssh.visualstudio.com/v3/account2/project/repo", "account2", "project", "repo")] [InlineData("ssh://account1@vs-ssh.visualstudio.com/v3/account2/project/team/repo", "account2", "project/team", "repo")] From 0c5eeb5ebe582d3a1cdab8fea4d3d3657d9f2b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Matou=C5=A1ek?= Date: Tue, 30 Jun 2026 14:18:30 -0700 Subject: [PATCH 3/5] Handle invalid URIs --- .../GitOperationsTests.cs | 3 +++ src/Microsoft.Build.Tasks.Git/GitOperations.cs | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs index bb399a3cce..ccb95432cc 100644 --- a/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs +++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs @@ -241,6 +241,9 @@ public void NormalizeUrl_PlatformAgnostic1(string url, string expected) [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); diff --git a/src/Microsoft.Build.Tasks.Git/GitOperations.cs b/src/Microsoft.Build.Tasks.Git/GitOperations.cs index 52df73119b..b135b60d5e 100644 --- a/src/Microsoft.Build.Tasks.Git/GitOperations.cs +++ b/src/Microsoft.Build.Tasks.Git/GitOperations.cs @@ -182,7 +182,14 @@ private static bool IsSupportedScheme(string scheme) Password = null, }; - return builder.Uri; + try + { + return builder.Uri; + } + catch + { + return null; + } } private static Uri? NormalizeUrlImpl(string url, string root) From 7a775f5f5b5033c628461bbd385ed2bee5573914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Matou=C5=A1ek?= Date: Tue, 30 Jun 2026 15:13:49 -0700 Subject: [PATCH 4/5] Fix test --- .../TranslateRepositoryUrlsTests.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) 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); From 8f36a6e927275b8f213a00daf77ee299aae7fc13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Matou=C5=A1ek?= Date: Wed, 1 Jul 2026 10:51:11 -0700 Subject: [PATCH 5/5] Use dotnet-public instead of nuget.org --- src/TestUtilities/DotNetSdk/DotNetSdkTestBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) => - +