diff --git a/Versionize.Tests/Changelog/BitbucketLinkBuilderTests.cs b/Versionize.Tests/Changelog/BitbucketLinkBuilderTests.cs new file mode 100644 index 0000000..abd9bff --- /dev/null +++ b/Versionize.Tests/Changelog/BitbucketLinkBuilderTests.cs @@ -0,0 +1,198 @@ +using System; +using System.Data; +using System.Linq; +using LibGit2Sharp; +using NuGet.Versioning; +using Shouldly; +using Versionize.Tests.TestSupport; +using Xunit; + +namespace Versionize.Changelog.Tests +{ + public class BitBucketLinkBuilderTests + { + private readonly string sshOrgPushUrl = "git@bitbucket.org:mobiloitteinc/dotnet-codebase.git"; + private readonly string sshComPushUrl = "git@bitbucket.com:mobiloitteinc/dotnet-codebase.git"; + private readonly string httpsOrgPushUrl = "https://saintedlama@bitbucket.org/mobiloitteinc/dotnet-codebase.git"; + private readonly string httpsComPushUrl = "https://saintedlama@bitbucket.com/mobiloitteinc/dotnet-codebase.git"; + + [Fact] + public void ShouldThrowIfUrlIsNoRecognizedSshOrHttpsUrl() + { + Should.Throw(() => new BitbucketLinkBuilder("bitbucket.org")); + } + + [Fact] + public void ShouldThrowIfUrlIsNoValidHttpsCloneUrl() + { + Should.Throw(() => new BitbucketLinkBuilder("https://saintedlama@bitbucket.org/")); + } + + [Fact] + public void ShouldThrowIfUrlIsNoValidSshCloneUrl() + { + Should.Throw(() => new BitbucketLinkBuilder("git@bitbucket.org:mobiloitteinc")); + } + + [Fact] + public void ShouldCreateAnOrgBitbucketUrlBuilderForHTTPSPushUrls() + { + var repo = SetupRepositoryWithRemote("origin", httpsOrgPushUrl); + var linkBuilder = LinkBuilderFactory.CreateFor(repo); + + linkBuilder.ShouldBeAssignableTo(); + } + + [Fact] + public void ShouldCreateAComBitbucketUrlBuilderForHTTPSPushUrls() + { + var repo = SetupRepositoryWithRemote("origin", httpsComPushUrl); + var linkBuilder = LinkBuilderFactory.CreateFor(repo); + + linkBuilder.ShouldBeAssignableTo(); + } + + [Fact] + public void ShouldCreateAnOrgBitbucketUrlBuilderForSSHPushUrls() + { + var repo = SetupRepositoryWithRemote("origin", sshOrgPushUrl); + var linkBuilder = LinkBuilderFactory.CreateFor(repo); + + linkBuilder.ShouldBeAssignableTo(); + } + + [Fact] + public void ShouldCreateAComBitbucketUrlBuilderForSSHPushUrls() + { + var repo = SetupRepositoryWithRemote("origin", sshComPushUrl); + var linkBuilder = LinkBuilderFactory.CreateFor(repo); + + linkBuilder.ShouldBeAssignableTo(); + } + + [Fact] + public void ShouldPickFirstRemoteInCaseNoOriginWasFound() + { + var repo = SetupRepositoryWithRemote("some", sshOrgPushUrl); + var linkBuilder = LinkBuilderFactory.CreateFor(repo); + + linkBuilder.ShouldBeAssignableTo(); + } + + [Fact] + public void ShouldFallbackToNoopInCaseNoBitbucketPushUrlWasDefined() + { + var repo = SetupRepositoryWithRemote("origin", "https://hostmeister.com/saintedlama/versionize.git"); + var linkBuilder = LinkBuilderFactory.CreateFor(repo); + + linkBuilder.ShouldBeAssignableTo(); + } + + [Fact] + public void ShouldBuildAnOrgSSHCommitLink() + { + var commit = new ConventionalCommit + { + Sha = "734713bc047d87bf7eac9674765ae793478c50d3" + }; + + var linkBuilder = new BitbucketLinkBuilder(sshOrgPushUrl); + var link = linkBuilder.BuildCommitLink(commit); + + link.ShouldBe("https://bitbucket.org/mobiloitteinc/dotnet-codebase/commits/734713bc047d87bf7eac9674765ae793478c50d3"); + } + + [Fact] + public void ShouldBuildAComSSHCommitLink() + { + var commit = new ConventionalCommit + { + Sha = "734713bc047d87bf7eac9674765ae793478c50d3" + }; + + var linkBuilder = new BitbucketLinkBuilder(sshComPushUrl); + var link = linkBuilder.BuildCommitLink(commit); + + link.ShouldBe("https://bitbucket.com/mobiloitteinc/dotnet-codebase/commits/734713bc047d87bf7eac9674765ae793478c50d3"); + } + + [Fact] + public void ShouldBuildAnOrgHTTPSCommitLink() + { + var commit = new ConventionalCommit + { + Sha = "734713bc047d87bf7eac9674765ae793478c50d3" + }; + + var linkBuilder = new BitbucketLinkBuilder(httpsOrgPushUrl); + var link = linkBuilder.BuildCommitLink(commit); + + link.ShouldBe("https://bitbucket.org/mobiloitteinc/dotnet-codebase/commits/734713bc047d87bf7eac9674765ae793478c50d3"); + } + + [Fact] + public void ShouldBuildAComHTTPSCommitLink() + { + var commit = new ConventionalCommit + { + Sha = "734713bc047d87bf7eac9674765ae793478c50d3" + }; + + var linkBuilder = new BitbucketLinkBuilder(httpsComPushUrl); + var link = linkBuilder.BuildCommitLink(commit); + + link.ShouldBe("https://bitbucket.com/mobiloitteinc/dotnet-codebase/commits/734713bc047d87bf7eac9674765ae793478c50d3"); + } + + [Fact] + public void ShouldBuildAnOrgSSHTagLink() + { + var linkBuilder = new BitbucketLinkBuilder(sshOrgPushUrl); + var link = linkBuilder.BuildVersionTagLink(new SemanticVersion(1, 0, 0)); + + link.ShouldBe("https://bitbucket.org/mobiloitteinc/dotnet-codebase/src/v1.0.0"); + } + + [Fact] + public void ShouldBuildAComSSHTagLink() + { + var linkBuilder = new BitbucketLinkBuilder(sshComPushUrl); + var link = linkBuilder.BuildVersionTagLink(new SemanticVersion(1, 0, 0)); + + link.ShouldBe("https://bitbucket.com/mobiloitteinc/dotnet-codebase/src/v1.0.0"); + } + + [Fact] + public void ShouldBuildAnOrgHTTPSTagLink() + { + var linkBuilder = new BitbucketLinkBuilder(httpsOrgPushUrl); + var link = linkBuilder.BuildVersionTagLink(new SemanticVersion(1, 0, 0)); + + link.ShouldBe("https://bitbucket.org/mobiloitteinc/dotnet-codebase/src/v1.0.0"); + } + + [Fact] + public void ShouldBuildAComHTTPSTagLink() + { + var linkBuilder = new BitbucketLinkBuilder(httpsComPushUrl); + var link = linkBuilder.BuildVersionTagLink(new SemanticVersion(1, 0, 0)); + + link.ShouldBe("https://bitbucket.com/mobiloitteinc/dotnet-codebase/src/v1.0.0"); + } + + private static Repository SetupRepositoryWithRemote(string remoteName, string pushUrl) + { + var workingDirectory = TempDir.Create(); + var repo = TempRepository.Create(workingDirectory); + + foreach (var existingRemoteName in repo.Network.Remotes.Select(remote => remote.Name)) + { + repo.Network.Remotes.Remove(existingRemoteName); + } + + repo.Network.Remotes.Add(remoteName, pushUrl); + + return repo; + } + } +} diff --git a/Versionize/Changelog/BitbucketLinkBuilder.cs b/Versionize/Changelog/BitbucketLinkBuilder.cs new file mode 100644 index 0000000..0acb537 --- /dev/null +++ b/Versionize/Changelog/BitbucketLinkBuilder.cs @@ -0,0 +1,73 @@ +using System; +using System.Text.RegularExpressions; +using Version = NuGet.Versioning.SemanticVersion; + +namespace Versionize.Changelog +{ + // TODO: Accept both .org and .com extensions + public class BitbucketLinkBuilder : IChangelogLinkBuilder + { + private const string OrgSshPrefix = "git@bitbucket.org:"; + private const string ComSshPrefix = "git@bitbucket.com:"; + + private readonly string _organization; + private readonly string _repository; + private readonly string _domain; + + public BitbucketLinkBuilder(string pushUrl) + { + if (pushUrl.StartsWith(OrgSshPrefix) || pushUrl.StartsWith(ComSshPrefix)) + { + var httpsPattern = new Regex("^git@bitbucket\\.(?org|com):(?.*?)/(?.*?)(?:\\.git)?$"); + var matches = httpsPattern.Match(pushUrl); + + if (!matches.Success) + { + throw new InvalidOperationException($"Remote url {pushUrl} is not recognized as valid Bitbucket SSH pattern"); + } + + _organization = matches.Groups["organization"].Value; + _repository = matches.Groups["repository"].Value; + _domain = matches.Groups["domain"].Value; + } + else if (IsHttpsPushUrl(pushUrl)) + { + var httpsPattern = new Regex("^https://.*?bitbucket\\.(?org|com)/(?.*?)/(?.*?)(?:\\.git)?$"); + var matches = httpsPattern.Match(pushUrl); + + if (!matches.Success) + { + throw new InvalidOperationException($"Remote url {pushUrl} is not recognized as valid Bitbucket HTTPS pattern"); + } + + _organization = matches.Groups["organization"].Value; + _repository = matches.Groups["repository"].Value; + _domain = matches.Groups["domain"].Value; + } + else + { + throw new InvalidOperationException($"Remote url {pushUrl} is not recognized as Bitbucket SSH or HTTPS url"); + } + } + + public static bool IsPushUrl(string pushUrl) + { + return pushUrl.StartsWith(ComSshPrefix) || pushUrl.StartsWith(OrgSshPrefix) || IsHttpsPushUrl(pushUrl); + } + + public string BuildVersionTagLink(Version version) + { + return $"https://bitbucket.{_domain}/{_organization}/{_repository}/src/v{version}"; + } + + public string BuildCommitLink(ConventionalCommit commit) + { + return $"https://bitbucket.{_domain}/{_organization}/{_repository}/commits/{commit.Sha}"; + } + + private static bool IsHttpsPushUrl(string pushUrl) + { + return new Regex("^https://.*?@bitbucket\\.(org|com)/.*$").IsMatch(pushUrl); + } + } +} diff --git a/Versionize/Changelog/LinkBuilderFactory.cs b/Versionize/Changelog/LinkBuilderFactory.cs index 0482309..ef11705 100644 --- a/Versionize/Changelog/LinkBuilderFactory.cs +++ b/Versionize/Changelog/LinkBuilderFactory.cs @@ -23,6 +23,10 @@ public static IChangelogLinkBuilder CreateFor(Repository repository) { return new AzureLinkBuilder(origin.PushUrl); } + else if (BitbucketLinkBuilder.IsPushUrl(origin.PushUrl)) + { + return new BitbucketLinkBuilder(origin.PushUrl); + } return new PlainLinkBuilder(); }