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