Skip to content

Commit

Permalink
Bitbucket enterprise support (#246)
Browse files Browse the repository at this point in the history
* Add support for Bitbucket Enterprise

* Update unit tests for Bitbucket to cover Enterprise edition

* Update Bitbucket integration tests to cover Enterprise edition

* Update Readme.md with info about Bitbucket host configuration

* Update BuildSourceLinkUrl for Bitbucket to support Enterprise edition with ssh uri.

* Update unit and integration tests for BitBucket Enterprise support of ssh repository uri.

* Code review refactoring for Bitbucket Enterprise

Adding SourceLinkBitbucketGitHost is not necessary for cloud scenario
For self-hosting scenario adding SourceLinkBitbucketGitHost  with proper host is required
EnterpriseEdition and Version settings are optional with default values.

* Fix unnecessary line break.

* Support for Bitbucket Enterprise

Add EnterpriseEdition="false" in defaulp props file so Bitbucket.org is recognized as cloud version
By default EnterpriseEdition is true so any other bitbucket repository is recognized as enterprise version by default
  • Loading branch information
adam-sajdak authored and tmat committed May 17, 2019
1 parent 4f40d36 commit 8144155
Show file tree
Hide file tree
Showing 5 changed files with 553 additions and 12 deletions.
15 changes: 14 additions & 1 deletion README.md
Expand Up @@ -92,14 +92,27 @@ 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.
Additional configuration is available when SourceLinkBitbucketGitHost is added to csproj:

- EnterpriseEdition - flag whether it is Enterprise Edition or Cloud Edition, by default it is true.
- Version="4.7" - for Enterprise Edition provides its version. URL format for accessing files is different for Bitbucket in version < 4.7, please add Bitbucket version if it is the case

```xml
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.Bitbucket.Git" Version="1.0.0-beta2-18618-05" PrivateAssets="All"/>
<SourceLinkBitbucketGitHost Include="bitbucket.yourdomain.com" EnterpriseEdition="true" Version="4.7"/>
</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
179 changes: 175 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 @@ -32,24 +37,190 @@ public void EmptyHosts()
[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);
}
}
}
73 changes: 72 additions & 1 deletion src/SourceLink.Bitbucket.Git/GetSourceLinkUrl.cs
Expand Up @@ -16,7 +16,78 @@ public sealed class GetSourceLinkUrl : GetSourceLinkUrlGitTask
protected override string HostsItemGroupName => "SourceLinkBitbucketGitHost";
protected override string ProviderDisplayName => "Bitbucket.Git";

private const string IsEnterpriseEditionMetadataName = "EnterpriseEdition";
private const string VersionMetadataName = "Version";
private const string VersionWithNewUrlFormat = "4.7";

protected override string BuildSourceLinkUrl(Uri contentUri, Uri gitUri, string relativeUrl, string revisionId, ITaskItem hostItem)
=> UriUtilities.Combine(UriUtilities.Combine(contentUri.ToString(), relativeUrl), "raw/" + revisionId + "/*");
{
return
bool.TryParse(hostItem?.GetMetadata(IsEnterpriseEditionMetadataName), out var isEnterpriseEdition) && !isEnterpriseEdition
? BuildSourceLinkUrlForCloudEdition(contentUri, relativeUrl, revisionId)
: BuildSourceLinkUrlForEnterpriseEdition(contentUri, relativeUrl, revisionId, hostItem);
}

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;
}
}
}
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<ItemGroup>
<SourceLinkBitbucketGitHost Include="bitbucket.org" />
<SourceLinkBitbucketGitHost Include="bitbucket.org" EnterpriseEdition="false"/>
</ItemGroup>
</Project>

0 comments on commit 8144155

Please sign in to comment.