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 |