diff --git a/docs/configuration/overview.mdx b/docs/configuration/overview.mdx index 8b6c6a14..9a7a01d8 100644 --- a/docs/configuration/overview.mdx +++ b/docs/configuration/overview.mdx @@ -30,12 +30,13 @@ Supported hosts: - GitHub - GitLab (https://gitlab.com) +- Bitbucket (https://bitbucket.org) ```yaml repository: https://github.com/invertase/melos ``` -When using a self-hosted GitHub or GitLab instance, you can specify the +When using a self-hosted GitHub, GitLab or Bitbucket instance, you can specify the repository location like this: ```yaml diff --git a/packages/melos/lib/src/common/git_repository.dart b/packages/melos/lib/src/common/git_repository.dart index 51da7f77..15363ed9 100644 --- a/packages/melos/lib/src/common/git_repository.dart +++ b/packages/melos/lib/src/common/git_repository.dart @@ -217,14 +217,61 @@ GitLabRepository( int get hashCode => origin.hashCode ^ owner.hashCode ^ name.hashCode; } +class BitbucketRepository extends HostedGitRepository { + BitbucketRepository({ + String origin = defaultOrigin, + required this.owner, + required this.name, + }) : origin = removeTrailingSlash(origin); + + factory BitbucketRepository.fromUrl(Uri uri) { + if (uri.scheme == 'https' && uri.host == 'bitbucket.org') { + final match = RegExp(r'^/(.+)?/(.+)/?$').firstMatch(uri.path); + if (match != null) { + return BitbucketRepository( + owner: match.group(1)!, + name: match.group(2)!, + ); + } + } + + throw FormatException( + 'The URL $uri is not a valid Bitbucket repository URL.', + ); + } + + static const defaultOrigin = 'https://bitbucket.org'; + + /// The origin of the Bitbucket server, defaults to `https://bitbucket.org`. + final String origin; + + /// The owning workspace name. + final String owner; + + @override + final String name; + + @override + Uri commitUrl(String id) => url.resolve('commits/$id'); + + // TODO(fenrirx22): Implementing an issueUrl for Bitbucket requires a Jira URL + @override + Uri issueUrl(String id) => Uri(); + + @override + Uri get url => Uri.parse('$origin/$owner/$name/'); +} + final _hostsToUrlParser = { 'GitHub': GitHubRepository.fromUrl, 'GitLab': GitLabRepository.fromUrl, + 'Bitbucket': BitbucketRepository.fromUrl, }; final _hostsToSpecParser = { 'GitHub': GitHubRepository.new, 'GitLab': GitLabRepository.new, + 'Bitbucket': BitbucketRepository.new, }; /// Tries to parse [url] into a [HostedGitRepository]. diff --git a/packages/melos/test/git_repository_test.dart b/packages/melos/test/git_repository_test.dart index 2d6fcb1f..b24a6cf7 100644 --- a/packages/melos/test/git_repository_test.dart +++ b/packages/melos/test/git_repository_test.dart @@ -225,6 +225,106 @@ void main() { }); }); + group('BitBucketRepository', () { + group('fromUrl', () { + test('parse Bitbucket repository URL correctly', () { + final url = Uri.parse('https://bitbucket.org/a/b'); + final repo = BitbucketRepository.fromUrl(url); + + expect(repo.origin, 'https://bitbucket.org'); + expect(repo.owner, 'a'); + expect(repo.name, 'b'); + expect(repo.url, Uri.parse('https://bitbucket.org/a/b/')); + }); + + test('throws if URL is not a valid GitLab repository URL', () { + void expectBadUrl(String url) { + final uri = Uri.parse(url); + expect( + () => BitbucketRepository.fromUrl(uri), + throwsFormatException, + reason: url, + ); + } + + const [ + '', + 'http://bitbucket.org/a/b', + 'https://gitlab.com/a/b', + 'https://github.com/a/b', + 'https://bitbucket.org/a', + 'https://bitbucket.org/', + 'https://bitbucket.org', + ].forEach(expectBadUrl); + }); + }); + + group('fromSpec', () { + test('parse Bitbucket repository spec correctly', () { + final repo = BitbucketRepository( + origin: 'https://bitbucket.invertase.dev', + owner: 'a', + name: 'b', + ); + + expect(repo.origin, 'https://bitbucket.invertase.dev'); + expect(repo.owner, 'a'); + expect(repo.name, 'b'); + expect(repo.url, Uri.parse('https://bitbucket.invertase.dev/a/b/')); + }); + + test('parse Bitbucket repository spec with nested groups correctly', () { + final repo = BitbucketRepository( + origin: 'https://bitbucket.invertase.dev', + owner: 'a/b', + name: 'c', + ); + + expect(repo.origin, 'https://bitbucket.invertase.dev'); + expect(repo.owner, 'a/b'); + expect(repo.name, 'c'); + expect(repo.url, Uri.parse('https://bitbucket.invertase.dev/a/b/c/')); + }); + + test( + 'parse Bitbucket repository spec with sub-path and nested groups ' + 'correctly', () { + final repo = BitbucketRepository( + origin: 'https://invertase.dev/bitbucket', + owner: 'a/b', + name: 'c', + ); + + expect(repo.origin, 'https://invertase.dev/bitbucket'); + expect(repo.owner, 'a/b'); + expect(repo.name, 'c'); + expect(repo.url, Uri.parse('https://invertase.dev/bitbucket/a/b/c/')); + }); + }); + + test('commitUrl returns correct URL', () { + final repo = BitbucketRepository(owner: 'a', name: 'b'); + const commitId = 'b2841394a48cd7d84a4966a788842690e543b2ef'; + + expect( + repo.commitUrl(commitId), + Uri.parse( + 'https://bitbucket.org/a/b/commits/b2841394a48cd7d84a4966a788842690e543b2ef', + ), + ); + }); + + test('issueUrl returns empty URL', () { + final repo = BitbucketRepository(owner: 'a', name: 'b'); + const issueId = '123'; + + expect( + repo.issueUrl(issueId), + Uri(), + ); + }); + }); + group('parseHostedGitRepositoryUrl', () { test('parses GitHub repository URL', () { final repo =