repo/
state_attachments.rs1use objects::{
4 object::{StateAttachment, StateAttachmentBody, StateAttachmentId, StateId},
5 store::ObjectStore,
6};
7use oplog::OpLogBackend;
8use refs::RefBackend;
9
10use crate::{HeddleError, Repository, Result};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum StateAttachmentKind {
14 Context,
15 RiskSignals,
16 ReviewSignatures,
17 Discussions,
18 StructuredConflicts,
19 Signature,
20}
21
22impl StateAttachmentKind {
23 fn of(body: &StateAttachmentBody) -> Self {
24 match body {
25 StateAttachmentBody::Context(_) => Self::Context,
26 StateAttachmentBody::RiskSignals(_) => Self::RiskSignals,
27 StateAttachmentBody::ReviewSignatures(_) => Self::ReviewSignatures,
28 StateAttachmentBody::Discussions(_) => Self::Discussions,
29 StateAttachmentBody::StructuredConflicts(_) => Self::StructuredConflicts,
30 StateAttachmentBody::Signature(_) => Self::Signature,
31 }
32 }
33}
34
35impl<R, O, S> Repository<R, O, S>
36where
37 R: RefBackend,
38 O: OpLogBackend,
39 S: ObjectStore,
40{
41 pub fn put_state_attachment(&self, attachment: &StateAttachment) -> Result<StateAttachmentId> {
42 if !self.store().has_state(&attachment.state_id)? {
43 return Err(HeddleError::StateNotFound(attachment.state_id));
44 }
45
46 if let Some(prior_id) = attachment.supersedes {
47 let prior = self
48 .get_state_attachment(&attachment.state_id, &prior_id)?
49 .ok_or_else(|| HeddleError::NotFound(format!("state attachment {prior_id}")))?;
50 if StateAttachmentKind::of(&prior.body) != StateAttachmentKind::of(&attachment.body) {
51 return Err(HeddleError::InvalidObject(
52 "state attachment can only supersede the same attachment kind".to_string(),
53 ));
54 }
55 }
56
57 let id = attachment.id();
58 if let Some(existing) = self
59 .store()
60 .get_state_attachment(&attachment.state_id, &id)?
61 {
62 return match self.get_state_attachment(&attachment.state_id, &id)? {
63 Some(_) if existing == *attachment => Ok(id),
64 _ => Err(HeddleError::InvalidObject(
65 "state attachment address collision".to_string(),
66 )),
67 };
68 }
69 self.store().put_state_attachment(attachment)
70 }
71
72 pub fn get_state_attachment(
73 &self,
74 state_id: &StateId,
75 attachment_id: &StateAttachmentId,
76 ) -> Result<Option<StateAttachment>> {
77 self.store().get_state_attachment(state_id, attachment_id)
78 }
79
80 pub fn list_state_attachments(&self, state_id: &StateId) -> Result<Vec<StateAttachment>> {
81 let mut attachments = self.store().list_state_attachments(state_id)?;
82 attachments.sort_by_key(|attachment| (attachment.created_at, attachment.id()));
83 Ok(attachments)
84 }
85
86 pub fn latest_state_attachment(
87 &self,
88 state_id: &StateId,
89 kind: StateAttachmentKind,
90 ) -> Result<Option<StateAttachment>> {
91 let attachments: Vec<_> = self
92 .list_state_attachments(state_id)?
93 .into_iter()
94 .filter(|attachment| StateAttachmentKind::of(&attachment.body) == kind)
95 .collect();
96 let superseded: std::collections::HashSet<_> = attachments
97 .iter()
98 .filter_map(|attachment| attachment.supersedes)
99 .collect();
100 Ok(attachments
101 .into_iter()
102 .filter(|attachment| !superseded.contains(&attachment.id()))
103 .max_by_key(StateAttachment::id))
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use chrono::{Duration, Utc};
110 use objects::object::{Attribution, ContentHash, Principal, StateAttachment};
111 use tempfile::TempDir;
112
113 use super::*;
114
115 fn attachment(state_id: StateId, body: StateAttachmentBody) -> StateAttachment {
116 StateAttachment {
117 state_id,
118 body,
119 attribution: Attribution::human(Principal::new("Test", "test@example.com")),
120 created_at: Utc::now(),
121 supersedes: None,
122 }
123 }
124
125 #[test]
126 fn attachment_history_preserves_state_bytes() {
127 let temp = TempDir::new().unwrap();
128 let repo = Repository::init_default(temp.path()).unwrap();
129 let state_id = repo.head().unwrap().unwrap();
130 let before = repo.store().get_state(&state_id).unwrap().unwrap();
131
132 let first = attachment(
133 state_id,
134 StateAttachmentBody::Context(ContentHash::compute(b"first")),
135 );
136 let first_id = repo.put_state_attachment(&first).unwrap();
137 let mut second = attachment(
138 state_id,
139 StateAttachmentBody::Context(ContentHash::compute(b"second")),
140 );
141 second.created_at = first.created_at + Duration::seconds(1);
142 second.supersedes = Some(first_id);
143 repo.put_state_attachment(&second).unwrap();
144
145 let latest = repo
146 .latest_state_attachment(&state_id, StateAttachmentKind::Context)
147 .unwrap()
148 .unwrap();
149 assert_eq!(latest, second);
150 assert_eq!(repo.list_state_attachments(&state_id).unwrap().len(), 2);
151 assert_eq!(repo.store().get_state(&state_id).unwrap().unwrap(), before);
152 }
153
154 #[test]
155 fn latest_follows_supersession_heads_not_wall_clock() {
156 let temp = TempDir::new().unwrap();
157 let repo = Repository::init_default(temp.path()).unwrap();
158 let state_id = repo.head().unwrap().unwrap();
159 let root = attachment(
160 state_id,
161 StateAttachmentBody::Context(ContentHash::compute(b"root")),
162 );
163 let root_id = repo.put_state_attachment(&root).unwrap();
164
165 let mut older_clock = attachment(
166 state_id,
167 StateAttachmentBody::Context(ContentHash::compute(b"older-clock")),
168 );
169 older_clock.created_at = root.created_at - Duration::seconds(10);
170 older_clock.supersedes = Some(root_id);
171 repo.put_state_attachment(&older_clock).unwrap();
172
173 let mut fork = attachment(
174 state_id,
175 StateAttachmentBody::Context(ContentHash::compute(b"fork")),
176 );
177 fork.created_at = root.created_at + Duration::seconds(10);
178 fork.supersedes = Some(root_id);
179 repo.put_state_attachment(&fork).unwrap();
180
181 let expected = [older_clock.clone(), fork.clone()]
182 .into_iter()
183 .max_by_key(StateAttachment::id)
184 .unwrap();
185 assert_eq!(
186 repo.latest_state_attachment(&state_id, StateAttachmentKind::Context)
187 .unwrap()
188 .unwrap(),
189 expected
190 );
191 }
192}