Skip to main content

release_kit/
depend.rs

1//! `rk depend`: another project as a dependency of a target, from how
2//! the source distributes itself and how the target manages its tools.
3//!
4//! Two offline observations meet in a matrix. `source` reads the
5//! dependency's checkout for its distribution channels, `target` reads
6//! the project for its tool managers, `version` resolves the pin,
7//! `matrix` pairs a manager with a channel and says whether the pair is a
8//! fragment, a native command, or a hand edit, `fragments` renders the
9//! authored texts, and `nix` holds the lexical scanners both sides share.
10//! Nothing here edits a file the target owns.
11
12pub mod fragments;
13pub mod matrix;
14pub mod nix;
15pub mod source;
16pub mod target;
17pub mod version;
18
19use camino::{Utf8Path, Utf8PathBuf};
20
21pub use crate::cli::depend::{Channel, Kind, Manager};
22use crate::diagnostic::{Diagnostic, Reason};
23use crate::error::RkError;
24
25/// A directory argument as a canonical path, or the missing refusal
26/// naming its role.
27///
28/// # Errors
29///
30/// Returns [`RkError::Missing`] where the path is not a directory.
31pub fn canonical_dir(path: &Utf8Path, role: &'static str) -> Result<Utf8PathBuf, RkError> {
32    if !path.is_dir() {
33        return Err(RkError::missing(
34            Diagnostic::new(
35                Reason::TargetNotFound,
36                format!("{role} {path} is not a directory"),
37            )
38            .expected(format!("an existing {role} directory to read")),
39        ));
40    }
41    Ok(path.canonicalize_utf8()?)
42}
43
44/// Refuse a `--source` that is a URL before the disk is read: the clone
45/// is the operator's step, and a fetch is never implied by a read verb.
46///
47/// # Errors
48///
49/// Returns [`RkError::Usage`] for a value carrying a scheme or the
50/// `git@` form.
51pub fn reject_url(raw: &Utf8Path) -> Result<(), RkError> {
52    let text = raw.as_str();
53    if text.contains("://") || text.starts_with("git@") {
54        return Err(RkError::Usage(
55            "--source takes a local checkout; clone the URL first, then pass the directory".into(),
56        ));
57    }
58    Ok(())
59}
60
61#[cfg(test)]
62mod tests {
63    use camino::Utf8Path;
64
65    use super::{canonical_dir, reject_url};
66    use crate::error::RkError;
67
68    #[test]
69    fn a_url_is_refused_before_the_disk_is_read() {
70        for raw in [
71            "https://github.com/owner/repo",
72            "git@github.com:owner/repo.git",
73            "ssh://git@github.com/owner/repo",
74        ] {
75            assert!(
76                matches!(reject_url(Utf8Path::new(raw)), Err(RkError::Usage(_))),
77                "{raw} is refused"
78            );
79        }
80        assert!(reject_url(Utf8Path::new("../repo")).is_ok());
81    }
82
83    #[test]
84    fn a_missing_directory_is_a_missing_error() {
85        let result = canonical_dir(Utf8Path::new("/nonexistent/depend/source"), "source");
86        assert!(matches!(result, Err(RkError::Missing(_))));
87        assert_eq!(result.err().map(|e| e.exit_code()), Some(66));
88    }
89}