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) => - +