| OLD | NEW |
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 library git_source; | 5 library git_source; |
| 6 | 6 |
| 7 import 'git.dart' as git; | 7 import 'git.dart' as git; |
| 8 import 'io.dart'; | 8 import 'io.dart'; |
| 9 import 'package.dart'; | 9 import 'package.dart'; |
| 10 import 'source.dart'; | 10 import 'source.dart'; |
| 11 import 'source_registry.dart'; | 11 import 'source_registry.dart'; |
| 12 import 'utils.dart'; | 12 import 'utils.dart'; |
| 13 | 13 |
| 14 /** | 14 /// A package source that installs packages from Git repos. |
| 15 * A package source that installs packages from Git repos. | |
| 16 */ | |
| 17 class GitSource extends Source { | 15 class GitSource extends Source { |
| 18 final String name = "git"; | 16 final String name = "git"; |
| 19 | 17 |
| 20 final bool shouldCache = true; | 18 final bool shouldCache = true; |
| 21 | 19 |
| 22 GitSource(); | 20 GitSource(); |
| 23 | 21 |
| 24 /** | 22 /// Clones a Git repo to the local filesystem. |
| 25 * Clones a Git repo to the local filesystem. | 23 /// |
| 26 * | 24 /// The Git cache directory is a little idiosyncratic. At the top level, it |
| 27 * The Git cache directory is a little idiosyncratic. At the top level, it | 25 /// contains a directory for each commit of each repository, named `<package |
| 28 * contains a directory for each commit of each repository, named `<package | 26 /// name>-<commit hash>`. These are the canonical package directories that are |
| 29 * name>-<commit hash>`. These are the canonical package directories that are | 27 /// linked to from the `packages/` directory. |
| 30 * linked to from the `packages/` directory. | 28 /// |
| 31 * | 29 /// In addition, the Git system cache contains a subdirectory named `cache/` |
| 32 * In addition, the Git system cache contains a subdirectory named `cache/` | 30 /// which contains a directory for each separate repository URL, named |
| 33 * which contains a directory for each separate repository URL, named | 31 /// `<package name>-<url hash>`. These are used to check out the repository |
| 34 * `<package name>-<url hash>`. These are used to check out the repository | 32 /// itself; each of the commit-specific directories are clones of a directory |
| 35 * itself; each of the commit-specific directories are clones of a directory | 33 /// in `cache/`. |
| 36 * in `cache/`. | |
| 37 */ | |
| 38 Future<Package> installToSystemCache(PackageId id) { | 34 Future<Package> installToSystemCache(PackageId id) { |
| 39 var revisionCachePath; | 35 var revisionCachePath; |
| 40 | 36 |
| 41 return git.isInstalled.chain((installed) { | 37 return git.isInstalled.chain((installed) { |
| 42 if (!installed) { | 38 if (!installed) { |
| 43 throw new Exception( | 39 throw new Exception( |
| 44 "Cannot install '${id.name}' from Git (${_getUrl(id)}).\n" | 40 "Cannot install '${id.name}' from Git (${_getUrl(id)}).\n" |
| 45 "Please ensure Git is correctly installed."); | 41 "Please ensure Git is correctly installed."); |
| 46 } | 42 } |
| 47 | 43 |
| 48 return ensureDir(join(systemCacheRoot, 'cache')); | 44 return ensureDir(join(systemCacheRoot, 'cache')); |
| 49 }).chain((_) => _ensureRepoCache(id)) | 45 }).chain((_) => _ensureRepoCache(id)) |
| 50 .chain((_) => _revisionCachePath(id)) | 46 .chain((_) => _revisionCachePath(id)) |
| 51 .chain((path) { | 47 .chain((path) { |
| 52 revisionCachePath = path; | 48 revisionCachePath = path; |
| 53 return exists(revisionCachePath); | 49 return exists(revisionCachePath); |
| 54 }).chain((exists) { | 50 }).chain((exists) { |
| 55 if (exists) return new Future.immediate(null); | 51 if (exists) return new Future.immediate(null); |
| 56 return _clone(_repoCachePath(id), revisionCachePath, mirror: false); | 52 return _clone(_repoCachePath(id), revisionCachePath, mirror: false); |
| 57 }).chain((_) { | 53 }).chain((_) { |
| 58 var ref = _getEffectiveRef(id); | 54 var ref = _getEffectiveRef(id); |
| 59 if (ref == 'HEAD') return new Future.immediate(null); | 55 if (ref == 'HEAD') return new Future.immediate(null); |
| 60 return _checkOut(revisionCachePath, ref); | 56 return _checkOut(revisionCachePath, ref); |
| 61 }).chain((_) { | 57 }).chain((_) { |
| 62 return Package.load(id.name, revisionCachePath, systemCache.sources); | 58 return Package.load(id.name, revisionCachePath, systemCache.sources); |
| 63 }); | 59 }); |
| 64 } | 60 } |
| 65 | 61 |
| 66 /** | 62 /// Ensures [description] is a Git URL. |
| 67 * Ensures [description] is a Git URL. | |
| 68 */ | |
| 69 void validateDescription(description, {bool fromLockFile: false}) { | 63 void validateDescription(description, {bool fromLockFile: false}) { |
| 70 // A single string is assumed to be a Git URL. | 64 // A single string is assumed to be a Git URL. |
| 71 if (description is String) return; | 65 if (description is String) return; |
| 72 if (description is! Map || !description.containsKey('url')) { | 66 if (description is! Map || !description.containsKey('url')) { |
| 73 throw new FormatException("The description must be a Git URL or a map " | 67 throw new FormatException("The description must be a Git URL or a map " |
| 74 "with a 'url' key."); | 68 "with a 'url' key."); |
| 75 } | 69 } |
| 76 description = new Map.from(description); | 70 description = new Map.from(description); |
| 77 description.remove('url'); | 71 description.remove('url'); |
| 78 description.remove('ref'); | 72 description.remove('ref'); |
| 79 if (fromLockFile) description.remove('resolved-ref'); | 73 if (fromLockFile) description.remove('resolved-ref'); |
| 80 | 74 |
| 81 if (!description.isEmpty) { | 75 if (!description.isEmpty) { |
| 82 var plural = description.length > 1; | 76 var plural = description.length > 1; |
| 83 var keys = Strings.join(description.keys, ', '); | 77 var keys = Strings.join(description.keys, ', '); |
| 84 throw new FormatException("Invalid key${plural ? 's' : ''}: $keys."); | 78 throw new FormatException("Invalid key${plural ? 's' : ''}: $keys."); |
| 85 } | 79 } |
| 86 } | 80 } |
| 87 | 81 |
| 88 /** | 82 /// Two Git descriptions are equal if both their URLs and their refs are |
| 89 * Two Git descriptions are equal if both their URLs and their refs are equal. | 83 /// equal. |
| 90 */ | |
| 91 bool descriptionsEqual(description1, description2) { | 84 bool descriptionsEqual(description1, description2) { |
| 92 // TODO(nweiz): Do we really want to throw an error if you have two | 85 // TODO(nweiz): Do we really want to throw an error if you have two |
| 93 // dependencies on some repo, one of which specifies a ref and one of which | 86 // dependencies on some repo, one of which specifies a ref and one of which |
| 94 // doesn't? If not, how do we handle that case in the version solver? | 87 // doesn't? If not, how do we handle that case in the version solver? |
| 95 return _getUrl(description1) == _getUrl(description2) && | 88 return _getUrl(description1) == _getUrl(description2) && |
| 96 _getRef(description1) == _getRef(description2); | 89 _getRef(description1) == _getRef(description2); |
| 97 } | 90 } |
| 98 | 91 |
| 99 /** | 92 /// Attaches a specific commit to [id] to disambiguate it. |
| 100 * Attaches a specific commit to [id] to disambiguate it. | |
| 101 */ | |
| 102 Future<PackageId> resolveId(PackageId id) { | 93 Future<PackageId> resolveId(PackageId id) { |
| 103 return _revisionAt(id).transform((revision) { | 94 return _revisionAt(id).transform((revision) { |
| 104 var description = {'url': _getUrl(id), 'ref': _getRef(id)}; | 95 var description = {'url': _getUrl(id), 'ref': _getRef(id)}; |
| 105 description['resolved-ref'] = revision; | 96 description['resolved-ref'] = revision; |
| 106 return new PackageId(id.name, this, id.version, description); | 97 return new PackageId(id.name, this, id.version, description); |
| 107 }); | 98 }); |
| 108 } | 99 } |
| 109 | 100 |
| 110 /** | 101 /// Ensure that the canonical clone of the repository referred to by [id] (the |
| 111 * Ensure that the canonical clone of the repository referred to by [id] (the | 102 /// one in `<system cache>/git/cache`) exists and is up-to-date. Returns a |
| 112 * one in `<system cache>/git/cache`) exists and is up-to-date. Returns a | 103 /// future that completes once this is finished and throws an exception if it |
| 113 * future that completes once this is finished and throws an exception if it | 104 /// fails. |
| 114 * fails. | |
| 115 */ | |
| 116 Future _ensureRepoCache(PackageId id) { | 105 Future _ensureRepoCache(PackageId id) { |
| 117 var path = _repoCachePath(id); | 106 var path = _repoCachePath(id); |
| 118 return exists(path).chain((exists) { | 107 return exists(path).chain((exists) { |
| 119 if (!exists) return _clone(_getUrl(id), path, mirror: true); | 108 if (!exists) return _clone(_getUrl(id), path, mirror: true); |
| 120 | 109 |
| 121 return git.run(["fetch"], workingDir: path).transform((result) => null); | 110 return git.run(["fetch"], workingDir: path).transform((result) => null); |
| 122 }); | 111 }); |
| 123 } | 112 } |
| 124 | 113 |
| 125 /** | 114 /// Returns a future that completes to the revision hash of [id]. |
| 126 * Returns a future that completes to the revision hash of [id]. | |
| 127 */ | |
| 128 Future<String> _revisionAt(PackageId id) { | 115 Future<String> _revisionAt(PackageId id) { |
| 129 return git.run(["rev-parse", _getEffectiveRef(id)], | 116 return git.run(["rev-parse", _getEffectiveRef(id)], |
| 130 workingDir: _repoCachePath(id)).transform((result) => result[0]); | 117 workingDir: _repoCachePath(id)).transform((result) => result[0]); |
| 131 } | 118 } |
| 132 | 119 |
| 133 /** | 120 /// Returns the path to the revision-specific cache of [id]. |
| 134 * Returns the path to the revision-specific cache of [id]. | |
| 135 */ | |
| 136 Future<String> _revisionCachePath(PackageId id) { | 121 Future<String> _revisionCachePath(PackageId id) { |
| 137 return _revisionAt(id).transform((rev) { | 122 return _revisionAt(id).transform((rev) { |
| 138 var revisionCacheName = '${id.name}-$rev'; | 123 var revisionCacheName = '${id.name}-$rev'; |
| 139 return join(systemCacheRoot, revisionCacheName); | 124 return join(systemCacheRoot, revisionCacheName); |
| 140 }); | 125 }); |
| 141 } | 126 } |
| 142 | 127 |
| 143 /** | 128 /// Clones the repo at the URI [from] to the path [to] on the local |
| 144 * Clones the repo at the URI [from] to the path [to] on the local filesystem. | 129 /// filesystem. |
| 145 * | 130 /// |
| 146 * If [mirror] is true, create a bare, mirrored clone. This doesn't check out | 131 /// If [mirror] is true, create a bare, mirrored clone. This doesn't check out |
| 147 * the working tree, but instead makes the repository a local mirror of the | 132 /// the working tree, but instead makes the repository a local mirror of the |
| 148 * remote repository. See the manpage for `git clone` for more information. | 133 /// remote repository. See the manpage for `git clone` for more information. |
| 149 */ | |
| 150 Future _clone(String from, String to, {bool mirror: false}) { | 134 Future _clone(String from, String to, {bool mirror: false}) { |
| 151 // Git on Windows does not seem to automatically create the destination | 135 // Git on Windows does not seem to automatically create the destination |
| 152 // directory. | 136 // directory. |
| 153 return ensureDir(to).chain((_) { | 137 return ensureDir(to).chain((_) { |
| 154 var args = ["clone", from, to]; | 138 var args = ["clone", from, to]; |
| 155 if (mirror) args.insertRange(1, 1, "--mirror"); | 139 if (mirror) args.insertRange(1, 1, "--mirror"); |
| 156 return git.run(args); | 140 return git.run(args); |
| 157 }).transform((result) => null); | 141 }).transform((result) => null); |
| 158 } | 142 } |
| 159 | 143 |
| 160 /** | 144 /// Checks out the reference [ref] in [repoPath]. |
| 161 * Checks out the reference [ref] in [repoPath]. | |
| 162 */ | |
| 163 Future _checkOut(String repoPath, String ref) { | 145 Future _checkOut(String repoPath, String ref) { |
| 164 return git.run(["checkout", ref], workingDir: repoPath).transform( | 146 return git.run(["checkout", ref], workingDir: repoPath).transform( |
| 165 (result) => null); | 147 (result) => null); |
| 166 } | 148 } |
| 167 | 149 |
| 168 /** | 150 /// Returns the path to the canonical clone of the repository referred to by |
| 169 * Returns the path to the canonical clone of the repository referred to by | 151 /// [id] (the one in `<system cache>/git/cache`). |
| 170 * [id] (the one in `<system cache>/git/cache`). | |
| 171 */ | |
| 172 String _repoCachePath(PackageId id) { | 152 String _repoCachePath(PackageId id) { |
| 173 var repoCacheName = '${id.name}-${sha1(_getUrl(id))}'; | 153 var repoCacheName = '${id.name}-${sha1(_getUrl(id))}'; |
| 174 return join(systemCacheRoot, 'cache', repoCacheName); | 154 return join(systemCacheRoot, 'cache', repoCacheName); |
| 175 } | 155 } |
| 176 | 156 |
| 177 /** | 157 /// Returns the repository URL for [id]. |
| 178 * Returns the repository URL for [id]. | 158 /// |
| 179 * | 159 /// [description] may be a description or a [PackageId]. |
| 180 * [description] may be a description or a [PackageId]. | |
| 181 */ | |
| 182 String _getUrl(description) { | 160 String _getUrl(description) { |
| 183 description = _getDescription(description); | 161 description = _getDescription(description); |
| 184 if (description is String) return description; | 162 if (description is String) return description; |
| 185 return description['url']; | 163 return description['url']; |
| 186 } | 164 } |
| 187 | 165 |
| 188 /** | 166 /// Returns the commit ref that should be checked out for [description]. |
| 189 * Returns the commit ref that should be checked out for [description]. | 167 /// |
| 190 * | 168 /// This differs from [_getRef] in that it doesn't just return the ref in |
| 191 * This differs from [_getRef] in that it doesn't just return the ref in | 169 /// [description]. It will return a sensible default if that ref doesn't |
| 192 * [description]. It will return a sensible default if that ref doesn't exist, | 170 /// exist, and it will respect the "resolved-ref" parameter set by |
| 193 * and it will respect the "resolved-ref" parameter set by [resolveId]. | 171 /// [resolveId]. |
| 194 * | 172 /// |
| 195 * [description] may be a description or a [PackageId]. | 173 /// [description] may be a description or a [PackageId]. |
| 196 */ | |
| 197 String _getEffectiveRef(description) { | 174 String _getEffectiveRef(description) { |
| 198 description = _getDescription(description); | 175 description = _getDescription(description); |
| 199 if (description is Map && description.containsKey('resolved-ref')) { | 176 if (description is Map && description.containsKey('resolved-ref')) { |
| 200 return description['resolved-ref']; | 177 return description['resolved-ref']; |
| 201 } | 178 } |
| 202 | 179 |
| 203 var ref = _getRef(description); | 180 var ref = _getRef(description); |
| 204 return ref == null ? 'HEAD' : ref; | 181 return ref == null ? 'HEAD' : ref; |
| 205 } | 182 } |
| 206 | 183 |
| 207 /** | 184 /// Returns the commit ref for [description], or null if none is given. |
| 208 * Returns the commit ref for [description], or null if none is given. | 185 /// |
| 209 * | 186 /// [description] may be a description or a [PackageId]. |
| 210 * [description] may be a description or a [PackageId]. | |
| 211 */ | |
| 212 String _getRef(description) { | 187 String _getRef(description) { |
| 213 description = _getDescription(description); | 188 description = _getDescription(description); |
| 214 if (description is String) return null; | 189 if (description is String) return null; |
| 215 return description['ref']; | 190 return description['ref']; |
| 216 } | 191 } |
| 217 | 192 |
| 218 /** | 193 /// Returns [description] if it's a description, or [PackageId.description] if |
| 219 * Returns [description] if it's a description, or [PackageId.description] if | 194 /// it's a [PackageId]. |
| 220 * it's a [PackageId]. | |
| 221 */ | |
| 222 _getDescription(description) { | 195 _getDescription(description) { |
| 223 if (description is PackageId) return description.description; | 196 if (description is PackageId) return description.description; |
| 224 return description; | 197 return description; |
| 225 } | 198 } |
| 226 } | 199 } |
| OLD | NEW |