git_internal/internal/object/
patchset.rs1use std::fmt;
17
18use serde::{Deserialize, Serialize};
19use uuid::Uuid;
20
21use crate::{
22 errors::GitError,
23 hash::ObjectHash,
24 internal::object::{
25 ObjectTrait,
26 integrity::IntegrityHash,
27 types::{ActorRef, ArtifactRef, Header, ObjectType},
28 },
29};
30
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33#[serde(rename_all = "snake_case")]
34pub enum ApplyStatus {
35 Proposed,
37 Applied,
39 Rejected,
41 Superseded,
43}
44
45impl ApplyStatus {
46 pub fn as_str(&self) -> &'static str {
47 match self {
48 ApplyStatus::Proposed => "proposed",
49 ApplyStatus::Applied => "applied",
50 ApplyStatus::Rejected => "rejected",
51 ApplyStatus::Superseded => "superseded",
52 }
53 }
54}
55
56impl fmt::Display for ApplyStatus {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 write!(f, "{}", self.as_str())
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
64#[serde(rename_all = "snake_case")]
65pub enum DiffFormat {
66 UnifiedDiff,
68 GitDiff,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
74#[serde(rename_all = "snake_case")]
75pub enum ChangeType {
76 Add,
77 Modify,
78 Delete,
79 Rename,
80 Copy,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct TouchedFile {
88 pub path: String,
89 pub change_type: ChangeType,
90 pub lines_added: u32,
91 pub lines_deleted: u32,
92}
93
94impl TouchedFile {
95 pub fn new(
96 path: impl Into<String>,
97 change_type: ChangeType,
98 lines_added: u32,
99 lines_deleted: u32,
100 ) -> Result<Self, String> {
101 let path = path.into();
102 if path.trim().is_empty() {
103 return Err("path cannot be empty".to_string());
104 }
105 Ok(Self {
106 path,
107 change_type,
108 lines_added,
109 lines_deleted,
110 })
111 }
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct PatchSet {
118 #[serde(flatten)]
119 header: Header,
120 run_id: Uuid,
121 generation: u32,
122 base_commit_sha: IntegrityHash,
123 diff_format: DiffFormat,
124 diff_artifact: Option<ArtifactRef>,
125 #[serde(default)]
126 touched_files: Vec<TouchedFile>,
127 rationale: Option<String>,
128 apply_status: ApplyStatus,
129}
130
131impl PatchSet {
132 pub fn new(
134 repo_id: Uuid,
135 created_by: ActorRef,
136 run_id: Uuid,
137 base_commit_sha: impl AsRef<str>,
138 generation: u32,
139 ) -> Result<Self, String> {
140 let base_commit_sha = base_commit_sha.as_ref().parse()?;
141 Ok(Self {
142 header: Header::new(ObjectType::PatchSet, repo_id, created_by)?,
143 run_id,
144 generation,
145 base_commit_sha,
146 diff_format: DiffFormat::UnifiedDiff,
147 diff_artifact: None,
148 touched_files: Vec::new(),
149 rationale: None,
150 apply_status: ApplyStatus::Proposed,
151 })
152 }
153
154 pub fn header(&self) -> &Header {
155 &self.header
156 }
157
158 pub fn run_id(&self) -> Uuid {
159 self.run_id
160 }
161
162 pub fn generation(&self) -> u32 {
163 self.generation
164 }
165
166 pub fn base_commit_sha(&self) -> &IntegrityHash {
167 &self.base_commit_sha
168 }
169
170 pub fn diff_format(&self) -> &DiffFormat {
171 &self.diff_format
172 }
173
174 pub fn diff_artifact(&self) -> Option<&ArtifactRef> {
175 self.diff_artifact.as_ref()
176 }
177
178 pub fn touched_files(&self) -> &[TouchedFile] {
179 &self.touched_files
180 }
181
182 pub fn rationale(&self) -> Option<&str> {
183 self.rationale.as_deref()
184 }
185
186 pub fn apply_status(&self) -> &ApplyStatus {
187 &self.apply_status
188 }
189
190 pub fn set_diff_artifact(&mut self, diff_artifact: Option<ArtifactRef>) {
191 self.diff_artifact = diff_artifact;
192 }
193
194 pub fn add_touched_file(&mut self, file: TouchedFile) {
195 self.touched_files.push(file);
196 }
197
198 pub fn set_rationale(&mut self, rationale: Option<String>) {
199 self.rationale = rationale;
200 }
201
202 pub fn set_apply_status(&mut self, apply_status: ApplyStatus) {
203 self.apply_status = apply_status;
204 }
205}
206
207impl fmt::Display for PatchSet {
208 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209 write!(f, "PatchSet: {}", self.header.object_id())
210 }
211}
212
213impl ObjectTrait for PatchSet {
214 fn from_bytes(data: &[u8], _hash: ObjectHash) -> Result<Self, GitError>
215 where
216 Self: Sized,
217 {
218 serde_json::from_slice(data).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
219 }
220
221 fn get_type(&self) -> ObjectType {
222 ObjectType::PatchSet
223 }
224
225 fn get_size(&self) -> usize {
226 serde_json::to_vec(self).map(|v| v.len()).unwrap_or(0)
227 }
228
229 fn to_data(&self) -> Result<Vec<u8>, GitError> {
230 serde_json::to_vec(self).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 fn test_hash_hex() -> String {
239 IntegrityHash::compute(b"ai-process-test").to_hex()
240 }
241
242 #[test]
243 fn test_patchset_creation() {
244 let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef);
245 let actor = ActorRef::agent("test-agent").expect("actor");
246 let run_id = Uuid::from_u128(0x1);
247 let base_hash = test_hash_hex();
248
249 let patchset = PatchSet::new(repo_id, actor, run_id, &base_hash, 1).expect("patchset");
250
251 assert_eq!(patchset.header().object_type(), &ObjectType::PatchSet);
252 assert_eq!(patchset.generation(), 1);
253 assert_eq!(patchset.diff_format(), &DiffFormat::UnifiedDiff);
254 assert_eq!(patchset.apply_status(), &ApplyStatus::Proposed);
255 assert!(patchset.touched_files().is_empty());
256 }
257}