Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bitbucket enterprise support #246

Merged
merged 10 commits into from May 17, 2019
13 changes: 12 additions & 1 deletion README.md
Expand Up @@ -92,14 +92,25 @@ For projects hosted by [GitLab](https://gitlab.com) reference [Microsoft.SourceL

### Bitbucket.org

For projects hosted on [Bitbucket.org](https://bitbucket.org) in git repositories reference [Microsoft.SourceLink.Bitbucket.Git](https://www.nuget.org/packages/Microsoft.SourceLink.Bitbucket.Git) package:
For projects hosted on [Bitbucket.org](https://bitbucket.org) in git repositories reference [Microsoft.SourceLink.Bitbucket.Git](https://www.nuget.org/packages/Microsoft.SourceLink.Bitbucket.Git) package:

```xml
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.Bitbucket.Git" Version="1.0.0-beta2-18618-05" PrivateAssets="All"/>
</ItemGroup>
```

For self-hosted Bitbucket projects reference [Microsoft.SourceLink.Bitbucket.Git](https://www.nuget.org/packages/Microsoft.SourceLink.Bitbucket.Git) package and add Bitbucket host configuration.
Additionally there are two optional attributes with default values:
EnterpriseEdition="true" - flag whether it is Enterprise Edtition or Cloud Edition
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edtition [](start = 57, length = 8)

typo: Edtition

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think markdown will format these as you intended - on separate lines.


In reply to: 283563555 [](ancestors = 283563555)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's include the EnterpriseEdition and Version parameters to the example below.


In reply to: 283563725 [](ancestors = 283563725,283563555)

Version="4.7" - for Enterprise Edition provides its version

```xml
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.Bitbucket.Git" Version="1.0.0-beta2-18618-05" PrivateAssets="All"/>
<SourceLinkBitbucketGitHost Include="bitbucket.yourdomain.com"/>
</ItemGroup>
```
### Multiple providers, repositories with submodules

If your repository contains submodules hosted by other git providers reference packages of all these providers. For example, projects in a repository hosted by Azure DevOps that links a GitHub repository via a submodule should reference both [Microsoft.SourceLink.Vsts.Git](https://www.nuget.org/packages/Microsoft.SourceLink.Vsts.Git) and [Microsoft.SourceLink.GitHub](https://www.nuget.org/packages/Microsoft.SourceLink.GitHub) packages. [Additional configuration](https://github.com/dotnet/sourcelink/blob/master/docs/README.md#configuring-projects-with-multiple-sourcelink-providers) might be needed if multiple Source Link packages are used in the project.
Expand Down
199 changes: 195 additions & 4 deletions src/SourceLink.Bitbucket.Git.UnitTests/GetSourceLinkUrlTests.cs
Expand Up @@ -8,6 +8,11 @@ namespace Microsoft.SourceLink.Bitbucket.Git.UnitTests
{
public class GetSourceLinkUrlTests
{
private const string ExpectedUrlForCloudEdition =
"https://domain.com/x/y/a/b/raw/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 @@ -27,29 +32,215 @@ public void EmptyHosts()
Assert.False(result);
}

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

bool result = task.Execute();
AssertEx.AssertEqualToleratingWhitespaceDifferences("", engine.Log);
AssertEx.AreEqual(ExpectedUrlForCloudEdition, task.SourceLinkUrl);
Assert.True(result);
}

[Theory]
[InlineData("", "")]
[InlineData("", "/")]
[InlineData("/", "")]
[InlineData("/", "/")]
public void BuildSourceLinkUrl(string s1, string s2)
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)),
new MockItem("mybitbucket.org", KVP("ContentUrl", "https://domain.com/x/y" + s2), isEnterpriseEditionSetting),
}
};

bool result = task.Execute();
AssertEx.AssertEqualToleratingWhitespaceDifferences("", engine.Log);
AssertEx.AreEqual(ExpectedUrlForCloudEdition, task.SourceLinkUrl);
Assert.True(result);
}

