From bf176f762519d1d1627f0306eb9dec8472b8b272 Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Fri, 15 May 2026 14:56:52 -0700 Subject: [PATCH 1/6] 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. --- src/Common/AzureDevOpsUrlParser.cs | 41 +++++-------- .../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 | 41 +++---------- .../CloudHostedProvidersTests.cs | 2 +- .../GitWebTests.cs | 4 +- ...oft.SourceLink.Git.IntegrationTests.csproj | 1 + 24 files changed, 173 insertions(+), 115 deletions(-) diff --git a/src/Common/AzureDevOpsUrlParser.cs b/src/Common/AzureDevOpsUrlParser.cs index e90eb3cd35..7648db664c 100644 --- a/src/Common/AzureDevOpsUrlParser.cs +++ b/src/Common/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,38 +131,21 @@ 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} account = parts[1]; + return account.Length > 0; } - else - { - // ssh v1/v2 url formats - // ssh://{account}@vs-ssh.visualstudio.com/ - account = uri.UserInfo; + // ssh v1/v2 url formats + // ssh://{account}@vs-ssh.visualstudio.com/ - int index = 0; - if (StringComparer.OrdinalIgnoreCase.Equals(parts[0], "DefaultCollection")) - { - index++; - } - - if (!TryParsePath(parts, index, "_ssh", out repositoryPath, out repositoryName)) - { - // Failed to parse path - return false; - } - } - - if (account.Length == 0) - { - return false; - } - - return true; + // 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; } 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 7a9122d2c3..defaa06def 100644 --- a/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs +++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs @@ -222,23 +222,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) { - Assert.Equal(url, GitOperations.NormalizeUrl(url, s_root)?.AbsoluteUri); + Assert.Equal(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/\u1234", "http://github.com/org/%E1%88%B4")] - [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) { Assert.Equal(expectedUrl, GitOperations.NormalizeUrl(url, s_root)?.AbsoluteUri); @@ -298,14 +298,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); } @@ -409,7 +409,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 36fe133367..32d210da4b 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 e8ef360d5d..a9b8a42953 100644 --- a/src/SourceLink.AzureRepos.Git/Microsoft.SourceLink.AzureRepos.Git.csproj +++ b/src/SourceLink.AzureRepos.Git/Microsoft.SourceLink.AzureRepos.Git.csproj @@ -30,5 +30,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 c9327fcc29..23d579cc21 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") { } @@ -77,56 +78,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\u1234%24%2572%2F"; - var repoName = "test-repo\u1234%24%2572%2F"; + 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)')", - "$(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=/*", - 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 58129d0b33..81821922e7 100644 --- a/src/SourceLink.Git.IntegrationTests/CloudHostedProvidersTests.cs +++ b/src/SourceLink.Git.IntegrationTests/CloudHostedProvidersTests.cs @@ -234,7 +234,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\u1234%24%2572%2F"; + var repoUrl = "ssh://test@vs-ssh.visualstudio.com/v3/account/test-org/test-%72epo\u1234%24%2572%2F"; var repoName = "test-repo\u1234%24%2572%2F"; 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 3f9a54e26a..f893518ea0 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@噸.com/test-%72epo\u1234%24%2572%2F.git"; + var repoUrl = $"ssh://user@噸.com/test-%72epo\u1234%24%2572%2F.git"; var repoName = "test-repo\u1234%24%2572%2F.git"; var repo = GitUtilities.CreateGitRepository(ProjectDir.Path, new[] { ProjectFileName }, repoUrl); @@ -55,6 +55,7 @@ public void FullValidation_Ssh() ProjectSourceRoot, $"https://噸.com/gitweb/?p={repoName};a=blob_plain;hb={commitSha};f=*", s_relativeSourceLinkJsonPath, + // note that "user" was replaced with "git" to avoid leaking user info $"ssh://git@噸.com/{repoName}", $"ssh://git@噸.com/{repoName}" }); @@ -67,6 +68,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 379995982eb55fcb5319c15680bf8cd95caedb08 Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Tue, 19 May 2026 09:25:33 -0700 Subject: [PATCH 2/6] Feedback --- src/Common/AzureDevOpsUrlParser.cs | 30 ++++++++++++++----- .../AzureDevOpsUrlParserHostedTests.cs | 9 ++++++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/Common/AzureDevOpsUrlParser.cs b/src/Common/AzureDevOpsUrlParser.cs index 7648db664c..0b22fe9038 100644 --- a/src/Common/AzureDevOpsUrlParser.cs +++ b/src/Common/AzureDevOpsUrlParser.cs @@ -136,16 +136,32 @@ public static bool TryParseHostedSsh( { // ssh://{user}@{domain}:{port}/v3/{account}/{repositoryPath}/{'_full'|'_optimized'|''}/{repositoryName} account = parts[1]; - return account.Length > 0; } + else + { + // ssh v1/v2 url formats + // ssh://{account}@vs-ssh.visualstudio.com/ - // ssh v1/v2 url formats - // ssh://{account}@vs-ssh.visualstudio.com/ + int index = 0; + if (StringComparer.OrdinalIgnoreCase.Equals(parts[0], "DefaultCollection")) + { + index++; + } - // 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; + if (!TryParsePath(parts, index, "_ssh", out repositoryPath, out repositoryName)) + { + // Failed to parse path + return false; + } + + // 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 account.Length > 0; } public static bool TryParseOnPremSsh(Uri uri, [NotNullWhen(true)]out string? repositoryPath, [NotNullWhen(true)]out string? repositoryName) 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 6eb26daf9fba7068cdb472f8e3bc52b2a78c2bd2 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/6] Handle invalid URIs --- .../GitOperationsTests.cs | 6 +++--- src/Microsoft.Build.Tasks.Git/GitOperations.cs | 9 ++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs index defaa06def..41a2548444 100644 --- a/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs +++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs @@ -266,9 +266,9 @@ public void NormalizeUrl_PlatformAgnostic2(string url, string expectedUrl) [InlineData(@"a:/../../relative/path", "file:///a:/relative/path")] [InlineData(@"Z:/a/b/../../relative/path", "file:///Z:/relative/path")] [InlineData(@"../.://../../relative/path", "file:///C:/src/a/relative/path")] - [InlineData(@"../.:./../../relative/path", "ssh://../relative/path")] - [InlineData(@".:/../../relative/path", "ssh://./relative/path")] - [InlineData(@"..:/../../relative/path", "ssh://../relative/path")] + [InlineData(@"../.:./../../relative/path", null)] + [InlineData(@".:/../../relative/path", null)] + [InlineData(@"..:/../../relative/path", null)] [InlineData(@"@:org/repo", "file:///C:/src/a/b/@:org/repo")] public void NormalizeUrl_Windows(string url, string expectedUrl) { diff --git a/src/Microsoft.Build.Tasks.Git/GitOperations.cs b/src/Microsoft.Build.Tasks.Git/GitOperations.cs index 32d210da4b..a8ea2e5258 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 0efef1fc2c4312ebd128a424ebb85d975d4559a6 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/6] 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 9fe12754f147841c5dcf4f92a4b434e990aa500e 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/6] 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) => - + From 35ba03117699ac320548243cd7435c02e6cf550a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Matou=C5=A1ek?= Date: Thu, 13 Aug 2026 12:30:33 -0700 Subject: [PATCH 6/6] Fix Git URL normalization test expectations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4a5c307-b052-42d2-9d6d-9db7c9ca7770 --- .../GitOperationsTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs index 41a2548444..2a9619fce5 100644 --- a/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs +++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs @@ -276,7 +276,7 @@ public void NormalizeUrl_Windows(string url, string expectedUrl) } [ConditionalTheory(typeof(UnixOnly))] - [InlineData(@"C:org/repo", @"ssh://c/org/repo")] + [InlineData(@"C:org/repo", @"ssh://git@c/org/repo")] [InlineData(@"/xyz/src", @"file:///xyz/src")] // [InlineData(@"/a%20b", @"file:///a%2520b")] // https://github.com/dotnet/sourcelink/issues/439 [InlineData(@"\path\a\b", @"file:///path/a/b")] @@ -288,9 +288,9 @@ public void NormalizeUrl_Windows(string url, string expectedUrl) // [InlineData(@"../relative/path*<>|\0%00", @"file:///usr/src/a/relative/path*%3C%3E%7C/0%2500")] // https://github.com/dotnet/sourcelink/issues/439 [InlineData(@"../../../../relative/path", @"file:///relative/path")] [InlineData(@"../.://../../relative/path", "file:///usr/src/a/relative/path")] - [InlineData(@"../.:./../../relative/path", "ssh://../relative/path")] - [InlineData(@".:/../../relative/path", "ssh://./relative/path")] - [InlineData(@"..:/../../relative/path", "ssh://../relative/path")] + [InlineData(@"../.:./../../relative/path", "ssh://git@../relative/path")] + [InlineData(@".:/../../relative/path", "ssh://git@./relative/path")] + [InlineData(@"..:/../../relative/path", "ssh://git@../relative/path")] [InlineData(@"@:org/repo", @"file:///usr/src/a/b/@:org/repo")] public void NormalizeUrl_Unix(string url, string expectedUrl) {