//! Checkpoints (spec ยง11): a serialized state kept per bookmark so loading //! a state costs the changes since the checkpoint rather than all of them. //! //! The newest checkpoint of each bookmark is pointed at by //! `.oxy/checkpoints/`; older ones become unreachable and are //! garbage collected. A checkpoint applies to a requested head set when all //! of its heads lie in that set's closure, which the loader establishes while //! walking from the requested heads and stopping at checkpoint members. use super::*; use crate::objects::Checkpoint; impl Repo { fn checkpoint_pointer(&self, bookmark: &str) -> PathBuf { self.oxy.join("checkpoints").join(bookmark) } /// The newest checkpoint recorded for a bookmark. pub fn latest_checkpoint(&self, bookmark: &str) -> Result> { match fs::read_to_string(self.checkpoint_pointer(bookmark)) { Ok(h) => { let r = ObjectRef::from_hex(h.trim()).ok_or_else(|| RepoError::Msg("corrupt checkpoint pointer".into()))?; match self.store.get_typed(&r, ObjectType::Checkpoint) { Ok(bytes) => Ok(Some((r, Checkpoint::decode(&bytes)?))), Err(StoreError::NotFound(_)) => Ok(None), Err(e) => Err(e.into()), } } Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), Err(e) => Err(e.into()), } } /// Every checkpoint pointer, for garbage collection roots. pub(crate) fn checkpoint_refs(&self) -> Result> { let dir = self.oxy.join("checkpoints"); let mut out = Vec::new(); if let Ok(rd) = fs::read_dir(&dir) { for e in rd.flatten() { if let Ok(h) = fs::read_to_string(e.path()) { if let Some(r) = ObjectRef::from_hex(h.trim()) { out.push(r); } } } } Ok(out) } /// Write a checkpoint of `state` (whose heads are `heads`) for `bookmark`. pub(crate) fn write_checkpoint(&self, bookmark: &str, state: &State, heads: &[ChangeHash]) -> Result<(ObjectRef, usize)> { let payload = state.checkpoint_payload(); let blob = self.store_blob(&payload)?; let cp = Checkpoint { heads: StateObject::new(heads.to_vec()).heads, count: state.len() as u64, payload: blob }; let r = self.store.put(ObjectType::Checkpoint, &cp.encode())?; fs::create_dir_all(self.oxy.join("checkpoints"))?; write_atomic(&self.checkpoint_pointer(bookmark), r.to_hex().as_bytes())?; Ok((r, payload.len())) } /// Checkpoint the current bookmark's state now. pub fn checkpoint(&self) -> Result<(ObjectRef, usize, usize)> { let heads = self.bookmark_heads()?; let state = self.load_state_full(&heads)?; let (r, bytes) = self.write_checkpoint(&self.config.bookmark, &state, &heads)?; Ok((r, state.len(), bytes)) } /// Load the state of `heads` using a checkpoint when one applies. pub(crate) fn load_state_checkpointed(&self, heads: &[ChangeHash]) -> Result> { let mut bookmarks: Vec = vec![self.config.bookmark.clone()]; if let Ok(rd) = fs::read_dir(self.oxy.join("checkpoints")) { for e in rd.flatten() { let name = e.file_name().to_string_lossy().to_string(); if !bookmarks.contains(&name) { bookmarks.push(name); } } } for b in bookmarks { let Some((_, cp)) = self.latest_checkpoint(&b)? else { continue }; // Cheap pre-check: every checkpoint head must be stored. if cp.heads.iter().any(|h| !self.store.has(h)) { continue; } let payload = self.read_blob(&cp.payload)?; let base = State::from_checkpoint_payload(&payload)?; let cp_heads: BTreeSet = cp.heads.iter().copied().collect(); let mut found: BTreeSet = BTreeSet::new(); let mut delta: Vec = Vec::new(); let mut seen: BTreeSet = BTreeSet::new(); let mut stack: Vec = heads.to_vec(); while let Some(h) = stack.pop() { if !seen.insert(h) { continue; } if base.contains(&h) { if cp_heads.contains(&h) { found.insert(h); } continue; } let c = self.store.get_change(&h)?; stack.extend(c.deps.iter().copied()); delta.push(c); } if found.len() != cp_heads.len() { continue; } let mut state = base; for c in delta { state.add_unchecked(c); } state.validate()?; return Ok(Some(state)); } Ok(None) } }