1use std::path::{Path, PathBuf};
9
10use serde_json::{json, Map, Value};
11
12use onevcs::{
13 Error, EventKind, Identity, PreservedBranch, Provenance, Recoverable, Result, Scope, Session,
14 SessionRequest, SessionToken, Vcs,
15};
16
17use crate::events::{self, Emission};
18use crate::state::{self, VcsState};
19use crate::store::{FileStore, MemoryStore, Store};
20
21pub const DEFAULT_BASE: &str = "main";
26
27#[derive(Debug)]
32pub struct Repository<T> {
33 store: T,
34 root: PathBuf,
35 trees: Trees,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40enum Trees {
41 Named,
44 Created,
46}
47
48pub type MemoryVcs = Repository<MemoryStore<VcsState>>;
55
56pub type FileVcs = Repository<FileStore<VcsState>>;
62
63impl MemoryVcs {
64 pub fn new() -> Self {
66 Self::seeded(VcsState::default())
67 }
68
69 pub fn seeded(state: VcsState) -> Self {
71 Self {
72 store: MemoryStore::new(state),
73 root: std::env::temp_dir().join("onevcs-testing-memory"),
74 trees: Trees::Named,
75 }
76 }
77
78 pub fn state(&self) -> VcsState {
80 self.store
81 .snapshot()
82 .expect("an in-memory store always answers")
83 }
84}
85
86impl Default for MemoryVcs {
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92impl FileVcs {
93 pub fn create(path: impl Into<PathBuf>) -> Result<Self> {
100 Self::over(FileStore::attach(path, &VcsState::default())?)
101 }
102
103 pub fn seeded(path: impl Into<PathBuf>, state: VcsState) -> Result<Self> {
106 Self::over(FileStore::replace(path, &state)?)
107 }
108
109 fn over(store: FileStore<VcsState>) -> Result<Self> {
110 let root = store
111 .path()
112 .parent()
113 .filter(|parent| !parent.as_os_str().is_empty())
114 .unwrap_or_else(|| Path::new("."))
115 .join("worktrees");
116 Ok(Self {
117 store,
118 root,
119 trees: Trees::Created,
120 })
121 }
122
123 pub fn state(&self) -> Result<VcsState> {
125 self.store.snapshot()
126 }
127}
128
129impl<T: Store<VcsState>> Vcs for Repository<T> {
130 fn resolve_identity(&self, origin_or_path: &str) -> Result<Identity> {
131 self.store.with(|state| {
132 state::identity_of(state, origin_or_path)
133 .cloned()
134 .ok_or_else(|| Error::Invalid {
135 reason: format!(
136 "{origin_or_path:?} does not name a repository this provider knows; {}",
137 state::known(state)
138 ),
139 })
140 })
141 }
142
143 fn open_session(&self, req: SessionRequest) -> Result<Session> {
144 let root = self.root.clone();
145 let (session, emission) = self.store.with(|state| {
146 let identity = state::identity_of(state, &req.repo)
147 .cloned()
148 .ok_or_else(|| Error::Invalid {
149 reason: format!(
150 "{:?} does not name a repository this provider knows; {}",
151 req.repo,
152 state::known(state)
153 ),
154 })?;
155 let token = SessionToken(format!("s-testing-{}", state.sessions.len() + 1));
158 let run_root = root.join(&token.0);
159 let base = req.base.clone().unwrap_or_else(|| DEFAULT_BASE.to_owned());
164 state::named_branch(&base, "the base")?;
165 let session = Session {
166 worktree: run_root.join("worktree"),
167 branch: state::requested_branch(&req, &token)?,
168 base,
169 token: token.clone(),
170 };
171 state.sessions.push(session.clone());
172 state
173 .session_identities
174 .insert(token.clone(), identity.origin.clone());
175 let emission = Emission {
176 stream: token.0.clone(),
177 identity: Some(identity.origin.clone()),
178 kind: EventKind::SessionOpened,
179 payload: object(json!({
180 "token": token.0,
181 "identity": identity.origin,
182 "branch": session.branch,
183 "base": session.base,
184 "worktree": session.worktree.display().to_string(),
185 "clone": run_root.join("clone").display().to_string(),
188 "execution_checkout": run_root.join("checkout").display().to_string(),
189 "publication_checkout": run_root.join("checkout").display().to_string(),
190 })),
191 };
192 Ok((session, emission))
193 })?;
194 if self.trees == Trees::Created {
195 std::fs::create_dir_all(&session.worktree).map_err(|e| Error::Invalid {
196 reason: format!("cannot create {}: {e}", session.worktree.display()),
197 })?;
198 }
199 events::emit(&emission);
200 Ok(session)
201 }
202
203 fn adopt_session(&self, token: SessionToken) -> Result<Session> {
204 self.store.with(|state| {
205 state::session_of(state, &token)
206 .cloned()
207 .ok_or_else(|| Error::Invalid {
208 reason: format!(
209 "no session {:?} is open; `onevcs session open` prints a token",
210 token.0
211 ),
212 })
213 })
214 }
215
216 fn preserve(&self, s: &Session, provenance: Provenance) -> Result<PreservedBranch> {
217 let (branch, emission) = self.store.with(|state| {
218 let identity = state
219 .session_identities
220 .get(&s.token)
221 .cloned()
222 .ok_or_else(|| Error::Invalid {
223 reason: format!(
224 "this provider has no record of session {:?}, so it cannot say which \
225 identity a branch preserved from it belongs to",
226 s.token.0
227 ),
228 })?;
229 let branch = PreservedBranch {
230 branch: s.branch.clone(),
231 base: s.base.clone(),
232 provenance,
233 change_url: None,
234 change_base: None,
235 };
236 let row = Recoverable {
237 identity: identity.clone(),
238 branch: branch.clone(),
239 checkout: s.worktree.clone(),
240 stopped_because: format!("session {} was left open", s.token.0),
241 recover_command: recover_command(&s.branch, &s.worktree, provenance),
242 };
243 state.preserved.retain(|kept| {
247 kept.identity != row.identity || kept.branch.branch != row.branch.branch
248 });
249 state.preserved.push(row);
250 let emission = Emission {
251 stream: s.token.0.clone(),
252 identity: None,
258 kind: EventKind::CommitPreserved,
259 payload: object(json!({
260 "branch": s.branch,
261 "sha": events::stable_sha(&[&s.token.0, &s.branch, spell(provenance)]),
262 "provenance": spell(provenance),
263 })),
264 };
265 Ok((branch, emission))
266 })?;
267 events::emit(&emission);
268 Ok(branch)
269 }
270
271 fn recoverable(&self, scope: Scope) -> Result<Vec<Recoverable>> {
272 self.store.with(|state| {
273 let wanted = match &scope {
274 Scope::All => None,
275 Scope::Repo(repo) => Some(
276 state::identity_of(state, repo)
277 .map(|identity| identity.origin.clone())
278 .ok_or_else(|| Error::Invalid {
279 reason: format!(
280 "{repo:?} does not name a repository this provider knows; {}",
281 state::known(state)
282 ),
283 })?,
284 ),
285 };
286 Ok(state
288 .preserved
289 .iter()
290 .rev()
291 .filter(|row| wanted.as_ref().is_none_or(|key| *key == row.identity))
292 .cloned()
293 .collect())
294 })
295 }
296}
297
298fn recover_command(branch: &str, checkout: &Path, provenance: Provenance) -> Vec<String> {
300 match provenance {
301 Provenance::IncompleteStep => vec![
302 "onevcs".to_owned(),
303 "recover".to_owned(),
304 branch.to_owned(),
305 "--repo".to_owned(),
306 checkout.display().to_string(),
307 ],
308 Provenance::Complete => vec![
309 "onevcs".to_owned(),
310 "integrate".to_owned(),
311 branch.to_owned(),
312 ],
313 }
314}
315
316fn spell(provenance: Provenance) -> &'static str {
318 match provenance {
319 Provenance::Complete => "complete",
320 Provenance::IncompleteStep => "incomplete-step",
321 }
322}
323
324fn object(value: Value) -> Map<String, Value> {
325 value.as_object().cloned().unwrap_or_default()
326}