[Fact]
public void BuildSourceLinkUrl_MetadataWithEnterpriseEditionButWithoutVersion_UseNewVersionAsDefauld()
{
var isEnterpriseEditionSetting = KVP("EnterpriseEdition", "true");
var engine = new MockEngine();
var task = new GetSourceLinkUrl()
{
BuildEngine = engine,
SourceRoot = new MockItem("/src/", KVP("RepositoryUrl", "http://bitbucket.domain.com:100/a/b"), KVP("SourceControl", "git"), KVP("RevisionId", "0123456789abcdefABCDEF000000000000000000")),
Hosts = new[]
{
new MockItem("domain.com", KVP("ContentUrl", "https://bitbucket.domain.com"), isEnterpriseEditionSetting),
}
};

bool result = task.Execute();
AssertEx.AssertEqualToleratingWhitespaceDifferences("", engine.Log);
AssertEx.AreEqual(ExpectedUrlForEnterpriseEditionNewVersion, task.SourceLinkUrl);
Assert.True(result);
}

[Theory]
[InlineData("", "", "4.4")]
[InlineData("", "/", "4.4")]
[InlineData("/", "", "4.4")]
[InlineData("/", "/", "4.4")]
[InlineData("", "", "4.6")]
[InlineData("", "/", "4.6")]
[InlineData("/", "", "4.6")]
[InlineData("/", "/", "4.6")]
public void BuildSourceLinkUrl_BitbucketEnterpriseOldVersionSsh(string s1, string s2, string bitbucketVersion)
{
var isEnterpriseEditionSetting = KVP("EnterpriseEdition", "true");
var version = KVP("Version", bitbucketVersion);
var engine = new MockEngine();
var task = new GetSourceLinkUrl()
{
BuildEngine = engine,
SourceRoot = new MockItem("/src/", KVP("RepositoryUrl", "http://bitbucket.domain.com:100/a/b" + s1), KVP("SourceControl", "git"), KVP("RevisionId", "0123456789abcdefABCDEF000000000000000000")),
Hosts = new[]
{
new MockItem("domain.com", KVP("ContentUrl", "https://bitbucket.domain.com" + s2), isEnterpriseEditionSetting, version),
}
};

bool result = task.Execute();
AssertEx.AssertEqualToleratingWhitespaceDifferences("", engine.Log);
AssertEx.AreEqual(ExpectedUrlForEnterpriseEditionOldVersion, task.SourceLinkUrl);
Assert.True(result);
}

[Theory]
[InlineData("", "", "4.4")]
[InlineData("", "/", "4.4")]
[InlineData("/", "", "4.4")]
[InlineData("/", "/", "4.4")]
[InlineData("", "", "4.6")]
[InlineData("", "/", "4.6")]
[InlineData("/", "", "4.6")]
[InlineData("/", "/", "4.6")]
public void BuildSourceLinkUrl_BitbucketEnterpriseOldVersionHttps(string s1, string s2, string bitbucketVersion)
{
var isEnterpriseEditionSetting = KVP("EnterpriseEdition", "true");
var version = KVP("Version", bitbucketVersion);
var engine = new MockEngine();
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")),
Hosts = new[]
{
new MockItem("domain.com", KVP("ContentUrl", "https://bitbucket.domain.com" + s2), isEnterpriseEditionSetting, version),
}
};

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

