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

feat: support self-hosted git repositories #417

Merged
merged 3 commits into from
Oct 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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