Skip to content

Commit d5ae980

Browse files
authored
Support base URL in BitBucket Enterprise URL format (#475)
1 parent 446d5be commit d5ae980

File tree

6 files changed

+116
-51
lines changed

6 files changed

+116
-51
lines changed

src/SourceLink.AzureRepos.Git/build/Microsoft.SourceLink.AzureRepos.Git.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
output property is set to the content URL corresponding to the domain, otherwise it is set to string "N/A".
2222
2323
Recognized domains are specified via Hosts (initialized from SourceLinkAzureReposGitHost item group).
24-
In addition SourceLinkHasSingleProvider is true an iplicit host is parsed from RepositoryUrl.
24+
In addition, if SourceLinkHasSingleProvider is true an implicit host is parsed from RepositoryUrl.
2525
2626
ContentUrl is optional. If not specified it defaults to "https://{domain}" or "http://{domain}", based on the scheme of SourceRoot.RepositoryUrl.
2727
-->

src/SourceLink.Bitbucket.Git.UnitTests/GetSourceLinkUrlTests.cs

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
2+
using System;
23
using Microsoft.Build.Tasks.SourceControl;
34
using TestUtilities;
45
using Xunit;
@@ -8,11 +9,6 @@ namespace Microsoft.SourceLink.Bitbucket.Git.UnitTests
89
{
910
public class GetSourceLinkUrlTests
1011
{
11-
private const string ExpectedUrlForCloudEdition =
12-
"https://api.domain.com/x/y/2.0/repositories/a/b/src/0123456789abcdefABCDEF000000000000000000/*";
13-
private const string ExpectedUrlForEnterpriseEditionOldVersion = "https://bitbucket.domain.com/projects/a/repos/b/browse/*?at=0123456789abcdefABCDEF000000000000000000&raw";
14-
private const string ExpectedUrlForEnterpriseEditionNewVersion = "https://bitbucket.domain.com/projects/a/repos/b/raw/*?at=0123456789abcdefABCDEF000000000000000000";
15-
1612
[Fact]
1713
public void EmptyHosts()
1814
{
@@ -32,31 +28,79 @@ public void EmptyHosts()
3228
Assert.False(result);
3329
}
3430

31+
[Theory]
32+
[InlineData("a/b", "", "a", "b")]
33+
[InlineData("/a/b", "", "a", "b")]
34+
[InlineData("/a/b/", "", "a", "b")]
35+
[InlineData("scm/a", "", "scm", "a")]
36+
[InlineData("scm/a/b", "", "a", "b")]
37+
[InlineData("/r/scm/a/b", "r", "a", "b")]
38+
[InlineData("/r/s/scm/a/b", "r/s", "a", "b")]
39+
[InlineData("/r/s/a/b", "r/s", "a", "b")]
40+
[InlineData("/r/s/scm/b", "r/s", "scm", "b")]
41+
public void TryParseEnterpriseUrl(string relativeUrl, string expectedBaseUrl, string expectedProjectName, string expectedRepositoryName)
42+
{
43+
Assert.True(GetSourceLinkUrl.TryParseEnterpriseUrl(relativeUrl, out var baseUrl, out var projectName, out var repositoryName));
44+
Assert.Equal(expectedBaseUrl, baseUrl);
45+
Assert.Equal(expectedProjectName, projectName);
46+
Assert.Equal(expectedRepositoryName, repositoryName);
47+
}
48+
49+
[Theory]
50+
[InlineData("")]
51+
[InlineData("/")]
52+
[InlineData("x")]
53+
public void TryParseEnterpriseUrl_Errors(string relativeUrl)
54+
{
55+
Assert.False(GetSourceLinkUrl.TryParseEnterpriseUrl(relativeUrl, out _, out _, out _));
56+
}
57+
3558
[Theory]
3659
[InlineData("", "")]
3760
[InlineData("", "/")]
3861
[InlineData("/", "")]
3962
[InlineData("/", "/")]
4063
public void BuildSourceLinkUrl_BitbucketCloud(string s1, string s2)
4164
{
42-
var isEnterpriseEditionSetting = KVP("EnterpriseEdition", "false");
4365
var engine = new MockEngine();
4466
var task = new GetSourceLinkUrl()
4567
{
4668
BuildEngine = engine,
4769
SourceRoot = new MockItem("/src/", KVP("RepositoryUrl", "http://subdomain.mybitbucket.org:100/a/b" + s1), KVP("SourceControl", "git"), KVP("RevisionId", "0123456789abcdefABCDEF000000000000000000")),
4870
Hosts = new[]
4971
{
50-
new MockItem("mybitbucket.org", KVP("ContentUrl", "https://domain.com/x/y" + s2), isEnterpriseEditionSetting),
72+
new MockItem("mybitbucket.org", KVP("ContentUrl", "https://domain.com/x/y" + s2), KVP("EnterpriseEdition", "false")),
5173
}
5274
};
5375

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

82+
[Fact]
83+
public void BuildSourceLinkUrl_BitbucketEnterprise_InvalidUrl()
84+
{
85+
var engine = new MockEngine();
86+
var task = new GetSourceLinkUrl()
87+
{
88+
BuildEngine = engine,
89+
SourceRoot = new MockItem("/src/", KVP("RepositoryUrl", "http://subdomain.mybitbucket.org/a"), KVP("SourceControl", "git"), KVP("RevisionId", "0123456789abcdefABCDEF000000000000000000")),
90+
Hosts = new[]
91+
{
92+
new MockItem("mybitbucket.org", KVP("ContentUrl", "https://domain.com/x/y")),
93+
}
94+
};
95+
96+
bool result = task.Execute();
97+
98+
AssertEx.AssertEqualToleratingWhitespaceDifferences(
99+
"ERROR : " + string.Format(CommonResources.ValueOfWithIdentityIsInvalid, "SourceRoot.RepositoryUrl", "/src/", "http://subdomain.mybitbucket.org/a"), engine.Log);
100+
101+
Assert.False(result);
102+
}
103+
60104
[Fact]
61105
public void BuildSourceLinkUrl_MetadataWithEnterpriseEditionButWithoutVersion_UseNewVersionAsDefauld()
62106
{
@@ -74,7 +118,7 @@ public void BuildSourceLinkUrl_MetadataWithEnterpriseEditionButWithoutVersion_Us
74118

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

@@ -104,7 +148,7 @@ public void BuildSourceLinkUrl_BitbucketEnterpriseOldVersionSsh(string s1, strin
104148

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

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

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

@@ -165,7 +209,7 @@ public void BuildSourceLinkUrl_BitbucketEnterpriseNewVersionSsh(string s1, strin
165209

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

@@ -196,7 +240,7 @@ public void BuildSourceLinkUrl_BitbucketEnterpriseNewVersionHttps(string s1, str
196240

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

Lines changed: 54 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
22

33
using System;
4+
using System.Diagnostics;
45
using Microsoft.Build.Framework;
56
using Microsoft.Build.Tasks.SourceControl;
67

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

1920
private const string IsEnterpriseEditionMetadataName = "EnterpriseEdition";
2021
private const string VersionMetadataName = "Version";
21-
private const string VersionWithNewUrlFormat = "4.7";
22+
private static readonly Version s_versionWithNewUrlFormat = new Version(4, 7);
2223

2324
protected override string BuildSourceLinkUrl(Uri contentUri, Uri gitUri, string relativeUrl, string revisionId, ITaskItem hostItem)
2425
{
25-
return
26-
bool.TryParse(hostItem?.GetMetadata(IsEnterpriseEditionMetadataName), out var isEnterpriseEdition) && !isEnterpriseEdition
27-
? BuildSourceLinkUrlForCloudEdition(contentUri, relativeUrl, revisionId)
28-
: BuildSourceLinkUrlForEnterpriseEdition(contentUri, relativeUrl, revisionId, hostItem);
26+
// The SourceLinkBitbucketGitHost item for bitbucket.org specifies EnterpriseEdition="false".
27+
// Other items that may be specified by the project default to EnterpriseEdition="true" without specifying it.
28+
bool isCloud = bool.TryParse(hostItem?.GetMetadata(IsEnterpriseEditionMetadataName), out var isEnterpriseEdition) && !isEnterpriseEdition;
29+
30+
if (isCloud)
31+
{
32+
return BuildSourceLinkUrlForCloudEdition(contentUri, relativeUrl, revisionId);
33+
}
34+
35+
if (TryParseEnterpriseUrl(relativeUrl, out var relativeBaseUrl, out var projectName, out var repositoryName))
36+
{
37+
var version = GetBitbucketEnterpriseVersion(hostItem);
38+
return BuildSourceLinkUrlForEnterpriseEdition(contentUri, relativeBaseUrl, projectName, repositoryName, revisionId, version);
39+
}
40+
41+
Log.LogError(CommonResources.ValueOfWithIdentityIsInvalid, Names.SourceRoot.RepositoryUrlFullName, SourceRoot.ItemSpec, gitUri);
42+
return null;
2943
}
3044

31-
private string BuildSourceLinkUrlForEnterpriseEdition(Uri contentUri, string relativeUrl, string revisionId,
32-
ITaskItem hostItem)
45+
internal static string BuildSourceLinkUrlForEnterpriseEdition(
46+
Uri contentUri,
47+
string relativeBaseUrl,
48+
string projectName,
49+
string repositoryName,
50+
string commitSha,
51+
Version version)
3352
{
34-
var bitbucketEnterpriseVersion = GetBitbucketEnterpriseVersion(hostItem);
53+
var relativeUrl = (version >= s_versionWithNewUrlFormat) ?
54+
$"projects/{projectName}/repos/{repositoryName}/raw/*?at={commitSha}" :
55+
$"projects/{projectName}/repos/{repositoryName}/browse/*?at={commitSha}&raw";
3556

36-
var splits = relativeUrl.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries);
37-
var isSshRepoUri = !(splits.Length == 3 && splits[0] == "scm");
38-
var projectName = isSshRepoUri ? splits[0] : splits[1];
39-
var repositoryName = isSshRepoUri ? splits[1] : splits[2];
57+
return UriUtilities.Combine(contentUri.ToString(), UriUtilities.Combine(relativeBaseUrl, relativeUrl));
58+
}
59+
60+
internal static bool TryParseEnterpriseUrl(string relativeUrl, out string relativeBaseUrl, out string projectName, out string repositoryName)
61+
{
62+
// HTTP: {baseUrl}/scm/{projectName}/{repositoryName}
63+
// SSH: {baseUrl}/{projectName}/{repositoryName}
64+
65+
if (!UriUtilities.TrySplitRelativeUrl(relativeUrl, out var parts) || parts.Length < 2)
66+
{
67+
relativeBaseUrl = projectName = repositoryName = null;
68+
return false;
69+
}
4070

41-
var relativeUrlForBitbucketEnterprise =
42-
GetRelativeUrlForBitbucketEnterprise(projectName, repositoryName, revisionId,
43-
bitbucketEnterpriseVersion);
71+
var i = parts.Length - 1;
4472

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

47-
return result;
76+
if (i >= 0 && parts[i] == "scm")
77+
{
78+
i--;
79+
}
80+
81+
Debug.Assert(i >= -1);
82+
relativeBaseUrl = string.Join("/", parts, 0, i + 1);
83+
return true;
4884
}
4985

5086
private Version GetBitbucketEnterpriseVersion(ITaskItem hostItem)
@@ -63,7 +99,7 @@ private Version GetBitbucketEnterpriseVersion(ITaskItem hostItem)
6399
}
64100
else
65101
{
66-
bitbucketEnterpriseVersion = Version.Parse(VersionWithNewUrlFormat);
102+
bitbucketEnterpriseVersion = s_versionWithNewUrlFormat;
67103
}
68104

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

80116
return UriUtilities.Combine(apiUriBuilder.Uri.ToString(), relativeApiUrl);
81117
}
82-
83-
private static string GetRelativeUrlForBitbucketEnterprise(string projectName, string repositoryName, string commitId, Version bitbucketVersion)
84-
{
85-
string relativeUrl;
86-
if (bitbucketVersion >= Version.Parse(VersionWithNewUrlFormat))
87-
{
88-
relativeUrl = $"projects/{projectName}/repos/{repositoryName}/raw/*?at={commitId}";
89-
}
90-
else
91-
{
92-
relativeUrl = $"projects/{projectName}/repos/{repositoryName}/browse/*?at={commitId}&raw";
93-
}
94-
95-
return relativeUrl;
96-
}
97118
}
98119
}

src/SourceLink.Bitbucket.Git/build/Microsoft.SourceLink.Bitbucket.Git.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
output property is set to the content URL corresponding to the domain, otherwise it is set to string "N/A".
2222
2323
Recognized domains are specified via Hosts (initialized from SourceLinkBitbucketGitHost item group).
24-
In addition SourceLinkHasSingleProvider is true an iplicit host is parsed from RepositoryUrl.
24+
In addition, if SourceLinkHasSingleProvider is true an implicit host is parsed from RepositoryUrl.
2525
2626
Example of SourceLinkBitbucketGitHost items:
2727

src/SourceLink.GitHub/build/Microsoft.SourceLink.GitHub.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
output property is set to the content URL corresponding to the domain, otherwise it is set to string "N/A".
2222
2323
Recognized domains are specified via Hosts (initialized from SourceLinkGitHubHost item group).
24-
In addition SourceLinkHasSingleProvider is true an iplicit host is parsed from RepositoryUrl.
24+
In addition, if SourceLinkHasSingleProvider is true an implicit host is parsed from RepositoryUrl.
2525
2626
Example of SourceLinkGitHubHost items:
2727

src/SourceLink.GitLab/build/Microsoft.SourceLink.GitLab.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
output property is set to the content URL corresponding to the domain, otherwise it is set to string "N/A".
2222
2323
Recognized domains are specified via Hosts (initialized from SourceLinkGitLabHost item group).
24-
In addition SourceLinkHasSingleProvider is true an iplicit host is parsed from RepositoryUrl.
24+
In addition, if SourceLinkHasSingleProvider is true an implicit host is parsed from RepositoryUrl.
2525
2626
Example of SourceLinkGitLabHost items:
2727

0 commit comments

Comments
 (0)