[Theory]
[InlineData("", "", "")]
[InlineData("", "", "4.7")]
[InlineData("", "/", "4.7")]
[InlineData("/", "", "4.7")]
[InlineData("/", "/", "4.7")]
[InlineData("", "", "5.6")]
[InlineData("", "/", "5.6")]
[InlineData("/", "", "5.6")]
[InlineData("/", "/", "5.6")]
public void BuildSourceLinkUrl_BitbucketEnterpriseNewVersionSsh(string s1, string s2, string bitbucketVersion)
{
var isEnterpriseEditionSetting = KVP("EnterpriseEdition", "true");
var version = KVP("Version", bitbucketVersion);
var engine = new MockEngine();
var task = new GetSourceLinkUrl()
{
BuildEngine = engine,
SourceRoot = new MockItem("/src/", KVP("RepositoryUrl", "http://bitbucket.domain.com:100/a/b" + s1), KVP("SourceControl", "git"), KVP("RevisionId", "0123456789abcdefABCDEF000000000000000000")),
Hosts = new[]
{
new MockItem("domain.com", KVP("ContentUrl", "https://bitbucket.domain.com" + s2), isEnterpriseEditionSetting, version),
}
};

bool result = task.Execute();
AssertEx.AssertEqualToleratingWhitespaceDifferences("", engine.Log);
AssertEx.AreEqual(ExpectedUrlForEnterpriseEditionNewVersion, task.SourceLinkUrl);
Assert.True(result);
}

[Theory]
[InlineData("", "", "")]
[InlineData("", "", "4.7")]
[InlineData("", "/", "4.7")]
[InlineData("/", "", "4.7")]
[InlineData("/", "/", "4.7")]
[InlineData("", "", "5.6")]
[InlineData("", "/", "5.6")]
[InlineData("/", "", "5.6")]
[InlineData("/", "/", "5.6")]
public void BuildSourceLinkUrl_BitbucketEnterpriseNewVersionHttps(string s1, string s2, string bitbucketVersion)
{
var isEnterpriseEditionSetting = KVP("EnterpriseEdition", "true");
var version = KVP("Version", bitbucketVersion);
var engine = new MockEngine();
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")),
Hosts = new[]
{
new MockItem("domain.com", KVP("ContentUrl", "https://bitbucket.domain.com" + s2), isEnterpriseEditionSetting, version),
}
};

bool result = task.Execute();
AssertEx.AssertEqualToleratingWhitespaceDifferences("", engine.Log);
AssertEx.AreEqual(ExpectedUrlForEnterpriseEditionNewVersion, task.SourceLinkUrl);
Assert.True(result);
}

[Fact]
public void BuildSourceLinkUrl_IncorrectVersionForEnterpriseEdition_ERROR()
{
var isEnterpriseEditionSetting = KVP("EnterpriseEdition", "true");
var version = KVP("Version", "incorrect_version");
var engine = new MockEngine();
var task = new GetSourceLinkUrl()
{
BuildEngine = engine,
SourceRoot = new MockItem("/src/", KVP("RepositoryUrl", "http://bitbucket.domain.com:100/a/b"), KVP("SourceControl", "git"), KVP("RevisionId", "0123456789abcdefABCDEF000000000000000000")),
Hosts = new[]
{
new MockItem("domain.com", KVP("ContentUrl", "https://bitbucket.domain.com"), isEnterpriseEditionSetting, version),
}
};

bool result = task.Execute();

AssertEx.AssertEqualToleratingWhitespaceDifferences(
"ERROR : " + string.Format(CommonResources.ItemOfItemGroupMustSpecifyMetadata, "domain.com", "SourceLinkBitbucketGitHost", "Version"), engine.Log);
Assert.False(result);
}
}
}
96 changes: 95 additions & 1 deletion src/SourceLink.Bitbucket.Git/GetSourceLinkUrl.cs
Expand Up @@ -16,7 +16,101 @@ public sealed class GetSourceLinkUrl : GetSourceLinkUrlGitTask
protected override string HostsItemGroupName => "SourceLinkBitbucketGitHost";
protected override string ProviderDisplayName => "Bitbucket.Git";

private const string BitbucketCloudHostingUrl = "bitbucket.org";
private const string IsEnterpriseEditionMetadataName = "EnterpriseEdition";
private const string VersionMetadataName = "Version";
private const string VersionWithNewUrlFormat = "4.7";

protected override bool SupportsImplicitHost => false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SupportsImplicitHost [](start = 32, length = 20)

