git_internal/internal/object/
note.rs1use std::fmt::Display;
14
15use crate::{
16 errors::GitError,
17 hash::ObjectHash,
18 internal::object::{ObjectTrait, ObjectType},
19};
20
21#[derive(
27 Eq,
28 Debug,
29 Clone,
30 serde::Serialize,
31 serde::Deserialize,
32 rkyv::Archive,
33 rkyv::Serialize,
34 rkyv::Deserialize,
35)]
36pub struct Note {
37 pub id: ObjectHash,
39 pub target_object_id: ObjectHash,
41 pub content: String,
43}
44
45impl PartialEq for Note {
46 fn eq(&self, other: &Self) -> bool {
48 self.id == other.id
49 }
50}
51
52impl Display for Note {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 writeln!(f, "Note for object: {}", self.target_object_id)?;
55 writeln!(f, "Content: {}", self.content)
56 }
57}
58
59impl Note {
60 pub fn new(target_object_id: ObjectHash, content: String) -> Self {
69 let id = ObjectHash::from_type_and_data(ObjectType::Blob, content.as_bytes());
72
73 Self {
74 id,
75 target_object_id,
76 content,
77 }
78 }
79
80 pub fn from_content(content: &str) -> Self {
91 Self::new(ObjectHash::default(), content.to_string())
92 }
93
94 pub fn content_size(&self) -> usize {
96 self.content.len()
97 }
98
99 pub fn is_empty(&self) -> bool {
101 self.content.is_empty()
102 }
103
104 pub fn set_target(&mut self, new_target: ObjectHash) {
112 self.target_object_id = new_target;
113 }
114
115 pub fn from_bytes_with_target(
128 data: &[u8],
129 hash: ObjectHash,
130 target_object_id: ObjectHash,
131 ) -> Result<Self, GitError> {
132 let content = String::from_utf8(data.to_vec())
133 .map_err(|e| GitError::InvalidNoteObject(format!("Invalid UTF-8 content: {e}")))?;
134
135 Ok(Note {
136 id: hash,
137 target_object_id,
138 content,
139 })
140 }
141
142 pub fn to_data_with_target(&self) -> Result<(Vec<u8>, ObjectHash), GitError> {
150 let data = self.to_data()?;
151 Ok((data, self.target_object_id))
152 }
153}
154
155impl ObjectTrait for Note {
156 fn from_bytes(data: &[u8], hash: ObjectHash) -> Result<Self, GitError>
165 where
166 Self: Sized,
167 {
168 let content = String::from_utf8(data.to_vec())
170 .map_err(|e| GitError::InvalidNoteObject(format!("Invalid UTF-8 content: {e}")))?;
171
172 Ok(Note {
173 id: hash,
174 target_object_id: ObjectHash::default(), content,
176 })
177 }
178
179 fn get_type(&self) -> ObjectType {
183 ObjectType::Blob
184 }
185
186 fn get_size(&self) -> usize {
188 self.content.len()
189 }
190
191 fn to_data(&self) -> Result<Vec<u8>, GitError> {
196 Ok(self.content.as_bytes().to_vec())
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use std::str::FromStr;
203
204 use super::*;
205 use crate::hash::{HashKind, ObjectHash, set_hash_kind_for_test};
206
207 fn round_trip(kind: HashKind) {
209 let _guard = set_hash_kind_for_test(kind);
210 let (target_id, hash_len) = match kind {
211 HashKind::Sha1 => (
212 ObjectHash::from_str("1234567890abcdef1234567890abcdef12345678").unwrap(),
213 40,
214 ),
215 HashKind::Sha256 => (
216 ObjectHash::from_str(
217 "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
218 )
219 .unwrap(),
220 64,
221 ),
222 };
223 let content = "This commit needs review".to_string();
224 let note = Note::new(target_id, content.clone());
225
226 assert_eq!(note.target_object_id, target_id);
227 assert_eq!(note.content, content);
228 assert_eq!(note.get_type(), ObjectType::Blob);
229 assert_eq!(note.id.to_string().len(), hash_len);
230
231 let data = note.to_data().unwrap();
233 assert_eq!(data, content.as_bytes());
234 assert_eq!(note.get_size(), content.len());
235
236 let basic = Note::from_bytes(&data, note.id).unwrap();
238 assert_eq!(basic.content, content);
239 assert_eq!(basic.id, note.id);
240 assert_eq!(basic.target_object_id, ObjectHash::default());
241
242 let (data_with_target, returned_target) = note.to_data_with_target().unwrap();
244 assert_eq!(returned_target, target_id);
245 let restored = Note::from_bytes_with_target(&data_with_target, note.id, target_id).unwrap();
246 assert_eq!(restored, note);
247 assert_eq!(restored.target_object_id, target_id);
248 assert_eq!(restored.content, content);
249 }
250
251 #[tokio::test]
253 async fn note_async_round_trip() {
254 round_trip(HashKind::Sha1);
255 round_trip(HashKind::Sha256);
256 }
257
258 #[test]
260 fn note_invalid_utf8_errors() {
261 let _guard = set_hash_kind_for_test(HashKind::Sha1);
262 let invalid_utf8 = vec![0xFF, 0xFE, 0xFD];
263 let hash = ObjectHash::from_str("3333333333333333333333333333333333333333").unwrap();
264 let target = ObjectHash::from_str("4444444444444444444444444444444444444444").unwrap();
265 assert!(Note::from_bytes(&invalid_utf8, hash).is_err());
266 assert!(Note::from_bytes_with_target(&invalid_utf8, hash, target).is_err());
267 }
268
269 #[test]
271 fn test_note_demo_functionality() {
272 let _guard = set_hash_kind_for_test(HashKind::Sha1);
273 println!("\n🚀 Git Note Object Demo - Best Practices");
276 println!("==========================================");
277
278 let commit_id = ObjectHash::from_str("a1b2c3d4e5f6789012345678901234567890abcd").unwrap();
279
280 println!("\n1️⃣ Creating a new Note object:");
281 let note = Note::new(
282 commit_id,
283 "Code review: LGTM! Great implementation.".to_string(),
284 );
285 println!(" Target Commit: {}", note.target_object_id);
286 println!(" Note ID: {}", note.id);
287 println!(" Content: {}", note.content);
288 println!(" Size: {} bytes", note.get_size());
289
290 println!("\n2️⃣ Serializing Note with target association:");
291 let (serialized_data, target_id) = note.to_data_with_target().unwrap();
292 println!(" Serialized size: {} bytes", serialized_data.len());
293 println!(" Target object ID: {}", target_id);
294 println!(
295 " Git object format: blob {}\\0<content>",
296 note.content.len()
297 );
298 println!(
299 " Raw data preview: {:?}...",
300 &serialized_data[..std::cmp::min(30, serialized_data.len())]
301 );
302
303 println!("\n3️⃣ Basic deserialization (ObjectTrait):");
304 let basic_note = Note::from_bytes(&serialized_data, note.id).unwrap();
305 println!(" Successfully deserialized!");
306 println!(
307 " Target Commit: {} (default - target managed externally)",
308 basic_note.target_object_id
309 );
310 println!(" Content: {}", basic_note.content);
311 println!(" Content matches: {}", note.content == basic_note.content);
312
313 println!("\n4️⃣ Best practice deserialization (with target):");
314 let complete_note =
315 Note::from_bytes_with_target(&serialized_data, note.id, target_id).unwrap();
316 println!(" Successfully deserialized with target!");
317 println!(" Target Commit: {}", complete_note.target_object_id);
318 println!(" Content: {}", complete_note.content);
319 println!(" Complete objects are equal: {}", note == complete_note);
320
321 assert_eq!(note, complete_note);
323 assert_eq!(target_id, commit_id);
324 }
325
326 #[test]
328 fn test_note_demo_functionality_sha256() {
329 let _guard = set_hash_kind_for_test(HashKind::Sha256);
330 println!("\n🚀 Git Note Object Demo - Best Practices");
333 println!("==========================================");
334
335 let commit_id = ObjectHash::from_str(
336 "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
337 )
338 .unwrap();
339
340 println!("\n1️⃣ Creating a new Note object:");
341 let note = Note::new(
342 commit_id,
343 "Code review: LGTM! Great implementation.".to_string(),
344 );
345 println!(" Target Commit: {}", note.target_object_id);
346 println!(" Note ID: {}", note.id);
347 println!(" Content: {}", note.content);
348 println!(" Size: {} bytes", note.get_size());
349
350 println!("\n2️⃣ Serializing Note with target association:");
351 let (serialized_data, target_id) = note.to_data_with_target().unwrap();
352 println!(" Serialized size: {} bytes", serialized_data.len());
353 println!(" Target object ID: {}", target_id);
354 println!(
355 " Git object format: blob {}\\0<content>",
356 note.content.len()
357 );
358 println!(
359 " Raw data preview: {:?}...",
360 &serialized_data[..std::cmp::min(30, serialized_data.len())]
361 );
362
363 println!("\n3️⃣ Basic deserialization (ObjectTrait):");
364 let basic_note = Note::from_bytes(&serialized_data, note.id).unwrap();
365 println!(" Successfully deserialized!");
366 println!(
367 " Target Commit: {} (default - target managed externally)",
368 basic_note.target_object_id
369 );
370 println!(" Content: {}", basic_note.content);
371 println!(" Content matches: {}", note.content == basic_note.content);
372
373 println!("\n4️⃣ Best practice deserialization (with target):");
374 let complete_note =
375 Note::from_bytes_with_target(&serialized_data, note.id, target_id).unwrap();
376 println!(" Successfully deserialized with target!");
377 println!(" Target Commit: {}", complete_note.target_object_id);
378 println!(" Content: {}", complete_note.content);
379 println!(" Complete objects are equal: {}", note == complete_note);
380
381 assert_eq!(note, complete_note);
383 assert_eq!(target_id, commit_id);
384 }
385}