git_internal/internal/object/
patchset.rs1use std::fmt;
28
29use serde::{Deserialize, Serialize};
30use uuid::Uuid;
31
32use crate::{
33 errors::GitError,
34 hash::ObjectHash,
35 internal::object::{
36 ObjectTrait,
37 integrity::IntegrityHash,
38 types::{ActorRef, ArtifactRef, Header, ObjectType},
39 },
40};
41
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
43#[serde(rename_all = "snake_case")]
44pub enum DiffFormat {
45 UnifiedDiff,
46 GitDiff,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50#[serde(rename_all = "snake_case")]
51pub enum ChangeType {
52 Add,
53 Modify,
54 Delete,
55 Rename,
56 Copy,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60#[serde(deny_unknown_fields)]
61pub struct TouchedFile {
62 pub path: String,
64 pub change_type: ChangeType,
66 pub lines_added: u32,
68 pub lines_deleted: u32,
70}
71
72impl TouchedFile {
73 pub fn new(
75 path: impl Into<String>,
76 change_type: ChangeType,
77 lines_added: u32,
78 lines_deleted: u32,
79 ) -> Result<Self, String> {
80 let path = path.into();
81 if path.trim().is_empty() {
82 return Err("path cannot be empty".to_string());
83 }
84 Ok(Self {
85 path,
86 change_type,
87 lines_added,
88 lines_deleted,
89 })
90 }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(deny_unknown_fields)]
100pub struct PatchSet {
101 #[serde(flatten)]
104 header: Header,
105 run: Uuid,
107 sequence: u32,
110 commit: IntegrityHash,
113 format: DiffFormat,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
117 artifact: Option<ArtifactRef>,
118 #[serde(default)]
120 touched: Vec<TouchedFile>,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
124 rationale: Option<String>,
125}
126
127impl PatchSet {
128 pub fn new(created_by: ActorRef, run: Uuid, commit: impl AsRef<str>) -> Result<Self, String> {
130 let commit = commit.as_ref().parse()?;
131 Ok(Self {
132 header: Header::new(ObjectType::PatchSet, created_by)?,
133 run,
134 sequence: 0,
135 commit,
136 format: DiffFormat::UnifiedDiff,
137 artifact: None,
138 touched: Vec::new(),
139 rationale: None,
140 })
141 }
142
143 pub fn header(&self) -> &Header {
145 &self.header
146 }
147
148 pub fn run(&self) -> Uuid {
150 self.run
151 }
152
153 pub fn sequence(&self) -> u32 {
155 self.sequence
156 }
157
158 pub fn set_sequence(&mut self, sequence: u32) {
160 self.sequence = sequence;
161 }
162
163 pub fn commit(&self) -> &IntegrityHash {
165 &self.commit
166 }
167
168 pub fn format(&self) -> &DiffFormat {
170 &self.format
171 }
172
173 pub fn artifact(&self) -> Option<&ArtifactRef> {
175 self.artifact.as_ref()
176 }
177
178 pub fn touched(&self) -> &[TouchedFile] {
180 &self.touched
181 }
182
183 pub fn rationale(&self) -> Option<&str> {
185 self.rationale.as_deref()
186 }
187
188 pub fn set_artifact(&mut self, artifact: Option<ArtifactRef>) {
190 self.artifact = artifact;
191 }
192
193 pub fn add_touched(&mut self, file: TouchedFile) {
195 self.touched.push(file);
196 }
197
198 pub fn set_rationale(&mut self, rationale: Option<String>) {
200 self.rationale = rationale;
201 }
202}
203
204impl fmt::Display for PatchSet {
205 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206 write!(f, "PatchSet: {}", self.header.object_id())
207 }
208}
209
210impl ObjectTrait for PatchSet {
211 fn from_bytes(data: &[u8], _hash: ObjectHash) -> Result<Self, GitError>
212 where
213 Self: Sized,
214 {
215 serde_json::from_slice(data).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
216 }
217
218 fn get_type(&self) -> ObjectType {
219 ObjectType::PatchSet
220 }
221
222 fn get_size(&self) -> usize {
223 match serde_json::to_vec(self) {
224 Ok(v) => v.len(),
225 Err(e) => {
226 tracing::warn!("failed to compute PatchSet size: {}", e);
227 0
228 }
229 }
230 }
231
232 fn to_data(&self) -> Result<Vec<u8>, GitError> {
233 serde_json::to_vec(self).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 fn test_hash_hex() -> String {
246 IntegrityHash::compute(b"ai-process-test").to_hex()
247 }
248
249 #[test]
250 fn test_patchset_creation() {
251 let actor = ActorRef::agent("test-agent").expect("actor");
252 let run = Uuid::from_u128(0x1);
253 let base_hash = test_hash_hex();
254
255 let patchset = PatchSet::new(actor, run, &base_hash).expect("patchset");
256
257 assert_eq!(patchset.header().object_type(), &ObjectType::PatchSet);
258 assert_eq!(patchset.run(), run);
259 assert_eq!(patchset.sequence(), 0);
260 assert_eq!(patchset.format(), &DiffFormat::UnifiedDiff);
261 assert!(patchset.touched().is_empty());
262 }
263}