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