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 |