Skip to content

Commit

Permalink
feat: support self-hosted git repositories (#417)
Browse files Browse the repository at this point in the history
  • Loading branch information
Almighty-Alpaca committed Oct 31, 2022
1 parent 1f9d4c3 commit ce6e4ef
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 27 deletions.
10 changes: 10 additions & 0 deletions docs/configuration/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ Supported hosts:
repository: https://github.com/invertase/melos
```

When using a self-hosted GitHub or GitLab instance, you can specify the repository location like this:

```yaml
repository:
type: gitlab
origin: https://gitlab.example.dev
owner: invertase
name: melos
```

## `sdkPath`

> optional
Expand Down
66 changes: 57 additions & 9 deletions packages/melos/lib/src/common/git_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import 'package:meta/meta.dart';

import 'git.dart';
import 'pending_package_update.dart';
import 'utils.dart';

/// A hosted git repository.
@immutable
Expand Down Expand Up @@ -70,14 +71,15 @@ mixin SupportsManualRelease on HostedGitRepository {
/// A git repository, hosted by GitHub.
@immutable
class GitHubRepository extends HostedGitRepository with SupportsManualRelease {
const GitHubRepository({
GitHubRepository({
String origin = defaultOrigin,
required this.owner,
required this.name,
});
}) : origin = removeTrailingSlash(origin);

factory GitHubRepository.fromUrl(Uri uri) {
if (uri.scheme == 'https' && uri.host == 'github.com') {
final match = RegExp(r'^\/(.+)\/(.+)\/?$').firstMatch(uri.path);
final match = RegExp(r'^/(.+)/(.+)/?$').firstMatch(uri.path);
if (match != null) {
return GitHubRepository(
owner: match.group(1)!,
Expand All @@ -89,14 +91,19 @@ class GitHubRepository extends HostedGitRepository with SupportsManualRelease {
throw FormatException('The URL $uri is not a valid GitHub repository URL.');
}

static const defaultOrigin = 'https://github.com';

/// The origin of the GitHub server, defaults to `https://github.com`.
final String origin;

/// The username of the owner of this repository.
final String owner;

@override
final String name;

@override
Uri get url => Uri.parse('https://github.com/$owner/$name/');
Uri get url => Uri.parse('$origin/$owner/$name/');

@override
Uri commitUrl(String id) => url.resolve('commit/$id');
Expand Down Expand Up @@ -125,6 +132,7 @@ class GitHubRepository extends HostedGitRepository with SupportsManualRelease {
String toString() {
return '''
GitHubRepository(
origin: $origin,
owner: $owner,
name: $name,
)''';
Expand All @@ -135,24 +143,26 @@ GitHubRepository(
identical(this, other) ||
other is GitHubRepository &&
other.runtimeType == runtimeType &&
other.origin == origin &&
other.owner == owner &&
other.name == name;

@override
int get hashCode => owner.hashCode ^ name.hashCode;
int get hashCode => origin.hashCode ^ owner.hashCode ^ name.hashCode;
}

/// A git repository, hosted by GitLab.
@immutable
class GitLabRepository extends HostedGitRepository {
GitLabRepository({
String origin = defaultOrigin,
required this.owner,
required this.name,
});
}) : origin = removeTrailingSlash(origin);

factory GitLabRepository.fromUrl(Uri uri) {
if (uri.scheme == 'https' && uri.host == 'gitlab.com') {
final match = RegExp(r'^\/((?:.+[\/]?))?\/(.+)\/?$').firstMatch(uri.path);
final match = RegExp(r'^/(.+)?/(.+)/?$').firstMatch(uri.path);
if (match != null) {
return GitLabRepository(
owner: match.group(1)!,
Expand All @@ -164,14 +174,19 @@ class GitLabRepository extends HostedGitRepository {
throw FormatException('The URL $uri is not a valid GitLab repository URL.');
}

static const defaultOrigin = 'https://gitlab.com';

/// The origin of the GitLab server, defaults to `https://gitlab.com`.
final String origin;

/// The username of the owner of this repository.
final String owner;

@override
final String name;

@override
Uri get url => Uri.parse('https://gitlab.com/$owner/$name/');
Uri get url => Uri.parse('$origin/$owner/$name/');

@override
Uri commitUrl(String id) => url.resolve('-/commit/$id');
Expand All @@ -183,6 +198,7 @@ class GitLabRepository extends HostedGitRepository {
String toString() {
return '''
GitLabRepository(
origin: $origin,
owner: $owner,
name: $name,
)''';
Expand All @@ -193,18 +209,28 @@ GitLabRepository(
identical(this, other) ||
other is GitHubRepository &&
other.runtimeType == runtimeType &&
other.origin == origin &&
other.owner == owner &&
other.name == name;

@override
int get hashCode => owner.hashCode ^ name.hashCode;
int get hashCode => origin.hashCode ^ owner.hashCode ^ name.hashCode;
}

final _hostsToUrlParser = {
'GitHub': (Uri url) => GitHubRepository.fromUrl(url),
'GitLab': (Uri url) => GitLabRepository.fromUrl(url),
};

final _hostsToSpecParser = {
'GitHub': (String origin, String owner, String name) {
return GitHubRepository(origin: origin, owner: owner, name: name);
},
'GitLab': (String origin, String owner, String name) {
return GitLabRepository(origin: origin, owner: owner, name: name);
},
};

/// Tries to parse [url] into a [HostedGitRepository].
///
/// Throws a [FormatException] it the given [url] cannot be parsed into an URL
Expand All @@ -222,3 +248,25 @@ HostedGitRepository parseHostedGitRepositoryUrl(Uri url) {
'hosts: ${_hostsToUrlParser.keys.join(', ')}',
);
}

/// Tries to find a [HostedGitRepository] for [type].
///
/// Throws a [FormatException] it the given [type] is not one of the supported
/// git repository host types.
HostedGitRepository parseHostedGitRepositorySpec(
String type,
String origin,
String owner,
String name,
) {
for (final entry in _hostsToSpecParser.entries) {
if (entry.key.toLowerCase() == type.toLowerCase()) {
return entry.value(origin, owner, name);
}
}

throw FormatException(
'$type is not a valid type for a repository on any of the supported '
'hosts: ${_hostsToSpecParser.keys.join(', ')}',
);
}
4 changes: 4 additions & 0 deletions packages/melos/lib/src/common/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -631,3 +631,7 @@ String prettyEncodeJson(Object? value) =>
extension OptionalArgResults on ArgResults {
dynamic optional(String name) => wasParsed(name) ? this[name] : null;
}

String removeTrailingSlash(String url) {
return url.endsWith('/') ? url.substring(0, url.length - 1) : url;
}
61 changes: 47 additions & 14 deletions packages/melos/lib/src/workspace_configs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -473,22 +473,55 @@ class MelosWorkspaceConfig {
}

HostedGitRepository? repository;
final repositoryUrlString =
assertKeyIsA<String?>(key: 'repository', map: yaml);
if (repositoryUrlString != null) {
Uri repositoryUrl;
try {
repositoryUrl = Uri.parse(repositoryUrlString);
} on FormatException catch (e) {
throw MelosConfigException(
'The repository URL $repositoryUrlString is not a valid URL:\n $e',
if (yaml.containsKey('repository')) {
final repositoryYaml = yaml['repository'];

if (repositoryYaml is Map<Object?, Object?>) {
final type = assertKeyIsA<String>(
key: 'type',
map: repositoryYaml,
path: 'repository',
);
final origin = assertKeyIsA<String>(
key: 'origin',
map: repositoryYaml,
path: 'repository',
);
final owner = assertKeyIsA<String>(
key: 'owner',
map: repositoryYaml,
path: 'repository',
);
final name = assertKeyIsA<String>(
key: 'name',
map: repositoryYaml,
path: 'repository',
);
}

try {
repository = parseHostedGitRepositoryUrl(repositoryUrl);
} on FormatException catch (e) {
throw MelosConfigException(e.toString());
try {
repository = parseHostedGitRepositorySpec(type, origin, owner, name);
} on FormatException catch (e) {
throw MelosConfigException(e.toString());
}
} else if (repositoryYaml is String) {
Uri repositoryUrl;
try {
repositoryUrl = Uri.parse(repositoryYaml);
} on FormatException catch (e) {
throw MelosConfigException(
'The repository URL $repositoryYaml is not a valid URL:\n $e',
);
}

try {
repository = parseHostedGitRepositoryUrl(repositoryUrl);
} on FormatException catch (e) {
throw MelosConfigException(e.toString());
}
} else if (repositoryYaml != null) {
throw MelosConfigException(
'The repository value must be a string or repository spec',
);
}
}

Expand Down
Loading

0 comments on commit ce6e4ef

Please sign in to comment.