Skip to content

Commit

Permalink
Support base URL in BitBucket Enterprise URL format (#475)
Browse files Browse the repository at this point in the history
  • Loading branch information
tmat committed Oct 31, 2019
1 parent 446d5be commit d5ae980
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 51 deletions.
Expand Up @@ -21,7 +21,7 @@
output property is set to the content URL corresponding to the domain, otherwise it is set to string "N/A".
Recognized domains are specified via Hosts (initialized from SourceLinkAzureReposGitHost item group).
In addition SourceLinkHasSingleProvider is true an iplicit host is parsed from RepositoryUrl.
In addition, if SourceLinkHasSingleProvider is true an implicit host is parsed from RepositoryUrl.
ContentUrl is optional. If not specified it defaults to "https://{domain}" or "http://{domain}", based on the scheme of SourceRoot.RepositoryUrl.
-->
Expand Down
72 changes: 58 additions & 14 deletions src/SourceLink.Bitbucket.Git.UnitTests/GetSourceLinkUrlTests.cs
@@ -1,4 +1,5 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Build.Tasks.SourceControl;
using TestUtilities;
using Xunit;
Expand All @@ -8,11 +9,6 @@ namespace Microsoft.SourceLink.Bitbucket.Git.UnitTests
{
public class GetSourceLinkUrlTests
{
private const string ExpectedUrlForCloudEdition =
"https://api.domain.com/x/y/2.0/repositories/a/b/src/0123456789abcdefABCDEF000000000000000000/*";
private const string ExpectedUrlForEnterpriseEditionOldVersion = "https://bitbucket.domain.com/projects/a/repos/b/browse/*?at=0123456789abcdefABCDEF000000000000000000&raw";
private const string ExpectedUrlForEnterpriseEditionNewVersion = "https://bitbucket.domain.com/projects/a/repos/b/raw/*?at=0123456789abcdefABCDEF000000000000000000";

[Fact]
public void EmptyHosts()
{
Expand All @@ -32,31 +28,79 @@ public void EmptyHosts()
Assert.False(result);
}

[Theory]
[InlineData("a/b", "", "a", "b")]
[InlineData("/a/b", "", "a", "b")]
[InlineData("/a/b/", "", "a", "b")]
[InlineData("scm/a", "", "scm", "a")]
[InlineData("scm/a/b", "", "a", "b")]
[InlineData("/r/scm/a/b", "r", "a", "b")]
[InlineData("/r/s/scm/a/b", "r/s", "a", "b")]
[InlineData("/r/s/a/b", "r/s", "a", "b")]
[InlineData("/r/s/scm/b", "r/s", "scm", "b")]
public void TryParseEnterpriseUrl(string relativeUrl, string expectedBaseUrl, string expectedProjectName, string expectedRepositoryName)
{
Assert.True(GetSourceLinkUrl.TryParseEnterpriseUrl(relativeUrl, out var baseUrl, out var projectName, out var repositoryName));
Assert.Equal(expectedBaseUrl, baseUrl);
Assert.Equal(expectedProjectName, projectName);
Assert.Equal(expectedRepositoryName, repositoryName);
}

[Theory]
[InlineData("")]
[InlineData("/")]
[InlineData("x")]
public void TryParseEnterpriseUrl_Errors(string relativeUrl)
{
Assert.False(GetSourceLinkUrl.TryParseEnterpriseUrl(relativeUrl, out _, out _, out _));
}

[Theory]
[InlineData("", "")]
[InlineData("", "/")]
[InlineData("/", "")]
[InlineData("/", "/")]
public void BuildSourceLinkUrl_BitbucketCloud(string s1, string s2)
{
var isEnterpriseEditionSetting = KVP("EnterpriseEdition", "false");
var engine = new MockEngine();
var task = new GetSourceLinkUrl()
{
BuildEngine = engine,
SourceRoot = new MockItem("/src/", KVP("RepositoryUrl", "http://subdomain.mybitbucket.org:100/a/b" + s1), KVP("SourceControl", "git"), KVP("RevisionId", "0123456789abcdefABCDEF000000000000000000")),
Hosts = new[]
{
new MockItem("mybitbucket.org", KVP("ContentUrl", "https://domain.com/x/y" + s2), isEnterpriseEditionSetting),
new MockItem("mybitbucket.org", KVP("ContentUrl", "https://domain.com/x/y" + s2), KVP("EnterpriseEdition", "false")),
}
};

bool result = task.Execute();
AssertEx.AssertEqualToleratingWhitespaceDifferences("", engine.Log);
AssertEx.AreEqual(ExpectedUrlForCloudEdition, task.SourceLinkUrl);
AssertEx.AreEqual("https://api.domain.com/x/y/2.0/repositories/a/b/src/0123456789abcdefABCDEF000000000000000000/*", task.SourceLinkUrl);
Assert.True(result);
}

[Fact]
public void BuildSourceLinkUrl_BitbucketEnterprise_InvalidUrl()
{
var engine = new MockEngine();
var task = new GetSourceLinkUrl()
{
BuildEngine = engine,
SourceRoot = new MockItem("/src/", KVP("RepositoryUrl", "http://subdomain.mybitbucket.org/a"), KVP("SourceControl", "git"), KVP("RevisionId", "0123456789abcdefABCDEF000000000000000000")),
Hosts = new[]
{
new MockItem("mybitbucket.org", KVP("ContentUrl", "https://domain.com/x/y")),
}
};

bool result = task.Execute();

AssertEx.AssertEqualToleratingWhitespaceDifferences(
"ERROR : " + string.Format(CommonResources.ValueOfWithIdentityIsInvalid, "SourceRoot.RepositoryUrl", "/src/", "http://subdomain.mybitbucket.org/a"), engine.Log);

Assert.False(result);
}

[Fact]
public void BuildSourceLinkUrl_MetadataWithEnterpriseEditionButWithoutVersion_UseNewVersionAsDefauld()
{
Expand All @@ -74,7 +118,7 @@ public void BuildSourceLinkUrl_MetadataWithEnterpriseEditionButWithoutVersion_Us

bool result = task.Execute();
AssertEx.AssertEqualToleratingWhitespaceDifferences("", engine.Log);
AssertEx.AreEqual(ExpectedUrlForEnterpriseEditionNewVersion, task.SourceLinkUrl);
AssertEx.AreEqual("https://bitbucket.domain.com/projects/a/repos/b/raw/*?at=0123456789abcdefABCDEF000000000000000000", task.SourceLinkUrl);
Assert.True(result);
}

Expand Down Expand Up @@ -104,7 +148,7 @@ public void BuildSourceLinkUrl_BitbucketEnterpriseOldVersionSsh(string s1, strin

bool result = task.Execute();
AssertEx.AssertEqualToleratingWhitespaceDifferences("", engine.Log);
AssertEx.AreEqual(ExpectedUrlForEnterpriseEditionOldVersion, task.SourceLinkUrl);
AssertEx.AreEqual("https://bitbucket.domain.com/projects/a/repos/b/browse/*?at=0123456789abcdefABCDEF000000000000000000&raw", task.SourceLinkUrl);
Assert.True(result);
}

Expand All @@ -125,7 +169,7 @@ public void BuildSourceLinkUrl_BitbucketEnterpriseOldVersionHttps(string s1, str
var task = new GetSourceLinkUrl()
{
BuildEngine = engine,
SourceRoot = new MockItem("/src/", KVP("RepositoryUrl", "http://bitbucket.domain.com:100/scm/a/b" + s1), KVP("SourceControl", "git"), KVP("RevisionId", "0123456789abcdefABCDEF000000000000000000")),
SourceRoot = new MockItem("/src/", KVP("RepositoryUrl", "http://bitbucket.domain.com:100/base/scm/a/b" + s1), KVP("SourceControl", "git"), KVP("RevisionId", "0123456789abcdefABCDEF000000000000000000")),
Hosts = new[]
{
new MockItem("domain.com", KVP("ContentUrl", "https://bitbucket.domain.com" + s2), isEnterpriseEditionSetting, version),
Expand All @@ -134,7 +178,7 @@ public void BuildSourceLinkUrl_BitbucketEnterpriseOldVersionHttps(string s1, str

bool result = task.Execute();
AssertEx.AssertEqualToleratingWhitespaceDifferences("", engine.Log);
AssertEx.AreEqual(ExpectedUrlForEnterpriseEditionOldVersion, task.SourceLinkUrl);
AssertEx.AreEqual("https://bitbucket.domain.com/base/projects/a/repos/b/browse/*?at=0123456789abcdefABCDEF000000000000000000&raw", task.SourceLinkUrl);
Assert.True(result);
}

Expand Down Expand Up @@ -165,7 +209,7 @@ public void BuildSourceLinkUrl_BitbucketEnterpriseNewVersionSsh(string s1, strin

bool result = task.Execute();
AssertEx.AssertEqualToleratingWhitespaceDifferences("", engine.Log);
AssertEx.AreEqual(ExpectedUrlForEnterpriseEditionNewVersion, task.SourceLinkUrl);
AssertEx.AreEqual("https://bitbucket.domain.com/projects/a/repos/b/raw/*?at=0123456789abcdefABCDEF000000000000000000", task.SourceLinkUrl);
Assert.True(result);
}

Expand Down Expand Up @@ -196,7 +240,7 @@ public void BuildSourceLinkUrl_BitbucketEnterpriseNewVersionHttps(string s1, str

bool result = task.Execute();
AssertEx.AssertEqualToleratingWhitespaceDifferences("", engine.Log);
AssertEx.AreEqual(ExpectedUrlForEnterpriseEditionNewVersion, task.SourceLinkUrl);
AssertEx.AreEqual("https://bitbucket.domain.com/projects/a/repos/b/raw/*?at=0123456789abcdefABCDEF000000000000000000", task.SourceLinkUrl);
Assert.True(result);
}

Expand Down
87 changes: 54 additions & 33 deletions src/SourceLink.Bitbucket.Git/GetSourceLinkUrl.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Diagnostics;
using Microsoft.Build.Framework;
using Microsoft.Build.Tasks.SourceControl;

Expand All @@ -18,33 +19,68 @@ public sealed class GetSourceLinkUrl : GetSourceLinkUrlGitTask

private const string IsEnterpriseEditionMetadataName = "EnterpriseEdition";
private const string VersionMetadataName = "Version";
private const string VersionWithNewUrlFormat = "4.7";
private static readonly Version s_versionWithNewUrlFormat = new Version(4, 7);

protected override string BuildSourceLinkUrl(Uri contentUri, Uri gitUri, string relativeUrl, string revisionId, ITaskItem hostItem)
{
return
bool.TryParse(hostItem?.GetMetadata(IsEnterpriseEditionMetadataName), out var isEnterpriseEdition) && !isEnterpriseEdition
? BuildSourceLinkUrlForCloudEdition(contentUri, relativeUrl, revisionId)
: BuildSourceLinkUrlForEnterpriseEdition(contentUri, relativeUrl, revisionId, hostItem);
// The SourceLinkBitbucketGitHost item for bitbucket.org specifies EnterpriseEdition="false".
// Other items that may be specified by the project default to EnterpriseEdition="true" without specifying it.
bool isCloud = bool.TryParse(hostItem?.GetMetadata(IsEnterpriseEditionMetadataName), out var isEnterpriseEdition) && !isEnterpriseEdition;

if (isCloud)
{
return BuildSourceLinkUrlForCloudEdition(contentUri, relativeUrl, revisionId);
}

if (TryParseEnterpriseUrl(relativeUrl, out var relativeBaseUrl, out var projectName, out var repositoryName))
{
var version = GetBitbucketEnterpriseVersion(hostItem);
return BuildSourceLinkUrlForEnterpriseEdition(contentUri, relativeBaseUrl, projectName, repositoryName, revisionId, version);
}

Log.LogError(CommonResources.ValueOfWithIdentityIsInvalid, Names.SourceRoot.RepositoryUrlFullName, SourceRoot.ItemSpec, gitUri);
return null;
}

private string BuildSourceLinkUrlForEnterpriseEdition(Uri contentUri, string relativeUrl, string revisionId,
ITaskItem hostItem)
internal static string BuildSourceLinkUrlForEnterpriseEdition(
Uri contentUri,
string relativeBaseUrl,
string projectName,
string repositoryName,
string commitSha,
Version version)
{
var bitbucketEnterpriseVersion = GetBitbucketEnterpriseVersion(hostItem);
var relativeUrl = (version >= s_versionWithNewUrlFormat) ?
$"projects/{projectName}/repos/{repositoryName}/raw/*?at={commitSha}" :
$"projects/{projectName}/repos/{repositoryName}/browse/*?at={commitSha}&raw";

var splits = relativeUrl.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries);
var isSshRepoUri = !(splits.Length == 3 && splits[0] == "scm");
var projectName = isSshRepoUri ? splits[0] : splits[1];
var repositoryName = isSshRepoUri ? splits[1] : splits[2];
return UriUtilities.Combine(contentUri.ToString(), UriUtilities.Combine(relativeBaseUrl, relativeUrl));
}

internal static bool TryParseEnterpriseUrl(string relativeUrl, out string relativeBaseUrl, out string projectName, out string repositoryName)
{
// HTTP: {baseUrl}/scm/{projectName}/{repositoryName}
// SSH: {baseUrl}/{projectName}/{repositoryName}

if (!UriUtilities.TrySplitRelativeUrl(relativeUrl, out var parts) || parts.Length < 2)
{
relativeBaseUrl = projectName = repositoryName = null;
return false;
}

var relativeUrlForBitbucketEnterprise =
GetRelativeUrlForBitbucketEnterprise(projectName, repositoryName, revisionId,
bitbucketEnterpriseVersion);
var i = parts.Length - 1;

var result = UriUtilities.Combine(contentUri.ToString(), relativeUrlForBitbucketEnterprise);
repositoryName = parts[i--];
projectName = parts[i--];

return result;
if (i >= 0 && parts[i] == "scm")
{
i--;
}

Debug.Assert(i >= -1);
relativeBaseUrl = string.Join("/", parts, 0, i + 1);
return true;
}

private Version GetBitbucketEnterpriseVersion(ITaskItem hostItem)
Expand All @@ -63,7 +99,7 @@ private Version GetBitbucketEnterpriseVersion(ITaskItem hostItem)
}
else
{
bitbucketEnterpriseVersion = Version.Parse(VersionWithNewUrlFormat);
bitbucketEnterpriseVersion = s_versionWithNewUrlFormat;
}

return bitbucketEnterpriseVersion;
Expand All @@ -79,20 +115,5 @@ private static string BuildSourceLinkUrlForCloudEdition(Uri contentUri, string r

return UriUtilities.Combine(apiUriBuilder.Uri.ToString(), relativeApiUrl);
}

private static string GetRelativeUrlForBitbucketEnterprise(string projectName, string repositoryName, string commitId, Version bitbucketVersion)
{
string relativeUrl;
if (bitbucketVersion >= Version.Parse(VersionWithNewUrlFormat))
{
relativeUrl = $"projects/{projectName}/repos/{repositoryName}/raw/*?at={commitId}";
}
else
{
relativeUrl = $"projects/{projectName}/repos/{repositoryName}/browse/*?at={commitId}&raw";
}

return relativeUrl;
}
}
}
Expand Up @@ -21,7 +21,7 @@
output property is set to the content URL corresponding to the domain, otherwise it is set to string "N/A".
Recognized domains are specified via Hosts (initialized from SourceLinkBitbucketGitHost item group).
In addition SourceLinkHasSingleProvider is true an iplicit host is parsed from RepositoryUrl.
In addition, if SourceLinkHasSingleProvider is true an implicit host is parsed from RepositoryUrl.
Example of SourceLinkBitbucketGitHost items:
Expand Down
Expand Up @@ -21,7 +21,7 @@
output property is set to the content URL corresponding to the domain, otherwise it is set to string "N/A".
Recognized domains are specified via Hosts (initialized from SourceLinkGitHubHost item group).
In addition SourceLinkHasSingleProvider is true an iplicit host is parsed from RepositoryUrl.
In addition, if SourceLinkHasSingleProvider is true an implicit host is parsed from RepositoryUrl.
Example of SourceLinkGitHubHost items:
Expand Down
Expand Up @@ -21,7 +21,7 @@
output property is set to the content URL corresponding to the domain, otherwise it is set to string "N/A".
Recognized domains are specified via Hosts (initialized from SourceLinkGitLabHost item group).
In addition SourceLinkHasSingleProvider is true an iplicit host is parsed from RepositoryUrl.
In addition, if SourceLinkHasSingleProvider is true an implicit host is parsed from RepositoryUrl.
Example of SourceLinkGitLabHost items:
Expand Down

0 comments on commit d5ae980

Please sign in to comment.