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::SHA1;
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 SHA-1 hash of this Note object (same as the underlying Blob)
31    pub id: SHA1,
32    /// The SHA-1 hash of the object this Note annotates (usually a commit)
33    pub target_object_id: SHA1,
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 SHA-1 hash 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: SHA1, content: String) -> Self {
62        // Calculate the SHA-1 hash for this Note's content
63        // Notes are stored as Blob objects in Git
64        let id = SHA1::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(SHA1::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 hash
104    pub fn set_target(&mut self, new_target: SHA1) {
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 hash of this Note object
116    /// * `target_object_id` - The SHA-1 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: SHA1,
123        target_object_id: SHA1,
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>, SHA1), 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 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: SHA1) -> 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: SHA1::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 std::str::FromStr;
197
198    #[test]
199    fn test_note_creation_and_serialization() {
200        let target_id = SHA1::from_str("1234567890abcdef1234567890abcdef12345678").unwrap();
201        let content = "This commit needs review".to_string();
202        let note = Note::new(target_id, content.clone());
203
204        assert_eq!(note.target_object_id, target_id);
205        assert_eq!(note.content, content);
206        assert_ne!(note.id, SHA1::default());
207        assert_eq!(note.get_type(), ObjectType::Blob);
208
209        // Test serialization
210        let data = note.to_data().unwrap();
211        assert_eq!(data, content.as_bytes());
212        assert_eq!(note.get_size(), content.len());
213    }
214
215    #[test]
216    fn test_note_deserialization() {
217        let content = "Deserialization test content";
218        let hash = SHA1::from_str("fedcba0987654321fedcba0987654321fedcba09").unwrap();
219        let target_id = SHA1::from_str("abcdef1234567890abcdef1234567890abcdef12").unwrap();
220
221        // Test basic deserialization
222        let note = Note::from_bytes(content.as_bytes(), hash).unwrap();
223        assert_eq!(note.content, content);
224        assert_eq!(note.id, hash);
225        assert_eq!(note.target_object_id, SHA1::default());
226
227        // Test deserialization with target
228        let note_with_target =
229            Note::from_bytes_with_target(content.as_bytes(), hash, target_id).unwrap();
230        assert_eq!(note_with_target.content, content);
231        assert_eq!(note_with_target.id, hash);
232        assert_eq!(note_with_target.target_object_id, target_id);
233    }
234
235    #[test]
236    fn test_note_with_target_methods() {
237        let target_id = SHA1::from_str("1234567890abcdef1234567890abcdef12345678").unwrap();
238        let content = "Test note with target methods";
239        let note = Note::new(target_id, content.to_string());
240
241        // Test serialization with target
242        let (data, returned_target) = note.to_data_with_target().unwrap();
243        assert_eq!(data, content.as_bytes());
244        assert_eq!(returned_target, target_id);
245
246        // Test deserialization with target
247        let restored_note = Note::from_bytes_with_target(&data, note.id, target_id).unwrap();
248        assert_eq!(restored_note, note);
249        assert_eq!(restored_note.target_object_id, target_id);
250        assert_eq!(restored_note.content, content);
251    }
252
253    #[test]
254    fn test_note_error_handling() {
255        // Test invalid UTF-8
256        let invalid_utf8 = vec![0xFF, 0xFE, 0xFD];
257        let hash = SHA1::from_str("3333333333333333333333333333333333333333").unwrap();
258        let target = SHA1::from_str("4444444444444444444444444444444444444444").unwrap();
259
260        let result = Note::from_bytes(&invalid_utf8, hash);
261        assert!(result.is_err());
262
263        let result_with_target = Note::from_bytes_with_target(&invalid_utf8, hash, target);
264        assert!(result_with_target.is_err());
265    }
266
267    #[test]
268    fn test_note_demo_functionality() {
269        // This is a demonstration test that shows the complete functionality
270        // It's kept separate from unit tests for clarity
271        println!("\n🚀 Git Note Object Demo - Best Practices");
272        println!("==========================================");
273
274        let commit_id = SHA1::from_str("a1b2c3d4e5f6789012345678901234567890abcd").unwrap();
275
276        println!("\n1️⃣ Creating a new Note object:");
277        let note = Note::new(
278            commit_id,
279            "Code review: LGTM! Great implementation.".to_string(),
280        );
281        println!("   Target Commit: {}", note.target_object_id);
282        println!("   Note ID: {}", note.id);
283        println!("   Content: {}", note.content);
284        println!("   Size: {} bytes", note.get_size());
285
286        println!("\n2️⃣ Serializing Note with target association:");
287        let (serialized_data, target_id) = note.to_data_with_target().unwrap();
288        println!("   Serialized size: {} bytes", serialized_data.len());
289        println!("   Target object ID: {}", target_id);
290        println!(
291            "   Git object format: blob {}\\0<content>",
292            note.content.len()
293        );
294        println!(
295            "   Raw data preview: {:?}...",
296            &serialized_data[..std::cmp::min(30, serialized_data.len())]
297        );
298
299        println!("\n3️⃣ Basic deserialization (ObjectTrait):");
300        let basic_note = Note::from_bytes(&serialized_data, note.id).unwrap();
301        println!("   Successfully deserialized!");
302        println!(
303            "   Target Commit: {} (default - target managed externally)",
304            basic_note.target_object_id
305        );
306        println!("   Content: {}", basic_note.content);
307        println!("   Content matches: {}", note.content == basic_note.content);
308
309        println!("\n4️⃣ Best practice deserialization (with target):");
310        let complete_note =
311            Note::from_bytes_with_target(&serialized_data, note.id, target_id).unwrap();
312        println!("   Successfully deserialized with target!");
313        println!("   Target Commit: {}", complete_note.target_object_id);
314        println!("   Content: {}", complete_note.content);
315        println!("   Complete objects are equal: {}", note == complete_note);
316
317        // Basic assertions to ensure demo works
318        assert_eq!(note, complete_note);
319        assert_eq!(target_id, commit_id);
320    }
321}