Index: lib/src/source/git.dart |
diff --git a/lib/src/source/git.dart b/lib/src/source/git.dart |
index 8211f5ce65828118e6ecfd2f5af2a6287a7e7895..06981fab70f821a28b3836f5956bd46a29c45f58 100644 |
--- a/lib/src/source/git.dart |
+++ b/lib/src/source/git.dart |
@@ -7,6 +7,7 @@ library pub.source.git; |
import 'dart:async'; |
import 'package:path/path.dart' as path; |
+import 'package:pub_semver/pub_semver.dart'; |
import '../git.dart' as git; |
import '../io.dart'; |
@@ -18,6 +19,12 @@ import 'cached.dart'; |
/// A package source that gets packages from Git repos. |
class GitSource extends CachedSource { |
+ /// Returns a reference to a git package with the given [name] and [url]. |
+ /// |
+ /// If passed, [reference] is the Git reference. It defaults to `"HEAD"`. |
+ static PackageRef refFor(String name, String url, {String reference}) => |
+ new PackageRef(name, "git", {'url': url, 'ref': reference ?? 'HEAD'}); |
+ |
/// Given a valid git package description, returns the URL of the repository |
/// it pulls from. |
static String urlFromDescription(description) => description["url"]; |
@@ -40,10 +47,43 @@ class GitSource extends CachedSource { |
}); |
} |
+ Future<List<PackageId>> doGetVersions(PackageRef ref) async { |
+ await _ensureRepoCache(ref); |
+ var path = _repoCachePath(ref); |
+ var revision = await _firstRevision(path, ref.description['ref']); |
+ var pubspec = await _describeUncached(ref, revision); |
+ |
+ return [ |
+ new PackageId(ref.name, name, pubspec.version, { |
+ 'url': ref.description['url'], |
+ 'ref': ref.description['ref'], |
+ 'resolved-ref': revision |
+ }) |
+ ]; |
+ } |
+ |
/// Since we don't have an easy way to read from a remote Git repo, this |
/// just installs [id] into the system cache, then describes it from there. |
- Future<Pubspec> describeUncached(PackageId id) { |
- return downloadToSystemCache(id).then((package) => package.pubspec); |
+ Future<Pubspec> describeUncached(PackageId id) => |
+ _describeUncached(id.toRef(), id.description['resolved-ref']); |
+ |
+ /// Like [describeUncached], but takes a separate [ref] and Git [revision] |
+ /// rather than a single ID. |
+ Future<Pubspec> _describeUncached(PackageRef ref, String revision) async { |
+ await _ensureRevision(ref, revision); |
+ var path = _repoCachePath(ref); |
+ |
+ var lines; |
+ try { |
+ lines = await git.run(["show", "$revision:pubspec.yaml"], |
+ workingDir: path); |
+ } on git.GitException catch (_) { |
+ fail('Could not find a file named "pubspec.yaml" in ' |
+ '${ref.description['url']} $revision.'); |
+ } |
+ |
+ return new Pubspec.parse(lines.join("\n"), systemCache.sources, |
+ expectedName: ref.name); |
} |
/// Clones a Git repo to the local filesystem. |
@@ -59,60 +99,87 @@ class GitSource extends CachedSource { |
/// itself; each of the commit-specific directories are clones of a directory |
/// in `cache/`. |
Future<Package> downloadToSystemCache(PackageId id) async { |
+ var ref = id.toRef(); |
if (!git.isInstalled) { |
- fail("Cannot get ${id.name} from Git (${_getUrl(id)}).\n" |
+ fail("Cannot get ${id.name} from Git (${ref.description['url']}).\n" |
"Please ensure Git is correctly installed."); |
} |
ensureDir(path.join(systemCacheRoot, 'cache')); |
- await _ensureRevision(id); |
- var revisionCachePath = getDirectory(await resolveId(id)); |
+ await _ensureRevision(ref, id.description['resolved-ref']); |
+ |
+ var revisionCachePath = getDirectory(id); |
if (!entryExists(revisionCachePath)) { |
- await _clone(_repoCachePath(id), revisionCachePath, mirror: false); |
+ await _clone(_repoCachePath(ref), revisionCachePath); |
+ await _checkOut(revisionCachePath, id.description['resolved-ref']); |
} |
- var ref = _getEffectiveRef(id); |
- if (ref != 'HEAD') await _checkOut(revisionCachePath, ref); |
- |
return new Package.load(id.name, revisionCachePath, systemCache.sources); |
} |
/// Returns the path to the revision-specific cache of [id]. |
- String getDirectory(PackageId id) { |
- if (id.description is! Map || !id.description.containsKey('resolved-ref')) { |
- throw new ArgumentError("Can't get the directory for unresolved id $id."); |
- } |
- |
- return path.join(systemCacheRoot, |
- "${id.name}-${id.description['resolved-ref']}"); |
- } |
+ String getDirectory(PackageId id) => path.join( |
+ systemCacheRoot, "${id.name}-${id.description['resolved-ref']}"); |
- /// Ensures [description] is a Git URL. |
- dynamic parseDescription(String containingPath, description, |
- {bool fromLockFile: false}) { |
+ PackageRef parseRef(String name, description, {String containingPath}) { |
// TODO(rnystrom): Handle git URLs that are relative file paths (#8570). |
- // TODO(rnystrom): Now that this function can modify the description, it |
- // may as well canonicalize it to a map so that other code in the source |
- // can assume that. |
- // A single string is assumed to be a Git URL. |
- if (description is String) return description; |
- if (description is! Map || !description.containsKey('url')) { |
+ if (description is String) description = {'url': description}; |
+ |
+ if (description is! Map) { |
throw new FormatException("The description must be a Git URL or a map " |
"with a 'url' key."); |
} |
- var parsed = new Map.from(description); |
- parsed.remove('url'); |
- parsed.remove('ref'); |
- if (fromLockFile) parsed.remove('resolved-ref'); |
+ if (description["url"] is! String) { |
+ throw new FormatException("The 'url' field of the description must be a " |
+ "string."); |
+ } |
+ |
+ // Ensure that it's a valid URL. |
+ Uri.parse(description["url"]); |
- if (!parsed.isEmpty) { |
- var plural = parsed.length > 1; |
- var keys = parsed.keys.join(', '); |
- throw new FormatException("Invalid key${plural ? 's' : ''}: $keys."); |
+ var ref = description["ref"]; |
+ if (ref != null && ref is! String) { |
+ throw new FormatException("The 'ref' field of the description must be a " |
+ "string."); |
} |
- return description; |
+ return new PackageRef(name, this.name, { |
+ "url": description["url"], |
+ "ref": description["ref"] ?? "HEAD" |
+ }); |
+ } |
+ |
+ PackageId parseId(String name, Version version, description) { |
+ if (description is! Map) { |
+ throw new FormatException("The description must be a map with a 'url' " |
+ "key."); |
+ } |
+ |
+ if (description["url"] is! String) { |
+ throw new FormatException("The 'url' field of the description must be a " |
+ "string."); |
+ } |
+ |
+ // Ensure that it's a valid URL. |
+ Uri.parse(description["url"]); |
+ |
+ var ref = description["ref"]; |
+ if (ref != null && ref is! String) { |
+ throw new FormatException("The 'ref' field of the description must be a " |
+ "string."); |
+ } |
+ |
+ if (description["resolved-ref"] is! String) { |
+ throw new FormatException("The 'resolved-ref' field of the description " |
+ "must be a string."); |
+ } |
+ |
+ return new PackageId(name, this.name, version, { |
+ "url": description["url"], |
+ "ref": description["ref"] ?? "HEAD", |
+ "resolved-ref": description["resolved-ref"] |
+ }); |
} |
/// If [description] has a resolved ref, print it out in short-form. |
@@ -134,26 +201,17 @@ class GitSource extends CachedSource { |
// TODO(nweiz): Do we really want to throw an error if you have two |
// dependencies on some repo, one of which specifies a ref and one of which |
// doesn't? If not, how do we handle that case in the version solver? |
- if (_getUrl(description1) != _getUrl(description2)) return false; |
- if (_getRef(description1) != _getRef(description2)) return false; |
+ if (description1['url'] != description2['url']) return false; |
+ if (description1['ref'] != description2['ref']) return false; |
- if (description1 is Map && description1.containsKey('resolved-ref') && |
- description2 is Map && description2.containsKey('resolved-ref')) { |
+ if (description1.containsKey('resolved-ref') && |
+ description2.containsKey('resolved-ref')) { |
return description1['resolved-ref'] == description2['resolved-ref']; |
} |
return true; |
} |
- /// Attaches a specific commit to [id] to disambiguate it. |
- Future<PackageId> resolveId(PackageId id) { |
- return _ensureRevision(id).then((revision) { |
- var description = {'url': _getUrl(id), 'ref': _getRef(id)}; |
- description['resolved-ref'] = revision; |
- return new PackageId(id.name, name, id.version, description); |
- }); |
- } |
- |
List<Package> getCachedPackages() { |
// TODO(keertip): Implement getCachedPackages(). |
throw new UnimplementedError( |
@@ -206,56 +264,65 @@ class GitSource extends CachedSource { |
return new Pair(successes, failures); |
} |
- /// Ensure that the canonical clone of the repository referred to by [id] (the |
- /// one in `<system cache>/git/cache`) exists and contains the revision |
- /// referred to by [id]. |
- /// |
- /// Returns a future that completes to the hash of the revision identified by |
- /// [id]. |
- Future<String> _ensureRevision(PackageId id) { |
- return new Future.sync(() { |
- var path = _repoCachePath(id); |
- if (!entryExists(path)) { |
- return _clone(_getUrl(id), path, mirror: true) |
- .then((_) => _getRev(id)); |
- } |
+ /// Ensures that the canonical clone of the repository referred to by [ref] |
+ /// contains the given Git [revision]. |
+ Future _ensureRevision(PackageRef ref, String revision) async { |
+ var path = _repoCachePath(ref); |
+ if (_updatedRepos.contains(path)) return; |
- // If [id] didn't come from a lockfile, it may be using a symbolic |
- // reference. We want to get the latest version of that reference. |
- var description = id.description; |
- if (description is! Map || !description.containsKey('resolved-ref')) { |
- return _updateRepoCache(id).then((_) => _getRev(id)); |
- } |
+ if (!entryExists(path)) await _createRepoCache(ref); |
- // If [id] did come from a lockfile, then we want to avoid running "git |
- // fetch" if possible to avoid networking time and errors. See if the |
- // revision exists in the repo cache before updating it. |
- return _getRev(id).catchError((error) { |
- if (error is! git.GitException) throw error; |
- return _updateRepoCache(id).then((_) => _getRev(id)); |
- }); |
- }); |
+ // Try to list the revision. If it doesn't exist, git will fail and we'll |
+ // know we have to update the repository. |
+ try { |
+ await _firstRevision(path, revision); |
+ } on git.GitException catch (_) { |
+ await _updateRepoCache(ref); |
+ } |
+ } |
+ |
+ /// Ensures that the canonical clone of the repository referred to by [ref] |
+ /// exists and is up-to-date. |
+ Future _ensureRepoCache(PackageRef ref) async { |
+ var path = _repoCachePath(ref); |
+ if (_updatedRepos.contains(path)) return; |
+ |
+ if (!entryExists(path)) { |
+ await _createRepoCache(ref); |
+ } else { |
+ await _updateRepoCache(ref); |
+ } |
+ } |
+ |
+ /// Creates the canonical clone of the repository referred to by [ref]. |
+ /// |
+ /// This assumes that the canonical clone doesn't yet exist. |
+ Future _createRepoCache(PackageRef ref) async { |
+ var path = _repoCachePath(ref); |
+ assert(!_updatedRepos.contains(path)); |
+ |
+ await _clone(ref.description['url'], path, mirror: true); |
+ _updatedRepos.add(path); |
} |
/// Runs "git fetch" in the canonical clone of the repository referred to by |
- /// [id]. |
+ /// [ref]. |
/// |
/// This assumes that the canonical clone already exists. |
- Future _updateRepoCache(PackageId id) { |
- var path = _repoCachePath(id); |
+ Future _updateRepoCache(PackageRef ref) async { |
+ var path = _repoCachePath(ref); |
if (_updatedRepos.contains(path)) return new Future.value(); |
- return git.run(["fetch"], workingDir: path).then((_) { |
- _updatedRepos.add(path); |
- }); |
+ await git.run(["fetch"], workingDir: path); |
+ _updatedRepos.add(path); |
} |
- /// Runs "git rev-list" in the canonical clone of the repository referred to |
- /// by [id] on the effective ref of [id]. |
+ /// Runs "git rev-list" on [reference] in [path] and returns the first result. |
/// |
/// This assumes that the canonical clone already exists. |
- Future<String> _getRev(PackageId id) { |
- return git.run(["rev-list", "--max-count=1", _getEffectiveRef(id)], |
- workingDir: _repoCachePath(id)).then((result) => result.first); |
+ Future<String> _firstRevision(String path, String reference) async { |
+ var lines = await git.run(["rev-list", "--max-count=1", reference], |
+ workingDir: path); |
+ return lines.first; |
} |
/// Clones the repo at the URI [from] to the path [to] on the local |
@@ -291,51 +358,8 @@ class GitSource extends CachedSource { |
/// Returns the path to the canonical clone of the repository referred to by |
/// [id] (the one in `<system cache>/git/cache`). |
- String _repoCachePath(PackageId id) { |
- var repoCacheName = '${id.name}-${sha1(_getUrl(id))}'; |
+ String _repoCachePath(PackageRef ref) { |
+ var repoCacheName = '${ref.name}-${sha1(ref.description['url'])}'; |
return path.join(systemCacheRoot, 'cache', repoCacheName); |
} |
- |
- /// Returns the repository URL for [id]. |
- /// |
- /// [description] may be a description or a [PackageId]. |
- String _getUrl(description) { |
- description = _getDescription(description); |
- if (description is String) return description; |
- return description['url']; |
- } |
- |
- /// Returns the commit ref that should be checked out for [description]. |
- /// |
- /// This differs from [_getRef] in that it doesn't just return the ref in |
- /// [description]. It will return a sensible default if that ref doesn't |
- /// exist, and it will respect the "resolved-ref" parameter set by |
- /// [resolveId]. |
- /// |
- /// [description] may be a description or a [PackageId]. |
- String _getEffectiveRef(description) { |
- description = _getDescription(description); |
- if (description is Map && description.containsKey('resolved-ref')) { |
- return description['resolved-ref']; |
- } |
- |
- var ref = _getRef(description); |
- return ref == null ? 'HEAD' : ref; |
- } |
- |
- /// Returns the commit ref for [description], or null if none is given. |
- /// |
- /// [description] may be a description or a [PackageId]. |
- String _getRef(description) { |
- description = _getDescription(description); |
- if (description is String) return null; |
- return description['ref']; |
- } |
- |
- /// Returns [description] if it's a description, or [PackageId.description] if |
- /// it's a [PackageId]. |
- _getDescription(description) { |
- if (description is PackageId) return description.description; |
- return description; |
- } |
} |