1use std::path::{Path, PathBuf};
14
15use strop_workspace::{RemoteEndpoint, RemoteFile};
16
17#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum RepoTarget {
22 Local {
24 #[serde(with = "strop_core::path_serde")]
25 workdir: PathBuf,
26 },
27 Remote {
31 endpoint: RemoteEndpoint,
32 #[serde(with = "strop_core::path_serde")]
33 workdir: PathBuf,
34 },
35}
36
37impl RepoTarget {
38 pub fn workdir(&self) -> &Path {
42 match self {
43 Self::Local { workdir } | Self::Remote { workdir, .. } => workdir,
44 }
45 }
46
47 pub fn remote_file(&self, rel: &Path) -> Option<RemoteFile> {
51 match self {
52 Self::Local { .. } => None,
53 Self::Remote { endpoint, workdir } => {
54 RemoteFile::from_path(endpoint.clone(), workdir.join(rel)).ok()
55 }
56 }
57 }
58
59 pub fn is_remote(&self) -> bool {
60 matches!(self, Self::Remote { .. })
61 }
62
63 pub fn rel_of(&self, path: &Path) -> Option<PathBuf> {
67 path.strip_prefix(self.workdir())
68 .ok()
69 .map(|rel| rel.to_path_buf())
70 }
71
72 pub fn abs_of(&self, rel: &Path) -> PathBuf {
75 self.workdir().join(rel)
76 }
77
78 pub fn endpoint(&self) -> Option<&RemoteEndpoint> {
79 match self {
80 Self::Local { .. } => None,
81 Self::Remote { endpoint, .. } => Some(endpoint),
82 }
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 fn remote_target() -> RepoTarget {
91 RepoTarget::Remote {
92 endpoint: RemoteEndpoint::parse("ssh://fixture@box.example:2222").unwrap(),
93 workdir: PathBuf::from("/srv/proj"),
94 }
95 }
96
97 #[test]
100 fn remote_identity_is_endpoint_plus_workdir() {
101 let a = remote_target();
102 let same = RepoTarget::Remote {
103 endpoint: RemoteEndpoint::parse("ssh://fixture@box.example:2222").unwrap(),
104 workdir: PathBuf::from("/srv/proj"),
105 };
106 let other_port = RepoTarget::Remote {
107 endpoint: RemoteEndpoint::parse("ssh://fixture@box.example:2223").unwrap(),
108 workdir: PathBuf::from("/srv/proj"),
109 };
110 let other_dir = RepoTarget::Remote {
111 endpoint: RemoteEndpoint::parse("ssh://fixture@box.example:2222").unwrap(),
112 workdir: PathBuf::from("/other"),
113 };
114 assert_eq!(a, same);
115 assert_ne!(a, other_port);
116 assert_ne!(a, other_dir);
117 assert_ne!(
118 a,
119 RepoTarget::Local {
120 workdir: PathBuf::from("/srv/proj")
121 }
122 );
123 }
124
125 #[test]
129 fn rel_of_strips_the_owning_workdir() {
130 let target = remote_target();
131 assert_eq!(
132 target.rel_of(Path::new("/srv/proj/src/main.rs")),
133 Some(PathBuf::from("src/main.rs"))
134 );
135 assert_eq!(target.rel_of(Path::new("/home/me/src/main.rs")), None);
136 }
137
138 #[test]
142 fn remote_file_carries_endpoint_and_native_path() {
143 let target = remote_target();
144 let file = target.remote_file(Path::new("src/a b.rs")).unwrap();
145 assert_eq!(
146 file.endpoint(),
147 &RemoteEndpoint::parse("ssh://fixture@box.example:2222").unwrap()
148 );
149 assert_eq!(file.path(), Path::new("/srv/proj/src/a b.rs"));
150 assert!(target.remote_file(Path::new("x")).is_some());
151 }
152
153 #[test]
156 fn local_target_has_no_remote_identity() {
157 let target = RepoTarget::Local {
158 workdir: PathBuf::from("/w"),
159 };
160 assert_eq!(target.endpoint(), None);
161 assert!(!target.is_remote());
162 assert_eq!(target.remote_file(Path::new("a.rs")), None);
163 }
164
165 #[test]
168 fn serde_round_trips_remote_provenance() {
169 let target = remote_target();
170 let text = serde_json::to_string(&target).unwrap();
171 let back: RepoTarget = serde_json::from_str(&text).unwrap();
172 assert_eq!(target, back);
173 }
174}