Add 'tag_pattern' feature to git dependencies (#4427)
diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart
index f5f8d22..7c6c701 100644
--- a/lib/src/command/add.dart
+++ b/lib/src/command/add.dart
@@ -118,6 +118,11 @@
hide: true,
);
argParser.addOption(
+ 'git-tag-pattern',
+ help: 'The tag-pattern to search for versions in repository',
+ hide: true,
+ );
+ argParser.addOption(
'hosted-url',
help: 'URL of package host server',
hide: true,
@@ -543,6 +548,11 @@
if (gitUrl == null) {
usageException('The `--git-url` is required for git dependencies.');
}
+ if (argResults.gitRef != null && argResults.tagPattern != null) {
+ usageException(
+ 'Cannot provide both `--git-ref` and `--git-tag-pattern`.',
+ );
+ }
/// Process the git options to return the simplest representation to be
/// added to the pubspec.
@@ -554,6 +564,7 @@
containingDir: p.current,
ref: argResults.gitRef,
path: argResults.gitPath,
+ tagPattern: argResults.tagPattern,
),
);
} on FormatException catch (e) {
@@ -789,6 +800,8 @@
bool get isDryRun => flag('dry-run');
String? get gitUrl => this['git-url'] as String?;
String? get gitPath => this['git-path'] as String?;
+ String? get tagPattern => this['git-tag-pattern'] as String?;
+
String? get gitRef => this['git-ref'] as String?;
String? get hostedUrl => this['hosted-url'] as String?;
String? get path => this['path'] as String?;
diff --git a/lib/src/command/dependency_services.dart b/lib/src/command/dependency_services.dart
index 738fedd..9b8c63a 100644
--- a/lib/src/command/dependency_services.dart
+++ b/lib/src/command/dependency_services.dart
@@ -424,6 +424,7 @@
} else if (targetRevision != null &&
(lockFileYaml['packages'] as Map).containsKey(targetPackage)) {
final ref = entrypoint.lockFile.packages[targetPackage]!.toRef();
+
final currentDescription = ref.description as GitDescription;
final updatedRef = PackageRef(
targetPackage,
@@ -432,6 +433,7 @@
path: currentDescription.path,
ref: targetRevision,
containingDir: directory,
+ tagPattern: currentDescription.tagPattern,
),
);
final versions = await cache.getVersions(updatedRef);
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart
index 5a30953..9332447 100644
--- a/lib/src/global_packages.dart
+++ b/lib/src/global_packages.dart
@@ -93,6 +93,7 @@
required bool overwriteBinStubs,
String? path,
String? ref,
+ String? tagPattern,
}) async {
final name = await cache.git.getPackageNameFromRepo(
repo,
@@ -100,6 +101,7 @@
path,
cache,
relativeTo: p.current,
+ tagPattern: tagPattern,
);
// TODO(nweiz): Add some special handling for git repos that contain path
diff --git a/lib/src/language_version.dart b/lib/src/language_version.dart
index 6bf4c8a..fdfb2bf 100644
--- a/lib/src/language_version.dart
+++ b/lib/src/language_version.dart
@@ -65,6 +65,8 @@
bool get supportsWorkspaces => this >= firstVersionWithWorkspaces;
+ bool get supportsTagPattern => this >= firstVersionWithTagPattern;
+
bool get forbidsUnknownDescriptionKeys =>
this >= firstVersionForbidingUnknownDescriptionKeys;
@@ -105,6 +107,7 @@
static const firstVersionWithNullSafety = LanguageVersion(2, 12);
static const firstVersionWithShorterHostedSyntax = LanguageVersion(2, 15);
static const firstVersionWithWorkspaces = LanguageVersion(3, 5);
+ static const firstVersionWithTagPattern = LanguageVersion(3, 9);
static const firstVersionForbidingUnknownDescriptionKeys = LanguageVersion(
3,
7,
diff --git a/lib/src/source/git.dart b/lib/src/source/git.dart
index 8c7ff04..91d55af 100644
--- a/lib/src/source/git.dart
+++ b/lib/src/source/git.dart
@@ -24,6 +24,8 @@
import 'path.dart';
import 'root.dart';
+typedef TaggedVersion = ({Version version, String commitId});
+
/// A package source that gets packages from Git repos.
class GitSource extends CachedSource {
static GitSource instance = GitSource._();
@@ -43,6 +45,7 @@
String url;
String? ref;
String? path;
+ String? tagPattern;
if (description is String) {
url = description;
} else if (description is! Map) {
@@ -77,9 +80,35 @@
}
path = descriptionPath;
+ final descriptionTagPattern = description['tag_pattern'];
+
+ if (descriptionTagPattern is! String?) {
+ throw const FormatException(
+ "The 'tag_pattern' field of the description "
+ 'must be a string or null.',
+ );
+ } else {
+ if (descriptionTagPattern != null) {
+ if (!languageVersion.supportsTagPattern) {
+ throw FormatException(
+ 'Using `git: {tagPattern: }` '
+ 'is only supported with a minimum SDK '
+ 'constraint of ${LanguageVersion.firstVersionWithTagPattern}.',
+ );
+ }
+ validateTagPattern(descriptionTagPattern);
+ }
+ tagPattern = descriptionTagPattern;
+ }
+
+ if (ref != null && tagPattern != null) {
+ throw const FormatException(
+ 'A git description cannot have both a ref and a `tag_pattern`.',
+ );
+ }
if (languageVersion.forbidsUnknownDescriptionKeys) {
for (final key in description.keys) {
- if (!['url', 'ref', 'path'].contains(key)) {
+ if (!['url', 'ref', 'path', 'tag_pattern'].contains(key)) {
throw FormatException('Unknown key "$key" in description.');
}
}
@@ -99,6 +128,7 @@
containingDir: containingDir,
ref: ref,
path: _validatedPath(path),
+ tagPattern: tagPattern,
),
);
}
@@ -140,6 +170,15 @@
'must be a string.',
);
}
+
+ final tagPattern = description['tag_pattern'];
+ if (tagPattern is! String?) {
+ throw const FormatException(
+ "The 'tag_pattern' field of the description "
+ 'must be a string.',
+ );
+ }
+
return PackageId(
name,
version,
@@ -149,6 +188,7 @@
ref: ref,
path: _validatedPath(description['path']),
containingDir: containingDir,
+ tagPattern: tagPattern,
),
resolvedRef,
),
@@ -251,17 +291,27 @@
String? path,
SystemCache cache, {
required String relativeTo,
+ required String? tagPattern,
}) async {
+ assert(
+ !(ref != null && tagPattern != null),
+ 'Cannot have both a `tagPattern` and a `ref`',
+ );
final description = GitDescription(
url: url,
ref: ref,
path: path,
containingDir: relativeTo,
+ tagPattern: tagPattern,
);
return await _pool.withResource(() async {
await _ensureRepoCache(description, cache);
final path = _repoCachePath(description, cache);
- final revision = await _firstRevision(path, description.ref);
+
+ final revision =
+ tagPattern != null
+ ? (await _listTaggedVersions(path, tagPattern)).last.commitId
+ : await _firstRevision(path, description.ref);
final resolvedDescription = ResolvedGitDescription(description, revision);
return Pubspec.parse(
@@ -322,16 +372,39 @@
return await _pool.withResource(() async {
await _ensureRepoCache(description, cache);
final path = _repoCachePath(description, cache);
- final revision = await _firstRevision(path, description.ref);
- final pubspec = await _describeUncached(ref, revision, cache);
+ final result = <PackageId>[];
+ if (description.tagPattern case final String tagPattern) {
+ final versions = await _listTaggedVersions(path, tagPattern);
+ for (final version in versions) {
+ result.add(
+ PackageId(
+ ref.name,
+ version.version,
+ ResolvedGitDescription(description, version.commitId),
+ ),
+ );
+ }
+ return result;
+ } else {
+ final revision = await _firstRevision(path, description.ref);
- return [
- PackageId(
- ref.name,
- pubspec.version,
- ResolvedGitDescription(description, revision),
- ),
- ];
+ final Pubspec pubspec;
+ pubspec = await _describeUncached(ref, revision, cache);
+ result.add(
+ PackageId(
+ ref.name,
+ pubspec.version,
+ ResolvedGitDescription(description, revision),
+ ),
+ );
+ return [
+ PackageId(
+ ref.name,
+ pubspec.version,
+ ResolvedGitDescription(description, revision),
+ ),
+ ];
+ }
});
}
@@ -688,6 +761,45 @@
String _packageListPath(String revisionCachePath) =>
p.join(revisionCachePath, '.git/pub-packages');
+ /// List all tags in [path] and returns all versions matching
+ /// [tagPattern].
+ Future<List<TaggedVersion>> _listTaggedVersions(
+ String path,
+ String tagPattern,
+ ) async {
+ final output = await git.run([
+ 'tag',
+ '--list',
+ '--format',
+ // We can use space here, as it is not allowed in a git tag
+ // https://git-scm.com/docs/git-check-ref-format
+ '%(refname:lstrip=2) %(objectname)',
+ ], workingDir: path);
+ final lines = output.trim().split('\n');
+ final result = <TaggedVersion>[];
+ final compiledTagPattern = compileTagPattern(tagPattern);
+ for (final line in lines) {
+ final parts = line.split(' ');
+ if (parts.length != 2) {
+ throw PackageNotFoundException('Bad output from `git tag --list`');
+ }
+ final match = compiledTagPattern.firstMatch(parts[0]);
+ if (match == null) continue;
+
+ final Version version;
+
+ try {
+ version = Version.parse(match[1]!);
+ } on FormatException catch (e) {
+ throw StateError(
+ 'Matched part ${Version.parse(match[1]!)} did not match version $e.',
+ );
+ }
+ result.add((version: version, commitId: parts[1]));
+ }
+ return result;
+ }
+
/// Runs "git rev-list" on [reference] in [path] and returns the first result.
///
/// This assumes that the canonical clone already exists.
@@ -801,6 +913,12 @@
/// not allow strings of the form: 'git@github.com:dart-lang/pub.git'.
final String url;
+ /// A string containing [tagPatternVersionMarker] used to match version
+ /// numbers in a git tag. For example "v{{version}}".
+ ///
+ /// Only one of [ref] and [tagPattern] can be non-`null` at a time.
+ final String? tagPattern;
+
/// `true` if [url] was parsed from a relative url.
final bool relative;
@@ -812,11 +930,14 @@
/// Represented as a relative url.
final String path;
+ late final RegExp compiledTagPattern = compileTagPattern(tagPattern!);
+
GitDescription.raw({
required this.url,
required this.relative,
required String? ref,
required String? path,
+ required this.tagPattern,
}) : ref = ref ?? 'HEAD',
path = path ?? '.';
@@ -825,6 +946,7 @@
required String? ref,
required String? path,
required String? containingDir,
+ required String? tagPattern,
}) {
final validatedUrl = GitSource._validatedUrl(url, containingDir);
return GitDescription.raw(
@@ -832,6 +954,7 @@
relative: validatedUrl.wasRelative,
ref: ref,
path: path,
+ tagPattern: tagPattern,
);
}
@@ -856,11 +979,12 @@
from: p.toUri(p.normalize(p.absolute(containingDir))).toString(),
)
: url;
- if (ref == 'HEAD' && path == '.') return relativeUrl;
+ if (ref == 'HEAD' && path == '.' && tagPattern == null) return relativeUrl;
return {
'url': relativeUrl,
if (ref != 'HEAD') 'ref': ref,
if (path != '.') 'path': path,
+ if (tagPattern != null) 'tag_pattern': tagPattern,
};
}
@@ -875,8 +999,13 @@
other.path == path;
}
- GitDescription withRef(String newRef) =>
- GitDescription.raw(url: url, relative: relative, ref: newRef, path: path);
+ GitDescription withRef(String newRef) => GitDescription.raw(
+ url: url,
+ relative: relative,
+ ref: newRef,
+ path: path,
+ tagPattern: tagPattern,
+ );
@override
int get hashCode => Object.hash(url, ref, path);
@@ -894,7 +1023,7 @@
}
@override
- bool get hasMultipleVersions => false;
+ bool get hasMultipleVersions => tagPattern != null;
}
class ResolvedGitDescription extends ResolvedDescription {
@@ -925,7 +1054,11 @@
: description.url;
return {
'url': url,
- 'ref': description.ref,
+
+ if (description.tagPattern == null)
+ 'ref': description.ref
+ else
+ 'tag-pattern': description.tagPattern,
'resolved-ref': resolvedRef,
'path': description.path,
};
@@ -954,3 +1087,42 @@
Platform.isWindows ? path.replaceAll('\\', '/') : path;
return '--git-dir=$forwardSlashPath';
}
+
+const String tagPatternVersionMarker = '{{version}}';
+
+// Adapted from pub_semver-2.1.4/lib/src/version.dart
+const versionPattern =
+ r'(\d+)\.(\d+)\.(\d+)' // Version number.
+ r'(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?' // Pre-release.
+ r'(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?'; // build
+
+/// Throws [FormatException] if it doesn't contain a single instance of
+/// [tagPatternVersionMarker].
+void validateTagPattern(String tagPattern) {
+ final parts = tagPattern.split(tagPatternVersionMarker);
+ if (parts.length != 2) {
+ throw const FormatException(
+ 'The `tag_pattern` must contain a single "{{version}}" '
+ 'to match different versions',
+ );
+ }
+}
+
+/// Takes a [tagPattern] and returns a [RegExp] matching the relevant tags.
+///
+/// The tagPattern should contain '{{version}}' which will match a pub_semver
+/// version. The rest of the tagPattern is matched verbatim.
+///
+/// Assumes that [tagPattern] has a single occurence of
+/// [tagPatternVersionMarker].
+RegExp compileTagPattern(String tagPattern) {
+ final parts = tagPattern.split(tagPatternVersionMarker);
+ final before = parts[0];
+ final after = parts[1];
+
+ return RegExp(
+ r'^'
+ '$before($versionPattern)$after'
+ r'$',
+ );
+}
diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart
index 9b73cb3..f28ce01 100644
--- a/lib/src/source/hosted.dart
+++ b/lib/src/source/hosted.dart
@@ -454,7 +454,6 @@
}
advisoriesDate = DateTime.parse(advisoriesUpdated);
}
-
final status = PackageStatus(
isDiscontinued: isDiscontinued,
discontinuedReplacedBy: replacedBy,
diff --git a/lib/src/source/path.dart b/lib/src/source/path.dart
index 7859e5e..a5939e7 100644
--- a/lib/src/source/path.dart
+++ b/lib/src/source/path.dart
@@ -115,6 +115,7 @@
relative: containingDescription.description.relative,
// Always refer to the same commit as the containing pubspec.
ref: containingDescription.resolvedRef,
+ tagPattern: null,
path: resolvedPath,
),
);
diff --git a/test/add/git/git_test.dart b/test/add/git/git_test.dart
index 90d8f39..5155ee3 100644
--- a/test/add/git/git_test.dart
+++ b/test/add/git/git_test.dart
@@ -280,4 +280,51 @@
}),
]).validate();
});
+
+ test('Can add git tag_pattern using descriptors', () async {
+ ensureGit();
+
+ await d.git('foo.git', [
+ d.libDir('foo'),
+ d.libPubspec('foo', '1.0.0'),
+ ]).create();
+
+ await d.git('foo.git').tag('v1.0.0');
+
+ await d.git('foo.git', [
+ d.libDir('foo'),
+ d.libPubspec('foo', '2.0.0'),
+ ]).commit();
+ await d.git('foo.git').tag('v2.0.0');
+ await d.git('foo.git', [
+ d.libDir('foo'),
+ d.libPubspec('foo', '3.0.0'),
+ ]).commit(); // Not tagged, we won't get this.
+ await d
+ .appDir(
+ dependencies: {},
+ pubspec: {
+ 'environment': {'sdk': '^3.9.0'},
+ },
+ )
+ .create();
+
+ await pubAdd(
+ args: ['foo:{"git":{"url": "../foo.git", "tag_pattern":"v{{version}}"}}'],
+ environment: {'_PUB_TEST_SDK_VERSION': '3.9.0'},
+ );
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'environment': {'sdk': '^3.9.0'},
+ 'dependencies': {
+ 'foo': {
+ 'git': {'url': '../foo.git', 'tag_pattern': 'v{{version}}'},
+ 'version': '^2.0.0',
+ },
+ },
+ }),
+ ]).validate();
+ });
}
diff --git a/test/descriptor/git.dart b/test/descriptor/git.dart
index 2261486..0ceb72f 100644
--- a/test/descriptor/git.dart
+++ b/test/descriptor/git.dart
@@ -36,6 +36,15 @@
]);
}
+ /// Adds a tag named [tag] to the repo described by `this`.
+ ///
+ /// [parent] defaults to [sandbox].
+ Future tag(String tag, [String? parent]) async {
+ await _runGitCommands(parent, [
+ ['tag', '-a', tag, '-m', 'Some message'],
+ ]);
+ }
+
/// Return a Future that completes to the commit in the git repository
/// referred to by [ref].
///
diff --git a/test/get/git/check_out_and_upgrade_test.dart b/test/get/git/check_out_and_upgrade_test.dart
index 2bf0743..bbe598a 100644
--- a/test/get/git/check_out_and_upgrade_test.dart
+++ b/test/get/git/check_out_and_upgrade_test.dart
@@ -61,4 +61,77 @@
expect(packageSpec('foo'), isNot(originalFooSpec));
});
+
+ test('checks out and upgrades a package from with a tag-pattern', () async {
+ ensureGit();
+
+ final repo = d.git('foo.git', [
+ d.libDir('foo'),
+ d.libPubspec('foo', '1.0.0'),
+ ]);
+ await repo.create();
+ await repo.tag('v1.0.0');
+
+ await d
+ .appDir(
+ dependencies: {
+ 'foo': {
+ 'git': {'url': '../foo.git', 'tag_pattern': 'v{{version}}'},
+ 'version': '^1.0.0',
+ },
+ },
+ pubspec: {
+ 'environment': {'sdk': '^3.9.0'},
+ },
+ )
+ .create();
+
+ await pubGet(
+ output: contains('+ foo 1.0.0'),
+ environment: {'_PUB_TEST_SDK_VERSION': '3.9.0'},
+ );
+
+ // This should be found by `pub upgrade`.
+ await d.git('foo.git', [
+ d.libDir('foo'),
+ d.libPubspec('foo', '1.5.0'),
+ ]).commit();
+ await repo.tag('v1.5.0');
+
+ // The untagged version should not be found by `pub upgrade`.
+ await d.git('foo.git', [
+ d.libDir('foo'),
+ d.libPubspec('foo', '1.7.0'),
+ ]).commit();
+
+ // This should be found by `pub upgrade --major-versions`
+ await d.git('foo.git', [
+ d.libDir('foo'),
+ d.libPubspec('foo', '2.0.0'),
+ ]).commit();
+ await repo.tag('v2.0.0');
+
+ // A version that is not tagged according to the pattern should not be
+ // chosen by the `upgrade --major-versions`.
+ await d.git('foo.git', [
+ d.libDir('foo'),
+ d.libPubspec('foo', '3.0.0'),
+ ]).commit();
+ await repo.tag('unrelatedTag');
+
+ await pubUpgrade(
+ output: allOf(contains('> foo 1.5.0'), contains('Changed 1 dependency!')),
+ environment: {'_PUB_TEST_SDK_VERSION': '3.9.0'},
+ );
+
+ await pubUpgrade(
+ args: ['--major-versions'],
+ output: allOf(
+ contains('> foo 2.0.0'),
+ contains('foo: ^1.0.0 -> ^2.0.0'),
+ contains('Changed 1 dependency!'),
+ ),
+ environment: {'_PUB_TEST_SDK_VERSION': '3.9.0'},
+ );
+ });
}
diff --git a/test/get/git/ssh_url_test.dart b/test/get/git/ssh_url_test.dart
index 81bf45e..43c13f6 100644
--- a/test/get/git/ssh_url_test.dart
+++ b/test/get/git/ssh_url_test.dart
@@ -23,6 +23,11 @@
ref: 'main',
path: 'abc/',
containingDir: null,
+ tagPattern: null,
+ );
+ expect(
+ description.format(),
+ 'git@github.com:dart-lang/pub.git at main in abc/',
);
expect(
description.format(),
diff --git a/test/get/git/tag_pattern_test.dart b/test/get/git/tag_pattern_test.dart
new file mode 100644
index 0000000..0f36ae4
--- /dev/null
+++ b/test/get/git/tag_pattern_test.dart
@@ -0,0 +1,316 @@
+// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:pub/src/exit_codes.dart';
+import 'package:test/test.dart';
+import 'package:yaml/yaml.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+void main() {
+ test('Versions inside a tag_pattern dependency can depend on versions from '
+ 'another commit', () async {
+ ensureGit();
+ await d.git('foo.git', [
+ d.libPubspec(
+ 'foo',
+ '1.0.0',
+ sdk: '^3.9.0',
+ deps: {
+ 'bar': {
+ 'git': {
+ 'url': p.join(d.sandbox, 'bar'),
+ 'tag_pattern': '{{version}}',
+ },
+ 'version': '^2.0.0',
+ },
+ },
+ ),
+ ]).create();
+ await d.git('foo.git', []).tag('1.0.0');
+
+ await d.git('foo.git', [
+ d.libPubspec(
+ 'foo',
+ '2.0.0',
+ sdk: '^3.9.0',
+ deps: {
+ 'bar': {
+ 'git': {
+ 'url': p.join(d.sandbox, 'bar.git'),
+ 'tag_pattern': '{{version}}',
+ },
+ 'version': '^1.0.0',
+ },
+ },
+ ),
+ ]).commit();
+ await d.git('foo.git', []).tag('2.0.0');
+
+ await d.git('bar.git', [
+ d.libPubspec(
+ 'bar',
+ '1.0.0',
+ sdk: '^3.9.0',
+ deps: {
+ 'foo': {
+ 'git': {
+ 'url': p.join(d.sandbox, 'bar.git'),
+ 'tag_pattern': '{{version}}',
+ },
+ 'version': '^2.0.0',
+ },
+ },
+ ),
+ ]).create();
+ await d.git('bar.git', []).tag('1.0.0');
+
+ await d.git('bar.git', [
+ d.libPubspec(
+ 'bar',
+ '2.0.0',
+ sdk: '^3.9.0',
+ deps: {
+ 'foo': {
+ 'git': {
+ 'url': p.join(d.sandbox, 'foo.git'),
+ 'tag_pattern': '{{version}}',
+ },
+ 'version': '^1.0.0',
+ },
+ },
+ ),
+ ]).commit();
+ await d.git('bar.git', []).tag('2.0.0');
+
+ await d
+ .appDir(
+ dependencies: {
+ 'foo': {
+ 'git': {
+ 'url': p.join(d.sandbox, 'foo.git'),
+ 'tag_pattern': '{{version}}',
+ },
+ 'version': '^1.0.0',
+ },
+ },
+ pubspec: {
+ 'environment': {'sdk': '^3.9.0'},
+ },
+ )
+ .create();
+
+ await pubGet(
+ output: allOf(contains('+ foo 1.0.0'), contains('+ bar 2.0.0')),
+ environment: {'_PUB_TEST_SDK_VERSION': '3.9.0'},
+ );
+ final pubspec = loadYaml(
+ File(p.join(d.sandbox, appPath, 'pubspec.lock')).readAsStringSync(),
+ );
+ final s = Platform.pathSeparator;
+ final foo = ((pubspec as Map)['packages'] as Map)['foo'];
+ expect(foo, {
+ 'dependency': 'direct main',
+ 'description': {
+ 'path': '.',
+ 'resolved-ref': isA<String>(),
+ 'tag-pattern': '{{version}}',
+ 'url': '${d.sandbox}${s}foo.git',
+ },
+ 'source': 'git',
+ 'version': '1.0.0',
+ });
+ });
+
+ test('Versions inside a tag_pattern dependency cannot depend on '
+ 'version from another commit via path-dependencies', () async {
+ ensureGit();
+
+ await d.git('repo.git', [
+ d.dir('foo', [
+ d.libPubspec(
+ 'foo',
+ '1.0.0',
+ deps: {
+ 'bar': {'path': '../bar', 'version': '^2.0.0'},
+ },
+ ),
+ ]),
+ d.dir('bar', [
+ d.libPubspec(
+ 'bar',
+ '2.0.0',
+ deps: {
+ 'foo': {'path': '../foo', 'version': '^1.0.0'},
+ },
+ ),
+ ]),
+ ]).create();
+ await d.git('repo.git', []).tag('foo-1.0.0');
+ await d.git('repo.git', []).tag('bar-2.0.0');
+
+ await d.git('repo.git', [
+ d.dir('foo', [
+ d.libPubspec(
+ 'foo',
+ '2.0.0',
+ deps: {
+ 'bar': {'path': '../bar', 'version': '^2.0.0'},
+ },
+ ),
+ ]),
+ d.dir('bar', [
+ d.libPubspec(
+ 'bar',
+ '1.0.0',
+ deps: {
+ 'foo': {'path': '../foo', 'version': '^1.0.0'},
+ },
+ ),
+ ]),
+ ]).commit();
+ await d.git('repo.git', []).tag('foo-2.0.0');
+ await d.git('repo.git', []).tag('bar-1.0.0');
+
+ await d
+ .appDir(
+ dependencies: {
+ 'foo': {
+ 'git': {
+ 'url': '../repo.git',
+ 'tag_pattern': 'foo-{{version}}',
+ 'path': 'foo',
+ },
+ 'version': '^1.0.0',
+ },
+ },
+ pubspec: {
+ 'environment': {'sdk': '^3.9.0'},
+ },
+ )
+ .create();
+ final s = RegExp.escape(p.separator);
+ await pubGet(
+ error: matches(
+ 'Because foo from git ..${s}repo.git at HEAD in foo '
+ 'depends on bar \\^2.0.0 from git '
+ 'which depends on foo from git ..${s}repo.git at [a-f0-9]+ in foo, '
+ 'foo <2.0.0 from git is forbidden',
+ ),
+ environment: {'_PUB_TEST_SDK_VERSION': '3.9.0'},
+ );
+ });
+
+ test('tag_pattern must contain "{{version}}"', () async {
+ await d
+ .appDir(
+ dependencies: {
+ 'foo': {
+ 'git': {'url': 'some/git/path', 'tag_pattern': 'v100'},
+ },
+ },
+ pubspec: {
+ 'environment': {'sdk': '^3.9.0'},
+ },
+ )
+ .create();
+
+ await pubGet(
+ environment: {'_PUB_TEST_SDK_VERSION': '3.9.0'},
+ error: contains(
+ 'Invalid description in the "myapp" pubspec on the "foo" dependency: '
+ 'The `tag_pattern` must contain a single "{{version}}" '
+ 'to match different versions',
+ ),
+ exitCode: DATA,
+ );
+ });
+
+ test('tag_pattern must contain at most one "{{version}}"', () async {
+ await d
+ .appDir(
+ dependencies: {
+ 'foo': {
+ 'git': {
+ 'url': 'some/git/path',
+ 'tag_pattern': 'v{{version}}{{version}}',
+ },
+ },
+ },
+ pubspec: {
+ 'environment': {'sdk': '^3.9.0'},
+ },
+ )
+ .create();
+
+ await pubGet(
+ environment: {'_PUB_TEST_SDK_VERSION': '3.9.0'},
+ error: contains(
+ 'Invalid description in the "myapp" pubspec on the "foo" dependency: '
+ 'The `tag_pattern` must contain a single "{{version}}" '
+ 'to match different versions',
+ ),
+ exitCode: DATA,
+ );
+ });
+
+ test(
+ 'tagged version must contain the correct version of dependency',
+ () async {
+ await d.git('foo.git', [d.libPubspec('foo', '1.0.0')]).create();
+ await d.git('foo.git', []).tag('v1.0.0');
+ await d.git('foo.git', [d.libPubspec('foo', '2.0.0')]).commit();
+ await d.git('foo.git', []).tag('v3.0.0'); // Wrong tag, will not be found.
+
+ await d
+ .appDir(
+ dependencies: {
+ 'foo': {
+ 'git': {'url': '../foo', 'tag_pattern': 'v{{version}}'},
+ },
+ },
+ pubspec: {
+ 'environment': {'sdk': '^3.9.0'},
+ },
+ )
+ .create();
+
+ await pubGet(
+ environment: {'_PUB_TEST_SDK_VERSION': '3.9.0'},
+ output: contains('+ foo 1.0.0'),
+ );
+ },
+ );
+
+ test('Reasonable error when no tagged versions exist', () async {
+ await d.git('foo.git', [d.libPubspec('foo', '1.0.0')]).create();
+ await d.git('foo.git', [d.libPubspec('foo', '2.0.0')]).commit();
+
+ await d
+ .appDir(
+ dependencies: {
+ 'foo': {
+ 'git': {'url': '../foo', 'tag_pattern': 'v{{version}}'},
+ },
+ },
+ pubspec: {
+ 'environment': {'sdk': '^3.9.0'},
+ },
+ )
+ .create();
+
+ await pubGet(
+ environment: {'_PUB_TEST_SDK_VERSION': '3.9.0'},
+ exitCode: UNAVAILABLE,
+ error: contains(
+ 'Because myapp depends on foo any from git which doesn\'t exist '
+ '(Bad output from `git tag --list`)',
+ ),
+ );
+ });
+}