]> git.proxmox.com Git - cargo.git/blame - src/cargo/sources/path.rs
Auto merge of #6615 - ehuss:progress-updates, r=alexcrichton
[cargo.git] / src / cargo / sources / path.rs
CommitLineData
213afc02 1use std::fmt::{self, Debug, Formatter};
a6dad622 2use std::fs;
a6dad622
AC
3use std::path::{Path, PathBuf};
4
7d49029a 5use filetime::FileTime;
046a6c59 6use glob::Pattern;
c072ba42 7use ignore::gitignore::GitignoreBuilder;
e5a11190 8use ignore::Match;
9ed82b57 9use log::{trace, warn};
e05b4dc8 10
04ddd4d0
DW
11use crate::core::source::MaybePackage;
12use crate::core::{Dependency, Package, PackageId, Source, SourceId, Summary};
13use crate::ops;
14use crate::util::paths;
15use crate::util::Config;
16use crate::util::{self, internal, CargoResult};
62bff631 17
2fe0bf83 18pub struct PathSource<'cfg> {
b02023ee 19 source_id: SourceId,
a6dad622 20 path: PathBuf,
21b7418a 21 updated: bool,
5d0cb3f2 22 packages: Vec<Package>,
2fe0bf83 23 config: &'cfg Config,
e56965fb 24 recursive: bool,
98322afd 25}
62bff631 26
2fe0bf83 27impl<'cfg> PathSource<'cfg> {
64ff29ff 28 /// Invoked with an absolute path to a directory that contains a Cargo.toml.
e56965fb
AC
29 ///
30 /// This source will only return the package at precisely the `path`
31 /// specified, and it will be an error if there's not a package at `path`.
e5a11190 32 pub fn new(path: &Path, source_id: SourceId, config: &'cfg Config) -> PathSource<'cfg> {
9224a5ae 33 PathSource {
e5a11190 34 source_id,
a6dad622 35 path: path.to_path_buf(),
21b7418a 36 updated: false,
5d0cb3f2 37 packages: Vec::new(),
0247dc42 38 config,
e56965fb
AC
39 recursive: false,
40 }
41 }
42
43 /// Creates a new source which is walked recursively to discover packages.
44 ///
45 /// This is similar to the `new` method except that instead of requiring a
46 /// valid package to be present at `root` the folder is walked entirely to
47 /// crawl for packages.
48 ///
49 /// Note that this should be used with care and likely shouldn't be chosen
50 /// by default!
e5a11190 51 pub fn new_recursive(root: &Path, id: SourceId, config: &'cfg Config) -> PathSource<'cfg> {
e56965fb
AC
52 PathSource {
53 recursive: true,
1e682848 54 ..PathSource::new(root, id, config)
9224a5ae 55 }
62bff631 56 }
c57fef45 57
51d23560
AC
58 pub fn preload_with(&mut self, pkg: Package) {
59 assert!(!self.updated);
60 assert!(!self.recursive);
61 assert!(self.packages.is_empty());
62 self.updated = true;
63 self.packages.push(pkg);
64 }
65
339146de 66 pub fn root_package(&mut self) -> CargoResult<Package> {
7a2facba 67 trace!("root_package; source={:?}", self);
9224a5ae 68
82655b46 69 self.update()?;
21b7418a 70
a6dad622 71 match self.packages.iter().find(|p| p.root() == &*self.path) {
9224a5ae 72 Some(pkg) => Ok(pkg.clone()),
1e682848 73 None => Err(internal("no package found in source")),
9224a5ae
YKCL
74 }
75 }
8d5acdfc 76
bc60f64b 77 pub fn read_packages(&self) -> CargoResult<Vec<Package>> {
8d5acdfc
AC
78 if self.updated {
79 Ok(self.packages.clone())
e56965fb 80 } else if self.recursive {
e5a11190 81 ops::read_packages(&self.path, self.source_id, self.config)
e56965fb 82 } else {
3bac6e06 83 let path = self.path.join("Cargo.toml");
e5a11190 84 let (pkg, _) = ops::read_package(&path, self.source_id, self.config)?;
3bac6e06 85 Ok(vec![pkg])
8d5acdfc
AC
86 }
87 }
8a19eb74
AC
88
89 /// List all files relevant to building this package inside this source.
90 ///
bacb6be3 91 /// This function will use the appropriate methods to determine the
8a19eb74
AC
92 /// set of files underneath this source's directory which are relevant for
93 /// building `pkg`.
94 ///
95 /// The basic assumption of this method is that all files in the directory
96 /// are relevant for building this package, but it also contains logic to
97 /// use other methods like .gitignore to filter the list of files.
c072ba42
BE
98 ///
99 /// ## Pattern matching strategy
100 ///
101 /// Migrating from a glob-like pattern matching (using `glob` crate) to a
102 /// gitignore-like pattern matching (using `ignore` crate). The migration
103 /// stages are:
104 ///
105 /// 1) Only warn users about the future change iff their matching rules are
106 /// affected. (CURRENT STAGE)
107 ///
670a3df4 108 /// 2) Switch to the new strategy and update documents. Still keep warning
c072ba42
BE
109 /// affected users.
110 ///
670a3df4 111 /// 3) Drop the old strategy and no more warnings.
c072ba42
BE
112 ///
113 /// See <https://github.com/rust-lang/cargo/issues/4268> for more info.
a6dad622
AC
114 pub fn list_files(&self, pkg: &Package) -> CargoResult<Vec<PathBuf>> {
115 let root = pkg.root();
c072ba42
BE
116 let no_include_option = pkg.manifest().include().is_empty();
117
118 // glob-like matching rules
8b07052b 119
c072ba42 120 let glob_parse = |p: &String| {
5641cbde
BE
121 let pattern: &str = if p.starts_with('/') {
122 &p[1..p.len()]
123 } else {
23591fe5 124 p
5641cbde 125 };
1e682848 126 Pattern::new(pattern)
54c42142 127 .map_err(|e| failure::format_err!("could not parse glob pattern `{}`: {}", p, e))
d73a110d 128 };
ed61ba20 129
e5a11190
E
130 let glob_exclude = pkg
131 .manifest()
c072ba42
BE
132 .exclude()
133 .iter()
134 .map(|p| glob_parse(p))
135 .collect::<Result<Vec<_>, _>>()?;
136
e5a11190
E
137 let glob_include = pkg
138 .manifest()
c072ba42
BE
139 .include()
140 .iter()
141 .map(|p| glob_parse(p))
142 .collect::<Result<Vec<_>, _>>()?;
143
144 let glob_should_package = |relative_path: &Path| -> bool {
385b54b3 145 fn glob_match(patterns: &[Pattern], relative_path: &Path) -> bool {
1e682848
AC
146 patterns
147 .iter()
148 .any(|pattern| pattern.matches_path(relative_path))
5641cbde
BE
149 }
150
c072ba42
BE
151 // include and exclude options are mutually exclusive.
152 if no_include_option {
5641cbde 153 !glob_match(&glob_exclude, relative_path)
c072ba42 154 } else {
5641cbde 155 glob_match(&glob_include, relative_path)
c072ba42
BE
156 }
157 };
158
159 // ignore-like matching rules
160
161 let mut exclude_builder = GitignoreBuilder::new(root);
162 for rule in pkg.manifest().exclude() {
163 exclude_builder.add_line(None, rule)?;
164 }
165 let ignore_exclude = exclude_builder.build()?;
166
167 let mut include_builder = GitignoreBuilder::new(root);
168 for rule in pkg.manifest().include() {
169 include_builder.add_line(None, rule)?;
170 }
171 let ignore_include = include_builder.build()?;
172
173 let ignore_should_package = |relative_path: &Path| -> CargoResult<bool> {
174 // include and exclude options are mutually exclusive.
175 if no_include_option {
1e682848
AC
176 match ignore_exclude
177 .matched_path_or_any_parents(relative_path, /* is_dir */ false)
178 {
c072ba42
BE
179 Match::None => Ok(true),
180 Match::Ignore(_) => Ok(false),
54c42142 181 Match::Whitelist(pattern) => Err(failure::format_err!(
c072ba42
BE
182 "exclude rules cannot start with `!`: {}",
183 pattern.original()
37cffbe0 184 )),
c072ba42
BE
185 }
186 } else {
1e682848
AC
187 match ignore_include
188 .matched_path_or_any_parents(relative_path, /* is_dir */ false)
189 {
c072ba42
BE
190 Match::None => Ok(false),
191 Match::Ignore(_) => Ok(true),
54c42142 192 Match::Whitelist(pattern) => Err(failure::format_err!(
c072ba42
BE
193 "include rules cannot start with `!`: {}",
194 pattern.original()
37cffbe0 195 )),
c072ba42
BE
196 }
197 }
198 };
199
200 // matching to paths
201
202 let mut filter = |path: &Path| -> CargoResult<bool> {
203 let relative_path = util::without_prefix(path, root).unwrap();
204 let glob_should_package = glob_should_package(relative_path);
205 let ignore_should_package = ignore_should_package(relative_path)?;
206
207 if glob_should_package != ignore_should_package {
208 if glob_should_package {
209 if no_include_option {
1e682848 210 self.config.shell().warn(format!(
24ba7c80 211 "Pattern matching for Cargo's include/exclude fields is changing and \
1e682848
AC
212 file `{}` WILL be excluded in a future Cargo version.\n\
213 See https://github.com/rust-lang/cargo/issues/4268 for more info",
24ba7c80
LL
214 relative_path.display()
215 ))?;
1e682848
AC
216 } else {
217 self.config.shell().warn(format!(
23591fe5 218 "Pattern matching for Cargo's include/exclude fields is changing and \
1e682848
AC
219 file `{}` WILL NOT be included in a future Cargo version.\n\
220 See https://github.com/rust-lang/cargo/issues/4268 for more info",
23591fe5
LL
221 relative_path.display()
222 ))?;
1e682848
AC
223 }
224 } else if no_include_option {
225 self.config.shell().warn(format!(
226 "Pattern matching for Cargo's include/exclude fields is changing and \
227 file `{}` WILL NOT be excluded in a future Cargo version.\n\
228 See https://github.com/rust-lang/cargo/issues/4268 for more info",
229 relative_path.display()
230 ))?;
231 } else {
232 self.config.shell().warn(format!(
233 "Pattern matching for Cargo's include/exclude fields is changing and \
234 file `{}` WILL be included in a future Cargo version.\n\
235 See https://github.com/rust-lang/cargo/issues/4268 for more info",
236 relative_path.display()
237 ))?;
c072ba42 238 }
8b07052b 239 }
c072ba42
BE
240
241 // Update to ignore_should_package for Stage 2
242 Ok(glob_should_package)
8dc7e8a4 243 };
0e8f8d50 244
6eaa964d 245 // attempt git-prepopulate only if no `include` (rust-lang/cargo#4135)
c072ba42 246 if no_include_option {
6eaa964d
FKI
247 if let Some(result) = self.discover_git_and_list_files(pkg, root, &mut filter) {
248 return result;
249 }
1c588524 250 }
1c588524
FKI
251 self.list_files_walk(pkg, &mut filter)
252 }
253
254 // Returns Some(_) if found sibling Cargo.toml and .git folder;
255 // otherwise caller should fall back on full file list.
1e682848
AC
256 fn discover_git_and_list_files(
257 &self,
258 pkg: &Package,
259 root: &Path,
b8b7faee 260 filter: &mut dyn FnMut(&Path) -> CargoResult<bool>,
1e682848 261 ) -> Option<CargoResult<Vec<PathBuf>>> {
9185445a
AC
262 // If this package is in a git repository, then we really do want to
263 // query the git repository as it takes into account items such as
264 // .gitignore. We're not quite sure where the git repository is,
265 // however, so we do a bit of a probe.
5935ec1d 266 //
9185445a
AC
267 // We walk this package's path upwards and look for a sibling
268 // Cargo.toml and .git folder. If we find one then we assume that we're
269 // part of that repository.
270 let mut cur = root;
271 loop {
272 if cur.join("Cargo.toml").is_file() {
273 // If we find a git repository next to this Cargo.toml, we still
274 // check to see if we are indeed part of the index. If not, then
275 // this is likely an unrelated git repo, so keep going.
276 if let Ok(repo) = git2::Repository::open(cur) {
1c588524
FKI
277 let index = match repo.index() {
278 Ok(index) => index,
279 Err(err) => return Some(Err(err.into())),
280 };
1e682848 281 let path = util::without_prefix(root, cur).unwrap().join("Cargo.toml");
9185445a 282 if index.get_path(&path, 0).is_some() {
385b54b3 283 return Some(self.list_files_git(pkg, &repo, filter));
9185445a
AC
284 }
285 }
286 }
287 // don't cross submodule boundaries
288 if cur.join(".git").is_dir() {
1e682848 289 break;
9185445a
AC
290 }
291 match cur.parent() {
292 Some(parent) => cur = parent,
293 None => break,
294 }
5935ec1d 295 }
23591fe5 296 None
0e8f8d50
AC
297 }
298
1e682848
AC
299 fn list_files_git(
300 &self,
301 pkg: &Package,
385b54b3 302 repo: &git2::Repository,
b8b7faee 303 filter: &mut dyn FnMut(&Path) -> CargoResult<bool>,
1e682848 304 ) -> CargoResult<Vec<PathBuf>> {
7a2facba 305 warn!("list_files_git {}", pkg.package_id());
82655b46 306 let index = repo.index()?;
e5a11190
E
307 let root = repo
308 .workdir()
1e682848 309 .ok_or_else(|| internal("Can't list files on a bare repository."))?;
a6dad622 310 let pkg_path = pkg.root();
8b07052b 311
9185445a 312 let mut ret = Vec::<PathBuf>::new();
a8e9ce22 313
bacb6be3 314 // We use information from the git repository to guide us in traversing
a8e9ce22
AC
315 // its tree. The primary purpose of this is to take advantage of the
316 // .gitignore and auto-ignore files that don't matter.
317 //
bacb6be3 318 // Here we're also careful to look at both tracked and untracked files as
a8e9ce22
AC
319 // the untracked files are often part of a build and may become relevant
320 // as part of a future commit.
046a6c59 321 let index_files = index.iter().map(|entry| {
e0dff0f2 322 use libgit2_sys::GIT_FILEMODE_COMMIT;
632780eb 323 let is_dir = entry.mode == GIT_FILEMODE_COMMIT as u32;
c5611a32 324 (join(root, &entry.path), Some(is_dir))
046a6c59 325 });
a8e9ce22
AC
326 let mut opts = git2::StatusOptions::new();
327 opts.include_untracked(true);
c5611a32 328 if let Some(suffix) = util::without_prefix(pkg_path, root) {
b6ad6fb4
AC
329 opts.pathspec(suffix);
330 }
82655b46 331 let statuses = repo.statuses(Some(&mut opts))?;
1e682848
AC
332 let untracked = statuses.iter().filter_map(|entry| match entry.status() {
333 git2::Status::WT_NEW => Some((join(root, entry.path_bytes()), None)),
334 _ => None,
a8e9ce22
AC
335 });
336
9185445a
AC
337 let mut subpackages_found = Vec::new();
338
c5611a32 339 for (file_path, is_dir) in index_files.chain(untracked) {
82655b46 340 let file_path = file_path?;
8b07052b 341
9185445a
AC
342 // Filter out files blatantly outside this package. This is helped a
343 // bit obove via the `pathspec` function call, but we need to filter
344 // the entries in the index as well.
345 if !file_path.starts_with(pkg_path) {
1e682848 346 continue;
a8e9ce22 347 }
8dc7e8a4 348
9185445a
AC
349 match file_path.file_name().and_then(|s| s.to_str()) {
350 // Filter out Cargo.lock and target always, we don't want to
351 // package a lock file no one will ever read and we also avoid
352 // build artifacts
1e682848 353 Some("Cargo.lock") | Some("target") => continue,
9185445a
AC
354
355 // Keep track of all sub-packages found and also strip out all
356 // matches we've found so far. Note, though, that if we find
357 // our own `Cargo.toml` we keep going.
358 Some("Cargo.toml") => {
359 let path = file_path.parent().unwrap();
360 if path != pkg_path {
361 warn!("subpackage found: {}", path.display());
362 ret.retain(|p| !p.starts_with(path));
363 subpackages_found.push(path.to_path_buf());
1e682848 364 continue;
9185445a 365 }
8b07052b 366 }
9185445a
AC
367
368 _ => {}
8a19eb74
AC
369 }
370
9185445a
AC
371 // If this file is part of any other sub-package we've found so far,
372 // skip it.
373 if subpackages_found.iter().any(|p| file_path.starts_with(p)) {
1e682848 374 continue;
9185445a
AC
375 }
376
377 if is_dir.unwrap_or_else(|| file_path.is_dir()) {
5935ec1d 378 warn!(" found submodule {}", file_path.display());
c5611a32 379 let rel = util::without_prefix(&file_path, root).unwrap();
54c42142
DW
380 let rel = rel.to_str().ok_or_else(|| {
381 failure::format_err!("invalid utf-8 filename: {}", rel.display())
382 })?;
1e0b04a0
AC
383 // Git submodules are currently only named through `/` path
384 // separators, explicitly not `\` which windows uses. Who knew?
385 let rel = rel.replace(r"\", "/");
a8e9ce22
AC
386 match repo.find_submodule(&rel).and_then(|s| s.open()) {
387 Ok(repo) => {
385b54b3 388 let files = self.list_files_git(pkg, &repo, filter)?;
a8e9ce22
AC
389 ret.extend(files.into_iter());
390 }
391 Err(..) => {
c072ba42 392 PathSource::walk(&file_path, &mut ret, false, filter)?;
a8e9ce22
AC
393 }
394 }
c072ba42 395 } else if (*filter)(&file_path)? {
5935ec1d
AC
396 // We found a file!
397 warn!(" found {}", file_path.display());
398 ret.push(file_path);
399 }
8dc7e8a4 400 }
a6dad622
AC
401 return Ok(ret);
402
403 #[cfg(unix)]
404 fn join(path: &Path, data: &[u8]) -> CargoResult<PathBuf> {
a6dad622 405 use std::ffi::OsStr;
e5a11190 406 use std::os::unix::prelude::*;
a6dad622
AC
407 Ok(path.join(<OsStr as OsStrExt>::from_bytes(data)))
408 }
409 #[cfg(windows)]
410 fn join(path: &Path, data: &[u8]) -> CargoResult<PathBuf> {
411 use std::str;
412 match str::from_utf8(data) {
413 Ok(s) => Ok(path.join(s)),
1e682848
AC
414 Err(..) => Err(internal(
415 "cannot process path in git with a non \
416 unicode filename",
417 )),
a6dad622
AC
418 }
419 }
0e8f8d50
AC
420 }
421
1e682848
AC
422 fn list_files_walk(
423 &self,
424 pkg: &Package,
b8b7faee 425 filter: &mut dyn FnMut(&Path) -> CargoResult<bool>,
1e682848 426 ) -> CargoResult<Vec<PathBuf>> {
0e8f8d50 427 let mut ret = Vec::new();
82655b46 428 PathSource::walk(pkg.root(), &mut ret, true, filter)?;
3a852a0f 429 Ok(ret)
a8e9ce22 430 }
8a19eb74 431
1e682848
AC
432 fn walk(
433 path: &Path,
434 ret: &mut Vec<PathBuf>,
435 is_root: bool,
b8b7faee 436 filter: &mut dyn FnMut(&Path) -> CargoResult<bool>,
1e682848 437 ) -> CargoResult<()> {
a8e9ce22 438 if !fs::metadata(&path).map(|m| m.is_dir()).unwrap_or(false) {
c072ba42 439 if (*filter)(path)? {
a8e9ce22 440 ret.push(path.to_path_buf());
8a19eb74 441 }
1e682848 442 return Ok(());
8a19eb74 443 }
a8e9ce22
AC
444 // Don't recurse into any sub-packages that we have
445 if !is_root && fs::metadata(&path.join("Cargo.toml")).is_ok() {
1e682848 446 return Ok(());
a8e9ce22 447 }
c072ba42
BE
448
449 // For package integration tests, we need to sort the paths in a deterministic order to
450 // be able to match stdout warnings in the same order.
451 //
57a7e126 452 // TODO: Drop collect and sort after transition period and dropping warning tests.
c072ba42
BE
453 // See <https://github.com/rust-lang/cargo/issues/4268>
454 // and <https://github.com/rust-lang/cargo/pull/4270>
c74aa949
E
455 let mut entries: Vec<PathBuf> = fs::read_dir(path)?.map(|e| e.unwrap().path()).collect();
456 entries.sort_unstable_by(|a, b| a.as_os_str().cmp(b.as_os_str()));
457 for path in entries {
c072ba42 458 let name = path.file_name().and_then(|s| s.to_str());
11144645 459 // Skip dotfile directories
4673f0a6 460 if name.map(|s| s.starts_with('.')) == Some(true) {
1e682848 461 continue;
676edacf
E
462 }
463 if is_root {
11144645
LB
464 // Skip cargo artifacts
465 match name {
466 Some("target") | Some("Cargo.lock") => continue,
467 _ => {}
468 }
a8e9ce22 469 }
c072ba42 470 PathSource::walk(&path, ret, false, filter)?;
a8e9ce22 471 }
3a852a0f 472 Ok(())
8a19eb74 473 }
9c1124fb
GF
474
475 pub fn last_modified_file(&self, pkg: &Package) -> CargoResult<(FileTime, PathBuf)> {
476 if !self.updated {
477 return Err(internal("BUG: source was not updated"));
478 }
479
480 let mut max = FileTime::zero();
481 let mut max_path = PathBuf::new();
482 for file in self.list_files(pkg)? {
483 // An fs::stat error here is either because path is a
484 // broken symlink, a permissions error, or a race
485 // condition where this path was rm'ed - either way,
486 // we can ignore the error and treat the path's mtime
487 // as 0.
385b54b3 488 let mtime = paths::mtime(&file).unwrap_or_else(|_| FileTime::zero());
9c1124fb
GF
489 if mtime > max {
490 max = mtime;
491 max_path = file;
492 }
493 }
494 trace!("last modified file {}: {}", self.path.display(), max);
495 Ok((max, max_path))
496 }
8921abd7
DW
497
498 pub fn path(&self) -> &Path {
499 &self.path
500 }
62bff631
C
501}
502
2fe0bf83 503impl<'cfg> Debug for PathSource<'cfg> {
b8b7faee 504 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
cfeabbc2 505 write!(f, "the paths source")
50f110a4
YK
506 }
507}
508
f947326a 509impl<'cfg> Source for PathSource<'cfg> {
b8b7faee 510 fn query(&mut self, dep: &Dependency, f: &mut dyn FnMut(Summary)) -> CargoResult<()> {
6acedd8a
DW
511 for s in self.packages.iter().map(|p| p.summary()) {
512 if dep.matches(s) {
513 f(s.clone())
514 }
515 }
516 Ok(())
f947326a 517 }
5b08b8fe 518
b8b7faee 519 fn fuzzy_query(&mut self, _dep: &Dependency, f: &mut dyn FnMut(Summary)) -> CargoResult<()> {
84cc3d8b
E
520 for s in self.packages.iter().map(|p| p.summary()) {
521 f(s.clone())
522 }
523 Ok(())
524 }
525
5b08b8fe
AC
526 fn supports_checksums(&self) -> bool {
527 false
528 }
529
530 fn requires_precise(&self) -> bool {
531 false
532 }
b02023ee 533
e5a11190
E
534 fn source_id(&self) -> SourceId {
535 self.source_id
53bd095f
DW
536 }
537
c665938d 538 fn update(&mut self) -> CargoResult<()> {
21b7418a 539 if !self.updated {
82655b46 540 let packages = self.read_packages()?;
3cdca46b 541 self.packages.extend(packages.into_iter());
8d5acdfc 542 self.updated = true;
21b7418a
CL
543 }
544
98322afd
YKCL
545 Ok(())
546 }
62bff631 547
dae87a26 548 fn download(&mut self, id: PackageId) -> CargoResult<MaybePackage> {
9cf70517 549 trace!("getting packages; id={}", id);
c57fef45 550
9cf70517 551 let pkg = self.packages.iter().find(|pkg| pkg.package_id() == id);
1e682848 552 pkg.cloned()
c94804bd 553 .map(MaybePackage::Ready)
1e682848 554 .ok_or_else(|| internal(format!("failed to find {} in path source", id)))
62bff631 555 }
e05b4dc8 556
dae87a26 557 fn finish_download(&mut self, _id: PackageId, _data: Vec<u8>) -> CargoResult<Package> {
c94804bd
AC
558 panic!("no download should have started")
559 }
560
8d5acdfc 561 fn fingerprint(&self, pkg: &Package) -> CargoResult<String> {
9c1124fb 562 let (max, max_path) = self.last_modified_file(pkg)?;
c0595fbc 563 Ok(format!("{} ({})", max, max_path.display()))
e05b4dc8 564 }
20cfb41e
AC
565
566 fn describe(&self) -> String {
567 match self.source_id.url().to_file_path() {
568 Ok(path) => path.display().to_string(),
569 Err(_) => self.source_id.to_string(),
570 }
571 }
62bff631 572}