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
4 changes: 3 additions & 1 deletion README.md
Expand Up @@ -92,11 +92,13 @@ 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
and add Bitbucket host configuration. Including flag whether it is Enterprise Edtition or Cloud. For Enterprise Edition provide its version and change value in include attribute to your Bitbucket server name:

```xml
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.Bitbucket.Git" Version="1.0.0-beta2-18618-05" PrivateAssets="All"/>
<SourceLinkBitbucketGitHost Include="bitbucket.org" EnterpriseEdition="true" Version="4.1" />
Copy link
Member

Choose a reason for hiding this comment

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

[](start = 2, length = 93)

This should be default. Customer who uses repository hosted on bitbucket.org shouldn't need to specify anything in their project file.

Copy link
Member

Choose a reason for hiding this comment

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

When using a server hosted in different domain we should default to enterprise edition and the latest version with the option to specify an older version.


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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.
Now for bitbucket.org cloud hosting there is no need to provide SourceLinkBitbucketGitHost configuration in csproj.
When the domain is different than bitbucket.org it defaults to enterprise edition and the latest version.

</ItemGroup>
```

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 @@ -32,24 +37,210 @@ 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_EmptyMetadata_UseCloudEditionAsDefault()
{
var engine = new MockEngine();
var task = new GetSourceLinkUrl
{
BuildEngine = engine,
SourceRoot = new MockItem("/src/", KVP("RepositoryUrl", "http://subdomain.mybitbucket.org:100/a/b"), 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("", 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);
}
}
}
85 changes: 83 additions & 2 deletions src/SourceLink.Bitbucket.Git/GetSourceLinkUrl.cs
Expand Up @@ -16,7 +16,88 @@ public sealed class GetSourceLinkUrl : GetSourceLinkUrlGitTask
protected override string HostsItemGroupName => "SourceLinkBitbucketGitHost";
protected override string ProviderDisplayName => "Bitbucket.Git";

protected override string BuildSourceLinkUrl(Uri contentUri, Uri gitUri, string relativeUrl, string revisionId, ITaskItem hostItem)
=> UriUtilities.Combine(UriUtilities.Combine(contentUri.ToString(), relativeUrl), "raw/" + revisionId + "/*");
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)
Copy link
Member

Choose a reason for hiding this comment

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

ITaskItem hostItem [](start = 12, length = 18)

Style: keep the parameter on the previous line. We either indent all parameters or none.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

{
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 && isEnterpriseEdition)
{
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;
}
}
}