git_internal/internal/object/
note.rs

1//! Git Note object implementation
2//!
3//! Git Notes are a mechanism for adding metadata to existing Git objects (usually commits)
4//! without modifying the original objects. Notes are commonly used for:
5//!
6//! - Adding review comments or approval metadata
7//! - Storing CI/CD build status and code scan results  
8//! - Attaching author signatures, annotations, or other metadata
9//!
10//! In Git's object model, Notes are stored as Blob objects, with the association between
11//! notes and target objects managed through the refs/notes/* namespace.
12
13use std::fmt::Display;
14
15use bincode::{Decode, Encode};
16use serde::{Deserialize, Serialize};
17
18use crate::errors::GitError;
19use crate::hash::ObjectHash;
20use crate::internal::object::ObjectTrait;
21use crate::internal::object::ObjectType;
22
23/// Git Note object structure
24///
25/// A Note represents additional metadata attached to a Git object (typically a commit).
26/// The Note itself is stored as a Blob object in Git's object database, with the
27/// association managed through Git's reference system.
28#[derive(Eq, Debug, Clone, Serialize, Deserialize, Decode, Encode)]
29pub struct Note {
30    /// The ObjectHash of this Note object (same as the underlying Blob)
31    pub id: ObjectHash,
32    /// The ObjectHash of the object this Note annotates (usually a commit)
33    pub target_object_id: ObjectHash,
34    /// The textual content of the Note
35    pub content: String,
36}
37
38impl PartialEq for Note {
39    /// Two Notes are equal if they have the same ID
40    fn eq(&self, other: &Self) -> bool {
41        self.id == other.id
42    }
43}
44
45impl Display for Note {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        writeln!(f, "Note for object: {}", self.target_object_id)?;
48        writeln!(f, "Content: {}", self.content)
49    }
50}
51
52impl Note {
53    /// Create a new Note for the specified target object with the given content
54    ///
55    /// # Arguments
56    /// * `target_object_id` - The ObjectHash of the object to annotate
57    /// * `content` - The textual content of the note
58    ///
59    /// # Returns
60    /// A new Note instance with calculated ID based on the content
61    pub fn new(target_object_id: ObjectHash, content: String) -> Self {
62        // Calculate the SHA-1/ SHA-256 hash for this Note's content
63        // Notes are stored as Blob objects in Git
64        let id = ObjectHash::from_type_and_data(ObjectType::Blob, content.as_bytes());
65
66        Self {
67            id,
68            target_object_id,
69            content,
70        }
71    }
72
73    /// Create a Note from content string, with default target object
74    ///
75    /// This is a convenience method for creating Notes when the target
76    /// will be set later by the notes management system.
77    ///
78    /// # Arguments
79    /// * `content` - The textual content of the note
80    ///
81    /// # Returns
82    /// A new Note instance with default target object ID
83    pub fn from_content(content: &str) -> Self {
84        Self::new(ObjectHash::default(), content.to_string())
85    }
86
87    /// Get the size of the Note content in bytes
88    pub fn content_size(&self) -> usize {
89        self.content.len()
90    }
91
92    /// Check if the Note is empty (has no content)
93    pub fn is_empty(&self) -> bool {
94        self.content.is_empty()
95    }
96
97    /// Update the target object ID for this Note
98    ///
99    /// This method allows changing which object this Note annotates
100    /// without changing the Note's content or ID.
101    ///
102    /// # Arguments
103    /// * `new_target` - The new target object SHA-1/ SHA-256 hash
104    pub fn set_target(&mut self, new_target: ObjectHash) {
105        self.target_object_id = new_target;
106    }
107
108    /// Create a Note object from raw bytes with explicit target object ID
109    ///
110    /// This is the preferred method when you know both the content and the target object,
111    /// as it preserves the complete Note association information.
112    ///
113    /// # Arguments
114    /// * `data` - The raw byte data (UTF-8 encoded text content)
115    /// * `hash` - The SHA-1/ SHA-256 hash of this Note object
116    /// * `target_object_id` - The SHA-1/ SHA-256 hash of the object this Note annotates
117    ///
118    /// # Returns
119    /// A Result containing the Note object with complete association info
120    pub fn from_bytes_with_target(
121        data: &[u8],
122        hash: ObjectHash,
123        target_object_id: ObjectHash,
124    ) -> Result<Self, GitError> {
125        let content = String::from_utf8(data.to_vec())
126            .map_err(|e| GitError::InvalidNoteObject(format!("Invalid UTF-8 content: {}", e)))?;
127
128        Ok(Note {
129            id: hash,
130            target_object_id,
131            content,
132        })
133    }
134
135    /// Serialize a Note with its target association for external storage
136    ///
137    /// This method returns both the Git object data and the target object ID,
138    /// which can be used by higher-level systems to manage the refs/notes/* references.
139    ///
140    /// # Returns
141    /// A tuple of (object_data, target_object_id)
142    pub fn to_data_with_target(&self) -> Result<(Vec<u8>, ObjectHash), GitError> {
143        let data = self.to_data()?;
144        Ok((data, self.target_object_id))
145    }
146}
147
148impl ObjectTrait for Note {
149    /// Create a Note object from raw bytes and hash
150    ///
151    /// # Arguments
152    /// * `data` - The raw byte data (UTF-8 encoded text content)
153    /// * `hash` - The SHA-1/ SHA-256 hash of this Note object
154    ///
155    /// # Returns
156    /// A Result containing the Note object or an error
157    fn from_bytes(data: &[u8], hash: ObjectHash) -> Result<Self, GitError>
158    where
159        Self: Sized,
160    {
161        // Convert bytes to UTF-8 string
162        let content = String::from_utf8(data.to_vec())
163            .map_err(|e| GitError::InvalidNoteObject(format!("Invalid UTF-8 content: {}", e)))?;
164
165        Ok(Note {
166            id: hash,
167            target_object_id: ObjectHash::default(), // Target association managed externally
168            content,
169        })
170    }
171
172    /// Get the Git object type for Notes
173    ///
174    /// Notes are stored as Blob objects in Git's object database
175    fn get_type(&self) -> ObjectType {
176        ObjectType::Blob
177    }
178
179    /// Get the size of the Note content
180    fn get_size(&self) -> usize {
181        self.content.len()
182    }
183
184    /// Convert the Note to raw byte data for storage
185    ///
186    /// # Returns
187    /// A Result containing the byte representation or an error
188    fn to_data(&self) -> Result<Vec<u8>, GitError> {
189        Ok(self.content.as_bytes().to_vec())
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use crate::hash::{HashKind, ObjectHash, set_hash_kind_for_test};
197    use std::str::FromStr;
198
199    #[test]
200    fn test_note_creation_and_serialization() {
201        let _guard = set_hash_kind_for_test(HashKind::Sha1);
202        let target_id = ObjectHash::from_str("1234567890abcdef1234567890abcdef12345678").unwrap();
203        let content = "This commit needs review".to_string();
204        let note = Note::new(target_id, content.clone());
205
206        assert_eq!(note.target_object_id, target_id);
207        assert_eq!(note.content, content);
208        assert_ne!(note.id, ObjectHash::default());
209        assert_eq!(note.get_type(), ObjectType::Blob);
210
211        // Test serialization
212        let data = note.to_data().unwrap();
213        assert_eq!(data, content.as_bytes());
214        assert_eq!(note.get_size(), content.len());
215    }
216    #[test]
217    fn test_note_creation_and_serialization_sha256() {
218        let _guard = set_hash_kind_for_test(HashKind::Sha256);
219        let target_id = ObjectHash::from_str(
220            "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
221        )
222        .unwrap();
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_ne!(note.id, ObjectHash::default());
229        assert_eq!(note.get_type(), ObjectType::Blob);
230
231        // Test serialization
232        let data = note.to_data().unwrap();
233        assert_eq!(data, content.as_bytes());
234        assert_eq!(note.get_size(), content.len());
235    }
236
237    #[test]
238    fn test_note_deserialization() {
239        let _guard = set_hash_kind_for_test(HashKind::Sha1);
240        let content = "Deserialization test content";
241        let hash = ObjectHash::from_str("fedcba0987654321fedcba0987654321fedcba09").unwrap();
242        let target_id = ObjectHash::from_str("abcdef1234567890abcdef1234567890abcdef12").unwrap();
243
244        // Test basic deserialization
245        let note = Note::from_bytes(content.as_bytes(), hash).unwrap();
246        assert_eq!(note.content, content);
247        assert_eq!(note.id, hash);
248        assert_eq!(note.target_object_id, ObjectHash::default());
249
250        // Test deserialization with target
251        let note_with_target =
252            Note::from_bytes_with_target(content.as_bytes(), hash, target_id).unwrap();
253        assert_eq!(note_with_target.content, content);
254        assert_eq!(note_with_target.id, hash);
255        assert_eq!(note_with_target.target_object_id, target_id);
256    }
257    #[test]
258    fn test_note_deserialization_sha256() {
259        let _guard = set_hash_kind_for_test(HashKind::Sha256);
260        let content = "Deserialization test content";
261        let hash = ObjectHash::from_str(
262            "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321",
263        )
264        .unwrap();
265        let target_id = ObjectHash::from_str(
266            "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
267        )
268        .unwrap();
269
270        // Test basic deserialization
271        let note = Note::from_bytes(content.as_bytes(), hash).unwrap();
272        assert_eq!(note.content, content);
273        assert_eq!(note.id, hash);
274        assert_eq!(note.target_object_id, ObjectHash::default());
275
276        // Test deserialization with target
277        let note_with_target =
278            Note::from_bytes_with_target(content.as_bytes(), hash, target_id).unwrap();
279        assert_eq!(note_with_target.content, content);
280        assert_eq!(note_with_target.id, hash);
281        assert_eq!(note_with_target.target_object_id, target_id);
282    }
283
284    #[test]
285    fn test_note_with_target_methods() {
286        let _guard = set_hash_kind_for_test(HashKind::Sha1);
287        let target_id = ObjectHash::from_str("1234567890abcdef1234567890abcdef12345678").unwrap();
288        let content = "Test note with target methods";
289        let note = Note::new(target_id, content.to_string());
290
291        // Test serialization with target
292        let (data, returned_target) = note.to_data_with_target().unwrap();
293        assert_eq!(data, content.as_bytes());
294        assert_eq!(returned_target, target_id);
295
296        // Test deserialization with target
297        let restored_note = Note::from_bytes_with_target(&data, note.id, target_id).unwrap();
298        assert_eq!(restored_note, note);
299        assert_eq!(restored_note.target_object_id, target_id);
300        assert_eq!(restored_note.content, content);
301    }
302    #[test]
303    fn test_note_with_target_methods_sha256() {
304        let _guard = set_hash_kind_for_test(HashKind::Sha256);
305        let target_id = ObjectHash::from_str(
306            "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
307        )
308        .unwrap();
309        let content = "Test note with target methods";
310        let note = Note::new(target_id, content.to_string());
311
312        // Test serialization with target
313        let (data, returned_target) = note.to_data_with_target().unwrap();
314        assert_eq!(data, content.as_bytes());
315        assert_eq!(returned_target, target_id);
316
317        // Test deserialization with target
318        let restored_note = Note::from_bytes_with_target(&data, note.id, target_id).unwrap();
319        assert_eq!(restored_note, note);
320        assert_eq!(restored_note.target_object_id, target_id);
321        assert_eq!(restored_note.content, content);
322    }
323
324    #[test]
325    fn test_note_error_handling() {
326        let _guard = set_hash_kind_for_test(HashKind::Sha1);
327        // Test invalid UTF-8
328        let invalid_utf8 = vec![0xFF, 0xFE, 0xFD];
329        let hash = ObjectHash::from_str("3333333333333333333333333333333333333333").unwrap();
330        let target = ObjectHash::from_str("4444444444444444444444444444444444444444").unwrap();
331        let result = Note::from_bytes(&invalid_utf8, hash);
332        assert!(result.is_err());
333
334        let result_with_target = Note::from_bytes_with_target(&invalid_utf8, hash, target);
335        assert!(result_with_target.is_err());
336    }
337    #[test]
338    fn test_note_error_handling_sha256() {
339        let _guard = set_hash_kind_for_test(HashKind::Sha256);
340        // Test invalid UTF-8
341        let invalid_utf8 = vec![0xFF, 0xFE, 0xFD];
342        let hash = ObjectHash::from_str(
343            "3333333333333333333333333333333333333333333333333333333333333333",
344        )
345        .unwrap();
346        let target = ObjectHash::from_str(
347            "4444444444444444444444444444444444444444444444444444444444444444",
348        )
349        .unwrap();
350        let result = Note::from_bytes(&invalid_utf8, hash);
351        assert!(result.is_err());
352
353        let result_with_target = Note::from_bytes_with_target(&invalid_utf8, hash, target);
354        assert!(result_with_target.is_err());
355    }
356    #[test]
357    fn test_note_demo_functionality() {
358        let _guard = set_hash_kind_for_test(HashKind::Sha1);
359        // This is a demonstration test that shows the complete functionality
360        // It's kept separate from unit tests for clarity
361        println!("\n🚀 Git Note Object Demo - Best Practices");
362        println!("==========================================");
363
364        let commit_id = ObjectHash::from_str("a1b2c3d4e5f6789012345678901234567890abcd").unwrap();
365
366        println!("\n1️⃣ Creating a new Note object:");
367        let note = Note::new(
368            commit_id,
369            "Code review: LGTM! Great implementation.".to_string(),
370        );
371        println!("   Target Commit: {}", note.target_object_id);
372        println!("   Note ID: {}", note.id);
373        println!("   Content: {}", note.content);
374        println!("   Size: {} bytes", note.get_size());
375
376        println!("\n2️⃣ Serializing Note with target association:");
377        let (serialized_data, target_id) = note.to_data_with_target().unwrap();
378        println!("   Serialized size: {} bytes", serialized_data.len());
379        println!("   Target object ID: {}", target_id);
380        println!(
381            "   Git object format: blob {}\\0<content>",
382            note.content.len()
383        );
384        println!(
385            "   Raw data preview: {:?}...",
386            &serialized_data[..std::cmp::min(30, serialized_data.len())]
387        );
388
389        println!("\n3️⃣ Basic deserialization (ObjectTrait):");
390        let basic_note = Note::from_bytes(&serialized_data, note.id).unwrap();
391        println!("   Successfully deserialized!");
392        println!(
393            "   Target Commit: {} (default - target managed externally)",
394            basic_note.target_object_id
395        );
396        println!("   Content: {}", basic_note.content);
397        println!("   Content matches: {}", note.content == basic_note.content);
398
399        println!("\n4️⃣ Best practice deserialization (with target):");
400        let complete_note =
401            Note::from_bytes_with_target(&serialized_data, note.id, target_id).unwrap();
402        println!("   Successfully deserialized with target!");
403        println!("   Target Commit: {}", complete_note.target_object_id);
404        println!("   Content: {}", complete_note.content);
405        println!("   Complete objects are equal: {}", note == complete_note);
406
407        // Basic assertions to ensure demo works
408        assert_eq!(note, complete_note);
409        assert_eq!(target_id, commit_id);
410    }
411    #[test]
412    fn test_note_demo_functionality_sha256() {
413        let _guard = set_hash_kind_for_test(HashKind::Sha256);
414        // This is a demonstration test that shows the complete functionality
415        // It's kept separate from unit tests for clarity
416        println!("\n🚀 Git Note Object Demo - Best Practices");
417        println!("==========================================");
418
419        let commit_id = ObjectHash::from_str(
420            "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
421        )
422        .unwrap();
423
424        println!("\n1️⃣ Creating a new Note object:");
425        let note = Note::new(
426            commit_id,
427            "Code review: LGTM! Great implementation.".to_string(),
428        );
429        println!("   Target Commit: {}", note.target_object_id);
430        println!("   Note ID: {}", note.id);
431        println!("   Content: {}", note.content);
432        println!("   Size: {} bytes", note.get_size());
433
434        println!("\n2️⃣ Serializing Note with target association:");
435        let (serialized_data, target_id) = note.to_data_with_target().unwrap();
436        println!("   Serialized size: {} bytes", serialized_data.len());
437        println!("   Target object ID: {}", target_id);
438        println!(
439            "   Git object format: blob {}\\0<content>",
440            note.content.len()
441        );
442        println!(
443            "   Raw data preview: {:?}...",
444            &serialized_data[..std::cmp::min(30, serialized_data.len())]
445        );
446
447        println!("\n3️⃣ Basic deserialization (ObjectTrait):");
448        let basic_note = Note::from_bytes(&serialized_data, note.id).unwrap();
449        println!("   Successfully deserialized!");
450        println!(
451            "   Target Commit: {} (default - target managed externally)",
452            basic_note.target_object_id
453        );
454        println!("   Content: {}", basic_note.content);
455        println!("   Content matches: {}", note.content == basic_note.content);
456
457        println!("\n4️⃣ Best practice deserialization (with target):");
458        let complete_note =
459            Note::from_bytes_with_target(&serialized_data, note.id, target_id).unwrap();
460        println!("   Successfully deserialized with target!");
461        println!("   Target Commit: {}", complete_note.target_object_id);
462        println!("   Content: {}", complete_note.content);
463        println!("   Complete objects are equal: {}", note == complete_note);
464
465        // Basic assertions to ensure demo works
466        assert_eq!(note, complete_note);
467        assert_eq!(target_id, commit_id);
468    }
469}