If you keep this set to true (the default) it won't be necessary to specify the host, unless the user needs to specify the version and edition. I think that would be better. Specifying the domain when the defaults are used is unnecessary.


protected override string BuildSourceLinkUrl(Uri contentUri, Uri gitUri, string relativeUrl, string revisionId, ITaskItem hostItem)
=> UriUtilities.Combine(UriUtilities.Combine(contentUri.ToString(), relativeUrl), "raw/" + revisionId + "/*");
{
var isEnterpriseEditionFlagAvailable =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isEnterpriseEditionFlagAvailable [](start = 16, length = 32)

This can be simplified as

return boo.TryParse(..., out var isEnterpriseEditionValue) && isEnterpriseEditionValue ?
            BuildSourceLinkUrlForEnterpriseEdition(contentUri, relativeUrl, revisionId, hostItem) : 
            BuildSourceLinkUrlForCloudEdition(contentUri, relativeUrl, revisionId); 

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tmat I modified pull request according to your suggestions.

  • Now the SourceLinkBitbucketGitHost for bitbucket.org in props have EnterpriseEdition="false"
  • Removed SupportsImplicitHost => false;
  • I had to check if hostItem is null. It is null if SourceLinkBitbucketGitHost does not match actual git url host.

Bottom line, now it will be not necessary to put SourceLinkBitbucketGitHost in csproj(unless you want to set specifiv Version attribute) and it will work for both cloud and enterprise scenarios.

bool.TryParse(hostItem.GetMetadata(IsEnterpriseEditionMetadataName), out var isEnterpriseEdition);

if (isEnterpriseEditionFlagAvailable)
{
if (isEnterpriseEdition)
{
return BuildSourceLinkUrlForEnterpriseEdition(contentUri, relativeUrl, revisionId, hostItem);
}
else
{
return BuildSourceLinkUrlForCloudEdition(contentUri, relativeUrl, revisionId);
}
}

if (!BitbucketCloudHostingUrl.Equals(gitUri.GetHost(), StringComparison.OrdinalIgnoreCase))
{
return BuildSourceLinkUrlForEnterpriseEdition(contentUri, relativeUrl, revisionId, hostItem);
}
else
{
return BuildSourceLinkUrlForCloudEdition(contentUri, relativeUrl, revisionId);
}

}

private string BuildSourceLinkUrlForEnterpriseEdition(Uri contentUri, string relativeUrl, string revisionId,
ITaskItem hostItem)
{
var bitbucketEnterpriseVersion = GetBitbucketEnterpriseVersion(hostItem);

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];

var relativeUrlForBitbucketEnterprise =
GetRelativeUrlForBitbucketEnterprise(projectName, repositoryName, revisionId,
bitbucketEnterpriseVersion);

var result = UriUtilities.Combine(contentUri.ToString(), relativeUrlForBitbucketEnterprise);

return result;
}

private Version GetBitbucketEnterpriseVersion(ITaskItem hostItem)
{
var bitbucketEnterpriseVersionAsString = hostItem.GetMetadata(VersionMetadataName);
Version bitbucketEnterpriseVersion;
if (!string.IsNullOrEmpty(bitbucketEnterpriseVersionAsString))
{
if (!Version.TryParse(bitbucketEnterpriseVersionAsString, out bitbucketEnterpriseVersion))
{
Log.LogError(CommonResources.ItemOfItemGroupMustSpecifyMetadata, hostItem.ItemSpec,
HostsItemGroupName, VersionMetadataName);

return null;
}
}
else
{
bitbucketEnterpriseVersion = Version.Parse(VersionWithNewUrlFormat);
}

return bitbucketEnterpriseVersion;
}

private static string BuildSourceLinkUrlForCloudEdition(Uri contentUri, string relativeUrl, string revisionId)
{
return UriUtilities.Combine(UriUtilities.Combine(contentUri.ToString(), relativeUrl),
"raw/" + revisionId + "/*");
}

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;
}
}
}