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