use std::path::{Path, PathBuf};
use strop_workspace::{ContainerId, RemoteEndpoint, RemoteFile};
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RepoTarget {
Local {
#[serde(with = "strop_core::path_serde")]
workdir: PathBuf,
},
Remote {
endpoint: RemoteEndpoint,
#[serde(with = "strop_core::path_serde")]
workdir: PathBuf,
},
Container {
container: ContainerId,
#[serde(with = "strop_core::path_serde")]
workdir: PathBuf,
},
}
impl RepoTarget {
pub fn workdir(&self) -> &Path {
match self {
Self::Local { workdir } | Self::Remote { workdir, .. } => workdir,
Self::Container { workdir, .. } => workdir,
}
}
pub fn remote_file(&self, rel: &Path) -> Option<RemoteFile> {
match self {
Self::Local { .. } | Self::Container { .. } => None,
Self::Remote { endpoint, workdir } => {
RemoteFile::from_path(endpoint.clone(), workdir.join(rel)).ok()
}
}
}
pub fn is_remote(&self) -> bool {
matches!(self, Self::Remote { .. })
}
pub fn is_container(&self) -> bool {
matches!(self, Self::Container { .. })
}
pub fn rel_of(&self, path: &Path) -> Option<PathBuf> {
path.strip_prefix(self.workdir())
.ok()
.map(|rel| rel.to_path_buf())
}
pub fn abs_of(&self, rel: &Path) -> PathBuf {
self.workdir().join(rel)
}
pub fn endpoint(&self) -> Option<&RemoteEndpoint> {
match self {
Self::Local { .. } | Self::Container { .. } => None,
Self::Remote { endpoint, .. } => Some(endpoint),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn remote_target() -> RepoTarget {
RepoTarget::Remote {
endpoint: RemoteEndpoint::parse("ssh://fixture@box.example:2222").unwrap(),
workdir: PathBuf::from("/srv/proj"),
}
}
#[test]
fn remote_identity_is_endpoint_plus_workdir() {
let a = remote_target();
let same = RepoTarget::Remote {
endpoint: RemoteEndpoint::parse("ssh://fixture@box.example:2222").unwrap(),
workdir: PathBuf::from("/srv/proj"),
};
let other_port = RepoTarget::Remote {
endpoint: RemoteEndpoint::parse("ssh://fixture@box.example:2223").unwrap(),
workdir: PathBuf::from("/srv/proj"),
};
let other_dir = RepoTarget::Remote {
endpoint: RemoteEndpoint::parse("ssh://fixture@box.example:2222").unwrap(),
workdir: PathBuf::from("/other"),
};
assert_eq!(a, same);
assert_ne!(a, other_port);
assert_ne!(a, other_dir);
assert_ne!(
a,
RepoTarget::Local {
workdir: PathBuf::from("/srv/proj")
}
);
}
#[test]
fn rel_of_strips_the_owning_workdir() {
let target = remote_target();
assert_eq!(
target.rel_of(Path::new("/srv/proj/src/main.rs")),
Some(PathBuf::from("src/main.rs"))
);
assert_eq!(target.rel_of(Path::new("/home/me/src/main.rs")), None);
}
#[test]
fn remote_file_carries_endpoint_and_native_path() {
let target = remote_target();
let file = target.remote_file(Path::new("src/a b.rs")).unwrap();
assert_eq!(
file.endpoint(),
&RemoteEndpoint::parse("ssh://fixture@box.example:2222").unwrap()
);
assert_eq!(file.path(), Path::new("/srv/proj/src/a b.rs"));
assert!(target.remote_file(Path::new("x")).is_some());
}
#[test]
fn local_target_has_no_remote_identity() {
let target = RepoTarget::Local {
workdir: PathBuf::from("/w"),
};
assert_eq!(target.endpoint(), None);
assert!(!target.is_remote());
assert_eq!(target.remote_file(Path::new("a.rs")), None);
}
#[test]
fn serde_round_trips_remote_provenance() {
let target = remote_target();
let text = serde_json::to_string(&target).unwrap();
let back: RepoTarget = serde_json::from_str(&text).unwrap();
assert_eq!(target, back);
}
fn container_target() -> RepoTarget {
RepoTarget::Container {
container: ContainerId::canonical("b".repeat(64)).unwrap(),
workdir: PathBuf::from("/work/src"),
}
}
#[test]
fn container_target_is_neither_remote_nor_local() {
let target = container_target();
assert!(target.is_container());
assert!(!target.is_remote());
assert_eq!(target.endpoint(), None);
assert_eq!(target.remote_file(Path::new("a.rs")), None);
assert_eq!(target.workdir(), Path::new("/work/src"));
assert_eq!(
target.rel_of(Path::new("/work/src/lib.rs")),
Some(PathBuf::from("lib.rs"))
);
}
#[test]
fn container_identity_is_id_plus_workdir() {
let a = container_target();
let other_id = RepoTarget::Container {
container: ContainerId::canonical("c".repeat(64)).unwrap(),
workdir: PathBuf::from("/work/src"),
};
assert_ne!(a, other_id);
assert_ne!(
a,
RepoTarget::Local {
workdir: PathBuf::from("/work/src")
}
);
}
#[test]
fn serde_round_trips_container_and_decodes_legacy_variants() {
let target = container_target();
let text = serde_json::to_string(&target).unwrap();
assert!(text.contains("\"container\":"), "{text}");
let back: RepoTarget = serde_json::from_str(&text).unwrap();
assert_eq!(target, back);
let legacy_local: RepoTarget =
serde_json::from_str(r#"{"local":{"workdir":"/w"}}"#).unwrap();
assert_eq!(
legacy_local,
RepoTarget::Local {
workdir: PathBuf::from("/w")
}
);
let legacy_remote: RepoTarget = serde_json::from_str(
r#"{"remote":{"endpoint":"ssh://fixture@box.example:2222","workdir":"/srv/proj"}}"#,
)
.unwrap();
assert_eq!(legacy_remote, remote_target());
}
}