Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions src/Common/AzureDevOps/AzureDevOpsUrlParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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}
Expand All @@ -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"))
{
Expand All @@ -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)
Expand Down
41 changes: 22 additions & 19 deletions src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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));
Expand Down
28 changes: 28 additions & 0 deletions src/Microsoft.Build.Tasks.Git/GitOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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);
Expand Down
Loading
Loading