git_internal/internal/object/
note.rs1use std::fmt::Display;
14
15use bincode::{Decode, Encode};
16use serde::{Deserialize, Serialize};
17
18use crate::{
19 errors::GitError,
20 hash::ObjectHash,
21 internal::object::{ObjectTrait, ObjectType},
22};
23
24#[derive(Eq, Debug, Clone, Serialize, Deserialize, Decode, Encode)]
30pub struct Note {
31 pub id: ObjectHash,
33 pub target_object_id: ObjectHash,
35 pub content: String,
37}
38
39impl PartialEq for Note {
40 fn eq(&self, other: &Self) -> bool {
42 self.id == other.id
43 }
44}
45
46impl Display for Note {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 writeln!(f, "Note for object: {}", self.target_object_id)?;
49 writeln!(f, "Content: {}", self.content)
50 }
51}
52
53impl Note {
54 pub fn new(target_object_id: ObjectHash, content: String) -> Self {
63 let id = ObjectHash::from_type_and_data(ObjectType::Blob, content.as_bytes());
66
67 Self {
68 id,
69 target_object_id,
70 content,
71 }
72 }
73
74 pub fn from_content(content: &str) -> Self {
85 Self::new(ObjectHash::default(), content.to_string())
86 }
87
88 pub fn content_size(&self) -> usize {
90 self.content.len()
91 }
92
93 pub fn is_empty(&self) -> bool {
95 self.content.is_empty()
96 }
97
98 pub fn set_target(&mut self, new_target: ObjectHash) {
106 self.target_object_id = new_target;
107 }
108
109 pub fn from_bytes_with_target(
122 data: &[u8],
123 hash: ObjectHash,
124 target_object_id: ObjectHash,
125 ) -> Result<Self, GitError> {
126 let content = String::from_utf8(data.to_vec())
127 .map_err(|e| GitError::InvalidNoteObject(format!("Invalid UTF-8 content: {e}")))?;
128
129 Ok(Note {
130 id: hash,
131 target_object_id,
132 content,
133 })
134 }
135
136 pub fn to_data_with_target(&self) -> Result<(Vec<u8>, ObjectHash), GitError> {
144 let data = self.to_data()?;
145 Ok((data, self.target_object_id))
146 }
147}
148
149impl ObjectTrait for Note {
150 fn from_bytes(data: &[u8], hash: ObjectHash) -> Result<Self, GitError>
159 where
160 Self: Sized,
161 {
162 let content = String::from_utf8(data.to_vec())
164 .map_err(|e| GitError::InvalidNoteObject(format!("Invalid UTF-8 content: {e}")))?;
165
166 Ok(Note {
167 id: hash,
168 target_object_id: ObjectHash::default(), content,
170 })
171 }
172
173 fn get_type(&self) -> ObjectType {
177 ObjectType::Blob
178 }
179
180 fn get_size(&self) -> usize {
182 self.content.len()
183 }
184
185 fn to_data(&self) -> Result<Vec<u8>, GitError> {
190 Ok(self.content.as_bytes().to_vec())
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use std::str::FromStr;
197
198 use super::*;
199 use crate::hash::{HashKind, ObjectHash, set_hash_kind_for_test};
200
201 fn round_trip(kind: HashKind) {
203 let _guard = set_hash_kind_for_test(kind);
204 let (target_id, hash_len) = match kind {
205 HashKind::Sha1 => (
206 ObjectHash::from_str("1234567890abcdef1234567890abcdef12345678").unwrap(),
207 40,
208 ),
209 HashKind::Sha256 => (
210 ObjectHash::from_str(
211 "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
212 )
213 .unwrap(),
214 64,
215 ),
216 };
217 let content = "This commit needs review".to_string();
218 let note = Note::new(target_id, content.clone());
219
220 assert_eq!(note.target_object_id, target_id);
221 assert_eq!(note.content, content);
222 assert_eq!(note.get_type(), ObjectType::Blob);
223 assert_eq!(note.id.to_string().len(), hash_len);
224
225 let data = note.to_data().unwrap();
227 assert_eq!(data, content.as_bytes());
228 assert_eq!(note.get_size(), content.len());
229
230 let basic = Note::from_bytes(&data, note.id).unwrap();
232 assert_eq!(basic.content, content);
233 assert_eq!(basic.id, note.id);
234 assert_eq!(basic.target_object_id, ObjectHash::default());
235
236 let (data_with_target, returned_target) = note.to_data_with_target().unwrap();
238 assert_eq!(returned_target, target_id);
239 let restored = Note::from_bytes_with_target(&data_with_target, note.id, target_id).unwrap();
240 assert_eq!(restored, note);
241 assert_eq!(restored.target_object_id, target_id);
242 assert_eq!(restored.content, content);
243 }
244
245 #[tokio::test]
247 async fn note_async_round_trip() {
248 round_trip(HashKind::Sha1);
249 round_trip(HashKind::Sha256);
250 }
251
252 #[test]
254 fn note_invalid_utf8_errors() {
255 let _guard = set_hash_kind_for_test(HashKind::Sha1);
256 let invalid_utf8 = vec![0xFF, 0xFE, 0xFD];
257 let hash = ObjectHash::from_str("3333333333333333333333333333333333333333").unwrap();
258 let target = ObjectHash::from_str("4444444444444444444444444444444444444444").unwrap();
259 assert!(Note::from_bytes(&invalid_utf8, hash).is_err());
260 assert!(Note::from_bytes_with_target(&invalid_utf8, hash, target).is_err());
261 }
262
263 #[test]
265 fn test_note_demo_functionality() {
266 let _guard = set_hash_kind_for_test(HashKind::Sha1);
267 println!("\n🚀 Git Note Object Demo - Best Practices");
270 println!("==========================================");
271
272 let commit_id = ObjectHash::from_str("a1b2c3d4e5f6789012345678901234567890abcd").unwrap();
273
274 println!("\n1️⃣ Creating a new Note object:");
275 let note = Note::new(
276 commit_id,
277 "Code review: LGTM! Great implementation.".to_string(),
278 );
279 println!(" Target Commit: {}", note.target_object_id);
280 println!(" Note ID: {}", note.id);
281 println!(" Content: {}", note.content);
282 println!(" Size: {} bytes", note.get_size());
283
284 println!("\n2️⃣ Serializing Note with target association:");
285 let (serialized_data, target_id) = note.to_data_with_target().unwrap();
286 println!(" Serialized size: {} bytes", serialized_data.len());
287 println!(" Target object ID: {}", target_id);
288 println!(
289 " Git object format: blob {}\\0<content>",
290 note.content.len()
291 );
292 println!(
293 " Raw data preview: {:?}...",
294 &serialized_data[..std::cmp::min(30, serialized_data.len())]
295 );
296
297 println!("\n3️⃣ Basic deserialization (ObjectTrait):");
298 let basic_note = Note::from_bytes(&serialized_data, note.id).unwrap();
299 println!(" Successfully deserialized!");
300 println!(
301 " Target Commit: {} (default - target managed externally)",
302 basic_note.target_object_id
303 );
304 println!(" Content: {}", basic_note.content);
305 println!(" Content matches: {}", note.content == basic_note.content);
306
307 println!("\n4️⃣ Best practice deserialization (with target):");
308 let complete_note =
309 Note::from_bytes_with_target(&serialized_data, note.id, target_id).unwrap();
310 println!(" Successfully deserialized with target!");
311 println!(" Target Commit: {}", complete_note.target_object_id);
312 println!(" Content: {}", complete_note.content);
313 println!(" Complete objects are equal: {}", note == complete_note);
314
315 assert_eq!(note, complete_note);
317 assert_eq!(target_id, commit_id);
318 }
319
320 #[test]
322 fn test_note_demo_functionality_sha256() {
323 let _guard = set_hash_kind_for_test(HashKind::Sha256);
324 println!("\n🚀 Git Note Object Demo - Best Practices");
327 println!("==========================================");
328
329 let commit_id = ObjectHash::from_str(
330 "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
331 )
332 .unwrap();
333
334 println!("\n1️⃣ Creating a new Note object:");
335 let note = Note::new(
336 commit_id,
337 "Code review: LGTM! Great implementation.".to_string(),
338 );
339 println!(" Target Commit: {}", note.target_object_id);
340 println!(" Note ID: {}", note.id);
341 println!(" Content: {}", note.content);
342 println!(" Size: {} bytes", note.get_size());
343
344 println!("\n2️⃣ Serializing Note with target association:");
345 let (serialized_data, target_id) = note.to_data_with_target().unwrap();
346 println!(" Serialized size: {} bytes", serialized_data.len());
347 println!(" Target object ID: {}", target_id);
348 println!(
349 " Git object format: blob {}\\0<content>",
350 note.content.len()
351 );
352 println!(
353 " Raw data preview: {:?}...",
354 &serialized_data[..std::cmp::min(30, serialized_data.len())]
355 );
356
357 println!("\n3️⃣ Basic deserialization (ObjectTrait):");
358 let basic_note = Note::from_bytes(&serialized_data, note.id).unwrap();
359 println!(" Successfully deserialized!");
360 println!(
361 " Target Commit: {} (default - target managed externally)",
362 basic_note.target_object_id
363 );
364 println!(" Content: {}", basic_note.content);
365 println!(" Content matches: {}", note.content == basic_note.content);
366
367 println!("\n4️⃣ Best practice deserialization (with target):");
368 let complete_note =
369 Note::from_bytes_with_target(&serialized_data, note.id, target_id).unwrap();
370 println!(" Successfully deserialized with target!");
371 println!(" Target Commit: {}", complete_note.target_object_id);
372 println!(" Content: {}", complete_note.content);
373 println!(" Complete objects are equal: {}", note == complete_note);
374
375 assert_eq!(note, complete_note);
377 assert_eq!(target_id, commit_id);
378 }
379}