mod link;
mod process;
pub(crate) mod shepignore;
pub use link::{FromCheckout, link_cache, link_into, to_link};
pub(crate) use process::{kill_group, off_thread, run_git, run_git_within};
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
pub(crate) fn printable(text: impl std::fmt::Display) -> String {
text.to_string()
.chars()
.map(|c| if forges_a_line(c) { '?' } else { c })
.collect()
}
pub(crate) fn forges_a_line(c: char) -> bool {
c.is_control()
|| matches!(
c,
'\u{00ad}'
| '\u{061c}'
| '\u{180e}'
| '\u{200b}'..='\u{200f}'
| '\u{2028}'..='\u{202e}'
| '\u{2060}'..='\u{2069}'
| '\u{feff}'
| '\u{e0000}'..='\u{e007f}'
)
}
pub(crate) const fn o_nofollow() -> i32 {
#[cfg(target_os = "linux")]
{
0o400_000
}
#[cfg(target_vendor = "apple")]
{
0x0100
}
#[cfg(not(any(target_os = "linux", target_vendor = "apple")))]
{
compile_error!("O_NOFOLLOW's value is not known for this target")
}
}
pub(crate) fn is_eloop(err: &io::Error) -> bool {
err.raw_os_error() == Some(nix::errno::Errno::ELOOP as i32)
}
#[must_use]
pub fn resolved(path: &Path) -> PathBuf {
if let Ok(real) = fs::canonicalize(path) {
return real;
}
if let (Some(parent), Some(name)) = (path.parent(), path.file_name())
&& let Ok(real_parent) = fs::canonicalize(parent)
{
return real_parent.join(name);
}
path.to_path_buf()
}
#[must_use]
pub fn same_path(a: &Path, b: &Path) -> bool {
resolved(a) == resolved(b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn two_spellings_of_one_path_resolve_alike_even_when_the_target_is_gone() {
let root = tempfile::tempdir().expect("tempdir");
let home = root.path().join("home");
fs::create_dir_all(home.join("deploy/web")).expect("tree");
let link = root.path().join("link");
std::os::unix::fs::symlink(&home, &link).expect("a second spelling");
let current = home.join("deploy/web/current");
let through_link = link.join("deploy/web/current");
std::os::unix::fs::symlink(home.join("deploy/web/releases/gone"), ¤t)
.expect("current");
assert!(
same_path(¤t, &through_link),
"parent resolves, name kept"
);
fs::remove_file(¤t).expect("unlink");
assert!(
same_path(¤t, &through_link),
"nothing exists but the parent"
);
assert!(!same_path(¤t, &home.join("deploy/web/previous")));
assert_eq!(
resolved(Path::new("/nonexistent/anywhere/x")),
PathBuf::from("/nonexistent/anywhere/x"),
"unresolvable stays as spelled"
);
}
#[test]
fn printable_replaces_everything_that_can_forge_a_line() {
let shown = printable("a\u{1b}[2Jb\u{202e}c\u{200b}d\ne\u{061c}f\u{e0041}g");
assert_eq!(shown, "a?[2Jb?c?d?e?f?g");
assert_eq!(printable("plain/name.txt"), "plain/name.txt");
}
}