| OLD | NEW |
| (Empty) |
| 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 | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 library pub.source.git; | |
| 6 | |
| 7 import 'dart:async'; | |
| 8 | |
| 9 import 'package:path/path.dart' as path; | |
| 10 | |
| 11 import '../git.dart' as git; | |
| 12 import '../io.dart'; | |
| 13 import '../log.dart' as log; | |
| 14 import '../package.dart'; | |
| 15 import '../pubspec.dart'; | |
| 16 import '../utils.dart'; | |
| 17 import 'cached.dart'; | |
| 18 | |
| 19 /// A package source that gets packages from Git repos. | |
| 20 class GitSource extends CachedSource { | |
| 21 /// Given a valid git package description, returns the URL of the repository | |
| 22 /// it pulls from. | |
| 23 static String urlFromDescription(description) => description["url"]; | |
| 24 | |
| 25 final name = "git"; | |
| 26 | |
| 27 /// The paths to the canonical clones of repositories for which "git fetch" | |
| 28 /// has already been run during this run of pub. | |
| 29 final _updatedRepos = new Set<String>(); | |
| 30 | |
| 31 /// Given a Git repo that contains a pub package, gets the name of the pub | |
| 32 /// package. | |
| 33 Future<String> getPackageNameFromRepo(String repo) { | |
| 34 // Clone the repo to a temp directory. | |
| 35 return withTempDir((tempDir) { | |
| 36 return _clone(repo, tempDir, shallow: true).then((_) { | |
| 37 var pubspec = new Pubspec.load(tempDir, systemCache.sources); | |
| 38 return pubspec.name; | |
| 39 }); | |
| 40 }); | |
| 41 } | |
| 42 | |
| 43 /// Since we don't have an easy way to read from a remote Git repo, this | |
| 44 /// just installs [id] into the system cache, then describes it from there. | |
| 45 Future<Pubspec> describeUncached(PackageId id) { | |
| 46 return downloadToSystemCache(id).then((package) => package.pubspec); | |
| 47 } | |
| 48 | |
| 49 /// Clones a Git repo to the local filesystem. | |
| 50 /// | |
| 51 /// The Git cache directory is a little idiosyncratic. At the top level, it | |
| 52 /// contains a directory for each commit of each repository, named `<package | |
| 53 /// name>-<commit hash>`. These are the canonical package directories that are | |
| 54 /// linked to from the `packages/` directory. | |
| 55 /// | |
| 56 /// In addition, the Git system cache contains a subdirectory named `cache/` | |
| 57 /// which contains a directory for each separate repository URL, named | |
| 58 /// `<package name>-<url hash>`. These are used to check out the repository | |
| 59 /// itself; each of the commit-specific directories are clones of a directory | |
| 60 /// in `cache/`. | |
| 61 Future<Package> downloadToSystemCache(PackageId id) { | |
| 62 var revisionCachePath; | |
| 63 | |
| 64 if (!git.isInstalled) { | |
| 65 fail( | |
| 66 "Cannot get ${id.name} from Git (${_getUrl(id)}).\n" | |
| 67 "Please ensure Git is correctly installed."); | |
| 68 } | |
| 69 | |
| 70 ensureDir(path.join(systemCacheRoot, 'cache')); | |
| 71 return _ensureRevision(id).then((_) => getDirectory(id)).then((path) { | |
| 72 revisionCachePath = path; | |
| 73 if (entryExists(revisionCachePath)) return null; | |
| 74 return _clone(_repoCachePath(id), revisionCachePath, mirror: false); | |
| 75 }).then((_) { | |
| 76 var ref = _getEffectiveRef(id); | |
| 77 if (ref == 'HEAD') return null; | |
| 78 return _checkOut(revisionCachePath, ref); | |
| 79 }).then((_) { | |
| 80 return new Package.load(id.name, revisionCachePath, systemCache.sources); | |
| 81 }); | |
| 82 } | |
| 83 | |
| 84 /// Returns the path to the revision-specific cache of [id]. | |
| 85 Future<String> getDirectory(PackageId id) { | |
| 86 return _ensureRevision(id).then((rev) { | |
| 87 var revisionCacheName = '${id.name}-$rev'; | |
| 88 return path.join(systemCacheRoot, revisionCacheName); | |
| 89 }); | |
| 90 } | |
| 91 | |
| 92 /// Ensures [description] is a Git URL. | |
| 93 dynamic parseDescription(String containingPath, description, | |
| 94 {bool fromLockFile: false}) { | |
| 95 // TODO(rnystrom): Handle git URLs that are relative file paths (#8570). | |
| 96 // TODO(rnystrom): Now that this function can modify the description, it | |
| 97 // may as well canonicalize it to a map so that other code in the source | |
| 98 // can assume that. | |
| 99 // A single string is assumed to be a Git URL. | |
| 100 if (description is String) return description; | |
| 101 if (description is! Map || !description.containsKey('url')) { | |
| 102 throw new FormatException( | |
| 103 "The description must be a Git URL or a map " "with a 'url' key."); | |
| 104 } | |
| 105 | |
| 106 var parsed = new Map.from(description); | |
| 107 parsed.remove('url'); | |
| 108 parsed.remove('ref'); | |
| 109 if (fromLockFile) parsed.remove('resolved-ref'); | |
| 110 | |
| 111 if (!parsed.isEmpty) { | |
| 112 var plural = parsed.length > 1; | |
| 113 var keys = parsed.keys.join(', '); | |
| 114 throw new FormatException("Invalid key${plural ? 's' : ''}: $keys."); | |
| 115 } | |
| 116 | |
| 117 return description; | |
| 118 } | |
| 119 | |
| 120 /// If [description] has a resolved ref, print it out in short-form. | |
| 121 /// | |
| 122 /// This helps distinguish different git commits with the same pubspec | |
| 123 /// version. | |
| 124 String formatDescription(String containingPath, description) { | |
| 125 if (description is Map && description.containsKey('resolved-ref')) { | |
| 126 return "${description['url']} at " | |
| 127 "${description['resolved-ref'].substring(0, 6)}"; | |
| 128 } else { | |
| 129 return super.formatDescription(containingPath, description); | |
| 130 } | |
| 131 } | |
| 132 | |
| 133 /// Two Git descriptions are equal if both their URLs and their refs are | |
| 134 /// equal. | |
| 135 bool descriptionsEqual(description1, description2) { | |
| 136 // TODO(nweiz): Do we really want to throw an error if you have two | |
| 137 // dependencies on some repo, one of which specifies a ref and one of which | |
| 138 // doesn't? If not, how do we handle that case in the version solver? | |
| 139 if (_getUrl(description1) != _getUrl(description2)) return false; | |
| 140 if (_getRef(description1) != _getRef(description2)) return false; | |
| 141 | |
| 142 if (description1 is Map && | |
| 143 description1.containsKey('resolved-ref') && | |
| 144 description2 is Map && | |
| 145 description2.containsKey('resolved-ref')) { | |
| 146 return description1['resolved-ref'] == description2['resolved-ref']; | |
| 147 } | |
| 148 | |
| 149 return true; | |
| 150 } | |
| 151 | |
| 152 /// Attaches a specific commit to [id] to disambiguate it. | |
| 153 Future<PackageId> resolveId(PackageId id) { | |
| 154 return _ensureRevision(id).then((revision) { | |
| 155 var description = { | |
| 156 'url': _getUrl(id), | |
| 157 'ref': _getRef(id) | |
| 158 }; | |
| 159 description['resolved-ref'] = revision; | |
| 160 return new PackageId(id.name, name, id.version, description); | |
| 161 }); | |
| 162 } | |
| 163 | |
| 164 List<Package> getCachedPackages() { | |
| 165 // TODO(keertip): Implement getCachedPackages(). | |
| 166 throw new UnimplementedError( | |
| 167 "The git source doesn't support listing its cached packages yet."); | |
| 168 } | |
| 169 | |
| 170 /// Resets all cached packages back to the pristine state of the Git | |
| 171 /// repository at the revision they are pinned to. | |
| 172 Future<Pair<int, int>> repairCachedPackages() { | |
| 173 final completer0 = new Completer(); | |
| 174 scheduleMicrotask(() { | |
| 175 try { | |
| 176 join0() { | |
| 177 var successes = 0; | |
| 178 var failures = 0; | |
| 179 var packages = listDir(systemCacheRoot).where(((entry) { | |
| 180 return dirExists(path.join(entry, ".git")); | |
| 181 })).map(((packageDir) { | |
| 182 return new Package.load(null, packageDir, systemCache.sources); | |
| 183 })).toList(); | |
| 184 packages.sort(Package.orderByNameAndVersion); | |
| 185 var it0 = packages.iterator; | |
| 186 break0() { | |
| 187 completer0.complete(new Pair(successes, failures)); | |
| 188 } | |
| 189 var trampoline0; | |
| 190 continue0() { | |
| 191 trampoline0 = null; | |
| 192 if (it0.moveNext()) { | |
| 193 var package = it0.current; | |
| 194 log.message( | |
| 195 "Resetting Git repository for " | |
| 196 "${log.bold(package.name)} ${package.version}..."); | |
| 197 join1() { | |
| 198 trampoline0 = continue0; | |
| 199 do trampoline0(); while (trampoline0 != null); | |
| 200 } | |
| 201 catch0(error, stackTrace) { | |
| 202 try { | |
| 203 if (error is git.GitException) { | |
| 204 log.error( | |
| 205 "Failed to reset ${log.bold(package.name)} " | |
| 206 "${package.version}. Error:\n${error}"); | |
| 207 log.fine(stackTrace); | |
| 208 failures++; | |
| 209 tryDeleteEntry(package.dir); | |
| 210 join1(); | |
| 211 } else { | |
| 212 throw error; | |
| 213 } | |
| 214 } catch (error, stackTrace) { | |
| 215 completer0.completeError(error, stackTrace); | |
| 216 } | |
| 217 } | |
| 218 try { | |
| 219 new Future.value( | |
| 220 git.run(["clean", "-d", "--force", "-x"], workingDir: packag
e.dir)).then((x0) { | |
| 221 trampoline0 = () { | |
| 222 trampoline0 = null; | |
| 223 try { | |
| 224 x0; | |
| 225 new Future.value( | |
| 226 git.run(["reset", "--hard", "HEAD"], workingDir: packa
ge.dir)).then((x1) { | |
| 227 trampoline0 = () { | |
| 228 trampoline0 = null; | |
| 229 try { | |
| 230 x1; | |
| 231 successes++; | |
| 232 join1(); | |
| 233 } catch (e0, s0) { | |
| 234 catch0(e0, s0); | |
| 235 } | |
| 236 }; | |
| 237 do trampoline0(); while (trampoline0 != null); | |
| 238 }, onError: catch0); | |
| 239 } catch (e1, s1) { | |
| 240 catch0(e1, s1); | |
| 241 } | |
| 242 }; | |
| 243 do trampoline0(); while (trampoline0 != null); | |
| 244 }, onError: catch0); | |
| 245 } catch (e2, s2) { | |
| 246 catch0(e2, s2); | |
| 247 } | |
| 248 } else { | |
| 249 break0(); | |
| 250 } | |
| 251 } | |
| 252 trampoline0 = continue0; | |
| 253 do trampoline0(); while (trampoline0 != null); | |
| 254 } | |
| 255 if (!dirExists(systemCacheRoot)) { | |
| 256 completer0.complete(new Pair(0, 0)); | |
| 257 } else { | |
| 258 join0(); | |
| 259 } | |
| 260 } catch (e, s) { | |
| 261 completer0.completeError(e, s); | |
| 262 } | |
| 263 }); | |
| 264 return completer0.future; | |
| 265 } | |
| 266 | |
| 267 /// Ensure that the canonical clone of the repository referred to by [id] (the | |
| 268 /// one in `<system cache>/git/cache`) exists and contains the revision | |
| 269 /// referred to by [id]. | |
| 270 /// | |
| 271 /// Returns a future that completes to the hash of the revision identified by | |
| 272 /// [id]. | |
| 273 Future<String> _ensureRevision(PackageId id) { | |
| 274 return new Future.sync(() { | |
| 275 var path = _repoCachePath(id); | |
| 276 if (!entryExists(path)) { | |
| 277 return _clone(_getUrl(id), path, mirror: true).then((_) => _getRev(id)); | |
| 278 } | |
| 279 | |
| 280 // If [id] didn't come from a lockfile, it may be using a symbolic | |
| 281 // reference. We want to get the latest version of that reference. | |
| 282 var description = id.description; | |
| 283 if (description is! Map || !description.containsKey('resolved-ref')) { | |
| 284 return _updateRepoCache(id).then((_) => _getRev(id)); | |
| 285 } | |
| 286 | |
| 287 // If [id] did come from a lockfile, then we want to avoid running "git | |
| 288 // fetch" if possible to avoid networking time and errors. See if the | |
| 289 // revision exists in the repo cache before updating it. | |
| 290 return _getRev(id).catchError((error) { | |
| 291 if (error is! git.GitException) throw error; | |
| 292 return _updateRepoCache(id).then((_) => _getRev(id)); | |
| 293 }); | |
| 294 }); | |
| 295 } | |
| 296 | |
| 297 /// Runs "git fetch" in the canonical clone of the repository referred to by | |
| 298 /// [id]. | |
| 299 /// | |
| 300 /// This assumes that the canonical clone already exists. | |
| 301 Future _updateRepoCache(PackageId id) { | |
| 302 var path = _repoCachePath(id); | |
| 303 if (_updatedRepos.contains(path)) return new Future.value(); | |
| 304 return git.run(["fetch"], workingDir: path).then((_) { | |
| 305 _updatedRepos.add(path); | |
| 306 }); | |
| 307 } | |
| 308 | |
| 309 /// Runs "git rev-list" in the canonical clone of the repository referred to | |
| 310 /// by [id] on the effective ref of [id]. | |
| 311 /// | |
| 312 /// This assumes that the canonical clone already exists. | |
| 313 Future<String> _getRev(PackageId id) { | |
| 314 return git.run( | |
| 315 ["rev-list", "--max-count=1", _getEffectiveRef(id)], | |
| 316 workingDir: _repoCachePath(id)).then((result) => result.first); | |
| 317 } | |
| 318 | |
| 319 /// Clones the repo at the URI [from] to the path [to] on the local | |
| 320 /// filesystem. | |
| 321 /// | |
| 322 /// If [mirror] is true, creates a bare, mirrored clone. This doesn't check | |
| 323 /// out the working tree, but instead makes the repository a local mirror of | |
| 324 /// the remote repository. See the manpage for `git clone` for more | |
| 325 /// information. | |
| 326 /// | |
| 327 /// If [shallow] is true, creates a shallow clone that contains no history | |
| 328 /// for the repository. | |
| 329 Future _clone(String from, String to, {bool mirror: false, bool shallow: | |
| 330 false}) { | |
| 331 return new Future.sync(() { | |
| 332 // Git on Windows does not seem to automatically create the destination | |
| 333 // directory. | |
| 334 ensureDir(to); | |
| 335 var args = ["clone", from, to]; | |
| 336 | |
| 337 if (mirror) args.insert(1, "--mirror"); | |
| 338 if (shallow) args.insertAll(1, ["--depth", "1"]); | |
| 339 | |
| 340 return git.run(args); | |
| 341 }).then((result) => null); | |
| 342 } | |
| 343 | |
| 344 /// Checks out the reference [ref] in [repoPath]. | |
| 345 Future _checkOut(String repoPath, String ref) { | |
| 346 return git.run( | |
| 347 ["checkout", ref], | |
| 348 workingDir: repoPath).then((result) => null); | |
| 349 } | |
| 350 | |
| 351 /// Returns the path to the canonical clone of the repository referred to by | |
| 352 /// [id] (the one in `<system cache>/git/cache`). | |
| 353 String _repoCachePath(PackageId id) { | |
| 354 var repoCacheName = '${id.name}-${sha1(_getUrl(id))}'; | |
| 355 return path.join(systemCacheRoot, 'cache', repoCacheName); | |
| 356 } | |
| 357 | |
| 358 /// Returns the repository URL for [id]. | |
| 359 /// | |
| 360 /// [description] may be a description or a [PackageId]. | |
| 361 String _getUrl(description) { | |
| 362 description = _getDescription(description); | |
| 363 if (description is String) return description; | |
| 364 return description['url']; | |
| 365 } | |
| 366 | |
| 367 /// Returns the commit ref that should be checked out for [description]. | |
| 368 /// | |
| 369 /// This differs from [_getRef] in that it doesn't just return the ref in | |
| 370 /// [description]. It will return a sensible default if that ref doesn't | |
| 371 /// exist, and it will respect the "resolved-ref" parameter set by | |
| 372 /// [resolveId]. | |
| 373 /// | |
| 374 /// [description] may be a description or a [PackageId]. | |
| 375 String _getEffectiveRef(description) { | |
| 376 description = _getDescription(description); | |
| 377 if (description is Map && description.containsKey('resolved-ref')) { | |
| 378 return description['resolved-ref']; | |
| 379 } | |
| 380 | |
| 381 var ref = _getRef(description); | |
| 382 return ref == null ? 'HEAD' : ref; | |
| 383 } | |
| 384 | |
| 385 /// Returns the commit ref for [description], or null if none is given. | |
| 386 /// | |
| 387 /// [description] may be a description or a [PackageId]. | |
| 388 String _getRef(description) { | |
| 389 description = _getDescription(description); | |
| 390 if (description is String) return null; | |
| 391 return description['ref']; | |
| 392 } | |
| 393 | |
| 394 /// Returns [description] if it's a description, or [PackageId.description] if | |
| 395 /// it's a [PackageId]. | |
| 396 _getDescription(description) { | |
| 397 if (description is PackageId) return description.description; | |
| 398 return description; | |
| 399 } | |
| 400 } | |
| OLD | NEW |